Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,10 @@ The Tauri v2 permission model is defined in `src-tauri/capabilities/desktop-main

## Configuration files

- `~/.leanspec/desktop-config.json` — Desktop app configuration and project registry
- `~/.lean-spec/desktop.json` — Desktop app configuration (window size, shortcuts, theme, etc.)
- `~/.lean-spec/projects.json` — Project registry

The config file is automatically created on first launch and updated as you add/remove projects.
These config files are automatically created on first launch and updated as you add/remove projects.

## Features

Expand Down
48 changes: 41 additions & 7 deletions packages/desktop/src-tauri/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ use serde::{Deserialize, Serialize};
static CONFIG: Lazy<RwLock<DesktopConfig>> = Lazy::new(|| RwLock::new(DesktopConfig::load_or_default()));

const CONFIG_DIR: &str = ".lean-spec";
const CONFIG_FILE: &str = "desktop.yaml";
const CONFIG_FILE: &str = "desktop.json";
const LEGACY_CONFIG_FILE: &str = "desktop.yaml";

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
Expand Down Expand Up @@ -98,26 +99,39 @@ impl Default for DesktopConfig {
impl DesktopConfig {
fn load_or_default() -> Self {
let path = config_file_path();

// Try JSON first
match fs::read_to_string(&path) {
Ok(raw) => match serde_yaml::from_str::<DesktopConfig>(&raw) {
Ok(raw) => match serde_json::from_str::<DesktopConfig>(&raw) {
Ok(mut config) => {
normalize_config(&mut config);
config
return config;
}
Err(error) => {
eprintln!("Failed to parse desktop config: {error}");
Self::default()
eprintln!("Failed to parse desktop config as JSON: {error}");
eprintln!("Will attempt migration from legacy YAML format");
}
},
Err(_) => Self::default(),
Err(_) => {}
}

// Migration: Try legacy YAML
if let Some(legacy_config) = load_legacy_yaml() {
eprintln!("Migrating desktop config from YAML to JSON format");
legacy_config.persist(); // Save as JSON
backup_legacy_yaml();
eprintln!("Migration complete: desktop.yaml → desktop.json");
return legacy_config;
}

Self::default()
}

fn persist(&self) {
if let Some(dir) = config_dir() {
if fs::create_dir_all(&dir).is_ok() {
let file = dir.join(CONFIG_FILE);
if let Ok(serialized) = serde_yaml::to_string(self) {
if let Ok(serialized) = serde_json::to_string_pretty(self) {
if let Err(error) = fs::write(file, serialized) {
eprintln!("Unable to write desktop config: {error}");
}
Expand All @@ -137,6 +151,26 @@ fn normalize_config(config: &mut DesktopConfig) {
}
}

fn load_legacy_yaml() -> Option<DesktopConfig> {
let dir = config_dir()?;
let path = dir.join(LEGACY_CONFIG_FILE);
let raw = fs::read_to_string(path).ok()?;
let mut config: DesktopConfig = serde_yaml::from_str(&raw).ok()?;
normalize_config(&mut config);
Some(config)
}

fn backup_legacy_yaml() {
if let Some(dir) = config_dir() {
let legacy = dir.join(LEGACY_CONFIG_FILE);
let backup = dir.join("desktop.yaml.bak");
match fs::rename(&legacy, &backup) {
Ok(_) => eprintln!("Legacy config backed up: desktop.yaml.bak"),
Err(error) => eprintln!("Failed to backup legacy config: {error}"),
}
}
}

pub fn config_dir() -> Option<PathBuf> {
home_dir().map(|home| home.join(CONFIG_DIR))
}
Expand Down
64 changes: 33 additions & 31 deletions specs/148-leanspec-desktop-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ The current web-based UI (`lean-spec ui`) requires:
## Implementation Summary (Dec 10, 2025)

- **New package:** `packages/desktop` ships a Vite-powered chrome plus a Rust/Tauri backend. The shell embeds the existing Next.js UI by launching its standalone server in the background (dev uses `pnpm --filter @leanspec/ui dev`, production bundles `.next/standalone`).
- **Windowing:** Frameless window with custom title bar + native controls, backed by `tauri-plugin-window-state` for automatic persistence and close-to-tray behavior (configurable via `desktop.yaml`).
- **Windowing:** Frameless window with custom title bar + native controls, backed by `tauri-plugin-window-state` for automatic persistence and close-to-tray behavior (configurable via `desktop.json`).
- **Project registry:** Rust port of the project registry keeps `~/.lean-spec/projects.json` in sync, validates folders, and exposes commands for refresh/add/switch. Config-driven active project switches restart the embedded UI with the right `SPECS_DIR`.
- **Tray + shortcuts:** Dedicated modules (`tray.rs`, `shortcuts.rs`) manage recent-project menus, quick actions (open, add, refresh, check for updates), and global shortcuts (`Cmd/Ctrl+Shift+L/K/N`). Frontend listeners open the project switcher or project picker when shortcuts fire.
- **Notifications + updater:** Desktop emits OS notifications on project changes and wires a `desktop_check_updates` command to the Tauri updater so tray actions can trigger update checks. Auto-update channels (`stable`/`beta`) live in `desktop.yaml`.
- **Notifications + updater:** Desktop emits OS notifications on project changes and wires a `desktop_check_updates` command to the Tauri updater so tray actions can trigger update checks. Auto-update channels (`stable`/`beta`) live in `desktop.json`.
- **Documentation:** Root `README.md` and `packages/desktop/README.md` describe the desktop workflow. A helper script (`pnpm prepare:ui`) copies the Next standalone build so `pnpm build:desktop` produces platform bundles.

### Developer Workflow
Expand Down Expand Up @@ -237,33 +237,35 @@ The desktop app wraps `@leanspec/ui` with minimal changes:

### Configuration

**Desktop-Specific Config (~/.lean-spec/desktop.yaml):**
```yaml
window:
width: 1400
height: 900
x: 100
y: 100
maximized: false

