Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b4e85ea
Improve desktop app: dictation fix, scroll-to-bottom, delete sessions…
will-osborne Mar 25, 2026
6a8e57a
Load MCP servers from native backend config, fix interrupt during age…
will-osborne Mar 25, 2026
3619bd3
Fix MCP server config format for ACP protocol compatibility
will-osborne Mar 25, 2026
2ca9b41
Fix MCP env format: use {name, value} objects instead of array pairs
will-osborne Mar 25, 2026
024c947
Fix Copilot MCP: use CLI flag instead of ACP mcpServers parameter
will-osborne Mar 25, 2026
79414a8
Add unread session indicator and fix notifications
will-osborne Mar 26, 2026
e74eade
Fix model/toggle changes resetting session state and spurious respawns
will-osborne Mar 26, 2026
cf96289
Fix CI: apt-get update before cross-compiler install
will-osborne Mar 26, 2026
f2bde1f
feat: desktop app v2 — LLM features, run analytics, and UI polish
will-osborne Mar 27, 2026
a00b211
Fix layout cutoff, debrief UI, and brew desktop app install
will-osborne Mar 27, 2026
053dbd5
Fix desktop resource URL version interpolation in brew formula
will-osborne Mar 27, 2026
286fde5
Fix brew desktop app install: wrap .app in parent dir before zipping
will-osborne Mar 27, 2026
d4b079b
Fix brew post_install: copy .app instead of symlinking
will-osborne Mar 27, 2026
a381704
Fix macOS notifications: set delegate in applicationDidFinishLaunching
will-osborne Mar 27, 2026
bb0cd7b
Fix command palette keyboard navigation
will-osborne Mar 27, 2026
0450e76
Add markdown rendering to debrief card
will-osborne Mar 27, 2026
d747540
Fix brew post_install: use ditto to update app bundle in-place
will-osborne Mar 27, 2026
f33f74e
feat: file history panel with per-run diff viewer
will-osborne Mar 27, 2026
3bfe8f0
Fix Swift build: add RunRecord/RunHistoryView to xcodeproj, fix theme.bg
will-osborne Mar 27, 2026
3be4874
Fix theme.bg in RunHistoryView — should be theme.panel
will-osborne Mar 27, 2026
8fb71d1
Fix post_install ditto EPERM: chmod -R u+w before removing old app bu…
will-osborne Mar 30, 2026
3c98ec1
Fix nil pointer crash on session restore: initialize RunHistory
will-osborne Mar 30, 2026
446420f
Fix file history tracking and desktop notifications
will-osborne Mar 30, 2026
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
11 changes: 9 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ jobs:

- name: Install cross-compiler and cmake (Linux ARM64)
if: matrix.goos == 'linux' && matrix.goarch == 'arm64'
run: sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu cmake
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu cmake

- name: Build whisper-cpp from source (Linux AMD64)
if: matrix.goos == 'linux' && matrix.goarch == 'amd64'
Expand Down Expand Up @@ -223,7 +225,9 @@ jobs:
- name: Package app
run: |
APP_PATH="build/Build/Products/Release/Orbitor.app"
ditto -c -k --keepParent "$APP_PATH" orbitor-desktop-macos.zip
mkdir -p /tmp/orbitor-desktop
cp -R "$APP_PATH" /tmp/orbitor-desktop/
ditto -c -k --keepParent /tmp/orbitor-desktop orbitor-desktop-macos.zip

- name: Upload desktop artifact
uses: actions/upload-artifact@v4
Expand Down Expand Up @@ -272,6 +276,7 @@ jobs:
SHA_DARWIN_AMD64=$(grep 'orbitor-darwin-amd64' dist/checksums.txt | awk '{print $1}')
SHA_LINUX_ARM64=$(grep 'orbitor-linux-arm64' dist/checksums.txt | awk '{print $1}')
SHA_LINUX_AMD64=$(grep 'orbitor-linux-amd64' dist/checksums.txt | awk '{print $1}')
SHA_DESKTOP_MACOS=$(grep 'orbitor-desktop-macos.zip' dist/checksums.txt | awk '{print $1}')

# Strip leading 'v' from tag for the formula version field
SEMVER="${VERSION#v}"
Expand All @@ -283,10 +288,12 @@ jobs:

