diff --git a/docs-ai/063-agent-workflows/000-plan.md b/docs-ai/063-agent-workflows/000-plan.md index 1600f4f4..e7af7060 100644 --- a/docs-ai/063-agent-workflows/000-plan.md +++ b/docs-ai/063-agent-workflows/000-plan.md @@ -564,4 +564,4 @@ attaches hooks through A2's launch boundary. ## Amendments -(append `- Updated 2026-MM-DD: ... — see [00N-topic.md](00N-topic.md)` lines here) +- Updated 2026-08-22: Shipped C0 with the Agents sidebar group, Profiles page, and Command Line Tool page; Workflows remains deferred to D1 — see [001-settings-agents-group.md](001-settings-agents-group.md). diff --git a/docs-ai/063-agent-workflows/001-settings-agents-group.md b/docs-ai/063-agent-workflows/001-settings-agents-group.md new file mode 100644 index 00000000..755d35dd --- /dev/null +++ b/docs-ai/063-agent-workflows/001-settings-agents-group.md @@ -0,0 +1,30 @@ +# 063.001 — Settings Agents Group (C0) + +## Context + +Release R1 starts by establishing the Settings information architecture that later workflow UI will extend. The existing Agent Profiles page was a standalone “Agents” sidebar item, while the `prowl` installation controls lived under Advanced. + +## Change + +- Settings now has an `Agents` sidebar group with `Profiles` and `Command Line Tool` pages. +- The existing profile list is titled `Profiles`; `openAgentProfilesSettings` and “Manage Agent Profiles…” continue to select it. +- The Command Line Tool page owns the existing install/status controls and also exposes the active Unix socket path and the existing agent-help prompt. +- Advanced now contains only analytics, crash reporting, and terminal-layout controls. +- The Workflows page remains absent until 063-D1, as planned. + +## Current state + +The selection boundary is represented by `SettingsSection.profiles` and `.commandLineTool`. Profile editor state is initialized only for the Profiles page and is cleared when another page is selected. CLI installation behavior remains owned by `SettingsFeature`; C0 only moves its Settings presentation. + +## Verification + +- Focused Settings selection and CLI-install suites: 11 tests passed. +- `make check` passed. +- `make test` verified 2,366 tests with zero failures; five dependency-scan warnings remain pre-existing. +- `make build-app` passed with zero warnings and errors. +- An isolated Debug app was navigated through Settings to Profiles and Command Line Tool; both rendered with the expected native window titles and ordering. + +## Refs + +- Slice: 063-C0 +- Branch: `feat/settings-agents-group` diff --git a/docs/components/agent-profiles.md b/docs/components/agent-profiles.md index 7e0c5f34..701f480d 100644 --- a/docs/components/agent-profiles.md +++ b/docs/components/agent-profiles.md @@ -32,7 +32,8 @@ respawn. row shows the profile name with the runtime name trailing. Rows for runtimes that look unavailable are dimmed with a warning but stay clickable — availability signals can be wrong, so they never block a launch. "Manage - Agent Profiles…" opens Settings → Agents. When launch rows exist the capsule + Agent Profiles…" opens Settings → Agents → Profiles. When launch rows exist + the capsule carries a trailing **quick-launch segment** (a `play.circle` split button): one click launches the Recommended profile directly, skipping the popover. - **Command Palette** (`⌘P`) — "Launch Agent: " rows dispatch the exact @@ -52,8 +53,8 @@ launched" memory behind the Recommended resolution. ## Managing profiles -Open **Settings → Agents** to see the ordered profile list. Click a profile to -push its editor; the native Back control returns to the list while the Settings +Open **Settings → Agents → Profiles** to see the ordered profile list. Click a +profile to push its editor; the native Back control returns to the list while the Settings sidebar remains available. Adding a profile opens the same editor immediately. Changing another Settings sidebar section leaves the editor and opens that section's root. diff --git a/docs/components/cli.md b/docs/components/cli.md index d2b7fbcd..85de5524 100644 --- a/docs/components/cli.md +++ b/docs/components/cli.md @@ -22,9 +22,10 @@ inside a repo, and not for how-to questions about Prowl's settings. ## Install -From the app: **Settings → Advanced → Install Command Line Tool**, or Command +From the app: **Settings → Agents → Command Line Tool → Install**, or Command Palette → "Install Command Line Tool". This symlinks `prowl` into -`/usr/local/bin` (prompting for admin if needed). +`/usr/local/bin` (prompting for admin if needed). The Settings page also shows +the active local socket path used by the CLI. ## Global options diff --git a/docs/components/settings.md b/docs/components/settings.md index aaddf133..fe6e4ffd 100644 --- a/docs/components/settings.md +++ b/docs/components/settings.md @@ -16,10 +16,10 @@ Settings brings the existing Settings window forward rather than creating another one. `⌘W` closes it. Most sections are roots in the detail pane. A section can drill in without -losing the sidebar; for example, **Agents** → a profile pushes its editor and -macOS shows the standard Back control beside the editor title. Back returns to -the Agents list, while selecting another sidebar item leaves the drill-in and -opens that section's root. +losing the sidebar; for example, **Agents → Profiles** → a profile pushes its +editor and macOS shows the standard Back control beside the editor title. Back +returns to Profiles, while selecting another sidebar item leaves the drill-in +and opens that section's root. ## Sections @@ -30,10 +30,11 @@ opens that section's root. | **Shortcuts** | Remap app keyboard shortcuts; view defaults; resolve conflicts. → [keyboard-shortcuts](../reference/keyboard-shortcuts.md) | | **Worktree** | Worktree creation/deletion defaults: prompt on create, fetch before create, base directory, copy ignored/untracked files, automatic local-branch cleanup, merged-worktree action, archived auto-delete period. | | **Updates** | Auto-check toggle, "Check for Updates Now". → [updates](updates.md) | -| **Advanced** | Analytics, crash reports, restore terminal layout on launch (experimental) + clear saved layout, and the **Install Command Line Tool** (`prowl` CLI) action. | +| **Advanced** | Analytics, crash reports, restore terminal layout on launch (experimental) + clear saved layout. | | **GitHub** | Enable GitHub integration (uses the `gh` CLI). → [github-pull-requests](github-pull-requests.md) | | **Commands** | Global Custom Commands. Enabled commands appear in the window toolbar; each repo can independently hide a Global command. → [custom-actions](custom-actions.md) | -| **Agents** | Agent Profiles: named launch presets for Claude Code/Codex (model, effort, execution mode, tab/split placement, extra arguments, opt-in dedicated home for a separate account) with a live launch preview. List order is the recommendation fallback. → [agent-profiles](agent-profiles.md) | +| **Agents → Profiles** | Named launch presets for supported agent runtimes (model, effort, execution mode, tab/split placement, extra arguments, opt-in dedicated home for a separate account) with a live launch preview. List order is the recommendation fallback. → [agent-profiles](agent-profiles.md) | +| **Agents → Command Line Tool** | Install/status for the bundled `prowl` CLI, its local socket path, and an “Ask Agent About Prowl…” prompt entry. → [cli](cli.md) | | **Repositories / Repo Settings** | Per-repository: setup/archive/run scripts, **Custom Commands**, Global-command visibility, **Default Agent Profile**, default base ref & directory, copy-files overrides, open-with app, custom title, icon & color, PR merge strategy, line-diff & PR-state fetching. Reached from the sidebar context menu → "Repo Settings". → [custom-actions](custom-actions.md), [repositories-and-worktrees](repositories-and-worktrees.md) | ## Where settings live on disk @@ -48,7 +49,7 @@ Legacy `~/.supacode` is migrated to `~/.prowl` on first launch. ## Install the CLI from here -**Advanced → Install Command Line Tool** symlinks `prowl` into `/usr/local/bin` +**Agents → Command Line Tool → Install** symlinks `prowl` into `/usr/local/bin` (prompting for admin rights if needed). Also available via Command Palette → "Install Command Line Tool". See [cli](cli.md). diff --git a/supacode/Features/App/Reducer/AppFeature.swift b/supacode/Features/App/Reducer/AppFeature.swift index 65059c30..f653bc61 100644 --- a/supacode/Features/App/Reducer/AppFeature.swift +++ b/supacode/Features/App/Reducer/AppFeature.swift @@ -380,7 +380,7 @@ struct AppFeature { state.settings.repositorySettings = nil state.settings.globalCustomCommands = .init() state.settings.agentProfiles = nil - case .agents: + case .profiles: state.settings.repositorySettings = nil state.settings.globalCustomCommands = nil state.settings.agentProfiles = .init() @@ -407,7 +407,7 @@ struct AppFeature { state.settings.repositorySettings = repoSettingsState state.settings.globalCustomCommands = nil state.settings.agentProfiles = nil - case .general, .notifications, .shortcuts, .worktree, .updates, .advanced, .github: + case .general, .notifications, .shortcuts, .worktree, .updates, .advanced, .github, .commandLineTool: state.settings.repositorySettings = nil state.settings.globalCustomCommands = nil state.settings.agentProfiles = nil @@ -715,7 +715,7 @@ struct AppFeature { return launchAgentProfile(profileID, state: &state) case .openAgentProfilesSettings: - return openSettingsEffect(selecting: .agents) + return openSettingsEffect(selecting: .profiles) case .runCustomCommand(let commandID): guard let worktree = actionTargetWorktree(repositories: state.repositories) else { diff --git a/supacode/Features/Settings/Views/AdvancedSettingsView.swift b/supacode/Features/Settings/Views/AdvancedSettingsView.swift index 4c19d398..d5e8c869 100644 --- a/supacode/Features/Settings/Views/AdvancedSettingsView.swift +++ b/supacode/Features/Settings/Views/AdvancedSettingsView.swift @@ -7,62 +7,6 @@ struct AdvancedSettingsView: View { var body: some View { VStack(alignment: .leading) { Form { - Section("Command Line Tool") { - VStack(alignment: .leading, spacing: 8) { - HStack(spacing: 6) { - switch store.cliInstallStatus { - case .installed(let path): - Image(systemName: "checkmark.circle.fill") - .foregroundStyle(.green) - .accessibilityLabel("Installed") - Text("Installed at \(path)") - case .installedDifferentSource(let path): - Image(systemName: "exclamationmark.triangle.fill") - .foregroundStyle(.yellow) - .accessibilityLabel("Different version") - Text("A different version exists at \(path)") - case .notInstalled: - Image(systemName: "xmark.circle") - .foregroundStyle(.secondary) - .accessibilityLabel("Not installed") - Text("Not installed") - } - } - .font(.callout) - - Text("Install the prowl command to control Prowl from the terminal.") - .foregroundStyle(.secondary) - .font(.callout) - - HStack(spacing: 8) { - switch store.cliInstallStatus { - case .notInstalled: - Button("Install") { - store.send(.installCLIButtonTapped()) - } - .help("Install prowl command line tool to /usr/local/bin") - .buttonStyle(.bordered) - case .installed: - Button("Uninstall") { - store.send(.uninstallCLIButtonTapped) - } - .help("Remove prowl command line tool from /usr/local/bin") - .buttonStyle(.bordered) - case .installedDifferentSource: - Button("Reinstall") { - store.send(.installCLIButtonTapped()) - } - .help("Replace the existing prowl command with the version bundled in this app") - .buttonStyle(.bordered) - } - } - } - .frame(maxWidth: .infinity, alignment: .leading) - .onAppear { - store.send(.refreshCLIInstallStatus) - } - } - Section("Advanced") { VStack(alignment: .leading) { Toggle( diff --git a/supacode/Features/Settings/Views/AgentProfilesSettingsView.swift b/supacode/Features/Settings/Views/AgentProfilesSettingsView.swift index 357e5d20..a673ce9b 100644 --- a/supacode/Features/Settings/Views/AgentProfilesSettingsView.swift +++ b/supacode/Features/Settings/Views/AgentProfilesSettingsView.swift @@ -1,7 +1,7 @@ import ComposableArchitecture import SwiftUI -/// Settings → Agents: the profile list, with a native drill-in editor page per +/// Settings → Agents → Profiles: the profile list, with a native drill-in editor page per /// profile. `NavigationStack` is driven by TCA's `StackState`, so the system /// Back control writes its pop directly to the reducer-owned route. /// List order is the recommendation fallback order. @@ -14,7 +14,7 @@ struct AgentProfilesSettingsView: View { profileListSection } .formStyle(.grouped) - .navigationTitle("Agents") + .navigationTitle("Profiles") .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .task { store.send(.task) } } destination: { editorStore in diff --git a/supacode/Features/Settings/Views/CommandLineToolSettingsView.swift b/supacode/Features/Settings/Views/CommandLineToolSettingsView.swift new file mode 100644 index 00000000..dec87af7 --- /dev/null +++ b/supacode/Features/Settings/Views/CommandLineToolSettingsView.swift @@ -0,0 +1,103 @@ +import ComposableArchitecture +import SwiftUI + +struct CommandLineToolSettingsView: View { + @Bindable var store: StoreOf + @State private var isAskAgentHelpPresented = false + + var body: some View { + Form { + Section("Installation") { + VStack(alignment: .leading, spacing: 8) { + HStack(spacing: 6) { + switch store.cliInstallStatus { + case .installed(let path): + Image(systemName: "checkmark.circle.fill") + .foregroundStyle(.green) + .accessibilityLabel("Installed") + Text("Installed at \(path)") + case .installedDifferentSource(let path): + Image(systemName: "exclamationmark.triangle.fill") + .foregroundStyle(.yellow) + .accessibilityLabel("Different version") + Text("A different version exists at \(path)") + case .notInstalled: + Image(systemName: "xmark.circle") + .foregroundStyle(.secondary) + .accessibilityLabel("Not installed") + Text("Not installed") + } + } + .font(.callout) + + Text("Install the prowl command to let terminals and coding agents control Prowl.") + .foregroundStyle(.secondary) + .font(.callout) + + HStack(spacing: 8) { + switch store.cliInstallStatus { + case .notInstalled: + Button("Install") { + store.send(.installCLIButtonTapped()) + } + .help("Install prowl command line tool to /usr/local/bin") + .buttonStyle(.bordered) + case .installed: + Button("Uninstall") { + store.send(.uninstallCLIButtonTapped) + } + .help("Remove prowl command line tool from /usr/local/bin") + .buttonStyle(.bordered) + case .installedDifferentSource: + Button("Reinstall") { + store.send(.installCLIButtonTapped()) + } + .help("Replace the existing prowl command with the version bundled in this app") + .buttonStyle(.bordered) + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .onAppear { + store.send(.refreshCLIInstallStatus) + } + } + + Section("Connection") { + LabeledContent("Socket") { + Text(ProwlSocket.defaultPath) + .font(.callout.monospaced()) + .foregroundStyle(.secondary) + .textSelection(.enabled) + .lineLimit(1) + .truncationMode(.middle) + } + + Text("prowl connects to the running app through this local Unix socket.") + .foregroundStyle(.secondary) + .font(.callout) + } + + Section("Ask Your Agent") { + VStack(alignment: .leading, spacing: 8) { + Text("Give your coding agent a prompt that points it at Prowl's bundled documentation.") + .foregroundStyle(.secondary) + .font(.callout) + Button("Ask Agent About Prowl…") { + isAskAgentHelpPresented = true + } + .help("Copy a prompt that points your coding agent at Prowl's bundled docs") + .buttonStyle(.bordered) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + } + .formStyle(.grouped) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .sheet(isPresented: $isAskAgentHelpPresented) { + AskAgentHelpView { + isAskAgentHelpPresented = false + } + } + } +} diff --git a/supacode/Features/Settings/Views/SettingsSection.swift b/supacode/Features/Settings/Views/SettingsSection.swift index 2e81144b..230b9bf6 100644 --- a/supacode/Features/Settings/Views/SettingsSection.swift +++ b/supacode/Features/Settings/Views/SettingsSection.swift @@ -9,6 +9,7 @@ enum SettingsSection: Hashable { case advanced case github case customCommands - case agents + case profiles + case commandLineTool case repository(Repository.ID) } diff --git a/supacode/Features/Settings/Views/SettingsView.swift b/supacode/Features/Settings/Views/SettingsView.swift index 21e50ece..3fd4a5d5 100644 --- a/supacode/Features/Settings/Views/SettingsView.swift +++ b/supacode/Features/Settings/Views/SettingsView.swift @@ -36,8 +36,13 @@ struct SettingsView: View { .tag(SettingsSection.github) Label("Commands", systemImage: "globe") .tag(SettingsSection.customCommands) - Label("Agents", systemImage: "sparkles") - .tag(SettingsSection.agents) + + Section("Agents") { + Label("Profiles", systemImage: "person.crop.circle") + .tag(SettingsSection.profiles) + Label("Command Line Tool", systemImage: "terminal") + .tag(SettingsSection.commandLineTool) + } Section("Repositories") { ForEach(repositories) { repository in @@ -102,7 +107,7 @@ struct SettingsView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) } } - case .agents: + case .profiles: SettingsDetailView { if let agentProfilesStore = settingsStore.scope( state: \.agentProfiles, @@ -114,6 +119,11 @@ struct SettingsView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) } } + case .commandLineTool: + SettingsDetailView { + CommandLineToolSettingsView(store: settingsStore) + .navigationTitle("Command Line Tool") + } case .repository(let repositoryID): if let repository = repositories[id: repositoryID] { SettingsDetailView { diff --git a/supacodeTests/AppFeatureSettingsSelectionTests.swift b/supacodeTests/AppFeatureSettingsSelectionTests.swift index 98ed021b..3792b848 100644 --- a/supacodeTests/AppFeatureSettingsSelectionTests.swift +++ b/supacodeTests/AppFeatureSettingsSelectionTests.swift @@ -163,10 +163,32 @@ struct AppFeatureSettingsSelectionTests { } } + @Test(.dependencies) func openAgentProfilesSettingsSelectsProfiles() async { + let shown = LockIsolated(false) + var state = AppFeature.State(settings: SettingsFeature.State()) + state.settings.selection = .commandLineTool + let store = TestStore(initialState: state) { + AppFeature() + } withDependencies: { + $0.settingsWindowClient.show = { + shown.withValue { $0 = true } + } + } + + await store.send(.openAgentProfilesSettings) + await store.receive(\.settings.setSelection) { + $0.settings.selection = .profiles + $0.settings.agentProfiles = .init() + } + await store.finish() + + #expect(shown.value) + } + @Test func selectingAnotherSectionClearsAgentProfileEditorState() async { let profile = AgentProfile(name: "Codex", runtime: .codex) var state = AppFeature.State(settings: SettingsFeature.State()) - state.settings.selection = .agents + state.settings.selection = .profiles var agentProfiles = AgentProfilesFeature.State() agentProfiles.settings = UserGlobalSettings(customCommands: [], agentProfiles: [profile]) agentProfiles.path.append(AgentProfileEditorFeature.State(profile: profile))