behavior:
startMinimized: false
minimizeToTray: true
launchAtLogin: false

shortcuts:
global:
toggleWindow: "CommandOrControl+Shift+L"
quickSwitcher: "CommandOrControl+Shift+K"
newSpec: "CommandOrControl+Shift+N"

updates:
autoCheck: true
autoInstall: false
channel: "stable" # or "beta"

appearance:
theme: "system" # "light", "dark", or "system"
**Desktop-Specific Config (~/.lean-spec/desktop.json):**
```json
{
"window": {
"width": 1400,
"height": 900,
"x": 100,
"y": 100,
"maximized": false
},
"behavior": {
"startMinimized": false,
"minimizeToTray": true,
"launchAtLogin": false
},
"shortcuts": {
"toggleWindow": "CommandOrControl+Shift+L",
"quickSwitcher": "CommandOrControl+Shift+K",
"newSpec": "CommandOrControl+Shift+N"
},
"updates": {
"autoCheck": true,
"autoInstall": false,
"channel": "stable"
},
"appearance": {
"theme": "system"
}
}
```

### Distribution
Expand Down Expand Up @@ -325,7 +327,7 @@ appearance:

**Day 10: Notifications**
- [x] Implement native OS notifications (project add/switch events)
- [ ] Add notification preferences _(desktop.yaml toggle TBD)_
- [ ] Add notification preferences _(desktop.json toggle TBD)_
- [ ] Test on all platforms

### Phase 3: Polish & Distribution (Week 3)
Expand All @@ -334,7 +336,7 @@ appearance:
- [x] Configure Tauri updater (endpoints + channel config)
- [ ] Set up update server (GitHub Releases)
- [x] Implement update UI hooks (tray action + command)
- [x] Add update channel selection (stable/beta via `desktop.yaml`)
- [x] Add update channel selection (stable/beta via `desktop.json`)
- [ ] Test update flow end-to-end _(requires release infra)_

**Day 13-14: Build & Release**
Expand Down
11 changes: 8 additions & 3 deletions specs/162-desktop-config-json-migration/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
status: planned
status: complete
created: '2025-12-10'
tags:
- desktop
Expand All @@ -12,12 +12,17 @@ created_at: '2025-12-10T08:49:08.237Z'
depends_on:
- 147-json-config-format
- 148-leanspec-desktop-app
updated_at: '2025-12-10T08:49:08.287Z'
updated_at: '2025-12-18T09:56:48.390Z'
completed_at: '2025-12-18T09:56:48.390Z'
completed: '2025-12-18'
transitions:
- status: complete
at: '2025-12-18T09:56:48.390Z'
---

# Migrate Desktop Config from YAML to JSON