sed \
-e "s|version \".*\"|version \"${SEMVER}\"|" \
-e "s|/releases/download/v[0-9][^/]*/orbitor-desktop|/releases/download/v${SEMVER}/orbitor-desktop|" \
-e "s|PLACEHOLDER_DARWIN_ARM64_SHA256|${SHA_DARWIN_ARM64}|" \
-e "s|PLACEHOLDER_DARWIN_AMD64_SHA256|${SHA_DARWIN_AMD64}|" \
-e "s|PLACEHOLDER_LINUX_ARM64_SHA256|${SHA_LINUX_ARM64}|" \
-e "s|PLACEHOLDER_LINUX_AMD64_SHA256|${SHA_LINUX_AMD64}|" \
-e "s|PLACEHOLDER_DESKTOP_MACOS_SHA256|${SHA_DESKTOP_MACOS}|" \
Formula/orbitor.rb > /tmp/tap/Formula/orbitor.rb

cd /tmp/tap
Expand Down
35 changes: 33 additions & 2 deletions Formula/orbitor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ class Orbitor < Formula
url "https://github.com/will-osborne/orbitor/releases/download/v#{version}/orbitor-darwin-amd64"
sha256 "PLACEHOLDER_DARWIN_AMD64_SHA256"
end

resource "desktop" do
url "https://github.com/will-osborne/orbitor/releases/download/v0.1.0/orbitor-desktop-macos.zip"
sha256 "PLACEHOLDER_DESKTOP_MACOS_SHA256"
end
end

on_linux do
Expand All @@ -27,13 +32,39 @@ class Orbitor < Formula

def install
bin.install Dir["orbitor-*"].first => "orbitor"

if OS.mac?
resource("desktop").stage do
prefix.install "Orbitor.app"
end
end
end

def post_install
(Pathname.new(ENV["HOME"]) / ".orbitor").mkpath
# Restart the background service after upgrade so the new binary is used.
# quiet_system avoids errors when the service isn't running yet.
quiet_system "brew", "services", "restart", "orbitor"

if OS.mac?
user_apps = Pathname.new(ENV["HOME"]) / "Applications"
user_apps.mkpath
app_dest = user_apps / "Orbitor.app"
app_src = opt_prefix / "Orbitor.app"
if app_dest.exist?
# macOS 13+ sets com.apple.provenance on app bundles; remove it so
# the bundle can be deleted. On macOS 15+ (Darwin 24+), ~/Applications
# is TCC-protected and brew's subprocess may lack permission — fall back
# to a manual-install instruction in that case.
quiet_system "xattr", "-d", "com.apple.provenance", app_dest.to_s
quiet_system "rm", "-rf", app_dest.to_s
end
unless quiet_system("ditto", app_src.to_s, app_dest.to_s)
opoo "Could not update #{app_dest.basename} automatically (macOS permission restriction)."
opoo "Run this from your terminal to update it:"
opoo " ditto #{app_src} #{app_dest}"
end
end
end

service do
Expand All @@ -50,12 +81,12 @@ def caveats
orbitor setup

To start the server as a background service:
orbitor service install
or via Homebrew services:
brew services start orbitor

Open the TUI:
orbitor

The macOS desktop app has been installed to ~/Applications/Orbitor.app.
EOS
end

Expand Down
9 changes: 6 additions & 3 deletions acp.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,12 @@ func (c *ACPClient) Initialize() (*InitializeResult, error) {
return &result, nil
}

