Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ struct SurfaceInsight: Identifiable {
return "doc"
case .rightSidebarTool:
return "sidebar.right"
case .project:
return "folder"
case .unknown:
return "rectangle"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public enum CMUXSidebarAction: Codable, Equatable, Sendable {
case closeWorkspace(UUID)
case selectNextWorkspace
case selectPreviousWorkspace
case createTerminalSurface(workspaceID: UUID?, initialInput: String?)
case createTerminalSurface(workspaceID: UUID?)
case createBrowserSurface(workspaceID: UUID?, url: String?)
case selectSurface(workspaceID: UUID, surfaceID: UUID)
case selectNextSurface
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,12 @@ public struct CmuxSidebarHost {
await perform(.openURL(url.absoluteString))
}

public func createTerminalSurface(
in workspaceID: UUID? = nil,
initialInput: String? = nil
) async -> CMUXExtensionActionResult {
await perform(.createTerminalSurface(workspaceID: workspaceID, initialInput: initialInput))
/// Requests that CMUX create a terminal surface.
///
/// Extensions can ask CMUX to create the surface, but cannot seed shell
/// input. This keeps `.createSurface` separate from command execution.
public func createTerminalSurface(in workspaceID: UUID? = nil) async -> CMUXExtensionActionResult {
await perform(.createTerminalSurface(workspaceID: workspaceID))
}

public func createBrowserSurface(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,15 +254,21 @@ struct CMUXExtensionKitTests {
host.refresh()
let selectResult = await host.selectWorkspace(workspaceID)
let closeResult = await host.closeWorkspace(workspaceID)
let terminalResult = await host.createTerminalSurface(in: workspaceID)
let browserResult = await host.createBrowserSurface(in: workspaceID, url: url)
let openResult = await host.openURL(url)

#expect(refreshCount == 1)
#expect(selectResult.accepted)
#expect(closeResult.accepted)
#expect(terminalResult.accepted)
#expect(browserResult.accepted)
#expect(openResult.accepted)
#expect(actions == [
.selectWorkspace(workspaceID),
.closeWorkspace(workspaceID),
.createTerminalSurface(workspaceID: workspaceID),
.createBrowserSurface(workspaceID: workspaceID, url: "https://example.com/pr/1"),
.openURL("https://example.com/pr/1"),
])
}
Expand Down
176 changes: 104 additions & 72 deletions Sources/CMUXInstalledExtensionSidebarHostView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -311,46 +311,56 @@ struct CMUXInstalledExtensionSidebarHostView: View {

@ViewBuilder
private func extensionEmptyActions() -> some View {
HStack(spacing: 8) {
if enabledIdentities.count > 1 {
Menu {
ForEach(enabledIdentities, id: \.bundleIdentifier) { enabledIdentity in
Button {
selectExtension(enabledIdentity)
} label: {
Label(enabledIdentity.localizedName, systemImage: "puzzlepiece.extension")
}
}
} label: {
Label(
String(localized: "sidebar.extensions.choose.action", defaultValue: "Choose Extension"),
systemImage: "puzzlepiece.extension"
)
}
.menuStyle(.button)
.controlSize(.small)
ViewThatFits(in: .horizontal) {
HStack(spacing: 8) {
extensionEmptyActionButtons()
}
VStack(alignment: .leading, spacing: 8) {
extensionEmptyActionButtons()
}
}
}

Button {
presentExtensionBrowser()
@ViewBuilder
private func extensionEmptyActionButtons() -> some View {
if enabledIdentities.count > 1 {
Menu {
ForEach(enabledIdentities, id: \.bundleIdentifier) { enabledIdentity in
Button {
selectExtension(enabledIdentity)
} label: {
Label(enabledIdentity.localizedName, systemImage: "puzzlepiece.extension")
}
}
} label: {
Label(
String(localized: "sidebar.extensions.manage.short", defaultValue: "Manage"),
String(localized: "sidebar.extensions.choose.action", defaultValue: "Choose Extension"),
systemImage: "puzzlepiece.extension"
)
}
.menuStyle(.button)
.controlSize(.small)
}

Button {
onUseDefaultSidebar()
} label: {
Label(
String(localized: "sidebar.extensions.useDefault.short", defaultValue: "Use Default"),
systemImage: "sidebar.left"
)
}
.controlSize(.small)
Button {
presentExtensionBrowser()
} label: {
Label(
String(localized: "sidebar.extensions.manage.short", defaultValue: "Manage"),
systemImage: "puzzlepiece.extension"
)
}
.controlSize(.small)

Button {
onUseDefaultSidebar()
} label: {
Label(
String(localized: "sidebar.extensions.useDefault.short", defaultValue: "Use Default"),
systemImage: "sidebar.left"
)
}
.controlSize(.small)
}

private func extensionControlStrip(activeIdentity: AppExtensionIdentity?) -> some View {
Expand Down Expand Up @@ -479,39 +489,13 @@ struct CMUXInstalledExtensionSidebarHostView: View {
.font(.system(size: 12))
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
HStack(spacing: 8) {
Button {
blockedManifestReason = nil
effectiveGrant = nil
xpcHost.invalidate()
hostReloadToken &+= 1
} label: {
Label(
String(localized: "sidebar.extensions.retry", defaultValue: "Try Again"),
systemImage: "arrow.clockwise"
)
}
.controlSize(.small)

Button {
onUseDefaultSidebar()
} label: {
Label(
String(localized: "sidebar.extensions.useDefault.short", defaultValue: "Use Default"),
systemImage: "sidebar.left"
)
ViewThatFits(in: .horizontal) {
HStack(spacing: 8) {
blockedExtensionActionButtons()
}
.controlSize(.small)

Button {
presentExtensionBrowser()
} label: {
Label(
String(localized: "sidebar.extensions.manage.short", defaultValue: "Manage"),
systemImage: "puzzlepiece.extension"
)
VStack(alignment: .leading, spacing: 8) {
blockedExtensionActionButtons()
}
.controlSize(.small)
}
}
.padding(.horizontal, 14)
Expand All @@ -520,6 +504,41 @@ struct CMUXInstalledExtensionSidebarHostView: View {
.accessibilityIdentifier("CMUXExtensionSidebarBlockedState")
}

@ViewBuilder
private func blockedExtensionActionButtons() -> some View {
Button {
blockedManifestReason = nil
effectiveGrant = nil
xpcHost.invalidate()
hostReloadToken &+= 1
} label: {
Label(
String(localized: "sidebar.extensions.retry", defaultValue: "Try Again"),
systemImage: "arrow.clockwise"
)
}
.controlSize(.small)

Button {
onUseDefaultSidebar()
} label: {
Label(
String(localized: "sidebar.extensions.useDefault.short", defaultValue: "Use Default"),
systemImage: "sidebar.left"
)
}
.controlSize(.small)

Button {
presentExtensionBrowser()
} label: {
Label(
String(localized: "sidebar.extensions.manage.short", defaultValue: "Manage"),
systemImage: "puzzlepiece.extension")
}
.controlSize(.small)
}

private func blockedStatusText(reason: String) -> String {
switch reason {
case "connectionInterrupted":
Expand Down Expand Up @@ -668,19 +687,13 @@ struct CMUXInstalledExtensionSidebarHostView: View {
}
}
.padding(.top, 2)
HStack(spacing: 8) {
Button {
isShowingAccessReview = true
} label: {
Text(String(localized: "sidebar.extensions.access.review", defaultValue: "Review Access..."))
ViewThatFits(in: .horizontal) {
HStack(spacing: 8) {
limitedAccessActionButtons(identity: identity, effectiveGrant: effectiveGrant)
}
.controlSize(.small)
Button {
keepLimitedAccess(identity: identity, effectiveGrant: effectiveGrant)
} label: {
Text(String(localized: "sidebar.extensions.access.keepLimited", defaultValue: "Keep Limited"))
VStack(alignment: .leading, spacing: 8) {
limitedAccessActionButtons(identity: identity, effectiveGrant: effectiveGrant)
}
.controlSize(.small)
}
}
.padding(.horizontal, 12)
Expand All @@ -689,6 +702,25 @@ struct CMUXInstalledExtensionSidebarHostView: View {
.background(Color(nsColor: .controlBackgroundColor).opacity(0.88))
}

@ViewBuilder
private func limitedAccessActionButtons(
identity: AppExtensionIdentity?,
effectiveGrant: CMUXSidebarExtensionEffectiveGrant
) -> some View {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Button {
isShowingAccessReview = true
} label: {
Text(String(localized: "sidebar.extensions.access.review", defaultValue: "Review Access..."))
}
.controlSize(.small)
Button {
keepLimitedAccess(identity: identity, effectiveGrant: effectiveGrant)
} label: {
Text(String(localized: "sidebar.extensions.access.keepLimited", defaultValue: "Keep Limited"))
}
.controlSize(.small)
}

private func accessReviewSheet(
identity: AppExtensionIdentity,
effectiveGrant: CMUXSidebarExtensionEffectiveGrant
Expand Down
Loading
Loading