feat: v0 Etherpad Desktop — Linux + Windows MVP#1
Merged
Conversation
Code quality review of Task 1.1 flagged Electron 32 EOL, missing private flag, missing prefer-frozen-lockfile, engine-strict, packageManager. Plan updated so the implementer fix-up commit has matching reference text.
- bump electron to ^35.0.0 (32.x is past Electron's support window; upgrade brings active CVE patches) - add "private": true (prevents accidental npm publish) - add "packageManager": "pnpm@10.33.0" (Corepack pin) - .npmrc: prefer-frozen-lockfile, engine-strict (CI hygiene)
Picking up the deferred .gitignore item from Task 1.1's code review.
- Remove dual-inclusion of shared sources from leaf configs (sources are now consumed via project references' .d.ts outputs only). - Add 'paths' mapping for @shared/* on each leaf so type resolution matches the vite alias. - tsBuildInfoFile under out/shared so all build artifacts live in out/. - allowUnreachableCode/allowUnusedLabels=false for stricter dead-code detection. Defers Issue 2 (verbatimModuleSyntax) and Issue 4 (preload types) follow-ups to a later cleanup.
- Remove src/shared from leaf include arrays (composite projects consume shared via .d.ts; re-including the source duplicated work and would have caused issues once real shared types landed). - Add 'paths' mapping for @shared/* on each leaf config so tsc resolves the alias the same way vite does. - tsBuildInfoFile under out/shared/ for consistent build-artifact placement. - allowUnreachableCode/allowUnusedLabels=false for stricter dead-code detection. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Code review flagged that script-src 'self' blocks Vite HMR's eval(), breaking 'pnpm dev' (blank screen). Relaxed shell renderer CSP to allow 'unsafe-eval' and HMR WebSocket. Pad content runs in separate WebContentsView sessions and is unaffected.
Vite HMR uses eval() in its dev runtime; without 'unsafe-eval' the shell renders blank in dev mode. WebSocket connect-src is needed for the HMR connection. Pad content runs in separate WebContentsView sessions, unaffected by this relaxation.
- lint script: --no-error-on-unmatched-pattern (avoids failure when src/tests dirs are empty during early M1 setup). - Vitest 2.x: split into vitest.config.ts (base) + vitest.workspace.ts (defineWorkspace), since 'workspace' on defineConfig is now a path, not an inline array.
…paths) TypeScript 5.6 errors with TS5090 if 'paths' contains non-relative patterns like 'src/shared/*' without a 'baseUrl'. The implementer adapted Task 1.8 with this fix; updating the plan to match.
… + version guard)
electron-log/main imports electron eagerly which fails in Vitest's node env. Dynamic import inside async configureLogging/getLogger keeps the test-importable surface clean. Lifecycle now awaits both calls.
Disabled UI in production is a code smell. The "Use a local Etherpad server" checkbox was previously gated behind an "in development" hint because the underlying flow (\`npx etherpad-lite@latest\`) was always broken — that npm package doesn't exist; Etherpad core is distributed via GitHub Releases only. Until the real implementation lands (clone + pnpm install + start node, with a \`/admin\` UI for plugins like ep_webrtc), no UI surface. The main-process EmbeddedServerController and the kind: 'embedded' schema field stay in place so future work can re-light it without plumbing changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ad rename The E2E fixture was spawning \`npx --yes etherpad-lite@latest\`, but that package isn't published on npm — it 404s. Etherpad core lives at ether/etherpad and is distributed via GitHub Releases only. Result: 20/20 recent CI runs failed at globalSetup before any test body ran, blocking release-please from ever firing. - Replace the spawn with a tiny in-process http.createServer that returns the JSON shape the desktop probes for (\`/api/\` → currentVersion) and serves a minimal HTML page for \`/p/<name>\` so tab-load + cookie-based partition isolation tests still work. Sets per-pad cookies so the partition-isolation suite can verify distinct cookie jars. - Fall back to using whatever's already on 9003 if listen() fails with EADDRINUSE and the existing service speaks Etherpad — supports local dev where the user runs the etherpad snap on the same port. - Drop the npx pre-warm step + 30-min timeout in ci.yml since we no longer wait on a multi-hundred-MB tarball download. Plus three i18n misses surfaced by the now-running tests: - "Close tab" → "Close pad" in keyboard-shortcuts, stress-flows, tab-error (matches the abc40e5 i18n rename). - updater-state.spec.ts now waits for the App module's e2e seam to attach before evaluating; firstWindow() resolves before the renderer has finished loading scripts. Outcome locally: 20→57 e2e passing. \`restore-on-relaunch\` flakes only under full-suite contention (passes in isolation) — pre-existing, not introduced here. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e stays clickable User flagged: clicking the minimize-left-bar button made it disappear, leaving no way to expand again. Root cause: the WebContentsView is painted by the main process *above* the shell renderer in the z-order. Setting x=0 on the pad area in collapsed mode covered the DOM-rendered handle at left:4, so focus mode became a one-way trip. Reserve COLLAPSED_LEFT_GUTTER (28px) on the left when railCollapsed is true. The handle sits in that strip; the WebContentsView starts at x=28 and fills the rest. The two values must agree — pinned by a regression test in app-window-layout.spec.ts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User reference: https://cdn-1.webcatalog.io/catalog/etherpad/etherpad-icon-filled-256.png The previous tray icon was a B&W silhouette derived from icon-32.png. This regenerates from the filled Etherpad logo: resize 256→32 with LANCZOS, then threshold luminance >180 → opaque black (preserving anti-aliased edge alpha), everything else → transparent. Result is the Etherpad logo's horizontal text lines as a clean monochrome silhouette that reads correctly under both light and dark tray themes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The repo already has a comprehensive AGENTS.md as its canonical AI guide. Adding a small CLAUDE.md so Claude Code (and any tool that looks specifically for CLAUDE.md) lands on the same source of truth without us duplicating the full guide. What's in CLAUDE.md beyond the pointer: - Project gotchas that bite without context (no etherpad-lite npm package, port 9003/9001 conventions, dirty.db is the pad store not a lock file, WebContentsView z-order vs DOM handle, all strings go through i18n). - Working-style notes for this repo (inline execution, push-on-every- fix, run backend tests, check CI promptly, never push to main/dev, skip ep_kaput). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…/reopen
User reported ep_webrtc's "Enable Audio/Video Chat" toggle didn't
persist across a reload, even though it persisted in the same
Etherpad opened in a regular browser. That points the finger at our
\`persist:ws-\${id}\` partition setup — if localStorage doesn't survive
a navigation inside a pad WebContentsView, plugins that store UI
state in localStorage will appear to forget their settings.
Both new tests pass against current code:
- localStorage written in a pad survives a same-origin reload.
- localStorage survives closing + reopening the same tab in the same
workspace (same partition).
So the desktop side is doing the right thing. If a setting fails to
persist after this contract is green, the bug is elsewhere
(server-side per-pad/per-user state, sessionStorage misuse in the
plugin, or a cookie-SameSite issue worth its own investigation).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI's e2e job has been failing with 'Electron failed to install correctly, please delete node_modules/electron and try installing again'. Root cause: pnpm 9+ blocks postinstall scripts by default unless the package is on the onlyBuiltDependencies allowlist. The .npmrc had \`approve-builds[]=electron\` which is not a valid pnpm config key — pnpm silently ignored it. Add the canonical \`pnpm.onlyBuiltDependencies\` array in package.json so CI runs the electron download postinstall and the binary is extracted. Verified locally with \`pnpm rebuild electron\` — produces the 206MB electron binary at the expected path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: previous tray icon was too small and dark — disappeared into dark tray backgrounds. Switch to white-on-transparent at 48x48, and tight-crop to the logo's bounding box so the silhouette actually fills the icon canvas instead of floating in the middle 40%. Method: threshold the source at full resolution (lum > 240 isolates the logo's white text lines from the green background), find the bounding box of the white pixels, crop tight + 8% padding, square the crop so circular tray cells don't squish the logo, then downsample the binary mask to 48x48 with LANCZOS for clean anti-aliased edges. Apply white as the colour and the downsampled mask as the alpha. Open question (filed mentally): cross-DE adaptation. White silhouette reads correctly on the dark tray themes most Linux DEs default to (GNOME, KDE Breeze Dark, etc.), but a light tray theme would benefit from a black variant. Electron Tray on Linux doesn't have a built-in template-icon mode (only macOS does). If this becomes a problem we'd need to detect the system theme and swap PNGs at runtime — out of scope for v0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: "If an Etherpad Instance URL changes we should be able to change it in Settings." The workspace.update IPC already accepted serverUrl; just no UI for it. Adds a URL field as a second row beneath each workspace's name/colour edit row. Behaviour: - Local draft state (useState) so transient mid-typing values don't thrash the persistent store. - Commits on blur OR on Enter, after a quick http(s) validity check with the URL constructor. - Invalid URLs surface an inline error and aren't persisted; the draft stays in the input so the user can fix it. - aria-invalid + role=alert error messaging. Open tabs in the workspace keep pointing at the old src until manually reloaded — that's the simpler v0 contract; auto-reloading every open tab on an URL edit can land later if it becomes annoying. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…orks
User reported: ep_webrtc's "Enable Audio/Video Chat" toggle in pad
settings did nothing on click in the desktop app, while the same
Etherpad worked fine in a browser.
Root cause: we installed setPermissionRequestHandler only. Without
setPermissionCheckHandler, navigator.permissions.query({name:'camera'})
returns 'denied' (Electron defaults a missing check handler to false),
so plugins that gate UI on the synchronous check never even render
their toggle's active path. The button visually existed but was
no-oping at click time. Without setDevicePermissionHandler, even a
granted media permission could fail at the device-selection step.
Three handlers now installed:
- setPermissionRequestHandler — async, fired by getUserMedia() etc.
- setPermissionCheckHandler — sync, fired by permissions.query()
- setDevicePermissionHandler — fired when picking a specific
camera/mic; allows audioinput,
videoinput, audiooutput; denies USB,
serial, hid, bluetooth.
Also added 'display-capture' to the allowlist so ep_webrtc's screen-
share path works, and wired the logger so main.log records each
permission request/check (debug for checks, info for requests). That
gives us a paper trail next time something like this is "silently
broken".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Help > Open Log Folder timed out at 120s in CI (e2e job: "Worker teardown timeout of 120000ms exceeded"). \`shell.openPath\` invokes \`xdg-open\` to open the system file manager — on a headless ubuntu runner with no DE, that call hangs the Electron app and the worker can never finish teardown. Under E2E_TEST=1, no-op the callback so the menu item still fires (its presence and click contract are what the test verifies) without spawning a process that won't return. Production behaviour unchanged. Local result with the fix: full menu-click suite goes from 1 timeout to 9/9 passing in 13s. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…st launch User report: "On first load stored pads don't load into UI, they show up when you click 'Open Pad' or 'New Pad'." Root cause: state.getInitial returned workspaces+settings only. The renderer's padHistory map started empty and was only populated when EV_PAD_HISTORY_CHANGED fired — which only happens on touch/pin/unpin, i.e. the first user-driven mutation. Until then the sidebar's "Recent" and "Pinned" sections rendered empty. Now getInitial pre-bundles padHistory for every known workspace and hydrate() applies it. Pinned by 4 contract tests (eager bundle, empty- workspace map, fresh-workspace empty array, and the original getInitial shape). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User asked: "Would there be an expectation a user can use keyboard to fast switch between pads like they can a browser tab? Alt 1,2 etc." Yes. Wires Ctrl/Cmd+1..8 to focus the Nth open pad of the active workspace, with Ctrl/Cmd+9 jumping to the LAST pad — matching the browser convention from Chrome and Firefox. The shortcut is skipped when an input/textarea/contentEditable has focus so users can still type "1" in fields. Three E2E regression tests: - Ctrl+1 focuses the first pad. - Ctrl+9 focuses the last pad regardless of count. - Ctrl+1 in an input is a no-op (dialog stays open). Picked Ctrl/Cmd over Alt to match browsers; Linux DEs frequently already bind Alt+N for window-manager actions, so going with browser conventions reduces conflicts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
collapse Settings color column into the swatch Three UI carryovers from this round of click-through feedback: 1. "the slit being visible" in collapsed mode — paint .shell-root-wrapper.rail-collapsed with var(--rail-bg) so the 28px gutter on the left of the WebContentsView reads as an intentional thin "rail strip with a handle", not as random body colour bleeding through. 2. "if the window is smaller than the settings the user cannot access the bottom of the settings dialog" — cap .dialog-panel at max-height: calc(100vh - 32px) and overflow-y: auto so any tall dialog scrolls inside the modal instead of running off the viewport. 3. "the remove button overlaps the color" — drop the dedicated <input type=color> column. The leftmost swatch is now a clickable <label> wrapping a visually-hidden colour input, so clicking the swatch opens the native picker. Saves a column, fixes the overlap, and adds a hover scale + accent-ring for affordance. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User asked for a verbose README with: - screenshot/gif placeholders - all features currently baked in - roadmap of upcoming work Sections covered: why-use, full feature inventory (workspaces, pads, tabs, quick switcher, settings, window/tray, theming, i18n, a11y, auto-update, packaging), keyboard shortcut cheat-sheet, Linux install paths (AppImage, deb, snap), develop quick-start, where-to-change-what guide, roadmap split into "Soon" and "Later", and a brief test-layer overview. Screenshot placeholders point at docs/images/ — replace with real captures when convenient. README states explicitly that those paths are placeholders so reviewers don't think they're broken assets. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two user-flagged regressions: 1. "+ NEW PAD button still visible when minimized" — the existing CSS targeted .workspace-rail and aside[aria-label], but in some flex/overflow combinations sidebar children could still paint into the 0-width grid column. Hide the wrapping cells too: tag the rail and sidebar wrapper divs with .rail-cell / .sidebar-cell and add them to the collapsed-state display:none rule. 2. "Alt 1,2,3 etc does nothing" — the previous keybinding accepted Ctrl/Cmd only and explicitly excluded altKey. User asked for Alt too. Accept any of Ctrl, Cmd, Alt as the modifier. Browser conventions (Ctrl/Cmd) and Linux DE conventions (Alt) both work. E2E test added for Alt+1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User reported two regressions when focus is inside a pad's WebContentsView (a separate Chromium instance from our shell): 1. "Alt 1,2,3 etc only works when focus is not in a pad. Control K works if in pad or not." 2. "Right clicking is suppressed, can't do it." Native menu accelerators (like Ctrl+K) capture from any focused WebContents, but our renderer-level keydown listener only sees keys when the shell view has focus. And sandboxed Electron WebContentsViews show no context menu by default — the host has to wire one. Fixes: - TabManager.wireViewEvents now hooks before-input-event on each pad view: when Alt/Ctrl/Cmd+1..9 is pressed inside a pad, the keystroke is preventDefault'd and forwarded to the shell webContents via a new \`shell.padFastSwitch\` IPC event, which the renderer handles with the same applyFastSwitch logic as its own keydown listener. - TabManager.wireViewEvents also hooks context-menu and delegates to a host-supplied callback. AppWindow renders a basic menu (undo/redo when editable, cut/copy/paste, select all, reload pad, plus Inspect Element under NODE_ENV !== production) via Menu.buildFromTemplate + popup. - Refactored applyFastSwitch out of the keydown effect so the IPC path and the keydown path share one source of truth. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User report + DevTools probe showed the smoking gun:
ua: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko)
etherpad-desktop/0.1.0 Chrome/146.0.7680.216 Electron/41.5.0
Safari/537.36
Permissions and devices were both fine ("camera: granted",
"mic: granted", 7 devices enumerated). ep_webrtc's "Enable A/V Chat"
button rendered but the click silently no-oped because the plugin
user-agent-sniffs and conservatively whitelists known browsers. The
"Electron/" token was tripping its rejection path — same pattern Slack,
Notion, Loom, and many other Electron apps work around.
PadViewFactory.create now sets the WebContentsView's user agent BEFORE
the first loadURL, with stripElectronTokens removing two specific
substrings:
- " Electron/<version>"
- " etherpad-desktop/<version>"
Chrome and platform tokens are preserved so server-side feature
detection still works correctly. Pinned by 7 unit tests (including
the order-of-operations contract: setUserAgent fires before loadURL).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User report: "pressing alt 1 focus to different pad worked but then no focus landed in pad after changing pad so you cant do alt 1, alt 2 as it will not switch." Root cause: applyVisibility called setVisible(true) on the new active view but never called webContents.focus(). The previously-focused (now-hidden) view kept keyboard focus, so the NEXT Alt+1..9 keystroke was delivered to the wrong WebContents — the one that's hidden — and its before-input-event handler ran instead. The forwarded fast-switch key would still work (it routes through main), but if the user typed ANY other key meant for the new pad it landed in the old hidden one. After the visibility flip, call webContents.focus() on the now-visible view inside a try/catch (defensive — focus is non-critical for the visibility transition). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ng strings
User asked: "Should we replace the terminology 'Workspaces' with
'Etherpad Instances'."
Decision: rename only at the user-facing string layer (i18n + JSX
copy + e2e/unit test selectors that match those strings). Internal
identifiers stay as workspace — IPC channels (workspace.list, .add,
.update, .remove, .reorder), the WorkspaceStore class, the
removeWorkspace dialog kind, the persist:ws-<uuid> partition format,
the workspaces[] state key, schema names — all unchanged. Renaming
those would have churned ~50 files for zero user-visible benefit.
Strings updated:
- Rail: "Add Etherpad instance", "Open instance {{name}}", "Hide
instances (focus mode)", "Search instances and pads (Ctrl+K)".
- Add dialog title: "Add an Etherpad instance".
- Remove dialog: "Remove this Etherpad instance?" + body retitled.
- Workspace edit row: "Instance name", "Instance colour",
+ new "Etherpad instances" section heading.
- Quick switcher: "Search instances and pads…", workspaceLabel
"Instance", empty-state "Type to search across all instances and
recent pads."
- Settings → userName hint: "Pre-filled when you add an Etherpad
instance…"
Test selectors updated to match the new strings (rail spec, e2e specs
that assert button labels). No source-code identifiers renamed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…y pass Stale HTTP cache caused a confusing ep_webrtc bug today: the plugin's JS/translation bundles were cached from an older Etherpad and the toggle silently no-oped until the user ticked "Disable cache" + force reload in DevTools. Adding two affordances to make the workaround discoverable without DevTools. Hard reload pad - New View → "Hard Reload Pad" menu item, accelerator Ctrl+Shift+R. - New tab.hardReload IPC channel + main-side handler that calls webContents.reloadIgnoringCache() (falls back to plain reload on older Electron). Renderer wires the menu broadcast through. Tray icon top-edge crop fix - The first rail icon's 2px active-state box-shadow ring was being clipped by .workspace-rail-scroll's overflow-y: auto. Add 4px top/bottom padding inside the scroll viewport so the ring breathes. README terminology - Sweep README mentions of "workspace" → "instance" / "Etherpad instance" to match the renderer i18n rename. Internal identifiers (test file names like scroll-with-30-workspaces) left alone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User asked: "include an 'Open by URL' type thing where a user just has to paste in the URL of the pad and it automatically adds the instance and the pad." Flow: File → Open Pad by URL… (Ctrl+L, browser address-bar convention). Paste e.g. https://pad.example.com/p/some-pad and: 1. parsePadUrl extracts (serverUrl, padName). 2. If an instance already exists for that serverUrl, the pad opens in it. 3. Otherwise the instance is added (host as default name, palette colour rotated through), the rail icon appears, the pad opens. One step from the user's perspective: paste, hit Enter. Wiring: - src/shared/url.ts: parsePadUrl utility — extracts (serverUrl, padName) with full handling of path-prefixed Etherpad installs, query/hash stripping, percent-encoded names, trailing slashes. 11 unit tests. - src/renderer/dialogs/OpenByUrlDialog.tsx: new dialog using the shared DialogShell. Submit-on-Enter, inline error/status messaging, AppError mapping for unreachable / not-Etherpad responses. - src/renderer/state/store.ts: 'openByUrl' added to DialogKind. - src/renderer/i18n/en.ts: openByUrl namespace (title, label, placeholder, errors, status). i18n shape contract updated. - src/main/app/menu.ts + lifecycle.ts + preload + App.tsx: File menu item, Ctrl+L accelerator, IPC broadcast through to the dialog. - 3 E2E tests: new instance branch, existing instance branch, malformed URL branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rename The earlier i18n rename pass missed several test selectors that match on heading/title text. Local tests pass because the runner shells out to a pre-built bundle that may be cached, but CI builds fresh and loads the new strings — so dialog-dismissal, first-launch, menu-click, url-validation, settings-flow, renderer-mount and rail-collapse all got element-not-found timeouts in CI even though the user-visible behaviour is correct. - /add a workspace/i → /add an etherpad instance/i (heading text) - /remove workspace/i → /remove this etherpad instance/i - /workspace rail/i → /etherpad instance rail/i (nav aria-label) - /hide workspaces/i → /hide instances/i (collapse-handle title) - /show workspaces/i → /show instances/i All 25 affected tests pass locally; pushing so CI lands green and we can cut the v0.1.0 beta. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…licate
User report: "I was able to get a pad to open twice if I click it
quickly multiple times. It shows up multiple times in the Tab view."
Root cause: TabManager.open() looks for an existing tab via
this.tabs.find(...) and bails early if found, but factory.create() is
async (it awaits webContents.loadURL). Two clicks <100ms apart both
pass the existence check while neither has pushed its tab yet, then
both push, then both register events — duplicate tabs.
Add an inflight Map<key, Promise<OpenTab>> keyed by
"\${workspaceId}|\${padName}". Concurrent calls that find an inflight
promise return that same promise, so all callers resolve to the same
OpenTab and only one factory.create + viewHost.add fires. Also re-
check for an existing tab AFTER the await, in case some other path
created one while we were waiting.
Three regression tests pin the contract:
- two concurrent opens for the same key resolve to one tab
- three concurrent opens still resolve to one tab
- after coalescing, a later open with the same key still dedups
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User wants Windows in the same beta release: "a lot of our testers
will want to test with windows."
Build pipeline:
- build/icons/icon.ico — multi-resolution Windows icon (16/24/32/48/
64/128/256 px) generated from the existing icon-256.png.
- build/electron-builder.yml — new \`win:\` block targeting:
1. NSIS — proper installer with desktop + Start Menu shortcuts
and an uninstaller. oneClick: false so the user can pick the
install dir; perMachine: false so installs go to the user
profile (no UAC prompt). Artefact: Etherpad-Desktop-Setup-X.Y.Z.exe
2. portable — single-file .exe that runs without installing.
Artefact: Etherpad-Desktop-X.Y.Z-portable.exe.
Both x64. Both UNSIGNED — Windows SmartScreen will warn on first
run (signing needs an EV cert; punted to a follow-up).
- .github/workflows/release.yml — new release-windows job runs in
parallel with release-linux on every \`v*\` tag. Uses
windows-latest runner so we get genuine Windows artefacts (not
Wine cross-compiled). Uploads .exe + latest.yml for
electron-updater. publish: always sends to GitHub Releases via the
existing GH_TOKEN.
- package.json — added package:linux and package:win scripts so the
default \`pnpm package\` (still Linux-only) doesn't change behaviour
but Windows can be invoked locally on a Windows machine.
Docs:
- README — status note bumped to "v0 beta" with Linux + Windows
listed; new "Windows" install section under Install describes both
artefacts and the SmartScreen warning. Roadmap "Windows and macOS
builds" entry split: macOS still pending, Windows-signing called
out as the remaining piece.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ⓘ You've reached your Qodo monthly free-tier limit. Reviews pause until next month — upgrade your plan to continue now, or link your paid account if you already have one. |
JohnMcLear
added a commit
that referenced
this pull request
May 12, 2026
… + X-Frame fallback + https:// auto-prefix
Six device-test issues from real-hardware run:
1. Dialogs (AddWorkspace / OpenPad / etc.) overflowed phone viewports —
DialogShell width now `min(${width}px, calc(100vw - 16px))` so a 420
panel caps at viewport-16 on phones. Tighter padding under 480px.
2. Bare hostname URLs were rejected — AddWorkspaceDialog auto-prefixes
`https://` when the input has no `://` scheme.
3. (Same as #1 — OpenPadDialog uses DialogShell.)
4. "Black screen" on pad open — likely X-Frame-Options DENY/SAMEORIGIN
from the server. PadIframeStack now starts a 6s timeout per iframe;
if no onLoad fires, an opaque overlay surfaces with an "Open in
browser" button that hands the URL off to @capacitor/browser.
5. Status bar overlapped the rail — shell-root-wrapper now applies
`env(safe-area-inset-*)` padding. Zero visual change on desktop;
stops mobile WebView from drawing under the status bar / notch.
6. Newly added workspaces didn't appear in the rail — mobile platform's
`events.onWorkspacesChanged` was a noop. workspace-store now owns
a tiny listener Set; CapacitorPlatform wires it through so the
shell's `onWorkspacesChanged` handler in App.tsx receives the new
`{workspaces, order}` after every add / update / remove / reorder.
DialogShell width-test dropped (jsdom can't parse CSS `min()`; behaviour
is exercised in Playwright on real Chromium).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
JohnMcLear
added a commit
that referenced
this pull request
May 12, 2026
…ame fallback + https auto-prefix (Phase 7 fixes) (#37) * fix(mobile): responsive dialogs + safe-area insets + workspace events + X-Frame fallback + https:// auto-prefix Six device-test issues from real-hardware run: 1. Dialogs (AddWorkspace / OpenPad / etc.) overflowed phone viewports — DialogShell width now `min(${width}px, calc(100vw - 16px))` so a 420 panel caps at viewport-16 on phones. Tighter padding under 480px. 2. Bare hostname URLs were rejected — AddWorkspaceDialog auto-prefixes `https://` when the input has no `://` scheme. 3. (Same as #1 — OpenPadDialog uses DialogShell.) 4. "Black screen" on pad open — likely X-Frame-Options DENY/SAMEORIGIN from the server. PadIframeStack now starts a 6s timeout per iframe; if no onLoad fires, an opaque overlay surfaces with an "Open in browser" button that hands the URL off to @capacitor/browser. 5. Status bar overlapped the rail — shell-root-wrapper now applies `env(safe-area-inset-*)` padding. Zero visual change on desktop; stops mobile WebView from drawing under the status bar / notch. 6. Newly added workspaces didn't appear in the rail — mobile platform's `events.onWorkspacesChanged` was a noop. workspace-store now owns a tiny listener Set; CapacitorPlatform wires it through so the shell's `onWorkspacesChanged` handler in App.tsx receives the new `{workspaces, order}` after every add / update / remove / reorder. DialogShell width-test dropped (jsdom can't parse CSS `min()`; behaviour is exercised in Playwright on real Chromium). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mobile): icons + collapse handle + auto-collapse + userName + pad-history + Ctrl-K hint + tests Round 2 of device feedback: - App icons: regenerated all five mipmap densities + adaptive foregrounds from packages/desktop/build/icons/icon-512.png. The Capacitor default icon is gone; Etherpad branding everywhere. - Removed the always-visible PadActionsOverlay (share is redundant with Etherpad's own UI; the "open in browser" button forced over content). - Collapse handle now flush to left edge, 14px wide (was 22 + 3px gap), right-flat border so it reads as a tab sticking out. - Tab.open auto-collapses the workspace rail (mobile UX) — fires only on the open event, doesn't fight subsequent manual expands. - Tab.open upserts pad-history so QuickSwitcher's name search finds the pad. Wired events.onPadHistoryChanged through. - Settings.userName threads into the iframe src as `&userName=` so the user's name applies to existing + new pads (Etherpad reads the query param at join time). - "Tip: Ctrl+K opens this from anywhere" hidden via `@media (pointer: coarse)` — touch devices can't issue keyboard shortcuts anyway. - Tests: 8 mobile Playwright cases now (added 3 — auto-collapse, pad-history populate, userName in src). X-Frame detection removed (Chromium fires onLoad even for blocked iframes; needs the native WebChromeClient hook in Phase 6b). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(mobile): swipe gestures + Android back-button + upsert error logging - Edge-swipe right from left edge expands the rail; swipe left from inside collapses it. Touch-only — doesn't fire when a dialog is open (dialogs handle their own gestures). - Android hardware/gesture back: dismiss open dialog first, else collapse rail, else minimise the app. Mirrors stock Android navigation expectations. - padHistory.upsert errors now log to console.warn instead of being swallowed by `void`. Earlier logcat confirmed upsert is firing and writing 'Jehejej' to SharedPreferences as expected. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
JohnMcLear
added a commit
that referenced
this pull request
May 12, 2026
…y relaunch test (#42) * fix(mobile): responsive dialogs + safe-area insets + workspace events + X-Frame fallback + https:// auto-prefix Six device-test issues from real-hardware run: 1. Dialogs (AddWorkspace / OpenPad / etc.) overflowed phone viewports — DialogShell width now `min(${width}px, calc(100vw - 16px))` so a 420 panel caps at viewport-16 on phones. Tighter padding under 480px. 2. Bare hostname URLs were rejected — AddWorkspaceDialog auto-prefixes `https://` when the input has no `://` scheme. 3. (Same as #1 — OpenPadDialog uses DialogShell.) 4. "Black screen" on pad open — likely X-Frame-Options DENY/SAMEORIGIN from the server. PadIframeStack now starts a 6s timeout per iframe; if no onLoad fires, an opaque overlay surfaces with an "Open in browser" button that hands the URL off to @capacitor/browser. 5. Status bar overlapped the rail — shell-root-wrapper now applies `env(safe-area-inset-*)` padding. Zero visual change on desktop; stops mobile WebView from drawing under the status bar / notch. 6. Newly added workspaces didn't appear in the rail — mobile platform's `events.onWorkspacesChanged` was a noop. workspace-store now owns a tiny listener Set; CapacitorPlatform wires it through so the shell's `onWorkspacesChanged` handler in App.tsx receives the new `{workspaces, order}` after every add / update / remove / reorder. DialogShell width-test dropped (jsdom can't parse CSS `min()`; behaviour is exercised in Playwright on real Chromium). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mobile): icons + collapse handle + auto-collapse + userName + pad-history + Ctrl-K hint + tests Round 2 of device feedback: - App icons: regenerated all five mipmap densities + adaptive foregrounds from packages/desktop/build/icons/icon-512.png. The Capacitor default icon is gone; Etherpad branding everywhere. - Removed the always-visible PadActionsOverlay (share is redundant with Etherpad's own UI; the "open in browser" button forced over content). - Collapse handle now flush to left edge, 14px wide (was 22 + 3px gap), right-flat border so it reads as a tab sticking out. - Tab.open auto-collapses the workspace rail (mobile UX) — fires only on the open event, doesn't fight subsequent manual expands. - Tab.open upserts pad-history so QuickSwitcher's name search finds the pad. Wired events.onPadHistoryChanged through. - Settings.userName threads into the iframe src as `&userName=` so the user's name applies to existing + new pads (Etherpad reads the query param at join time). - "Tip: Ctrl+K opens this from anywhere" hidden via `@media (pointer: coarse)` — touch devices can't issue keyboard shortcuts anyway. - Tests: 8 mobile Playwright cases now (added 3 — auto-collapse, pad-history populate, userName in src). X-Frame detection removed (Chromium fires onLoad even for blocked iframes; needs the native WebChromeClient hook in Phase 6b). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(mobile): swipe gestures + Android back-button + upsert error logging - Edge-swipe right from left edge expands the rail; swipe left from inside collapses it. Touch-only — doesn't fire when a dialog is open (dialogs handle their own gestures). - Android hardware/gesture back: dismiss open dialog first, else collapse rail, else minimise the app. Mirrors stock Android navigation expectations. - padHistory.upsert errors now log to console.warn instead of being swallowed by `void`. Earlier logcat confirmed upsert is firing and writing 'Jehejej' to SharedPreferences as expected. * feat(mobile): persist + restore open tabs across app restarts Closing the app then reopening it now puts the user back on the pad they were on. Implementation: - New `tab-persistence.ts` writes `{tabs, activeTabId, activeWorkspaceId}` to Capacitor Preferences under `etherpad:windowState` whenever the tab-store mutates (debounced 150ms). - `loadFromStorage()` is awaited inside `state.getInitial()` so the shell's first `onTabsChanged` subscription sees the restored set. - `window.setActiveWorkspace` now writes through to the persistence layer instead of being a no-op. Shell change: InitialState gains optional `activeWorkspaceId`. App.tsx prefers it over `workspaceOrder[0]` when present and valid. Desktop main process doesn't populate it (yet) so desktop behaviour is unchanged — the field is informational only when omitted. Also fix(mobile): wire `events.onSettingsChanged` so changing language or any other setting actually updates the iframe + UI in-place. Was a noop subscriber; settings-store now has an onChanged emitter and CapacitorPlatform routes the shell's subscription through it. Without this, `applySettings()` + `setLanguage()` never fired and the iframe's ?lang= param never refreshed. Also fix(desktop e2e): bump restore-on-relaunch.spec.ts timeout from 30s → 60s. The test has flaked on ~50% of recent PR runs (cold-start under xvfb contention); reruns succeed at the same 30s. Happy path still completes in 8-12s. * fix(mobile): write tab state immediately + B&W app icon + test round-trip - The 150ms debounce in tab-store.scheduleSave was eating writes on app-kill: open pad → \`am force-stop\` ≤150ms later → save never fires → on relaunch tab list is empty (the device-side bug the user hit). Write-through immediately on every mutation; tab.open is a per-user-gesture event so the write rate is fine. - New Playwright smoke "opening a pad then reloading restores the same pad (full write+read cycle)" — characterizes the bug above. Failed before this commit, passes after. Reload is the closest in-browser analogue to app-kill+relaunch (same JS context boundary, same Preferences read on init). - B&W app icon: regenerated all five mipmap densities + adaptive foregrounds from build/icons/tray-icon.png (the official Etherpad pencil-pad silhouette). Adaptive background flipped #FFFFFF → #000000. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This is the v0 beta merge — 197 commits implementing the Etherpad Desktop MVP for Linux and Windows. Squashed highlights:
Functionality
monkimatchesmonkey).prefers-color-schemesupport; HTMLlangfollows the active language.Build + Release
.deb, Snap (electron-builder + snapcraft)..exe(parallel job in release.yml onwindows-latest). Currently unsigned — SmartScreen will warn first run.Tests
Test plan
restore-on-relaunchoccasionally retries under load; passes on retry)release-windowsjob on thev0.1.0tagsnap-publish.ymlon thev0.1.0tag🤖 Generated with Claude Code