// SessionNew creates a new ACP session.
func (c *ACPClient) SessionNew(cwd string) (string, error) {
resp, err := c.Call("session/new", SessionNewParams{CWD: cwd, MCPServers: []any{}})
// SessionNew creates a new ACP session with optional MCP server configurations.
func (c *ACPClient) SessionNew(cwd string, mcpServers []any) (string, error) {
if mcpServers == nil {
mcpServers = []any{}
}
resp, err := c.Call("session/new", SessionNewParams{CWD: cwd, MCPServers: mcpServers})
if err != nil {
return "", err
}
Expand Down
110 changes: 110 additions & 0 deletions client_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,116 @@ func ClientConfigPath() (string, error) {
return filepath.Join(home, ".orbitor", "config.json"), nil
}

// LoadMCPServers reads MCP server definitions from the backend's native config
// file and returns them as a slice suitable for the ACP session/new mcpServers
// parameter. Returns an empty slice (never nil) if no servers are configured.
//
// Claude Code: ~/.claude.json → { "mcpServers": { "name": { ... } } }
// Copilot CLI: ~/.copilot/mcp-config.json → { "mcpServers": { "name": { ... } } }
//
// Both also support a project-local .mcp.json in the working directory.
func LoadMCPServers(backend, workingDir string) []any {
home, err := os.UserHomeDir()
if err != nil {
return []any{}
}

// Determine config file paths to read (global + project-local).
var paths []string
switch backend {
case "claude":
paths = append(paths, filepath.Join(home, ".claude.json"))
case "copilot":
paths = append(paths, filepath.Join(home, ".copilot", "mcp-config.json"))
}
if workingDir != "" {
paths = append(paths, filepath.Join(workingDir, ".mcp.json"))
}

// Merge servers from all config files (later files override earlier ones).
merged := map[string]json.RawMessage{}
for _, p := range paths {
data, err := os.ReadFile(p)
if err != nil {
continue
}
var cfg struct {
MCPServers map[string]json.RawMessage `json:"mcpServers"`
}
if json.Unmarshal(data, &cfg) != nil || cfg.MCPServers == nil {
continue
}
for name, server := range cfg.MCPServers {
merged[name] = server
}
}

if len(merged) == 0 {
return []any{}
}

// Convert the name→config map into the ACP array format.
// ACP expects a different schema than the native config files:
// - headers: array of [key, value] pairs (not an object)
// - env: array of [key, value] pairs (not an object)
// - type "local" → "stdio"
// - Extra fields (source, sourcePath, tools) are stripped.
var servers []any
for name, raw := range merged {
var obj map[string]any
if json.Unmarshal(raw, &obj) != nil {
continue
}
obj["name"] = name

// Normalize type: "local" is an alias for "stdio" in some configs.
if t, ok := obj["type"].(string); ok && t == "local" {
obj["type"] = "stdio"
}

// Convert headers object → array of [key, value] pairs.
if h, ok := obj["headers"].(map[string]any); ok {
pairs := make([]any, 0, len(h))
for k, v := range h {
pairs = append(pairs, []any{k, v})
}
obj["headers"] = pairs
} else if obj["headers"] == nil {
obj["headers"] = []any{}
}

// Convert env object → array of {name, value} objects.
if e, ok := obj["env"].(map[string]any); ok {
entries := make([]any, 0, len(e))
for k, v := range e {
entries = append(entries, map[string]any{"name": k, "value": v})
}
obj["env"] = entries
}

// Ensure required fields for stdio servers.
if t, _ := obj["type"].(string); t == "stdio" {
if obj["args"] == nil {
obj["args"] = []any{}
}
if obj["env"] == nil {
obj["env"] = []any{}
}
}

// Strip fields that are not part of the ACP schema.
delete(obj, "source")
delete(obj, "sourcePath")
delete(obj, "tools")

servers = append(servers, obj)
}
if servers == nil {
return []any{}
}
return servers
}

// LoadClientConfig reads ~/.orbitor/config.json and merges it with built-in
// defaults. Missing or unreadable config silently falls back to defaults.
func LoadClientConfig() ClientConfig {
Expand Down
24 changes: 24 additions & 0 deletions desktop/Orbitor/Orbitor.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
A1B2C3D4E5F60001000A0012 /* SessionListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60002000A0012 /* SessionListView.swift */; };
A1B2C3D4E5F60001000A0013 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60002000A0013 /* Assets.xcassets */; };
A656BEB21F4D28778B492BC9 /* Speech.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 47063F531C80C12E9B85DACC /* Speech.framework */; };
A1B2C3D4E5F60001000A0016 /* DiffView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60002000A0016 /* DiffView.swift */; };
A1B2C3D4E5F60001000A0017 /* CommandPaletteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60002000A0017 /* CommandPaletteView.swift */; };
A1B2C3D4E5F60001000A0018 /* RunRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60002000A0018 /* RunRecord.swift */; };
A1B2C3D4E5F60001000A0019 /* RunHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60002000A0019 /* RunHistoryView.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand Down Expand Up @@ -56,6 +60,10 @@
A1B2C3D4E5F60002000A0013 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
A1B2C3D4E5F60002000A0014 /* Orbitor.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Orbitor.entitlements; sourceTree = "<group>"; };
A1B2C3D4E5F60002000A0015 /* HoverEffects.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoverEffects.swift; sourceTree = "<group>"; };
A1B2C3D4E5F60002000A0016 /* DiffView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffView.swift; sourceTree = "<group>"; };
A1B2C3D4E5F60002000A0017 /* CommandPaletteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteView.swift; sourceTree = "<group>"; };
A1B2C3D4E5F60002000A0018 /* RunRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunRecord.swift; sourceTree = "<group>"; };
A1B2C3D4E5F60002000A0019 /* RunHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunHistoryView.swift; sourceTree = "<group>"; };
A1B2C3D4E5F60003000A0001 /* Orbitor.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Orbitor.app; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -101,6 +109,7 @@
children = (
A1B2C3D4E5F60002000A0002 /* SessionInfo.swift */,
A1B2C3D4E5F60002000A0003 /* WSMessage.swift */,
A1B2C3D4E5F60002000A0018 /* RunRecord.swift */,
);
path = Models;
sourceTree = "<group>";
Expand Down Expand Up @@ -138,6 +147,7 @@
children = (
A1B2C3D4E5F60002000A000A /* ContentView.swift */,
A1B2C3D4E5F60005000A0008 /* Chat */,
A1B2C3D4E5F60005000A000C /* CommandPalette */,
A1B2C3D4E5F60005000A0009 /* Inspector */,
A1B2C3D4E5F60005000A000A /* Shared */,
A1B2C3D4E5F60005000A000B /* Sidebar */,
Expand All @@ -159,6 +169,7 @@
isa = PBXGroup;
children = (
A1B2C3D4E5F60002000A000E /* InspectorView.swift */,
A1B2C3D4E5F60002000A0019 /* RunHistoryView.swift */,
);
path = Inspector;
sourceTree = "<group>";
Expand All @@ -167,12 +178,21 @@
isa = PBXGroup;
children = (
A1B2C3D4E5F60002000A000F /* CodeBlockView.swift */,
A1B2C3D4E5F60002000A0016 /* DiffView.swift */,
A1B2C3D4E5F60002000A0015 /* HoverEffects.swift */,
A1B2C3D4E5F60002000A0010 /* StatusBadge.swift */,
);
path = Shared;
sourceTree = "<group>";
};
A1B2C3D4E5F60005000A000C /* CommandPalette */ = {
isa = PBXGroup;
children = (
A1B2C3D4E5F60002000A0017 /* CommandPaletteView.swift */,
);
path = CommandPalette;
sourceTree = "<group>";
};
A1B2C3D4E5F60005000A000B /* Sidebar */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -287,6 +307,10 @@
A1B2C3D4E5F60001000A0011 /* NewSessionSheet.swift in Sources */,
A1B2C3D4E5F60001000A0012 /* SessionListView.swift in Sources */,
143763597DA538F7889F8A20 /* DictationState.swift in Sources */,
A1B2C3D4E5F60001000A0016 /* DiffView.swift in Sources */,
A1B2C3D4E5F60001000A0017 /* CommandPaletteView.swift in Sources */,
A1B2C3D4E5F60001000A0018 /* RunRecord.swift in Sources */,
A1B2C3D4E5F60001000A0019 /* RunHistoryView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
18 changes: 18 additions & 0 deletions desktop/Orbitor/Orbitor/Models/RunRecord.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Foundation

struct RunRecord: Codable, Identifiable {
let id: String
let prompt: String
let startedAt: Date
let completedAt: Date?
let files: [FileChange]
}

struct FileChange: Codable, Identifiable {
let path: String
let relativePath: String
let before: String
let after: String

var id: String { path }
}
8 changes: 6 additions & 2 deletions desktop/Orbitor/Orbitor/Models/WSMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ enum ChatMessage: Identifiable {
case error(id: UUID, message: String, timestamp: Date)
/// Bulk history load — all messages arrive at once to avoid per-message re-renders.
case historyBatch(id: UUID, messages: [ChatMessage], timestamp: Date)
/// Session status change from the server (e.g. "respawning").
case sessionStatus(id: UUID, status: String, timestamp: Date)

var id: UUID {
switch self {
Expand All @@ -35,7 +37,8 @@ enum ChatMessage: Identifiable {
.runComplete(let id, _, _),
.interrupted(let id, _),
.error(let id, _, _),
.historyBatch(let id, _, _):
.historyBatch(let id, _, _),
.sessionStatus(let id, _, _):
return id
}
}
Expand All @@ -51,7 +54,8 @@ enum ChatMessage: Identifiable {
.runComplete(_, _, let t),
.interrupted(_, let t),
.error(_, _, let t),
.historyBatch(_, _, let t):
.historyBatch(_, _, let t),
.sessionStatus(_, _, let t):
return t
}
}
Expand Down
Loading
Loading