feat(mac): add Connect Claude button to Plan pane

The Plan pane previously told users to "run claude login in your
terminal, then retry" with no way to start the flow from the app.
Added a primary Connect Claude button on both the no-credentials and
failed states that launches Terminal.app with `claude login`, so the
OAuth flow is one click away.

TerminalLauncher.openClaudeLogin() uses a hardcoded literal, so no
user input reaches AppleScript. Refactored the common path into
runInTerminal(command:preValidated:) which re-validates any non-
literal input against CodeburnCLI.isSafe as defense-in-depth.

On machines without Terminal.app (iTerm/Ghostty/Warp), the button
surfaces an inline instruction to run `claude login` manually instead
of failing silently.
This commit is contained in:
AgentSeal 2026-04-18 06:54:57 -07:00
parent 9483d66e65
commit 43a938ff9e
2 changed files with 84 additions and 27 deletions

View file

@ -1039,6 +1039,7 @@ private struct PlanLoadingView: View {
private struct PlanNoCredentialsView: View {
@Environment(AppStore.self) private var store
@State private var showManualFallback = false
var body: some View {
VStack(spacing: 8) {
@ -1048,16 +1049,32 @@ private struct PlanNoCredentialsView: View {
Text("No Claude subscription connected")
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(.primary)
Text("Run `claude login` in your terminal, then retry.")
.font(.system(size: 10.5))
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.frame(maxWidth: 260)
Button("Retry") {
Task { await store.refreshSubscription() }
if showManualFallback {
Text("Terminal.app isn't available. Open your terminal and run `claude login`, then click Retry.")
.font(.system(size: 10.5))
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.frame(maxWidth: 280)
} else {
Text("Click Connect to sign in with Claude, then return here.")
.font(.system(size: 10.5))
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.frame(maxWidth: 260)
}
HStack(spacing: 8) {
Button("Connect Claude") {
if !TerminalLauncher.openClaudeLogin() { showManualFallback = true }
}
.controlSize(.small)
.buttonStyle(.borderedProminent)
.tint(Theme.brandAccent)
Button("Retry") {
Task { await store.refreshSubscription() }
}
.controlSize(.small)
.buttonStyle(.bordered)
}
.controlSize(.small)
.buttonStyle(.bordered)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
@ -1067,6 +1084,7 @@ private struct PlanNoCredentialsView: View {
private struct PlanFailedView: View {
@Environment(AppStore.self) private var store
let error: String?
@State private var showManualFallback = false
var body: some View {
VStack(spacing: 8) {
@ -1076,7 +1094,13 @@ private struct PlanFailedView: View {
Text("Couldn't load plan data")
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(.primary)
if let error {
if showManualFallback {
Text("Terminal.app isn't available. Open your terminal and run `claude login`, then click Retry.")
.font(.system(size: 10.5))
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.frame(maxWidth: 280)
} else if let error {
Text(error)
.font(.system(size: 10))
.foregroundStyle(.tertiary)
@ -1084,11 +1108,19 @@ private struct PlanFailedView: View {
.frame(maxWidth: 280)
.lineLimit(3)
}
Button("Retry") {
Task { await store.refreshSubscription() }
HStack(spacing: 8) {
Button("Reconnect Claude") {
if !TerminalLauncher.openClaudeLogin() { showManualFallback = true }
}
.controlSize(.small)
.buttonStyle(.borderedProminent)
.tint(Theme.brandAccent)
Button("Retry") {
Task { await store.refreshSubscription() }
}
.controlSize(.small)
.buttonStyle(.bordered)
}
.controlSize(.small)
.buttonStyle(.bordered)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 14)