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
6 changes: 6 additions & 0 deletions Packages/CrowCore/Sources/CrowCore/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ public final class AppState {
/// controlled from claude.ai / the Claude mobile app. Mirrors `AppConfig.remoteControlEnabled`.
public var remoteControlEnabled: Bool = false

/// Whether the Manager terminal launches with `--permission-mode auto` so it
/// can run `crow`, `gh`, and `git` commands without per-call approval.
/// Mirrors `AppConfig.managerAutoPermissionMode`. Applies only to the Manager
/// launch; worker sessions and CLI-spawned terminals are unaffected.
public var managerAutoPermissionMode: Bool = true

/// Terminal IDs whose Claude Code was launched with `--rc` — drives the
/// per-session indicator badge. Survives toggle changes so existing sessions
/// keep showing the badge until they're restarted.
Expand Down
35 changes: 26 additions & 9 deletions Packages/CrowCore/Sources/CrowCore/ClaudeLaunchArgs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,33 @@ public enum ClaudeLaunchArgs {
"'" + s.replacingOccurrences(of: "'", with: "'\\''") + "'"
}

/// Flags to append after the `claude` binary path when remote control is enabled.
/// Flags to append after the `claude` binary path.
///
/// Returns a string beginning with a leading space — e.g. `" --rc --name 'Manager'"`
/// — or an empty string when `remoteControl` is `false`. The `--name` flag makes the
/// session show up with a recognizable label in claude.ai's Remote Control panel.
public static func argsSuffix(remoteControl: Bool, sessionName: String?) -> String {
guard remoteControl else { return "" }
var s = " --rc"
if let name = sessionName, !name.isEmpty {
s += " --name \(shellQuote(name))"
/// Returns a string beginning with a leading space — e.g.
/// `" --permission-mode auto --rc --name 'Manager'"` — or an empty string
/// when neither option is enabled. The `--name` flag makes the session show
/// up with a recognizable label in claude.ai's Remote Control panel.
///
/// - Parameters:
/// - remoteControl: Append `--rc` (and `--name …` if `sessionName` is provided).
/// - sessionName: Optional label for the remote-control panel.
/// - autoPermissionMode: Append `--permission-mode auto`. Used for the Manager
/// terminal so orchestration commands (`crow`, `gh`, `git`) can run without
/// per-call approval. Requires a supported Claude Code plan and provider.
public static func argsSuffix(
remoteControl: Bool,
sessionName: String?,
autoPermissionMode: Bool = false
) -> String {
var s = ""
if autoPermissionMode {
s += " --permission-mode auto"
}
if remoteControl {
s += " --rc"
if let name = sessionName, !name.isEmpty {
s += " --name \(shellQuote(name))"
}
}
return s
}
Expand Down
6 changes: 5 additions & 1 deletion Packages/CrowCore/Sources/CrowCore/Models/AppConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public struct AppConfig: Codable, Sendable, Equatable {
public var notifications: NotificationSettings
public var sidebar: SidebarSettings
public var remoteControlEnabled: Bool
public var managerAutoPermissionMode: Bool
public var telemetry: TelemetryConfig

public init(
Expand All @@ -19,13 +20,15 @@ public struct AppConfig: Codable, Sendable, Equatable {
notifications: NotificationSettings = NotificationSettings(),
sidebar: SidebarSettings = SidebarSettings(),
remoteControlEnabled: Bool = false,
managerAutoPermissionMode: Bool = true,
telemetry: TelemetryConfig = TelemetryConfig()
) {
self.workspaces = workspaces
self.defaults = defaults
self.notifications = notifications
self.sidebar = sidebar
self.remoteControlEnabled = remoteControlEnabled
self.managerAutoPermissionMode = managerAutoPermissionMode
self.telemetry = telemetry
}

Expand All @@ -36,11 +39,12 @@ public struct AppConfig: Codable, Sendable, Equatable {
notifications = try container.decodeIfPresent(NotificationSettings.self, forKey: .notifications) ?? NotificationSettings()
sidebar = try container.decodeIfPresent(SidebarSettings.self, forKey: .sidebar) ?? SidebarSettings()
remoteControlEnabled = try container.decodeIfPresent(Bool.self, forKey: .remoteControlEnabled) ?? false
managerAutoPermissionMode = try container.decodeIfPresent(Bool.self, forKey: .managerAutoPermissionMode) ?? true
telemetry = try container.decodeIfPresent(TelemetryConfig.self, forKey: .telemetry) ?? TelemetryConfig()
}

private enum CodingKeys: String, CodingKey {
case workspaces, defaults, notifications, sidebar, remoteControlEnabled, telemetry
case workspaces, defaults, notifications, sidebar, remoteControlEnabled, managerAutoPermissionMode, telemetry
}
}

Expand Down
23 changes: 23 additions & 0 deletions Packages/CrowCore/Tests/CrowCoreTests/AppConfigTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import Testing
#expect(config.notifications.globalMute == false)
#expect(config.sidebar.hideSessionDetails == false)
#expect(config.remoteControlEnabled == false)
#expect(config.managerAutoPermissionMode == true)
}

@Test func appConfigRemoteControlRoundTrip() throws {
Expand All @@ -50,6 +51,28 @@ import Testing
#expect(decoded.remoteControlEnabled == true)
}

@Test func appConfigManagerAutoPermissionModeRoundTrip() throws {
var config = AppConfig()
config.managerAutoPermissionMode = false

let data = try JSONEncoder().encode(config)
let decoded = try JSONDecoder().decode(AppConfig.self, from: data)
#expect(decoded.managerAutoPermissionMode == false)

config.managerAutoPermissionMode = true
let data2 = try JSONEncoder().encode(config)
let decoded2 = try JSONDecoder().decode(AppConfig.self, from: data2)
#expect(decoded2.managerAutoPermissionMode == true)
}

@Test func appConfigManagerAutoPermissionModeDefaultsTrueWhenKeyMissing() throws {
// Legacy configs without the key should opt in by default so the Manager
// benefits from auto mode without requiring users to re-save settings.
let json = #"{"workspaces": [], "remoteControlEnabled": false}"#.data(using: .utf8)!
let config = try JSONDecoder().decode(AppConfig.self, from: json)
#expect(config.managerAutoPermissionMode == true)
}

@Test func appConfigDecodeWithPartialKeys() throws {
let json = """
{"workspaces": [{"id": "00000000-0000-0000-0000-000000000001", "name": "Org", "provider": "github", "cli": "gh", "alwaysInclude": []}]}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,24 @@ import Testing
#expect(ClaudeLaunchArgs.shellQuote("with space") == "'with space'")
#expect(ClaudeLaunchArgs.shellQuote("has'quote") == "'has'\\''quote'")
}

@Test func claudeLaunchArgsAutoPermissionModeOnly() {
#expect(ClaudeLaunchArgs.argsSuffix(remoteControl: false, sessionName: nil, autoPermissionMode: true)
== " --permission-mode auto")
#expect(ClaudeLaunchArgs.argsSuffix(remoteControl: false, sessionName: "Manager", autoPermissionMode: true)
== " --permission-mode auto")
}

@Test func claudeLaunchArgsAutoPermissionModeWithRemoteControl() {
#expect(ClaudeLaunchArgs.argsSuffix(remoteControl: true, sessionName: "Manager", autoPermissionMode: true)
== " --permission-mode auto --rc --name 'Manager'")
#expect(ClaudeLaunchArgs.argsSuffix(remoteControl: true, sessionName: nil, autoPermissionMode: true)
== " --permission-mode auto --rc")
}

@Test func claudeLaunchArgsAutoPermissionModeDefaultsOff() {
// Existing callers that don't pass autoPermissionMode should be unaffected.
#expect(ClaudeLaunchArgs.argsSuffix(remoteControl: true, sessionName: "Manager")
== " --rc --name 'Manager'")
#expect(ClaudeLaunchArgs.argsSuffix(remoteControl: false, sessionName: nil) == "")
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,23 @@ import Testing
let loaded = ConfigStore.loadConfig(from: configURL)
#expect(loaded != nil)
#expect(loaded?.remoteControlEnabled == false)
// Legacy configs should opt in to auto permission mode by default so the
// Manager benefits without requiring users to re-save settings.
#expect(loaded?.managerAutoPermissionMode == true)
}

@Test func configStoreManagerAutoPermissionModeRoundTrip() throws {
let tmpDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
let claudeDir = tmpDir.appendingPathComponent(".claude", isDirectory: true)
defer { try? FileManager.default.removeItem(at: tmpDir) }

var config = AppConfig()
config.managerAutoPermissionMode = false
try ConfigStore.saveConfig(config, to: claudeDir)

let configURL = claudeDir.appendingPathComponent("config.json")
let loaded = ConfigStore.loadConfig(from: configURL)
#expect(loaded?.managerAutoPermissionMode == false)
}

@Test func configStoreLoadMissingFileReturnsNil() {
Expand Down
8 changes: 8 additions & 0 deletions Packages/CrowUI/Sources/CrowUI/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,14 @@ public struct SettingsView: View {
.foregroundStyle(.secondary)
}

Section("Manager Terminal") {
Toggle("Launch in auto permission mode", isOn: $config.managerAutoPermissionMode)
.onChange(of: config.managerAutoPermissionMode) { _, _ in save() }
Text("Passes --permission-mode auto so the Manager can run crow, gh, and git commands without per-call approval. Requires Claude Code 2.1.83+ on a Max, Team, Enterprise, or API plan with the Anthropic provider. Turn off if your account reports auto mode as unavailable. Takes effect on next app launch.")
.font(.caption)
.foregroundStyle(.secondary)
}

Section("Telemetry") {
Toggle("Enable session analytics", isOn: $config.telemetry.enabled)
.onChange(of: config.telemetry.enabled) { _, _ in save() }
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ On first launch, a setup wizard guides you through choosing your development roo
### The Sidebar

- **Tickets** — Assigned issues grouped by project board status (Backlog, Ready, In Progress, In Review, Done in last 24h). Click a status to filter.
- **Manager** — A persistent Claude Code terminal for orchestrating work. Use `/crow-workspace` here to create new sessions.
- **Manager** — A persistent Claude Code terminal for orchestrating work. Use `/crow-workspace` here to create new sessions. Launches in `--permission-mode auto` by default so orchestration commands (`crow`, `gh`, `git`) run without per-call approval; opt out via Settings → General → Manager Terminal.
- **Active Sessions** — One per work context. Shows repo, branch, issue/PR badges with pipeline and review status.
- **Completed Sessions** — Sessions whose PRs have been merged or issues closed.

Expand Down
2 changes: 2 additions & 0 deletions Sources/Crow/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
// set before hydrateState so the Manager terminal's stored command can
// be rebuilt to include (or drop) `--rc` before its surface is pre-initialized.
appState.remoteControlEnabled = config.remoteControlEnabled
appState.managerAutoPermissionMode = config.managerAutoPermissionMode

// Create session service and hydrate state
let service = SessionService(store: store, appState: appState, telemetryPort: config.telemetry.enabled ? config.telemetry.port : nil)
Expand Down Expand Up @@ -443,6 +444,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
notificationManager?.updateSettings(config.notifications)
appState.hideSessionDetails = config.sidebar.hideSessionDetails
appState.remoteControlEnabled = config.remoteControlEnabled
appState.managerAutoPermissionMode = config.managerAutoPermissionMode
}

// MARK: - Socket Server
Expand Down
19 changes: 14 additions & 5 deletions Sources/Crow/App/SessionService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,17 @@ final class SessionService {
}
} else {
// Manager terminal: rebuild its claude command to match the
// current remoteControlEnabled preference. Unlike worker sessions
// the Manager launches claude directly as the shell command, so
// the stored string needs to be correct before preInitialize runs.
// current remoteControlEnabled and managerAutoPermissionMode
// preferences. Unlike worker sessions the Manager launches
// claude directly as the shell command, so the stored string
// needs to be correct before preInitialize runs.
let claudePath = Self.findClaudeBinary() ?? "claude"
let rcEnabled = appState.remoteControlEnabled
let autoMode = appState.managerAutoPermissionMode
let managerCommand = claudePath + ClaudeLaunchArgs.argsSuffix(
remoteControl: rcEnabled, sessionName: "Manager"
remoteControl: rcEnabled,
sessionName: "Manager",
autoPermissionMode: autoMode
)
for i in terminals.indices {
if let cmd = terminals[i].command, cmd.contains("claude") {
Expand Down Expand Up @@ -261,7 +265,12 @@ final class SessionService {
// Find the real claude binary (skip CMUX wrapper)
let claudePath = Self.findClaudeBinary() ?? "claude"
let rcEnabled = appState.remoteControlEnabled
let managerCommand = claudePath + ClaudeLaunchArgs.argsSuffix(remoteControl: rcEnabled, sessionName: "Manager")
let autoMode = appState.managerAutoPermissionMode
let managerCommand = claudePath + ClaudeLaunchArgs.argsSuffix(
remoteControl: rcEnabled,
sessionName: "Manager",
autoPermissionMode: autoMode
)
let terminal = SessionTerminal(
sessionID: managerID,
name: "Manager",
Expand Down
9 changes: 9 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ All persistent state lives under `~/Library/Application Support/crow/` (see `Pac
- **`branchPrefix`** — used by the `/crow-workspace` skill when creating new branches.
- **`excludeDirs`** — ignored when scanning repos for git worktrees.

## Manager Terminal

The Manager tab runs Claude Code at the dev root and drives workspace orchestration. Its behavior is controlled by these top-level keys in `{devRoot}/.claude/config.json`:

- **`managerAutoPermissionMode`** (default: `true`) — passes `--permission-mode auto` to the Manager's `claude` launch so it can run `crow`, `gh`, and `git` commands without per-call approval. Requires Claude Code **v2.1.83+**, a **Max / Team / Enterprise / API** plan, the **Anthropic** API provider (not Bedrock / Vertex / Foundry), and a supported model (**Sonnet 4.6**, **Opus 4.6**, or **Opus 4.7**). On Team/Enterprise plans an admin must enable auto mode in Claude Code admin settings. Turn this off via **Settings → General → Manager Terminal** if your account reports auto mode as unavailable. Worker sessions and CLI-spawned terminals are unaffected by this setting.
- **`remoteControlEnabled`** (default: `false`) — launches new Claude Code sessions with `--rc` so you can control them from claude.ai or the Claude mobile app.

Changes take effect on next app launch — the Manager's stored command is rebuilt on hydration.

## Directory Structure

Crow expects repositories organized under workspace folders:
Expand Down
Loading