diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml
index 92c5b60..129186e 100644
--- a/.github/actions/setup/action.yml
+++ b/.github/actions/setup/action.yml
@@ -26,9 +26,15 @@ runs:
if: inputs.electron-cache == 'true'
uses: actions/cache@v6
with:
+ # One list works for every runner OS: actions/cache silently skips
+ # whichever paths don't exist on the current platform.
path: |
~/AppData/Local/electron/Cache
~/AppData/Local/electron-builder/Cache
+ ~/Library/Caches/electron
+ ~/Library/Caches/electron-builder
+ ~/.cache/electron
+ ~/.cache/electron-builder
key: ${{ runner.os }}-electron-${{ hashFiles('bun.lock') }}
restore-keys: ${{ runner.os }}-electron-
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 90a98d4..2928b3c 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -125,6 +125,7 @@ jobs:
"title=NativePi $version ($shortSha)" >> $env:GITHUB_OUTPUT
- name: Publish GitHub Release
+ if: false
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ needs.release.outputs.tag }}
@@ -139,6 +140,144 @@ jobs:
draft: false
prerelease: ${{ contains(needs.release.outputs.tag, '-') }}
+ macos-installer:
+ name: Build macOS Installer & Publish
+ needs: release
+ if: needs.release.outputs.publish == 'true'
+ runs-on: macos-latest
+ timeout-minutes: 30
+ env:
+ # No Apple Developer ID configured; skip signing and notarization.
+ CSC_IDENTITY_AUTO_DISCOVERY: "false"
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v7
+ with:
+ ref: ${{ needs.release.outputs.sha }}
+
+ - name: Setup
+ uses: ./.github/actions/setup
+ with:
+ electron-cache: "true"
+
+ - name: Build macOS installer
+ run: bun run dist:mac --publish never
+ working-directory: apps/desktop
+
+ - name: Upload installer artifact
+ uses: actions/upload-artifact@v7
+ with:
+ name: nativepi-macos-installer
+ path: |
+ apps/desktop/release/*.dmg
+ apps/desktop/release/*.zip
+ apps/desktop/release/*.blockmap
+ apps/desktop/release/latest-mac.yml
+ if-no-files-found: error
+
+ - name: Prepare release title
+ id: release
+ run: |
+ version="${{ needs.release.outputs.tag }}"
+ version="${version#v}"
+ short_sha="${{ needs.release.outputs.sha }}"
+ short_sha="${short_sha:0:12}"
+ echo "title=NativePi $version ($short_sha)" >> "$GITHUB_OUTPUT"
+
+ - name: Publish GitHub Release
+ if: false
+ uses: softprops/action-gh-release@v3
+ with:
+ tag_name: ${{ needs.release.outputs.tag }}
+ target_commitish: ${{ needs.release.outputs.sha }}
+ name: ${{ steps.release.outputs.title }}
+ files: |
+ apps/desktop/release/*.dmg
+ apps/desktop/release/*.zip
+ apps/desktop/release/*.blockmap
+ apps/desktop/release/latest-mac.yml
+ generate_release_notes: true
+ previous_tag: ${{ needs.release.outputs.previous_tag }}
+ draft: false
+ prerelease: ${{ contains(needs.release.outputs.tag, '-') }}
+
+ linux-installer:
+ name: Build Linux Installer & Publish
+ needs: release
+ if: needs.release.outputs.publish == 'true'
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v7
+ with:
+ ref: ${{ needs.release.outputs.sha }}
+
+ - name: Setup
+ uses: ./.github/actions/setup
+ with:
+ electron-cache: "true"
+
+ - name: Build Linux installer
+ run: bun run dist:linux --publish never
+ working-directory: apps/desktop
+
+ - name: Upload installer artifact
+ uses: actions/upload-artifact@v7
+ with:
+ name: nativepi-linux-installer
+ path: |
+ apps/desktop/release/*.AppImage
+ apps/desktop/release/*.blockmap
+ apps/desktop/release/latest-linux.yml
+ if-no-files-found: error
+
+ - name: Prepare release title
+ id: release
+ run: |
+ version="${{ needs.release.outputs.tag }}"
+ version="${version#v}"
+ short_sha="${{ needs.release.outputs.sha }}"
+ short_sha="${short_sha:0:12}"
+ echo "title=NativePi $version ($short_sha)" >> "$GITHUB_OUTPUT"
+
+ - name: Publish GitHub Release
+ if: false
+ uses: softprops/action-gh-release@v3
+ with:
+ tag_name: ${{ needs.release.outputs.tag }}
+ target_commitish: ${{ needs.release.outputs.sha }}
+ name: ${{ steps.release.outputs.title }}
+ files: |
+ apps/desktop/release/*.AppImage
+ apps/desktop/release/*.blockmap
+ apps/desktop/release/latest-linux.yml
+ generate_release_notes: true
+ previous_tag: ${{ needs.release.outputs.previous_tag }}
+ draft: false
+ prerelease: ${{ contains(needs.release.outputs.tag, '-') }}
+
+ publish-release:
+ name: Publish GitHub Release
+ needs: [release, windows-installer, macos-installer, linux-installer]
+ if: needs.release.outputs.publish == 'true'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/download-artifact@v7
+ with:
+ path: release
+ merge-multiple: true
+ - uses: softprops/action-gh-release@v3
+ with:
+ tag_name: ${{ needs.release.outputs.tag }}
+ target_commitish: ${{ needs.release.outputs.sha }}
+ name: NativePi ${{ needs.release.outputs.tag }}
+ files: release/*
+ generate_release_notes: true
+ previous_tag: ${{ needs.release.outputs.previous_tag }}
+ draft: false
+ prerelease: ${{ contains(needs.release.outputs.tag, '-') }}
+
repair-release-notes:
name: Regenerate release notes
if: github.event_name == 'workflow_dispatch'
diff --git a/AGENTS.md b/AGENTS.md
index 3171e77..f0b4dd3 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -6,8 +6,8 @@ and pull request mechanics.
## Project
-NativePi is a small, local, Windows-first desktop interface for the Pi coding
-agent. It is a wrapper, not an agent harness.
+NativePi is a small, local desktop interface for the Pi coding agent, for
+Windows, macOS, and Linux. It is a wrapper, not an agent harness.
Read `apps/desktop/PRODUCT.md` before making product-scope decisions and
`apps/desktop/DESIGN.md` before changing the interface's visual language.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e41e9d8..d3bd8c9 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,8 +1,8 @@
# Contributing to NativePi
-Thanks for your interest in the project. NativePi is a small Windows-first
-desktop interface for the Pi coding agent, and it is deliberately narrow in
-scope. Reading this page before you start will save you a rewrite.
+Thanks for your interest in the project. NativePi is a small desktop interface
+for the Pi coding agent, for Windows, macOS, and Linux, and it is deliberately
+narrow in scope. Reading this page before you start will save you a rewrite.
## Scope
@@ -28,8 +28,8 @@ people.
## Getting set up
-You need [Bun](https://bun.sh/) and Git. Windows is the supported platform; the
-app builds elsewhere but is neither tested nor released for other systems.
+You need [Bun](https://bun.sh/) and Git. Windows, macOS, and Linux are all
+supported and released platforms.
```sh
git clone https://github.com/nonlooped/nativepi.git
@@ -70,9 +70,9 @@ They are not expected for static rendering, getters, or coverage counts.
## Reporting bugs
-Open an issue with the NativePi version, your Windows version, and the steps
-that reproduce the problem. Logs and screenshots help. If the problem involves
-a specific model or provider, say which one.
+Open an issue with the NativePi version, your operating system and its
+version, and the steps that reproduce the problem. Logs and screenshots help.
+If the problem involves a specific model or provider, say which one.
## Reporting vulnerabilities
diff --git a/README.md b/README.md
index ae5e611..243c7b6 100644
--- a/README.md
+++ b/README.md
@@ -4,12 +4,12 @@
Pi, at home on your desktop.
- A free, open-source Windows desktop interface for the Pi coding agent.
+ A free, open-source desktop interface for the Pi coding agent, for Windows, macOS, and Linux.
-
+
@@ -63,9 +63,11 @@ storage, their own login. NativePi takes the opposite approach.
## Install
-Download the latest Windows installer from
-[GitHub Releases](https://github.com/nonlooped/nativepi/releases). Releases are
-currently unsigned, so Windows SmartScreen will warn on first launch.
+Download the latest installer for your platform from
+[GitHub Releases](https://github.com/nonlooped/nativepi/releases): an `.exe`
+for Windows, a `.dmg` for macOS, or an `.AppImage` for Linux. Releases are
+currently unsigned (and, on macOS, not notarized), so Windows SmartScreen and
+macOS Gatekeeper will warn on first launch.
NativePi bundles Pi. A separate Pi installation is not required, and existing
Pi credentials, configuration, and sessions in `~/.pi/agent` are reused.
@@ -97,6 +99,8 @@ cd apps/desktop && bun test # run the test suite
cd ../.. && bun run build # build the app
bun run pack # package without installer
bun run dist:win # build the Windows installer
+bun run dist:mac # build the macOS installer
+bun run dist:linux # build the Linux installer
```
## Architecture
diff --git a/apps/desktop/PRODUCT.md b/apps/desktop/PRODUCT.md
index 96610b2..c845cd0 100644
--- a/apps/desktop/PRODUCT.md
+++ b/apps/desktop/PRODUCT.md
@@ -4,7 +4,7 @@
## Platform
-Windows desktop (Electron)
+Windows, macOS, and Linux desktop (Electron)
## Users
@@ -27,7 +27,7 @@ NativePi is a Pi-only desktop wrapper, not a separate agent harness. Pi remains
## Operating Context
-- NativePi is a Windows desktop application used alongside local code projects, Git repositories, and the existing Pi CLI ecosystem. Its workspace can be shared temporarily to browsers on the same local network.
+- NativePi is a Windows, macOS, or Linux desktop application used alongside local code projects, Git repositories, and the existing Pi CLI ecosystem. Its workspace can be shared temporarily to browsers on the same local network.
- Users pin project folders; create, discover, import, and manage Pi sessions; inspect streamed messages and tool activity; and review Git state and diffs.
- Existing Pi credentials, configuration, sessions, packages, skills, prompts, and extensions remain in Pi's normal storage and remain usable by the Pi CLI.
- NativePi keeps at most one Pi process per project. Different projects may run concurrently.
@@ -36,7 +36,7 @@ NativePi is a Pi-only desktop wrapper, not a separate agent harness. Pi remains
## Current Capabilities
-- Project and chat pinning, date-grouped chat discovery, filtering, title and transcript search, and direct opening in installed editors or Explorer.
+- Project and chat pinning, date-grouped chat discovery, filtering, title and transcript search, and direct opening in installed editors or the system file manager.
- New, resume, rename, clone, fork, delete, import, HTML export, session-tree, statistics, and compaction workflows backed by Pi.
- Streaming text, thinking, tool activity, file-change summaries, rich diffs, retries, steering, follow-ups, queues, and abort controls.
- Images attached to a message by paste, drag and drop, or the file picker, resized by Pi before they are sent and shown in the transcript alongside the message they went with.
@@ -57,7 +57,7 @@ NativePi is a Pi-only desktop wrapper, not a separate agent harness. Pi remains
## Boundaries and Constraints
-- NativePi's host is currently Windows-only, single-window, and dark-only. Browser access requires an explicitly started server, the access token in the link, and the running desktop app. Remote Access is a throwaway Cloudflare quick tunnel created for one session and closed after twelve hours; NativePi does not operate a hosting service, register a permanent address, or hold an account of any kind.
+- NativePi's host is currently single-window and dark-only. Browser access requires an explicitly started server, the access token in the link, and the running desktop app. Remote Access is a throwaway Cloudflare quick tunnel created for one session and closed after twelve hours; NativePi does not operate a hosting service, register a permanent address, or hold an account of any kind.
- NativePi does not own an agent loop, make its own LLM requests, add agent tools, or support other harnesses.
- Pi session files are the durable conversation source of truth. NativePi persists only pinned projects and chats, the last project and chat, text drafts, favorite models, pane state, and its own interface preferences.
- Agent configuration is Pi's. NativePi reads and writes it through Pi's own settings manager at user scope, so a change made here is a change the Pi command line sees; NativePi never writes Pi's configuration format itself, and exposes only the settings that have meaning in a desktop window. Project-scope overrides remain the Pi command line's business.
@@ -66,7 +66,7 @@ NativePi is a Pi-only desktop wrapper, not a separate agent harness. Pi remains
- Normal Pi extensions run unchanged. Optional graphical extensions contribute only through controlled NativePi UI slots and are trusted code, not sandboxed code.
- Terminal extension components are drawn by Pi and displayed, not reimplemented: NativePi runs the component in the Pi process and shows what it draws, so it looks as its author wrote it rather than as NativePi would have styled it. Two parts of Pi's terminal UI have no equivalent here and keep Pi's documented no-op: raw terminal input, and replacing the input editor, which in this window is the composer.
- NativePi has no cloud sync, collaboration, remote projects, SSH launching, configurable keybindings, product accounts, paid features, or telemetry.
-- NativePi is MIT licensed. GitHub publishes Windows installers, currently without code signing, so SmartScreen warnings are expected.
+- NativePi is MIT licensed. GitHub publishes Windows, macOS, and Linux installers, currently without code signing or notarization, so SmartScreen warnings on Windows and Gatekeeper warnings on macOS are expected.
## Brand Commitments
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
index 5d7f488..64fa8eb 100644
--- a/apps/desktop/package.json
+++ b/apps/desktop/package.json
@@ -2,7 +2,7 @@
"name": "@nativepi/desktop",
"private": true,
"version": "0.11.0",
- "description": "A Windows-first desktop interface for the Pi coding agent.",
+ "description": "A desktop interface for the Pi coding agent.",
"author": "nonlooped",
"type": "module",
"main": "./out/main/index.js",
@@ -13,7 +13,9 @@
"typecheck": "tsc --noEmit",
"pack": "electron-vite build && electron-builder --dir",
"dist": "electron-vite build && electron-builder",
- "dist:win": "electron-vite build && electron-builder --win"
+ "dist:win": "electron-vite build && electron-builder --win",
+ "dist:mac": "electron-vite build && electron-builder --mac",
+ "dist:linux": "electron-vite build && electron-builder --linux"
},
"dependencies": {
"@earendil-works/pi-coding-agent": "0.82.1",
@@ -103,6 +105,24 @@
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"installerSidebar": "resources/installer-sidebar.bmp"
+ },
+ "mac": {
+ "icon": "resources/icon.png",
+ "identity": null,
+ "target": [
+ "dmg",
+ "zip"
+ ]
+ },
+ "dmg": {
+ "artifactName": "${productName}-${version}.${ext}"
+ },
+ "linux": {
+ "icon": "resources/icon.png",
+ "category": "Development",
+ "target": [
+ "AppImage"
+ ]
}
}
}
diff --git a/apps/desktop/src/main/editors.ts b/apps/desktop/src/main/editors.ts
index d6c441f..0f1ad7e 100644
--- a/apps/desktop/src/main/editors.ts
+++ b/apps/desktop/src/main/editors.ts
@@ -1,6 +1,7 @@
-import { execFile, spawn } from "node:child_process";
+import { execFile, spawn, type ChildProcess } from "node:child_process";
import { existsSync, readdirSync, statSync } from "node:fs";
-import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
+import { homedir } from "node:os";
+import { basename, delimiter, isAbsolute, join, relative, resolve, sep } from "node:path";
import { promisify } from "node:util";
import { app, shell } from "electron";
import type { InstalledEditor } from "../shared/rpc-schema.ts";
@@ -14,7 +15,14 @@ type EditorSpec = InstalledEditor & {
type ScannedEditor = { editor: InstalledEditor; executable?: string };
-const EXPLORER: InstalledEditor = { id: "explorer", name: "Explorer", icon: "explorer" };
+/** The platform's own file manager, named the way its own users would name it. */
+export function fileManagerName(): string {
+ if (process.platform === "darwin") return "Finder";
+ if (process.platform === "linux") return "Files";
+ return "Explorer";
+}
+
+const FILE_MANAGER: InstalledEditor = { id: "explorer", name: fileManagerName(), icon: "explorer" };
const local = process.env["LOCALAPPDATA"];
const programFiles = process.env["ProgramFiles"];
@@ -24,7 +32,7 @@ function under(root: string | undefined, ...parts: string[]): string[] {
return root ? [join(root, ...parts)] : [];
}
-const editorSpecs: EditorSpec[] = [
+const windowsEditorSpecs: EditorSpec[] = [
{
id: "cursor",
name: "Cursor",
@@ -123,7 +131,6 @@ const jetBrainsEditors = [
] as const;
async function registeredExecutables(): Promise