Skip to content

Commit d97639c

Browse files
Add Neutralinojs desktop client app (#1)
* Add Neutralinojs desktop client app Lightweight (~2MB) desktop wrapper that connects to a local or remote Vibora server. Uses system webview via Neutralinojs. Includes packaging scripts for Linux AppImage and macOS DMG. * Use settings.json instead of separate desktop-settings.json * Fix: use Neutralino.os.getEnv for home directory * Remove remaining NL_USER reference from console.log * Add zoom keyboard shortcuts (Cmd/Ctrl +/-/0) * Add floating zoom controls (hover bottom-right corner) * Navigate directly to Vibora app instead of iframe Enables native webview zoom (Cmd+/-) with sharp text rendering. Removes custom zoom controls since native zoom works better. * Restore iframe with floating zoom controls Native webview zoom doesn't work via keyboard shortcuts, so using iframe with CSS transform zoom and floating controls instead. * Use CSS zoom instead of transform scale for sharper rendering * Persist zoom level to settings * Revert to transform scale zoom with floating buttons * Implement native zoom via root font-size Desktop client passes zoom level as query parameter (?zoom=1.25). Frontend reads this and sets html font-size (e.g., 20px for 125%). Since shadcn/ui uses rem units, all UI scales natively and sharply. * Preserve current page when changing zoom level * Add zoom debugging logs * Use Neutralino.debug.log and try contentWindow for current URL * Clean up zoom code, document cross-origin limitation * Add postMessage bridge for route preservation on zoom - Frontend posts route changes to parent window via postMessage - Desktop client listens and stores current route - Zoom reload now preserves the current page * Apply desktop zoom to terminal font size xterm.js uses pixel-based fonts, so it needs explicit zoom scaling * Add native desktop notifications via postMessage bridge - Frontend posts notifications to parent window when in iframe - Desktop client receives and shows native OS notifications via Neutralino * Add Neutralinojs desktop client app Lightweight (~2MB) desktop wrapper that connects to a local or remote Vibora server. Uses system webview via Neutralinojs. Includes packaging scripts for Linux AppImage and macOS DMG. * Use settings.json instead of separate desktop-settings.json * Fix: use Neutralino.os.getEnv for home directory * Remove remaining NL_USER reference from console.log * Add zoom keyboard shortcuts (Cmd/Ctrl +/-/0) * Add floating zoom controls (hover bottom-right corner) * Navigate directly to Vibora app instead of iframe Enables native webview zoom (Cmd+/-) with sharp text rendering. Removes custom zoom controls since native zoom works better. * Restore iframe with floating zoom controls Native webview zoom doesn't work via keyboard shortcuts, so using iframe with CSS transform zoom and floating controls instead. * Use CSS zoom instead of transform scale for sharper rendering * Persist zoom level to settings * Revert to transform scale zoom with floating buttons * Implement native zoom via root font-size Desktop client passes zoom level as query parameter (?zoom=1.25). Frontend reads this and sets html font-size (e.g., 20px for 125%). Since shadcn/ui uses rem units, all UI scales natively and sharply. * Preserve current page when changing zoom level * Add zoom debugging logs * Use Neutralino.debug.log and try contentWindow for current URL * Clean up zoom code, document cross-origin limitation * Add postMessage bridge for route preservation on zoom - Frontend posts route changes to parent window via postMessage - Desktop client listens and stores current route - Zoom reload now preserves the current page * Apply desktop zoom to terminal font size xterm.js uses pixel-based fonts, so it needs explicit zoom scaling * Add native desktop notifications via postMessage bridge - Frontend posts notifications to parent window when in iframe - Desktop client receives and shows native OS notifications via Neutralino * Add desktop:dev command for development mode - Add DEV_PORT constant (5173) for Vite dev server - Detect --dev flag via NL_ARGS in Neutralino - Use dev port when --dev flag is present - Add desktop:dev mise task that passes --dev to neu run * Add pre-generated icon.icns for macOS notifications - Generate icon.icns from icon.png for macOS app bundle - Update package-dmg.sh to use pre-generated icns if available - Packaged .app will show Vibora icon in notifications * Fix DMG packaging to use resources.neu bundle * Fix macOS app bundle: resources.neu must be in MacOS folder * Add bundled server and Claude plugin to desktop app - Add desktop:bundle task to bundle server, PTY libs, migrations, and plugin - Create vibora-launcher.sh that: - Checks for bun/dtach dependencies with helpful error dialogs - Installs Claude plugin to ~/.claude/plugins/vibora/ - Starts bundled server before launching Neutralino UI - Cleans up server on exit - Update package-dmg.sh to depend on bundle and include server - Desktop app now runs standalone with bundled backend * Add desktop app releases to GitHub Actions with auto-update support - Multi-platform builds: macOS (arm64/x64), Linux (x64) - AppImage now bundles server like DMG does - Update manifest.json generated for each release - Desktop app checks for updates on startup (daily) - Version sync enforcement via check:version task - Pre-commit hook blocks commits with mismatched versions - README updated: desktop app as recommended installation - Remote server setup documentation added * Add desktop build artifacts to .gitignore * Add plain English license summary to README Clarify that users can use Vibora for personal or commercial purposes, we have no claim over software they build with it, and only prohibit reselling Vibora itself for profit. * Remove text remnants from icon images Crop snake head cleanly from logo without the tops of "VIBORA" letters showing as white lines at the bottom. * Rename hostname to remoteHost and simplify desktop connection flow - Rename `hostname` setting to `remoteHost` for clarity (backward compatible) - Simplify desktop app: always start local server, prompt if remote configured - Update VSCode URL builder, hooks, settings UI, and locale strings - Add migration in settings.ts to read old `hostname` if `remoteHost` not set - Update README and desktop/README with new connection flow docs * Compile desktop server as standalone executable - Use `bun build --compile` to create a self-contained binary - No longer requires Bun to be installed on end-user machines - Only runtime dependency is dtach for terminal persistence - Add common paths to launcher PATH for GUI app compatibility * bump major * Remove unused xtermReady state variable * Fix WebKit terminal detection using initial terminal IDs snapshot Replace unreliable weCreatedTerminalRef with initialTerminalIdsRef approach that captures terminal IDs when terminals first load. A terminal not in this initial set is considered "new" and triggers Claude startup commands. This is more robust against WebKit timing issues during navigation. * Ensure clean builds by removing dist/ and forcing tsc rebuild Prevents stale cached artifacts from contaminating production builds. * Fix race condition by capturing terminal IDs during render Move initial terminal ID capture from useEffect to synchronous render-time code. Effects run after render and could interleave with async terminal list updates, causing the capture to include newly created terminals. Running during render ensures IDs are captured before any effects (including createTerminal) execute. * Fix task terminal startup race condition in React Strict Mode - Move synchronous guards before async operations to prevent duplicate startup command execution when React Strict Mode re-runs effects - Add debug logging gated behind VITE_VIBORA_DEBUG env var - Enable debug logging by default in dev mode (mise run dev) - Add /api/debug endpoint for frontend-to-server logging * Fix monitoring features for macOS compatibility - Memory metrics: Use vm_stat to calculate used/cache/free memory properly - Disk usage: Query /System/Volumes/Data for accurate APFS usage - Top processes: Add getTotalMemory() helper using sysctl hw.memsize - Claude instances: Use ps -Axo for dtach socket detection - Process cwd: Improve lsof parsing for working directory - Process start time: Add ps -o lstart= fallback - OAuth token: Add macOS Keychain lookup via security command All monitoring endpoints now work correctly on macOS instead of failing due to Linux-specific /proc filesystem assumptions. * Fix terminal/tab deduplication and task terminal navigation - Add state-level deduplication for terminals and tabs to prevent duplicates from WebSocket broadcasts to multiple clients - Add createdByMeRef tracking in TaskTerminal to properly detect when THIS component created a terminal (vs another instance) - Reset terminal tracking refs when cwd changes to fix blank screen when navigating between tasks - Add pending creation guards to prevent duplicate terminals/tabs from double-clicks or React Strict Mode - Add debug logging for terminal operations (gated behind VITE_VIBORA_DEBUG) * Add centralized JSONL logging system for AI-friendly debugging - Create shared logger types (LogEntry, Logger interface) in shared/logger/ - Add backend logger (server/lib/logger.ts) that writes JSON lines to stdout and ~/.vibora/vibora.log - Add frontend logger (src/lib/logger.ts) with batching to /api/logs endpoint - Migrate all console.log/error/warn calls across backend and frontend to use structured logger - Add /api/logs endpoint for frontend log batching, keep legacy /api/debug for compatibility - Update CLAUDE.md with logging documentation and search examples - Add debug build tasks in mise.toml for LOG_LEVEL=debug builds The JSONL format enables efficient AI searching with grep/jq while preserving structured context for each log entry. * Fix task terminal blank screen race condition in desktop app When creating task terminals, start() and attach() were called in rapid succession (within 16ms). The start() method spawned `dtach -n` which exits immediately after creating the socket, but was storing the PTY reference in this.pty. When attach() was called before dtach -n exited, it would return early due to `if (this.pty) return`, preventing the actual attachment via `dtach -a`. The fix: start() now uses a local `creationPty` variable instead of setting this.pty, so attach() always proceeds to spawn `dtach -a` and set up the proper data handlers. Also includes enhanced diagnostic logging for terminal output flow.
1 parent 1a8dca5 commit d97639c

60 files changed

Lines changed: 3900 additions & 299 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"name": "vibora",
99
"source": "./plugins/vibora",
1010
"description": "Task orchestration for Claude Code",
11-
"version": "1.15.0"
11+
"version": "2.0.0"
1212
}
1313
]
1414
}

