Skip to content
Merged
Show file tree
Hide file tree
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 May 18, 2026
877e3ab
Square full-width extension sidebar selection
lawrencecchen May 19, 2026
699ba0a
Animate extension sidebar disclosure
lawrencecchen May 19, 2026
a22190d
Add user sidebar extension examples
lawrencecchen May 19, 2026
845bd57
Add browser stack sidebar example
lawrencecchen May 19, 2026
988d1ee
Move sidebar variants into user extension examples
lawrencecchen May 19, 2026
6cb804c
merge: sync sidebar extension branch with main
lawrencecchen May 21, 2026
daa535f
fix: harden extension sidebar providers
lawrencecchen May 21, 2026
eb23057
fix: productionize extension sidebar snapshots
lawrencecchen May 21, 2026
b48d436
fix: guard sidebar debug logging
lawrencecchen May 21, 2026
b811bb7
fix: finish extension sidebar drag hardening
lawrencecchen May 21, 2026
cee6221
fix: address extension sidebar review feedback
lawrencecchen May 21, 2026
35de45d
fix: close extension sidebar event review gaps
lawrencecchen May 21, 2026
6793124
fix: tighten extension sidebar event reducers
lawrencecchen May 21, 2026
bec9eea
fix: close extension sidebar review gaps
lawrencecchen May 21, 2026
50b141a
fix: harden extension sidebar review followups
lawrencecchen May 21, 2026
d60881f
fix: address extension sidebar review issues
lawrencecchen May 21, 2026
c4a1251
fix: keep extension sidebar snapshots in sync
lawrencecchen May 21, 2026
f93e7be
fix: address extension sidebar review findings
lawrencecchen May 21, 2026
6e1c276
fix: tighten extension sidebar event sync
lawrencecchen May 21, 2026
fa3c701
fix: harden browser stack sidebar state updates
lawrencecchen May 21, 2026
ea286b8
fix: harden extension sidebar inspector interactions
lawrencecchen May 21, 2026
8e1d68b
fix: clear sidebar drag warning budget
lawrencecchen May 21, 2026
ef9ca3c
fix: scope browser stack sidebar state by window
lawrencecchen May 21, 2026
2467a29
fix: preserve extension sidebar window scope
lawrencecchen May 21, 2026
0019b35
fix: harden extension sidebar event reduction
lawrencecchen May 21, 2026
552eb23
fix: publish sidebar reorder events
lawrencecchen May 21, 2026
7073858
fix: complete sidebar reorder event flow
lawrencecchen May 21, 2026
c6eadac
fix: harden sidebar review feedback
lawrencecchen May 21, 2026
5cde0f9
fix: gate remote attention rows
lawrencecchen May 21, 2026
1f136dc
fix: publish pinned sidebar reorder state
lawrencecchen May 21, 2026
9cd8533
fix: isolate sidebar reorder side effects
lawrencecchen May 21, 2026
20a1db9
fix: harden extension sidebar event replay
lawrencecchen May 21, 2026
b8add4f
fix: preserve browser stack boundary drops
lawrencecchen May 23, 2026
3b43aad
merge: sync extension sidebar events with main
lawrencecchen May 23, 2026
fc35674
fix: address sidebar review followups
lawrencecchen May 23, 2026
cafedc2
fix: return browser stack scoped state
lawrencecchen May 23, 2026
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
23 changes: 23 additions & 0 deletions Examples/CmuxExtensionSidebarExamples/Package.swift
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"]
),
]
)
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)
}
}
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(":")
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

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)
}
}
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")
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

private func promptTrailingText(_ workspace: CmuxExtensionWorkspaceSnapshot) -> CmuxExtensionSidebarRenderText? {
workspace.latestSubmittedAt.map { .relativeDate($0, style: .compact) }
}
}
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",
Comment thread
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)
}
}
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
}
Loading