> **Status**: 🗓️ Planned · **Priority**: High · **Created**: 2025-12-10 · **Tags**: desktop, config, migration, breaking-change, consistency
> **Status**: ✅ Complete · **Priority**: High · **Created**: 2025-12-10 · **Tags**: desktop, config, migration, breaking-change, consistency

## Overview

Expand Down
154 changes: 78 additions & 76 deletions specs/168-leanspec-orchestration-platform/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -520,82 +520,84 @@ Continue to testing? [Y/n]: y

## Configuration

### Desktop Config (~/.lean-spec/desktop.yaml)

```yaml
orchestration:
defaultAgent: claude
guidedMode: true # Pause between phases
autoValidate: true # Run validation after implementation
autoComplete: false # Require manual completion

agents:
claude:
enabled: true
priority: 1
models:
default: claude-3-5-sonnet-20241022
fast: claude-3-5-haiku-20241022
copilot:
enabled: true
priority: 2
cursor:
enabled: false
aider:
enabled: true
priority: 3

agentRelay:
endpoint: "http://localhost:8080"
apiKey: "${AGENT_RELAY_API_KEY}"
timeout: 300000 # 5 minutes
retryAttempts: 3
retryDelay: 5000 # 5 seconds

devlog:
endpoint: "http://localhost:9090"
apiKey: "${DEVLOG_API_KEY}"
enabled: true
batchSize: 10 # Batch telemetry events
flushInterval: 5000 # Flush every 5 seconds

validation:
autoRun: true
runTests: true
runLinters: true
runTypeCheck: true
aiReview: true

# Test runner
testCommand: "npm test"
testCoverage: true

# Linter
linterCommand: "npm run lint"
linterIgnore: ["*.test.ts", "*.spec.ts"]

# Type checker
typeCheckCommand: "tsc --noEmit"

notifications:
phaseComplete: true
sessionComplete: true
validationFailed: true
validationPassed: false # Only notify on failure

shortcuts:
implement: "CommandOrControl+Shift+I"
validate: "CommandOrControl+Shift+V"
newSpec: "CommandOrControl+Shift+N"
quickSwitcher: "CommandOrControl+Shift+K"

ui:
theme: "system" # light, dark, or system
outputFontSize: 13
outputFontFamily: "Menlo, Monaco, 'Courier New', monospace"
animatePhaseProgress: true
showTokenCount: true
showDuration: true
### Desktop Config (~/.lean-spec/desktop.json)

```json
{
"orchestration": {
"defaultAgent": "claude",
"guidedMode": true,
"autoValidate": true,
"autoComplete": false
},
"agents": {
"claude": {
"enabled": true,
"priority": 1,
"models": {
"default": "claude-3-5-sonnet-20241022",
"fast": "claude-3-5-haiku-20241022"
}
},
"copilot": {
"enabled": true,
"priority": 2
},
"cursor": {
"enabled": false
},
"aider": {
"enabled": true,
"priority": 3
}
},
"agentRelay": {
"endpoint": "http://localhost:8080",
"apiKey": "${AGENT_RELAY_API_KEY}",
"timeout": 300000,
"retryAttempts": 3,
"retryDelay": 5000
},
"devlog": {
"endpoint": "http://localhost:9090",
"apiKey": "${DEVLOG_API_KEY}",
"enabled": true,
"batchSize": 10,
"flushInterval": 5000
},
"validation": {
"autoRun": true,
"runTests": true,
"runLinters": true,
"runTypeCheck": true,
"aiReview": true,
"testCommand": "npm test",
"testCoverage": true,
"linterCommand": "npm run lint",
"linterIgnore": ["*.test.ts", "*.spec.ts"],
"typeCheckCommand": "tsc --noEmit"
},
"notifications": {
"phaseComplete": true,
"sessionComplete": true,
"validationFailed": true,
"validationPassed": false
},
"shortcuts": {
"implement": "CommandOrControl+Shift+I",
"validate": "CommandOrControl+Shift+V",
"newSpec": "CommandOrControl+Shift+N",
"quickSwitcher": "CommandOrControl+Shift+K"
},
"ui": {
"theme": "system",
"outputFontSize": 13,
"outputFontFamily": "Menlo, Monaco, 'Courier New', monospace",
"animatePhaseProgress": true,
"showTokenCount": true,
"showDuration": true
}
}
```

### Project-Specific Config (.leanspec/config.yaml)
Expand Down
Loading