From 9de1eb43181e935c7aefac01a0232ffcf7c3367d Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 21 Mar 2026 14:44:45 -0700 Subject: [PATCH] Terminal fixes, sort fixes, flashbang fix --- .github/workflows/release.yml | 14 +++ electron-builder.yml | 1 + package.json | 3 +- pnpm-lock.yaml | 113 ++++++++++++++++++- src/main/index.ts | 10 +- src/main/ipc/settings.ts | 5 +- src/main/ipc/worktrees.ts | 7 +- src/main/persistence.ts | 2 +- src/renderer/src/components/TerminalPane.tsx | 4 +- src/renderer/src/lib/pane-manager.ts | 11 +- 10 files changed, 157 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 68d269ecb5f..17487f00b70 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,21 @@ on: - 'v*' jobs: + create-release: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Create release with auto-generated notes + run: gh release create ${{ github.ref_name }} --generate-notes --prerelease=${{ contains(github.ref_name, 'rc') }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + release: + needs: create-release strategy: fail-fast: false matrix: diff --git a/electron-builder.yml b/electron-builder.yml index 2ecc05c8f41..758f9241c57 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -53,3 +53,4 @@ publish: provider: github owner: stablyai repo: orca + releaseType: release diff --git a/package.json b/package.json index dd39c2fd709..47b4e18914f 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "build:icons": "bash icon/generate.sh", "build:mac": "electron-vite build && electron-builder --mac", "build:linux": "electron-vite build && electron-builder --linux", + "release:rc": "npm version prerelease --preid=rc && git push --follow-tags", "release:patch": "npm version patch && git push --follow-tags", "release:minor": "npm version minor && git push --follow-tags", "release:major": "npm version major && git push --follow-tags" @@ -66,7 +67,7 @@ "oxlint": "^1.56.0", "react": "^19.2.4", "react-dom": "^19.2.4", - "react-grab": "link:/Users/nwparker/projects/react-grab/packages/react-grab", + "react-grab": "^0.1.28", "tailwindcss": "^4.2.2", "typescript": "^5.9.3", "vite": "^7.3.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 818fb3a5342..081ed0fa1fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,8 +124,8 @@ importers: specifier: ^19.2.4 version: 19.2.4(react@19.2.4) react-grab: - specifier: link:/Users/nwparker/projects/react-grab/packages/react-grab - version: link:../react-grab/packages/react-grab + specifier: ^0.1.28 + version: 0.1.28(@types/react@19.2.14)(react@19.2.4) tailwindcss: specifier: ^4.2.2 version: 4.2.2 @@ -141,6 +141,10 @@ packages: 7zip-bin@5.2.0: resolution: {integrity: sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==} + '@antfu/ni@0.23.2': + resolution: {integrity: sha512-FSEVWXvwroExDXUu8qV6Wqp2X3D1nJ0Li4LFymCyvCVrm7I3lNfG0zZWSWvGU1RE7891eTnFTyh31L3igOwNKQ==} + hasBin: true + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -785,6 +789,9 @@ packages: resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==} engines: {node: '>= 10.0.0'} + '@medv/finder@4.0.2': + resolution: {integrity: sha512-RraNY9SCcx4KZV0Dh6BEW6XEW2swkqYca74pkFFRw6hHItSHiy+O/xMnpbofjYbzXj0tSpBGthUF1hHTsr3vIQ==} + '@modelcontextprotocol/sdk@1.27.1': resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==} engines: {node: '>=18'} @@ -1762,6 +1769,10 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@react-grab/cli@0.1.28': + resolution: {integrity: sha512-IE4bTeH0mCq0FBRaYRUtdiIfvO7NCv14lHS4TiTY8YNG5tPYwHnLZZtniud0ElmLIWGP95Kk6E7A6J2uDmOY+Q==} + hasBin: true + '@rolldown/pluginutils@1.0.0-rc.3': resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} @@ -2054,6 +2065,11 @@ packages: peerDependencies: '@types/react': ^19.2.0 + '@types/react-reconciler@0.28.9': + resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==} + peerDependencies: + '@types/react': '*' + '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} @@ -2209,6 +2225,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + bippy@0.5.32: + resolution: {integrity: sha512-yt1mC8eReTxjfg41YBZdN4PvsDwHFWxltoiQX0Q+Htlbf41aSniopb7ECZits01HwNAvXEh69RGk/ImlswDTEw==} + peerDependencies: + react: '>=17.0.1' + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -3004,6 +3025,10 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -3179,6 +3204,9 @@ packages: engines: {node: '>=6'} hasBin: true + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -3754,6 +3782,15 @@ packages: peerDependencies: react: ^19.2.4 + react-grab@0.1.28: + resolution: {integrity: sha512-u3fvu7a7ejHhuWKzf/N6sFavV04vcqwtbcqyxwNPtvd3ts9KSVerpHp1ZH0/XVKTsh3MZz1u1jyIt0KYC9L/Rg==} + hasBin: true + peerDependencies: + react: '>=17.0.0' + peerDependenciesMeta: + react: + optional: true + react-refresh@0.18.0: resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} engines: {node: '>=0.10.0'} @@ -3913,6 +3950,16 @@ packages: resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} engines: {node: '>=10'} + seroval-plugins@1.5.1: + resolution: {integrity: sha512-4FbuZ/TMl02sqv0RTFexu0SP6V+ywaIe5bAWCCEik0fk17BhALgwvUDVF7e3Uvf9pxmwCEJsRPmlkUE6HdzLAw==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.5.1: + resolution: {integrity: sha512-OwrZRZAfhHww0WEnKHDY8OM0U/Qs8OTfIDWhUD4BLpNJUfXK4cGmjiagGze086m+mhI+V2nD0gfbHEnJjb9STA==} + engines: {node: '>=10'} + serve-static@2.2.1: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} @@ -3981,6 +4028,10 @@ packages: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + smol-toml@1.6.0: + resolution: {integrity: sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==} + engines: {node: '>= 18'} + socks-proxy-agent@8.0.5: resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} engines: {node: '>= 14'} @@ -3989,6 +4040,9 @@ packages: resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + solid-js@1.9.11: + resolution: {integrity: sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -4428,6 +4482,8 @@ snapshots: 7zip-bin@5.2.0: {} + '@antfu/ni@0.23.2': {} + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -5045,6 +5101,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@medv/finder@4.0.2': {} + '@modelcontextprotocol/sdk@1.27.1(zod@3.25.76)': dependencies: '@hono/node-server': 1.19.11(hono@4.12.8) @@ -5983,6 +6041,17 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@react-grab/cli@0.1.28': + dependencies: + '@antfu/ni': 0.23.2 + commander: 14.0.3 + ignore: 7.0.5 + jsonc-parser: 3.3.1 + ora: 8.2.0 + picocolors: 1.1.1 + prompts: 2.4.2 + smol-toml: 1.6.0 + '@rolldown/pluginutils@1.0.0-rc.3': {} '@rollup/rollup-android-arm-eabi@4.59.1': @@ -6216,6 +6285,10 @@ snapshots: dependencies: '@types/react': 19.2.14 + '@types/react-reconciler@0.28.9(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + '@types/react@19.2.14': dependencies: csstype: 3.2.3 @@ -6384,6 +6457,13 @@ snapshots: baseline-browser-mapping@2.10.10: {} + bippy@0.5.32(@types/react@19.2.14)(react@19.2.4): + dependencies: + '@types/react-reconciler': 0.28.9(@types/react@19.2.14) + react: 19.2.4 + transitivePeerDependencies: + - '@types/react' + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -7349,6 +7429,8 @@ snapshots: ignore@5.3.2: {} + ignore@7.0.5: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -7466,6 +7548,8 @@ snapshots: json5@2.2.3: {} + jsonc-parser@3.3.1: {} + jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 @@ -8108,6 +8192,17 @@ snapshots: react: 19.2.4 scheduler: 0.27.0 + react-grab@0.1.28(@types/react@19.2.14)(react@19.2.4): + dependencies: + '@medv/finder': 4.0.2 + '@react-grab/cli': 0.1.28 + bippy: 0.5.32(@types/react@19.2.14)(react@19.2.4) + solid-js: 1.9.11 + optionalDependencies: + react: 19.2.4 + transitivePeerDependencies: + - '@types/react' + react-refresh@0.18.0: {} react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.4): @@ -8296,6 +8391,12 @@ snapshots: type-fest: 0.13.1 optional: true + seroval-plugins@1.5.1(seroval@1.5.1): + dependencies: + seroval: 1.5.1 + + seroval@1.5.1: {} + serve-static@2.2.1: dependencies: encodeurl: 2.0.0 @@ -8421,6 +8522,8 @@ snapshots: smart-buffer@4.2.0: {} + smol-toml@1.6.0: {} + socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.4 @@ -8434,6 +8537,12 @@ snapshots: ip-address: 10.1.0 smart-buffer: 4.2.0 + solid-js@1.9.11: + dependencies: + csstype: 3.2.3 + seroval: 1.5.1 + seroval-plugins: 1.5.1(seroval@1.5.1) + source-map-js@1.2.1: {} source-map-support@0.5.21: diff --git a/src/main/index.ts b/src/main/index.ts index a316b4a8921..efe63f9d4fd 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,4 +1,4 @@ -import { app, shell, BrowserWindow, Menu, nativeImage, ipcMain } from 'electron' +import { app, shell, BrowserWindow, Menu, nativeImage, ipcMain, nativeTheme } from 'electron' import { join } from 'path' import { electronApp, optimizer, is } from '@electron-toolkit/utils' import icon from '../../resources/icon.png?asset' @@ -40,6 +40,7 @@ function createWindow(): BrowserWindow { minHeight: 400, show: false, autoHideMenuBar: true, + backgroundColor: nativeTheme.shouldUseDarkColors ? '#0a0a0a' : '#ffffff', titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : undefined, ...(process.platform === 'darwin' ? { trafficLightPosition: { x: 16, y: 12 } } : {}), icon: is.dev ? devIcon : icon, @@ -203,6 +204,13 @@ app.whenReady().then(() => { // Initialize persistence store = new Store() + // Apply persisted theme before creating the window so that: + // 1. BrowserWindow gets the correct backgroundColor (no white flash) + // 2. nativeTheme.themeSource makes prefers-color-scheme report correctly + // so the renderer's applySystemTheme() picks the right mode immediately + const theme = store.getSettings().theme ?? 'system' + nativeTheme.themeSource = theme + // Create window mainWindow = createWindow() diff --git a/src/main/ipc/settings.ts b/src/main/ipc/settings.ts index e57955e52a0..443bcc25f7f 100644 --- a/src/main/ipc/settings.ts +++ b/src/main/ipc/settings.ts @@ -1,4 +1,4 @@ -import { ipcMain } from 'electron' +import { ipcMain, nativeTheme } from 'electron' import type { Store } from '../persistence' import type { GlobalSettings, PersistedState } from '../../shared/types' import { listSystemFontFamilies } from '../system-fonts' @@ -9,6 +9,9 @@ export function registerSettingsHandlers(store: Store): void { }) ipcMain.handle('settings:set', (_event, args: Partial) => { + if (args.theme) { + nativeTheme.themeSource = args.theme + } return store.updateSettings(args) }) diff --git a/src/main/ipc/worktrees.ts b/src/main/ipc/worktrees.ts index f45d80e9b38..09805a7f735 100644 --- a/src/main/ipc/worktrees.ts +++ b/src/main/ipc/worktrees.ts @@ -77,10 +77,9 @@ export function registerWorktreeHandlers(mainWindow: BrowserWindow, store: Store if (!created) throw new Error('Worktree created but not found in listing') const worktreeId = `${repo.id}::${worktreePath}` - const meta = - branchName === requestedName - ? undefined - : store.setWorktreeMeta(worktreeId, { displayName: requestedName }) + const metaUpdates: Partial = + branchName === requestedName ? {} : { displayName: requestedName } + const meta = store.setWorktreeMeta(worktreeId, metaUpdates) const worktree = mergeWorktree(repo.id, created, meta) // Run setup hook asynchronously (don't block the UI) diff --git a/src/main/persistence.ts b/src/main/persistence.ts index 2fab62d80a8..bb1e3b4f7ae 100644 --- a/src/main/persistence.ts +++ b/src/main/persistence.ts @@ -217,6 +217,6 @@ function getDefaultWorktreeMeta(): WorktreeMeta { linkedPR: null, isArchived: false, isUnread: false, - sortOrder: 0 + sortOrder: Date.now() } } diff --git a/src/renderer/src/components/TerminalPane.tsx b/src/renderer/src/components/TerminalPane.tsx index 91aaf6922cf..61bf6679973 100644 --- a/src/renderer/src/components/TerminalPane.tsx +++ b/src/renderer/src/components/TerminalPane.tsx @@ -619,9 +619,11 @@ export default function TerminalPane({ // Connect a pane's terminal to a PTY via IPC transport const connectPanePty = (pane: ManagedPane, manager: PaneManager): void => { const onExit = (ptyId: string): void => { + // Always clear the dead PTY ID from the store to avoid stale state + clearTabPtyId(tabId, ptyId) + const panes = manager.getPanes() if (panes.length <= 1) { - clearTabPtyId(tabId, ptyId) onPtyExitRef.current(ptyId) return } diff --git a/src/renderer/src/lib/pane-manager.ts b/src/renderer/src/lib/pane-manager.ts index 8dd84ec37f7..c34df180d1c 100644 --- a/src/renderer/src/lib/pane-manager.ts +++ b/src/renderer/src/lib/pane-manager.ts @@ -404,10 +404,13 @@ export class PaneManager { webglAddon: null } - // Focus handler: clicking a pane makes it active + // Focus handler: clicking a pane makes it active and explicitly focuses + // the terminal. We must call focus: true here because after DOM reparenting + // (e.g. splitPane moves the original pane into a flex container), xterm.js's + // native click-to-focus on its internal textarea may not fire reliably. container.addEventListener('pointerdown', () => { if (!this.destroyed && this.activePaneId !== id) { - this.setActivePane(id, { focus: false }) + this.setActivePane(id, { focus: true }) } }) @@ -503,6 +506,10 @@ export class PaneManager { el.style.minHeight = '0' el.style.position = 'relative' el.style.overflow = 'hidden' + // Clear any fixed width/height from createInitialPane so flex sizing + // controls the layout instead of the leftover 100% values. + el.style.width = '' + el.style.height = '' } private createDivider(isVertical: boolean): HTMLElement {