Repository navigation
Add CmuxExtensionKit sidebar prototypes #4309
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 4 commits
Commits
Show all changes
37 commits
Select commit
Hold shift + click to select a range
be877a9
Add CmuxExtensionKit sidebar prototypes
lawrencecchen 877e3ab
Square full-width extension sidebar selection
lawrencecchen 699ba0a
Animate extension sidebar disclosure
lawrencecchen a22190d
Add user sidebar extension examples
lawrencecchen 845bd57
Add browser stack sidebar example
lawrencecchen 988d1ee
Move sidebar variants into user extension examples
lawrencecchen 6cb804c
merge: sync sidebar extension branch with main
lawrencecchen daa535f
fix: harden extension sidebar providers
lawrencecchen eb23057
fix: productionize extension sidebar snapshots
lawrencecchen b48d436
fix: guard sidebar debug logging
lawrencecchen b811bb7
fix: finish extension sidebar drag hardening
lawrencecchen cee6221
fix: address extension sidebar review feedback
lawrencecchen 35de45d
fix: close extension sidebar event review gaps
lawrencecchen 6793124
fix: tighten extension sidebar event reducers
lawrencecchen bec9eea
fix: close extension sidebar review gaps
lawrencecchen 50b141a
fix: harden extension sidebar review followups
lawrencecchen d60881f
fix: address extension sidebar review issues
lawrencecchen c4a1251
fix: keep extension sidebar snapshots in sync
lawrencecchen f93e7be
fix: address extension sidebar review findings
lawrencecchen 6e1c276
fix: tighten extension sidebar event sync
lawrencecchen fa3c701
fix: harden browser stack sidebar state updates
lawrencecchen ea286b8
fix: harden extension sidebar inspector interactions
lawrencecchen 8e1d68b
fix: clear sidebar drag warning budget
lawrencecchen ef9ca3c
fix: scope browser stack sidebar state by window
lawrencecchen 2467a29
fix: preserve extension sidebar window scope
lawrencecchen 0019b35
fix: harden extension sidebar event reduction
lawrencecchen 552eb23
fix: publish sidebar reorder events
lawrencecchen 7073858
fix: complete sidebar reorder event flow
lawrencecchen c6eadac
fix: harden sidebar review feedback
lawrencecchen 5cde0f9
fix: gate remote attention rows
lawrencecchen 1f136dc
fix: publish pinned sidebar reorder state
lawrencecchen 9cd8533
fix: isolate sidebar reorder side effects
lawrencecchen 20a1db9
fix: harden extension sidebar event replay
lawrencecchen b8add4f
fix: preserve browser stack boundary drops
lawrencecchen 3b43aad
merge: sync extension sidebar events with main
lawrencecchen fc35674
fix: address sidebar review followups
lawrencecchen cafedc2
fix: return browser stack scoped state
lawrencecchen File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| // swift-tools-version: 6.0 | ||
|
|
||
| import PackageDescription | ||
|
|
||
| let package = Package( | ||
| name: "CmuxExtensionSidebarExamples", | ||
| platforms: [.macOS(.v14)], | ||
| products: [ | ||
| .library( | ||
| name: "CmuxExtensionSidebarExamples", | ||
| targets: ["CmuxExtensionSidebarExamples"] | ||
| ), | ||
| ], | ||
| dependencies: [ | ||
| .package(path: "../../Packages/CmuxExtensionKit"), | ||
| ], | ||
| targets: [ | ||
| .target( | ||
| name: "CmuxExtensionSidebarExamples", | ||
| dependencies: ["CmuxExtensionKit"] | ||
| ), | ||
| ] | ||
| ) |
81 changes: 81 additions & 0 deletions
81
...ExtensionSidebarExamples/Sources/CmuxExtensionSidebarExamples/AttentionQueueSidebar.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| import CmuxExtensionKit | ||
|
|
||
| public struct AttentionQueueSidebar: CmuxExtensionSidebarProvider { | ||
| public let descriptor = CmuxExtensionSidebarProviderDescriptor( | ||
| id: "com.example.cmux.sidebar.attention-queue", | ||
| title: localized("example.sidebar.attentionQueue.title", "Attention Queue"), | ||
| subtitle: localized("example.sidebar.attentionQueue.subtitle", "User extension"), | ||
| systemImageName: "bell", | ||
| mode: nil, | ||
| isHostProvided: false | ||
| ) | ||
|
|
||
| public init() {} | ||
|
|
||
| public func render(snapshot: CmuxExtensionSidebarSnapshot) -> CmuxExtensionSidebarRenderModel { | ||
| let selected = snapshot.selectedWorkspaceId | ||
| let active = snapshot.workspaces.filter { $0.id == selected } | ||
| let pinned = snapshot.workspaces.filter { $0.isPinned && $0.id != selected } | ||
| let attention = snapshot.workspaces.filter { workspace in | ||
| workspace.id != selected && !workspace.isPinned && needsAttention(workspace) | ||
| } | ||
| let quiet = snapshot.workspaces.filter { workspace in | ||
| workspace.id != selected && !workspace.isPinned && !needsAttention(workspace) | ||
| } | ||
|
|
||
| let sections = [ | ||
| ExampleSidebarSection( | ||
| id: "active", | ||
| title: localized("example.sidebar.group.active", "Active"), | ||
| systemImageName: "circle.fill", | ||
| projectRootPath: nil, | ||
| workspaces: active | ||
| ) | ||
| .render(subtitle: rowSubtitle), | ||
| ExampleSidebarSection( | ||
| id: "pinned", | ||
| title: localized("example.sidebar.group.pinned", "Pinned"), | ||
| systemImageName: "pin", | ||
| projectRootPath: nil, | ||
| workspaces: pinned | ||
| ) | ||
| .render(subtitle: rowSubtitle), | ||
| ExampleSidebarSection( | ||
| id: "attention", | ||
| title: localized("example.sidebar.group.needsAttention", "Needs Attention"), | ||
| systemImageName: "bell", | ||
| projectRootPath: nil, | ||
| workspaces: attention | ||
| ) | ||
| .render(subtitle: rowSubtitle), | ||
| ExampleSidebarSection( | ||
| id: "quiet", | ||
| title: localized("example.sidebar.group.quiet", "Quiet"), | ||
| systemImageName: "checkmark.circle", | ||
| projectRootPath: nil, | ||
| workspaces: quiet | ||
| ) | ||
| .render(subtitle: rowSubtitle), | ||
| ] | ||
|
|
||
| return renderModel(providerId: descriptor.id, snapshot: snapshot, sections: sections) | ||
| } | ||
|
|
||
| private func needsAttention(_ workspace: CmuxExtensionWorkspaceSnapshot) -> Bool { | ||
| workspace.unreadCount > 0 | ||
| || trimmed(workspace.latestNotificationText) != nil | ||
| || workspace.remoteConnectionState == "connecting" | ||
| || workspace.remoteConnectionState == "reconnecting" | ||
| || workspace.remoteConnectionState == "disconnected" | ||
| } | ||
|
|
||
| private func rowSubtitle(_ workspace: CmuxExtensionWorkspaceSnapshot) -> CmuxExtensionSidebarRenderText? { | ||
| if let notification = trimmed(workspace.latestNotificationText) { | ||
| return .plain(notification) | ||
| } | ||
| if let remoteState = trimmed(workspace.remoteConnectionState), remoteState != "connected" { | ||
| return .plain(remoteState) | ||
| } | ||
| return trimmed(workspace.customDescription).map(CmuxExtensionSidebarRenderText.plain) | ||
| } | ||
| } |
70 changes: 70 additions & 0 deletions
70
.../CmuxExtensionSidebarExamples/Sources/CmuxExtensionSidebarExamples/DevServerSidebar.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import CmuxExtensionKit | ||
|
|
||
| public struct DevServerSidebar: CmuxExtensionSidebarProvider { | ||
| public let descriptor = CmuxExtensionSidebarProviderDescriptor( | ||
| id: "com.example.cmux.sidebar.dev-servers", | ||
| title: localized("example.sidebar.devServers.title", "Dev Servers"), | ||
| subtitle: localized("example.sidebar.devServers.subtitle", "User extension"), | ||
| systemImageName: "terminal", | ||
| mode: nil, | ||
| isHostProvided: false | ||
| ) | ||
|
|
||
| public init() {} | ||
|
|
||
| public func render(snapshot: CmuxExtensionSidebarSnapshot) -> CmuxExtensionSidebarRenderModel { | ||
| let liveServers = snapshot.workspaces.filter(hasServerSignal) | ||
| let remote = snapshot.workspaces.filter { workspace in | ||
| !hasServerSignal(workspace) && trimmed(workspace.remoteDisplayTarget) != nil | ||
| } | ||
| let local = snapshot.workspaces.filter { workspace in | ||
| !hasServerSignal(workspace) && trimmed(workspace.remoteDisplayTarget) == nil | ||
| } | ||
|
|
||
| let sections = [ | ||
| ExampleSidebarSection( | ||
| id: "live", | ||
| title: localized("example.sidebar.group.liveServers", "Live Servers"), | ||
| systemImageName: "terminal", | ||
| projectRootPath: nil, | ||
| workspaces: liveServers | ||
| ) | ||
| .render(subtitle: serverSubtitle), | ||
| ExampleSidebarSection( | ||
| id: "remote", | ||
| title: localized("example.sidebar.group.remote", "Remote"), | ||
| systemImageName: "network", | ||
| projectRootPath: nil, | ||
| workspaces: remote | ||
| ) | ||
| .render(subtitle: serverSubtitle), | ||
| ExampleSidebarSection( | ||
| id: "local", | ||
| title: localized("example.sidebar.group.local", "Local"), | ||
| systemImageName: "folder", | ||
| projectRootPath: nil, | ||
| workspaces: local | ||
| ) | ||
| .render(subtitle: serverSubtitle), | ||
| ] | ||
|
|
||
| return renderModel(providerId: descriptor.id, snapshot: snapshot, sections: sections) | ||
| } | ||
|
|
||
| private func hasServerSignal(_ workspace: CmuxExtensionWorkspaceSnapshot) -> Bool { | ||
| if !workspace.listeningPorts.isEmpty { | ||
| return true | ||
| } | ||
| guard let description = trimmed(workspace.customDescription)?.lowercased() else { | ||
| return false | ||
| } | ||
| return description.contains("server") || description.contains(":") | ||
| } | ||
|
|
||
| private func serverSubtitle(_ workspace: CmuxExtensionWorkspaceSnapshot) -> CmuxExtensionSidebarRenderText? { | ||
| if !workspace.listeningPorts.isEmpty { | ||
| return .plain(workspace.listeningPorts.map { ":\($0)" }.joined(separator: ", ")) | ||
| } | ||
| return trimmed(workspace.customDescription).map(CmuxExtensionSidebarRenderText.plain) | ||
| } | ||
| } | ||
56 changes: 56 additions & 0 deletions
56
...CmuxExtensionSidebarExamples/Sources/CmuxExtensionSidebarExamples/LastPromptSidebar.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import CmuxExtensionKit | ||
|
|
||
| public struct LastPromptSidebar: CmuxExtensionSidebarProvider { | ||
| public let descriptor = CmuxExtensionSidebarProviderDescriptor( | ||
| id: "com.example.cmux.sidebar.last-prompt", | ||
| title: localized("example.sidebar.lastPrompt.title", "Last Prompt"), | ||
| subtitle: localized("example.sidebar.lastPrompt.subtitle", "User extension"), | ||
| systemImageName: "clock", | ||
| mode: nil, | ||
| isHostProvided: false | ||
| ) | ||
|
|
||
| public init() {} | ||
|
|
||
| public func render(snapshot: CmuxExtensionSidebarSnapshot) -> CmuxExtensionSidebarRenderModel { | ||
| let recent = snapshot.workspaces | ||
| .filter { $0.latestSubmittedAt != nil } | ||
| .sorted { lhs, rhs in | ||
| guard let lhsDate = lhs.latestSubmittedAt else { return false } | ||
| guard let rhsDate = rhs.latestSubmittedAt else { return true } | ||
| return lhsDate > rhsDate | ||
| } | ||
|
|
||
| let sections = [ | ||
| ExampleSidebarSection( | ||
| id: "recent", | ||
| title: localized("example.sidebar.group.recentPrompts", "Recent Prompts"), | ||
| systemImageName: "clock", | ||
| projectRootPath: nil, | ||
| workspaces: recent | ||
| ) | ||
| .render(subtitle: promptSubtitle, trailingText: promptTrailingText), | ||
| ExampleSidebarSection( | ||
| id: "none", | ||
| title: localized("example.sidebar.group.noPrompts", "No Prompts"), | ||
| systemImageName: "tray", | ||
| projectRootPath: nil, | ||
| workspaces: snapshot.workspaces.filter { $0.latestSubmittedAt == nil } | ||
| ) | ||
| .render(subtitle: promptSubtitle, trailingText: promptTrailingText), | ||
| ] | ||
|
|
||
| return renderModel(providerId: descriptor.id, snapshot: snapshot, sections: sections) | ||
| } | ||
|
|
||
| private func promptSubtitle(_ workspace: CmuxExtensionWorkspaceSnapshot) -> CmuxExtensionSidebarRenderText? { | ||
| if let message = trimmed(workspace.latestSubmittedMessage) { | ||
| return .plain(message) | ||
| } | ||
| return .plain("No prompts yet") | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
|
||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
|
|
||
| private func promptTrailingText(_ workspace: CmuxExtensionWorkspaceSnapshot) -> CmuxExtensionSidebarRenderText? { | ||
| workspace.latestSubmittedAt.map { .relativeDate($0, style: .compact) } | ||
| } | ||
| } | ||
62 changes: 62 additions & 0 deletions
62
...xtensionSidebarExamples/Sources/CmuxExtensionSidebarExamples/ProjectWorktreeSidebar.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| import CmuxExtensionKit | ||
| import Foundation | ||
|
|
||
| public struct ProjectWorktreeSidebar: CmuxExtensionSidebarProvider { | ||
| public let descriptor = CmuxExtensionSidebarProviderDescriptor( | ||
| id: "com.example.cmux.sidebar.project-worktrees", | ||
| title: localized("example.sidebar.projectWorktrees.title", "Project Worktrees"), | ||
| subtitle: localized("example.sidebar.projectWorktrees.subtitle", "User extension"), | ||
| systemImageName: "folder", | ||
| mode: .projectTree, | ||
| isHostProvided: false | ||
| ) | ||
|
|
||
| public init() {} | ||
|
|
||
| public func render(snapshot: CmuxExtensionSidebarSnapshot) -> CmuxExtensionSidebarRenderModel { | ||
| var sections: [CmuxExtensionSidebarRenderSection] = [] | ||
|
|
||
| sections.append( | ||
| ExampleSidebarSection( | ||
| id: "pinned", | ||
| title: localized("example.sidebar.group.pinned", "Pinned"), | ||
| systemImageName: "pin", | ||
| projectRootPath: nil, | ||
| workspaces: snapshot.workspaces.filter(\.isPinned) | ||
| ) | ||
| .render(subtitle: branchSubtitle) | ||
| ) | ||
|
|
||
| var grouped: [String: [CmuxExtensionWorkspaceSnapshot]] = [:] | ||
| var orderedProjectRoots: [String] = [] | ||
|
|
||
| for workspace in snapshot.workspaces where !workspace.isPinned { | ||
| let key = projectRoot(for: workspace) ?? "no-folder" | ||
| if grouped[key] == nil { | ||
| grouped[key] = [] | ||
| orderedProjectRoots.append(key) | ||
| } | ||
| grouped[key]?.append(workspace) | ||
| } | ||
|
|
||
| for root in orderedProjectRoots { | ||
| let title = root == "no-folder" ? "No Folder" : displayName(for: root) | ||
| sections.append( | ||
| ExampleSidebarSection( | ||
| id: "project:\(root)", | ||
| title: localized("example.sidebar.group.project.\(title)", title), | ||
| systemImageName: root == "no-folder" ? "tray" : "folder", | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| projectRootPath: root == "no-folder" ? nil : root, | ||
| workspaces: grouped[root] ?? [] | ||
| ) | ||
| .render(subtitle: branchSubtitle) | ||
| ) | ||
| } | ||
|
|
||
| return renderModel(providerId: descriptor.id, snapshot: snapshot, sections: sections) | ||
| } | ||
|
|
||
| private func branchSubtitle(_ workspace: CmuxExtensionWorkspaceSnapshot) -> CmuxExtensionSidebarRenderText? { | ||
| trimmed(workspace.branchSummary).map(CmuxExtensionSidebarRenderText.plain) | ||
| } | ||
| } | ||
90 changes: 90 additions & 0 deletions
90
...s/CmuxExtensionSidebarExamples/Sources/CmuxExtensionSidebarExamples/SidebarExamples.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| import CmuxExtensionKit | ||
| import Foundation | ||
|
|
||
| public enum SidebarExamples { | ||
| public static let providers: [any CmuxExtensionSidebarProvider] = [ | ||
| ProjectWorktreeSidebar(), | ||
| AttentionQueueSidebar(), | ||
| DevServerSidebar(), | ||
| LastPromptSidebar(), | ||
| SuperCompactSidebar(), | ||
| ] | ||
| } | ||
|
|
||
| struct ExampleSidebarSection { | ||
| var id: String | ||
| var title: CmuxExtensionLocalizedText | ||
| var systemImageName: String | ||
| var projectRootPath: String? | ||
| var workspaces: [CmuxExtensionWorkspaceSnapshot] | ||
|
|
||
| func render( | ||
| rowTitle: (CmuxExtensionWorkspaceSnapshot) -> String = { $0.title }, | ||
| accessory: CmuxExtensionWorkspaceRowAccessory? = .inspector, | ||
| subtitle: (CmuxExtensionWorkspaceSnapshot) -> CmuxExtensionSidebarRenderText? = { _ in nil }, | ||
| trailingText: (CmuxExtensionWorkspaceSnapshot) -> CmuxExtensionSidebarRenderText? = { _ in nil } | ||
| ) -> CmuxExtensionSidebarRenderSection { | ||
| CmuxExtensionSidebarRenderSection( | ||
| id: id, | ||
| treeSection: CmuxExtensionWorkspaceTreeSection( | ||
| id: id, | ||
| title: title.defaultValue, | ||
| titleText: title, | ||
| subtitle: nil, | ||
| systemImageName: systemImageName, | ||
| projectRootPath: projectRootPath, | ||
| workspaceIds: workspaces.map(\.id) | ||
| ), | ||
| rows: workspaces.map { workspace in | ||
| CmuxExtensionSidebarRenderRow( | ||
| id: workspace.id, | ||
| title: rowTitle(workspace), | ||
| workspaceId: workspace.id, | ||
| accessory: accessory, | ||
| subtitle: subtitle(workspace), | ||
| trailingText: trailingText(workspace) | ||
| ) | ||
| } | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| func localized(_ key: String, _ defaultValue: String) -> CmuxExtensionLocalizedText { | ||
| CmuxExtensionLocalizedText(key: key, defaultValue: defaultValue) | ||
| } | ||
|
|
||
| func renderModel( | ||
| providerId: String, | ||
| snapshot: CmuxExtensionSidebarSnapshot, | ||
| sections: [CmuxExtensionSidebarRenderSection] | ||
| ) -> CmuxExtensionSidebarRenderModel { | ||
| CmuxExtensionSidebarRenderModel( | ||
| providerId: providerId, | ||
| snapshotSequence: snapshot.sequence, | ||
| sections: sections.filter { !$0.rows.isEmpty } | ||
| ) | ||
| } | ||
|
|
||
| func trimmed(_ value: String?) -> String? { | ||
| guard let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines), | ||
| !trimmed.isEmpty else { | ||
| return nil | ||
| } | ||
| return trimmed | ||
| } | ||
|
|
||
| func projectRoot(for workspace: CmuxExtensionWorkspaceSnapshot) -> String? { | ||
| trimmed(workspace.projectRootPath) | ||
| ?? trimmed(workspace.rootPath).map { | ||
| URL(fileURLWithPath: $0, isDirectory: true) | ||
| .standardizedFileURL | ||
| .deletingLastPathComponent() | ||
| .path | ||
| } | ||
| } | ||
|
|
||
| func displayName(for path: String) -> String { | ||
| let url = URL(fileURLWithPath: path, isDirectory: true).standardizedFileURL | ||
| let name = url.lastPathComponent | ||
| return name.isEmpty ? path : name | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.