.github/workflows/publish.yml

Lines changed: 153 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ jobs:
1212
permissions:
1313
contents: write
1414
id-token: write # Required for OIDC
15+
outputs:
16+
version: ${{ steps.version.outputs.version }}
17+
tag: ${{ steps.version.outputs.tag }}
18+
should_release: ${{ steps.check_tag.outputs.exists == 'false' }}
1519
steps:
1620
- name: Checkout
1721
uses: actions/checkout@v4
@@ -53,6 +57,10 @@ jobs:
5357
if: steps.check_tag.outputs.exists == 'false'
5458
run: bun install
5559

60+
- name: Verify version sync
61+
if: steps.check_tag.outputs.exists == 'false'
62+
run: mise run check:version
63+
5664
- name: Build CLI
5765
if: steps.check_tag.outputs.exists == 'false'
5866
run: mise run cli:build
@@ -61,6 +69,8 @@ jobs:
6169
if: steps.check_tag.outputs.exists == 'false'
6270
working-directory: cli
6371
run: npm publish --access public --provenance
72+
env:
73+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
6474

6575
- name: Create and push tag
6676
if: steps.check_tag.outputs.exists == 'false'
@@ -84,19 +94,161 @@ jobs:
8494
echo "$CHANGELOG" >> $GITHUB_OUTPUT
8595
echo "EOF" >> $GITHUB_OUTPUT
8696
87-
- name: Create GitHub Release
97+
- name: Create GitHub Release (Draft)
8898
if: steps.check_tag.outputs.exists == 'false'
8999
uses: softprops/action-gh-release@v2
90100
with:
91101
tag_name: ${{ steps.version.outputs.tag }}
92102
name: ${{ steps.version.outputs.tag }}
103+
draft: true
93104
body: |
94105
## Changes
95106
96107
${{ steps.changelog.outputs.changelog }}
97108
98109
## Installation
99110
111+
### Desktop App (Recommended)
112+
113+
Download the appropriate package for your platform:
114+
- **macOS Apple Silicon**: `Vibora-${{ steps.version.outputs.version }}-macos-arm64.dmg`
115+
- **macOS Intel**: `Vibora-${{ steps.version.outputs.version }}-macos-x64.dmg`
116+
- **Linux**: `Vibora-${{ steps.version.outputs.version }}-linux-x64.AppImage`
117+
118+
### CLI
119+
100120
```bash
101121
npm install -g vibora
102122
```
123+
124+
# Desktop builds - run in parallel on multiple platforms
125+
build-desktop:
126+
needs: publish
127+
if: needs.publish.outputs.should_release == 'true'
128+
strategy:
129+
fail-fast: false
130+
matrix:
131+
include:
132+
# macOS Apple Silicon
133+
- os: macos-latest
134+
platform: macos
135+
arch: arm64
136+
artifact: Vibora-${{ needs.publish.outputs.version }}-macos-arm64.dmg
137+
# macOS Intel
138+
- os: macos-13
139+
platform: macos
140+
arch: x64
141+
artifact: Vibora-${{ needs.publish.outputs.version }}-macos-x64.dmg
142+
# Linux x64
143+
- os: ubuntu-latest
144+
platform: linux
145+
arch: x64
146+
artifact: Vibora-${{ needs.publish.outputs.version }}-linux-x64.AppImage
147+
148+
runs-on: ${{ matrix.os }}
149+
permissions:
150+
contents: write
151+
152+
steps:
153+
- name: Checkout
154+
uses: actions/checkout@v4
155+
156+
- name: Setup Bun
157+
uses: oven-sh/setup-bun@v2
158+
159+
- name: Setup Node.js
160+
uses: actions/setup-node@v4
161+
with:
162+
node-version: '24'
163+
164+
- name: Setup mise
165+
uses: jdx/mise-action@v2
166+
167+
- name: Install dependencies
168+
run: bun install
169+
170+
- name: Install create-dmg (macOS)
171+
if: matrix.platform == 'macos'
172+
run: brew install create-dmg
173+
174+
- name: Install Linux dependencies
175+
if: matrix.platform == 'linux'
176+
run: |
177+
sudo apt-get update
178+
sudo apt-get install -y libfuse2
179+
180+
- name: Build Neutralino app
181+
run: mise run desktop:build
182+
183+
- name: Bundle server
184+
run: mise run desktop:bundle
185+
186+
- name: Package DMG (macOS)
187+
if: matrix.platform == 'macos'
188+
run: ./desktop/scripts/package-dmg.sh ${{ matrix.arch }}
189+
190+
- name: Package AppImage (Linux)
191+
if: matrix.platform == 'linux'
192+
run: ./desktop/scripts/package-appimage.sh ${{ matrix.arch }}
193+
194+
- name: Upload artifact
195+
uses: actions/upload-artifact@v4
196+
with:
197+
name: desktop-${{ matrix.platform }}-${{ matrix.arch }}
198+
path: desktop/dist/${{ matrix.artifact }}
199+
200+
# Finalize release with all desktop builds
201+
finalize-release:
202+
needs: [publish, build-desktop]
203+
if: needs.publish.outputs.should_release == 'true'
204+
runs-on: ubuntu-latest
205+
permissions:
206+
contents: write
207+
208+
steps:
209+
- name: Download all desktop artifacts
210+
uses: actions/download-artifact@v4
211+
with:
212+
path: artifacts
213+
pattern: desktop-*
214+
215+
- name: Generate update manifest
216+
run: |
217+
VERSION="${{ needs.publish.outputs.version }}"
218+
TAG="${{ needs.publish.outputs.tag }}"
219+
REPO="${{ github.repository }}"
220+
221+
cat > manifest.json << EOF
222+
{
223+
"applicationId": "io.vibora.desktop",
224+
"version": "${VERSION}",
225+
"platforms": {
226+
"darwin-arm64": {
227+
"url": "https://github.com/${REPO}/releases/download/${TAG}/Vibora-${VERSION}-macos-arm64.dmg"
228+
},
229+
"darwin-x64": {
230+
"url": "https://github.com/${REPO}/releases/download/${TAG}/Vibora-${VERSION}-macos-x64.dmg"
231+
},
232+
"linux-x64": {
233+
"url": "https://github.com/${REPO}/releases/download/${TAG}/Vibora-${VERSION}-linux-x64.AppImage"
234+
}
235+
}
236+
}
237+
EOF
238+
239+
echo "Generated manifest.json:"
240+
cat manifest.json
241+
242+
- name: Flatten artifacts
243+
run: |
244+
mkdir -p release-assets
245+
find artifacts -type f \( -name "*.dmg" -o -name "*.AppImage" \) -exec cp {} release-assets/ \;
246+
cp manifest.json release-assets/
247+
ls -la release-assets/
248+
249+
- name: Upload assets to release
250+
uses: softprops/action-gh-release@v2
251+
with:
252+
tag_name: ${{ needs.publish.outputs.tag }}
253+
draft: false
254+
files: release-assets/*

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,10 @@ cli/bin/
4040
cli/README.md
4141
cli/LICENSE
4242

43+
# Desktop build artifacts (generated by mise run desktop:*)
44+
desktop/bundle/
45+
desktop/resources/js/neutralino.js
46+
desktop/resources/js/neutralino.d.ts
47+
4348
# Claude
4449
CLAUDE.local.md

CLAUDE.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,84 @@ When a task status changes in Vibora, the linked Linear ticket status is updated
146146
- `DONE` → "Done"
147147
- `CANCELED` → "Canceled"
148148

149+
## Logging
150+
151+
Vibora uses a centralized JSON Lines (JSONL) logging system optimized for AI analysis and debugging.
152+
153+
### Log Format
154+
155+
Each log entry is a single JSON line:
156+
```json
157+
{"ts":"2024-12-24T15:30:00.123Z","lvl":"info","src":"PTYManager","msg":"Restored terminal","ctx":{"terminalId":"abc-123","name":"Terminal 1"}}
158+
```
159+
160+
### Log Locations
161+
162+
| Platform | Location |
163+
|----------|----------|
164+
| Development | stdout (terminal) |
165+
| Production daemon | `~/.vibora/server.log` (stdout) + `~/.vibora/vibora.log` |
166+
| Desktop app | `~/.vibora/vibora.log` |
167+
168+
### Log Levels
169+
170+
| Level | Use Case |
171+
|-------|----------|
172+
| `debug` | Detailed diagnostics (message payloads, state changes) |
173+
| `info` | Normal operations (terminal created, server started) |
174+
| `warn` | Recoverable issues (retry, fallback used) |
175+
| `error` | Failures needing attention |
176+
177+
### Environment Variables
178+
179+
| Variable | Default | Description |
180+
|----------|---------|-------------|
181+
| `LOG_LEVEL` | `info` | Backend minimum log level |
182+
| `VITE_LOG_LEVEL` | `info` | Frontend minimum log level |
183+
| `DEBUG` | `0` | Enable frontend debug logging (console + server) |
184+
185+
### Using the Logger
186+
187+
**Backend** (`server/lib/logger.ts`):
188+
```typescript
189+
import { log } from '../lib/logger'
190+
191+
log.pty.info('Restored terminal', { terminalId: id, name })
192+
log.ws.error('Connection failed', { error: String(err) })
193+
```
194+
195+
**Frontend** (`src/lib/logger.ts`):
196+
```typescript
197+
import { log } from '@/lib/logger'
198+
199+
log.taskTerminal.debug('cwd changed', { cwd })
200+
log.ws.info('terminal:created', { terminalId, isNew: true })
201+
```
202+
203+
### Searching Logs
204+
205+
```bash
206+
# Find all errors
207+
grep '"lvl":"error"' ~/.vibora/vibora.log
208+
209+
# Find logs for specific terminal
210+
grep '"terminalId":"abc-123"' ~/.vibora/vibora.log
211+
212+
# Find PTYManager issues
213+
grep '"src":"PTYManager"' ~/.vibora/vibora.log
214+
215+
# Pretty print with jq
216+
cat ~/.vibora/vibora.log | jq 'select(.lvl == "error")'
217+
```
218+
219+
### Debug Build
220+
221+
Build with debug logging enabled:
222+
```bash
223+
mise run desktop:package-dmg:debug # DMG with debug logging
224+
mise run build:debug # Web build with debug logging
225+
```
226+
149227
### Development vs Production
150228

151229
The `mise run dev` command defaults to `~/.vibora/dev` (port 3222) to keep development data separate from production:

README.md

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,36 @@ The Vibe Engineer's Cockpit. A terminal-first tool for orchestrating AI coding a
1414

1515
## Quick Start
1616

17-
Requires [Node.js](https://nodejs.org/) and [Claude Code](https://claude.ai/code).
17+
Requires [Bun](https://bun.sh/) and [Claude Code](https://claude.ai/code).
1818

19-
### Install via curl (recommended)
19+
### Desktop App (Recommended)
20+
21+
Download the latest release for your platform from [GitHub Releases](https://github.com/knowsuchagency/vibora/releases/latest):
22+
23+
- **macOS Apple Silicon**: `Vibora-X.X.X-macos-arm64.dmg`
24+
- **macOS Intel**: `Vibora-X.X.X-macos-x64.dmg`
25+
- **Linux**: `Vibora-X.X.X-linux-x64.AppImage`
26+
27+
The desktop app bundles everything—just install and run. It will:
28+
- Start the Vibora server automatically
29+
- Install the Claude Code plugin
30+
- Check for updates on startup
31+
32+
> **macOS note**: On first launch, right-click the app and select "Open" to bypass Gatekeeper.
33+
34+
### Web Application (Alternative)
35+
36+
Run Vibora as a web server if you prefer browser access or need remote server deployment.
37+
38+
#### Install via curl
2039

2140
```bash
2241
curl -fsSL https://raw.githubusercontent.com/knowsuchagency/vibora/main/install.sh | bash
2342
```
2443

2544
This installs vibora, the Claude Code plugin, and starts the server.
2645

27-
### Install via npm
46+
#### Install via npm
2847

2948
```bash
3049
npx vibora@latest up
@@ -37,16 +56,40 @@ claude plugin marketplace add knowsuchagency/vibora
3756
claude plugin install vibora@vibora --scope user
3857
```
3958

40-
### Server Commands
59+
Open http://localhost:3333 in your browser.
60+
61+
### Remote Server Setup
62+
63+
Run the backend on a remote server and connect from the desktop app or browser:
64+
65+
1. **On the remote server:**
66+
```bash
67+
# Install and start
68+
npx vibora@latest up
69+
70+
# Configure for remote access
71+
vibora config set remoteHost your-server.example.com
72+
vibora config set basicAuthUsername admin
73+
vibora config set basicAuthPassword your-secure-password
74+
```
75+
76+
2. **Connect from desktop app:**
77+
- Launch the app
78+
- Click "Connect to Remote" (if local server not found)
79+
- Enter the server URL: `your-server.example.com:3333`
80+
- Enter credentials when prompted
81+
82+
3. **Or access via browser:**
83+
Open `http://your-server.example.com:3333`
84+
85+
### Server Commands (Web Application)
4186

4287
```bash
4388
vibora up # Start server daemon
4489
vibora down # Stop the server
4590
vibora status # Check if running
4691
```
4792

48-
Open http://localhost:3333 in your browser.
49-
5093
## Configuration
5194

5295
Settings are stored in `.vibora/settings.json`. The vibora directory is resolved in this order:
@@ -59,7 +102,7 @@ Settings are stored in `.vibora/settings.json`. The vibora directory is resolved
59102
|---------|---------|---------|
60103
| port | `PORT` | 3333 |
61104
| defaultGitReposDir | `VIBORA_GIT_REPOS_DIR` | ~ |
62-
| hostname | `VIBORA_HOSTNAME` | (empty) |
105+
| remoteHost | `VIBORA_REMOTE_HOST` | (empty) |
63106
| sshPort | `VIBORA_SSH_PORT` | 22 |
64107
| basicAuthUsername | `VIBORA_BASIC_AUTH_USERNAME` | null |
65108
| basicAuthPassword | `VIBORA_BASIC_AUTH_PASSWORD` | null |
@@ -189,3 +232,5 @@ See [DEVELOPMENT.md](DEVELOPMENT.md) for development setup, architecture, and co
189232
## License
190233

191234
[PolyForm Shield 1.0.0](LICENSE)
235+
236+
**In plain English:** You can use Vibora for any purpose—personal or commercial. KNOWSUCHAGENCY CORP has no claim over the software you build using Vibora. What's prohibited is reselling or redistributing Vibora itself for profit. The software is provided as-is with no warranty.

0 commit comments

Comments
 (0)