diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e831967..0bdac3a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,9 +32,13 @@ jobs: dist/*.deb dist/*.rpm dist/*.pacman + dist/latest-linux.yml - os: windows-latest dist: dist:win - artifacts: dist/*.exe + artifacts: | + dist/*.exe + dist/*.exe.blockmap + dist/latest.yml runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d9d813..d12a846 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## 2026-09-30 +- [sidebar] Collapse/expand animates the width (220 ms; content is revealed/clipped, not reflowed, and fades in; off under reduced motion) (files: src/renderer/components/Sidebar.tsx, src/renderer/styles/base.css) +- [sidebar] Right-click a folder (rename, collapse, new folder, delete) or a workspace (rename, change icon, two-step delete) via a new keyboard-accessible `ContextMenu`; icon popover split out as `IconPopover`; both portal to
(files: src/renderer/components/{ContextMenu,IconPicker,Sidebar,SidebarAccounts}.tsx, src/renderer/styles/base.css, tests/e2e/sidebar-menus.spec.ts) +- [updates] Settings "Update & restart" downloads the new release, installs it over this one (pacman/deb/rpm via pkexec, AppImage, silent NSIS) and relaunches; the new-release notification opens Settings instead of the browser. Works from v1.3.0 on (files: src/main/update-install.ts, src/main/{index,ipc,update-notify}.ts, src/shared/{ipc-channels,ipc-contract}.ts, src/preload/index.ts, src/renderer/app/store.ts, src/renderer/components/SettingsView.tsx, electron-builder.yml, package.json, .github/workflows/build.yml, tests/unit/update-install.test.ts) +- [tests] E2E check that Shift+Enter sends ESC CR and Enter a bare CR to claude panes (files: tests/e2e/keys.spec.ts, tests/fixtures/raw-keys.sh) - [release] v1.2.0 (files: package.json) - [docs] Refresh README screenshots and text for sharing, folders, icons, shortcuts and update alerts (files: README.md, docs/screenshots/) - [ui] Shorter per-account sharing note; font options stay on one row (files: src/renderer/components/AccountsView.tsx, src/renderer/styles/base.css) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 432f275..34de284 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -18,7 +18,8 @@ | `platform.ts` | Desktop/window-chrome detection, default shell, extra bin dirs, spawn command, login-shell PATH | | `shared-config.ts` | Symlink/junction/hardlink ~/.claude's CLAUDE.md, settings.json, skills, plugins, agents, commands into accounts; backs up real files, replaces stale links | | `update-check.ts` | `UpdateChecker`: GitHub Releases API, cached, report only | -| `update-notify.ts` | `notifyIfNew`: native Notification for a newer release, once per version; click opens the release page | +| `update-notify.ts` | `notifyIfNew`: native Notification for a newer release, once per version; click opens Settings → Updates | +| `update-install.ts` | `UpdateInstaller`: facade over electron-updater; check, download (progress events), install and relaunch | | `paths.ts` | Config file path, accounts root, `~` resolution, `isStrictlyInside` guard | ## Renderer @@ -40,10 +41,11 @@ ## IPC surface Full list in `src/shared/ipc-channels.ts`; schemas in `src/shared/ipc-contract.ts`. Groups: pty, config, dialog, window, claude detect, account dir create/delete, openExternal (http/https only), -shared apply, app info, update check. +shared apply, app info, update check/install (+ `update:progress`, `update:show` events). ## External services - GitHub Releases API for `cachewraith/wraithgrid` (optional update check; fixed URL). +- GitHub release assets `latest.yml` / `latest-linux.yml` + installers, read by electron-updater (repo fixed in `resources/app-update.yml`). - Nothing else. No telemetry. ## Persistence diff --git a/docs/CONTEXT.md b/docs/CONTEXT.md index 16be15f..4fa0689 100644 --- a/docs/CONTEXT.md +++ b/docs/CONTEXT.md @@ -2,7 +2,7 @@ ## Purpose Electron desktop app that runs many official `claude` CLI sessions side by side, one per pane, -each with its own account (`CLAUDE_CONFIG_DIR`) and project folder. Windows + Linux. v1.1.0. +each with its own account (`CLAUDE_CONFIG_DIR`) and project folder. Windows + Linux. v1.2.0. ## Stack Electron 44, electron-vite 5, React 19, TypeScript 6, zustand, zod 4, @xterm/xterm 6, node-pty, @@ -28,7 +28,8 @@ react-resizable-panels, @dnd-kit. Tests: Vitest (unit), Playwright (Electron e2e - `src/main/claude-detect.ts`: finds the `claude` binary and version - `src/main/platform.ts`: OS/desktop differences as pure functions (chrome, shell, PATH, spawn) - `src/main/shared-config.ts`: link ~/.claude's CLAUDE.md, settings.json, skills/, plugins/, agents/, commands/ into every account ("overall" mode) -- `src/main/update-check.ts`: GitHub Releases check (fixed URL, report only) +- `src/main/update-check.ts`: GitHub Releases check (fixed URL) for the Settings/status-bar notice +- `src/main/update-install.ts`: in-app update facade over electron-updater (check → download → install + relaunch) - `src/main/update-notify.ts`: OS notification for a new release, once per version (`update-notified.json` in userData) - `src/main/paths.ts`: config/account paths, `isStrictlyInside` guard - `src/preload/index.ts`: typed bridge; renderer never sees ipcRenderer @@ -40,11 +41,11 @@ react-resizable-panels, @dnd-kit. Tests: Vitest (unit), Playwright (Electron e2e - `src/renderer/main.tsx`: composition root (builds Services) - `src/renderer/app/store.ts`: zustand store, all app actions (largest file) - `src/renderer/app/{App,services,shortcuts}`: root view, DI context, key matching -- `src/renderer/components/`: UI (PaneGrid, Pane, Terminal, dialogs, Settings/Accounts views; SidebarAccounts = folders + drag-drop; AccountIcon/Avatar = badges; IconPicker) +- `src/renderer/components/`: UI (PaneGrid, Pane, Terminal, dialogs, Settings/Accounts views; SidebarAccounts = folders + drag-drop; AccountIcon/Avatar = badges; IconPicker/IconPopover; ContextMenu = right-click menus) - `src/renderer/layout/`: pure split-tree ops, presets, directional focus - `src/renderer/lib/`: PtyBus, status detection, terminal themes, ANSI strip, scheduling - `src/renderer/styles/`: design tokens + base CSS -- `tests/unit/`, `tests/e2e/`, `tests/fixtures/fake-claude.sh`: tests + fake CLI +- `tests/unit/`, `tests/e2e/`, `tests/fixtures/{fake-claude,raw-keys}.sh`: tests + fake CLIs (raw-keys prints input bytes as hex) - `scripts/`: Docker Linux build, multi-distro package smoke test - `build/`: icons; `.github/workflows/`: build.yml (CI checks; packages only when release.yml calls it), release.yml (tag → release) - `docs/REQUIREMENTS.md`: original requirements (FR-*/NFR-* IDs); `docs/screenshots/` @@ -76,6 +77,9 @@ react-resizable-panels, @dnd-kit. Tests: Vitest (unit), Playwright (Electron e2e - AppImage on Ubuntu 24.04+/Kali blocked by AppArmor; prefer .deb. - claude only has a "new line" key via ESC CR; the terminal maps Shift+Enter to it for claude panes. - Running claude reads CLAUDE.md/skills/plugins at start: a sharing change needs a pane restart. +- In-app update needs `latest*.yml` in the release (CI uploads them from v1.3.0); 1.2.0 and older must be updated by hand once. Local `dist` scripts pass `--publish never`. +- Popovers/menus must portal to : sidebar sections animate opacity (own stacking context) and the sidebar clips. +- e2e/manual tests: `pgrep -f 'out/main/index.js'` also matches your own shell command; match the electron binary path instead. ## Current focus -v1.1.0 released. Unreleased: overall sharing via ~/.claude, Shift+Enter newline, Ctrl+=/-/0 font size, account folders, account + workspace icons, UI clean-up, running animation, new-release OS notification. Open: Claude mascot animation report (needs repro). +v1.2.0 released (overall ~/.claude sharing, folders + icons, Shift+Enter, font-size keys, running animation, release notification). Open: Claude mascot animation report (needs repro). diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index d240ade..c2af093 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -20,4 +20,8 @@ Entries dated 2026-09-29 are reconstructed from the code and commit history duri - 2026-09-30 | Sharing changes offer a "Restart claude panes" button instead of restarting automatically | A restart ends the running conversation | Auto-restart on change - 2026-09-30 | Account folders are one level deep, stored as `accountFolders` + `Account.folderId` | Enough to group accounts; sanitize drops dangling refs; old configs default to [] / null | Nested folders (tree ops, more UI for little gain); folders owning account lists (two sources of truth) - 2026-09-30 | Account icon is an emoji string rendered as text | No asset storage or upload path; React escapes it | Uploaded images (file handling, size limits); fixed SVG icon set only +- 2026-09-30 | In-app updates via electron-updater (reverses "update check reports only") | Users want one click, not picking a package from the release page; electron-updater already handles NSIS, AppImage, deb, rpm and pacman, and sha512-checks downloads against latest*.yml; source stays fixed (app-update.yml, this repo) | Own per-package installer (re-implements pkexec/NSIS handling); keep report-only (user rejected it) +- 2026-09-30 | Updates download only on the button, never auto-install on quit | On Linux an install needs a pkexec password prompt; one appearing on quit would be a surprise | autoDownload + autoInstallOnAppQuit (electron-updater defaults) +- 2026-09-30 | Sidebar animates `width`, with content fixed-width only while moving (`.moving`) | Content is revealed/clipped instead of reflowing mid-animation; at rest it must still fit beside a scrollbar; terminal refit is already debounced (50 ms) so panes resize once | Transform/overlay slide (main area would jump at the end); permanent min-width (clips under a scrollbar) +- 2026-09-30 | Workspace delete from the right-click menu is two-step inside the menu | Deleting closes the workspace's panes; no extra dialog | Confirm dialog (heavier); one-click delete (loses running panes) - 2026-09-30 | New-release OS notification, once per version, state in `userData/update-notified.json` | config.json is owned and rewritten whole by the renderer, so main can't keep state there safely | Notify every launch (nags); store in config.json (lost on the next renderer save) diff --git a/docs/TODO.md b/docs/TODO.md index e5fddee..3e78f4d 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,10 +1,12 @@ # TODO ## Now +- Account/workspace icons: picker (click the letter badge) is not discoverable; user wants a default icon. Awaiting which default + where to pick +- Shift+Enter "no new line" report: bytes (ESC CR) verified end to end on Hyprland and claude 2.1.285 accepts them; needs a repro (what happens: submits or nothing?) - Claude mascot ("pet") not animating in panes: needs a repro from the user (which animation, when) ## Next -- (none recorded) +- Release v1.3.0 (first release with in-app updates); install it by hand once, then test 1.3.0 → 1.3.1 in-app ## Later - macOS support (REQUIREMENTS §4 lists it; only Windows + Linux ship today) diff --git a/electron-builder.yml b/electron-builder.yml index 46b0f3b..5562454 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -103,4 +103,10 @@ nsis: artifactName: ${productName}-Setup-${version}-${arch}.${ext} appImage: artifactName: ${productName}-${version}-${arch}.${ext} -publish: null +# In-app updates (electron-updater) read latest.yml / latest-linux.yml from this repo's +# GitHub releases. The build never uploads (dist scripts pass --publish never); release.yml does. +publish: + provider: github + owner: cachewraith + repo: wraithgrid + releaseType: release diff --git a/package.json b/package.json index a818f1f..dc7cc07 100644 --- a/package.json +++ b/package.json @@ -28,10 +28,10 @@ "format": "prettier --write .", "test": "vitest run", "test:e2e": "electron-vite build && playwright test", - "dist": "electron-vite build && electron-builder", - "dist:linux": "electron-vite build && electron-builder --linux", + "dist": "electron-vite build && electron-builder --publish never", + "dist:linux": "electron-vite build && electron-builder --linux --publish never", "dist:linux:portable": "bash scripts/dist-linux-docker.sh", - "dist:win": "electron-vite build && electron-builder --win", + "dist:win": "electron-vite build && electron-builder --win --publish never", "postinstall": "electron-builder install-app-deps" }, "devDependencies": { @@ -61,6 +61,7 @@ "@xterm/addon-unicode11": "0.9.0", "@xterm/addon-web-links": "0.12.0", "@xterm/xterm": "6.0.0", + "electron-updater": "6.8.9", "node-pty": "1.1.0", "react": "19.3.0", "react-dom": "19.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8ab0340..bd846c3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: '@xterm/xterm': specifier: 6.0.0 version: 6.0.0 + electron-updater: + specifier: 6.8.9 + version: 6.8.9(supports-color@7.2.0) node-pty: specifier: 1.1.0 version: 1.1.0 @@ -1297,6 +1300,9 @@ packages: electron-to-chromium@1.5.439: resolution: {integrity: sha512-qu6QIPXhsb+CRcAiTMNjR4A1y/7tCYKkKjr5CZXRVih6qkDf79peZ2BpEU3qoDBNCRrcy3Mra3X9nG5oruuA7Q==} + electron-updater@6.8.9: + resolution: {integrity: sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig==} + electron-vite@5.0.0: resolution: {integrity: sha512-OHp/vjdlubNlhNkPkL/+3JD34ii5ov7M0GpuXEVdQeqdQ3ulvVR7Dg/rNBLfS5XPIFwgoBLDf9sjjrL+CuDyRQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1730,6 +1736,13 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.escaperegexp@4.1.2: + resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} + + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} @@ -2149,6 +2162,9 @@ packages: tiny-async-pool@1.3.0: resolution: {integrity: sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==} + tiny-typed-emitter@2.1.0: + resolution: {integrity: sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==} + tinybench@6.2.0: resolution: {integrity: sha512-78U2TlB2CnVenajOFzf3BKSm0J6oz5L0NV7g32LCPccvYc0lbWvys4d3uUUCS2B1N8PAf2+aekR8i1KbC3HO7Q==} engines: {node: '>=20.0.0'} @@ -3578,6 +3594,19 @@ snapshots: electron-to-chromium@1.5.439: {} + electron-updater@6.8.9(supports-color@7.2.0): + dependencies: + builder-util-runtime: 9.7.0(supports-color@7.2.0) + fs-extra: 10.1.0 + js-yaml: 4.3.2 + lazy-val: 1.0.5 + lodash.escaperegexp: 4.1.2 + lodash.isequal: 4.5.0 + semver: 7.7.4 + tiny-typed-emitter: 2.1.0 + transitivePeerDependencies: + - supports-color + electron-vite@5.0.0(supports-color@7.2.0)(vite@7.3.6(@types/node@24.19.0)(jiti@2.7.0)): dependencies: '@babel/core': 7.29.7(supports-color@7.2.0) @@ -4096,6 +4125,10 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.escaperegexp@4.1.2: {} + + lodash.isequal@4.5.0: {} + lodash@4.18.1: {} lowercase-keys@2.0.0: {} @@ -4510,6 +4543,8 @@ snapshots: dependencies: semver: 5.7.2 + tiny-typed-emitter@2.1.0: {} + tinybench@6.2.0: {} tinyexec@1.3.1: {} diff --git a/src/main/index.ts b/src/main/index.ts index b5df9c6..b8475af 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -2,8 +2,9 @@ import os from 'node:os' import path from 'node:path' import { app, BrowserWindow, Menu, nativeTheme, net, Notification, session, shell } from 'electron' import { spawn } from 'node-pty' +import { autoUpdater, type ProgressInfo } from 'electron-updater' import { IPC } from '@shared/ipc-channels' -import type { PtyDataEvent, PtyExitEvent } from '@shared/ipc-contract' +import type { PtyDataEvent, PtyExitEvent, UpdateProgress } from '@shared/ipc-contract' import { resolveTheme, type ThemeName } from '@shared/types' import { ConfigStore } from './config-store' import { registerIpc } from './ipc' @@ -18,6 +19,7 @@ import { } from './platform' import { PtyManager } from './pty-manager' import { UpdateChecker } from './update-check' +import { UpdateInstaller } from './update-install' import { notifyIfNew, type UpdateNotice } from './update-notify' // Requirements §9 names ~/.config/wraithgrid; Electron would default to the productName. @@ -57,7 +59,7 @@ const envReady: Promise