Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
cd308d0
Add beta workspace tasks
azooz2003-bit Jun 23, 2026
6b9515a
Fix workspace task socket routing
azooz2003-bit Jun 23, 2026
2282e78
Gate existing workspace task views
azooz2003-bit Jun 23, 2026
4216645
Bound workspace task mutations
azooz2003-bit Jun 23, 2026
7ea152c
Restore mobile set-font path
azooz2003-bit Jun 23, 2026
ebbf4d0
Satisfy workspace tasks policy checks
azooz2003-bit Jun 23, 2026
5b006c5
Fix workspace task review findings
azooz2003-bit Jun 23, 2026
257ebfc
Localize mobile set font socket errors
azooz2003-bit Jun 23, 2026
f611e06
Reject stale workspace task pane targets
azooz2003-bit Jun 23, 2026
6835fba
Resolve workspace task refs in CLI
azooz2003-bit Jun 23, 2026
8ecf7c7
Fix workspace task transfer and mobile font scope parsing
azooz2003-bit Jun 23, 2026
a4ffb70
Preserve mobile live font streams across reconnects
azooz2003-bit Jun 23, 2026
4e02502
Decode mobile font events at consumer
azooz2003-bit Jun 23, 2026
17bce20
Merge remote-tracking branch 'origin/main' into task-workspace-tasks
azooz2003-bit Jun 23, 2026
b82d5cf
Address workspace tasks review feedback
azooz2003-bit Jun 23, 2026
3a77384
Extract workspace tasks CLI handlers
azooz2003-bit Jun 23, 2026
e4484e9
Localize workspace tasks CLI strings
azooz2003-bit Jun 23, 2026
1f2309f
Refresh Swift length budget for workspace tasks
azooz2003-bit Jun 23, 2026
ce6ba40
Restore mobile font event decode API
azooz2003-bit Jun 23, 2026
6c301eb
Focus target workspace for task surface open
azooz2003-bit Jun 23, 2026
9e9ca04
Focus task surface from sidebar popover
azooz2003-bit Jun 23, 2026
9f22f4e
Tighten workspace task open and index handling
azooz2003-bit Jun 23, 2026
dee193f
Iterate workspace tasks UX
azooz2003-bit Jun 23, 2026
3f7ce3e
Restyle workspace tasks UI
azooz2003-bit Jun 23, 2026
34934fe
Show workspace task status in sidebar rows
azooz2003-bit Jun 30, 2026
157570f
Merge origin/main into task-workspace-tasks
azooz2003-bit Jun 30, 2026
4aeb39a
Normalize project file after merge
azooz2003-bit Jun 30, 2026
1d95d9a
Merge remote-tracking branch 'origin/main' into task-workspace-tasks
azooz2003-bit Jun 30, 2026
f235782
Merge remote-tracking branch 'origin/main' into task-workspace-tasks
azooz2003-bit Jun 30, 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
516 changes: 509 additions & 7 deletions CLI/cmux.swift

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,23 @@ public struct BetaFeaturesCatalogSection: SettingCatalogSection {
userDefaultsKey: "remoteTmux.beta.enabled"
)

/// Workspace Tasks: per-workspace task lists surfaced as a native panel,
/// sidebar-row popover, and CLI/socket command set. Defaults off while the
/// data model and UI are in beta.
public let workspaceTasks = DefaultsKey<Bool>(
id: "workspaceTasks.beta.enabled",
defaultValue: false,
userDefaultsKey: "workspaceTasks.beta.enabled"
)

/// Workspace Controls: an experimental sidebar-row hover control chooser.
/// Defaults off so the sidebar keeps the existing close-only row chrome
/// unless users opt into the beta.
public let workspaceControls = DefaultsKey<Bool>(
id: "workspaceControls.beta.enabled",
defaultValue: false,
userDefaultsKey: "workspaceControls.beta.enabled"
)

public init() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ public struct SidebarCatalogSection: SettingCatalogSection {
userDefaultsKey: RightSidebarWidthSettings.maxWidthKey
)

/// Built-in hover controls exposed on each sidebar workspace row.
public let workspaceControls = DefaultsKey<[WorkspaceRowControlOption]>(
id: "sidebar.workspaceControls",
defaultValue: WorkspaceRowControlOption.defaultControls,
userDefaultsKey: "sidebarWorkspaceControls"
)

public let rememberedRightMaxWidth = DefaultsKey<Double>(
id: "sidebar.rightMaxWidth.remembered",
defaultValue: RightSidebarWidthSettings.defaultConfiguredMaximumWidth,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import Foundation

/// Built-in hover controls that cmux can show on a sidebar workspace row.
///
/// Stored under ``SidebarCatalogSection/workspaceControls`` as raw strings in
/// `sidebar.workspaceControls`. The close control is always included so users
/// cannot strand themselves without the existing hover close affordance.
public enum WorkspaceRowControlOption: String, CaseIterable, Sendable, SettingCodable {
/// Close the workspace.
case close
/// Open the workspace-scoped task list.
case tasks

/// Maximum number of built-in controls a workspace row may expose.
public static let maximumVisibleControls = 3

/// Controls shown when no preference is stored.
public static let defaultControls: [WorkspaceRowControlOption] = [.close]

/// Returns a duplicate-free, capped list that always includes ``close``.
///
/// Unknown raw values are dropped by callers before reaching this method.
/// The user's order is otherwise preserved, with ``close`` inserted first
/// only when it was absent.
public static func sanitized(_ controls: [WorkspaceRowControlOption]) -> [WorkspaceRowControlOption] {
var output: [WorkspaceRowControlOption] = []
var seen = Set<WorkspaceRowControlOption>()

func append(_ option: WorkspaceRowControlOption) {
guard output.count < maximumVisibleControls, seen.insert(option).inserted else { return }
output.append(option)
}

if !controls.contains(.close) {
append(.close)
}
for option in controls {
append(option)
}
if output.isEmpty {
return defaultControls
}
return output
}

/// Decodes a raw string array from `cmux.json` / `UserDefaults`, dropping
/// unknown entries before applying the close-control invariant and cap.
public static func sanitizedRawValues(_ rawValues: [String]) -> [WorkspaceRowControlOption] {
sanitized(rawValues.compactMap { WorkspaceRowControlOption(rawValue: $0) })
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Foundation
import Testing
@testable import CmuxSettings

@Suite("sidebar.workspaceControls")
struct WorkspaceRowControlOptionTests {
private func makeStore() -> (UserDefaultsSettingsStore, String, SettingCatalog) {
let suiteName = "cmux.workspaceControls.tests.\(UUID().uuidString)"
let store = UserDefaultsSettingsStore(defaults: UserDefaults(suiteName: suiteName)!)
return (store, suiteName, SettingCatalog())
}

@Test func sanitizedAlwaysIncludesCloseAndCapsControls() {
let controls = WorkspaceRowControlOption.sanitized([.tasks, .tasks, .close])
#expect(controls == [.tasks, .close])

let missingClose = WorkspaceRowControlOption.sanitized([.tasks])
#expect(missingClose == [.close, .tasks])
}

@Test func sanitizedRawValuesDropsUnknownEntries() {
let controls = WorkspaceRowControlOption.sanitizedRawValues(["tasks", "unknown", "close", "tasks"])
#expect(controls == [.tasks, .close])
}

@Test func defaultsToCloseWhenUnset() async {
let (store, _, catalog) = makeStore()
let value = await store.value(for: catalog.sidebar.workspaceControls)
#expect(value == [.close])
}

@Test func roundTripsThroughTheStore() async {
let (store, suiteName, catalog) = makeStore()
await store.set([.close, .tasks], for: catalog.sidebar.workspaceControls)
let value = await store.value(for: catalog.sidebar.workspaceControls)
#expect(value == [.close, .tasks])
let defaults = UserDefaults(suiteName: suiteName)!
#expect(defaults.stringArray(forKey: catalog.sidebar.workspaceControls.userDefaultsKey) == ["close", "tasks"])
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ extension Array where Element == CuratedSettingEntry {
.init(section: .sidebarAppearance, id: "show-progress", title: "Show Progress in Sidebar", synonyms: "sidebar.showProgress progress bar percent status set_progress"),
.init(section: .sidebarAppearance, id: "show-metadata", title: "Show Custom Metadata in Sidebar", synonyms: "sidebar.showCustomMetadata metadata meta report_meta status custom block"),
.init(section: .sidebarAppearance, id: "right-max-width", title: "Dock Max Width", synonyms: "sidebar.rightMaxWidth dock right sidebar max width terminal reservation cap logs lazygit"),
.init(section: .sidebarAppearance, id: "workspace-controls", title: String(localized: "settings.sidebar.workspaceControls", defaultValue: "Workspace Controls"), synonyms: "sidebar.workspaceControls hover controls close tasks workspace row beta"),

// Mobile
.init(section: .mobile, id: "pairDevice", title: "Pair a Device", synonyms: "pair pairing add device qr qr code scan iphone ipad ios mobile tailscale connect onboarding sign in"),
Expand All @@ -154,6 +155,8 @@ extension Array where Element == CuratedSettingEntry {
.init(section: .betaFeatures, id: "dock", title: "Dock", synonyms: "dock right sidebar terminal controls tui beta unstable"),
.init(section: .betaFeatures, id: "customSidebars", title: "Custom Sidebars", synonyms: "custom sidebars swift json interpreted vibe beta unstable"),
.init(section: .betaFeatures, id: "remoteTmux", title: "Remote tmux", synonyms: "remote tmux ssh control mode -CC mirror session window pane sidebar workspace beta unstable"),
.init(section: .betaFeatures, id: "workspaceTasks", title: String(localized: "settings.betaFeatures.workspaceTasks", defaultValue: "Workspace Tasks"), synonyms: "workspace tasks todos goals deliverables per workspace beta unstable"),
.init(section: .betaFeatures, id: "workspaceControls", title: String(localized: "settings.betaFeatures.workspaceControls", defaultValue: "Workspace Controls"), synonyms: "workspace controls sidebar row hover close tasks beta unstable"),

// Automation
.init(section: .automation, id: "socket-mode", title: "Socket Control Mode", synonyms: "automation.socketControlMode api socket unix domain control server auth allow password disabled"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@ public struct BetaFeaturesSection: View {
@State private var extensions: DefaultsValueModel<Bool>
@State private var customSidebars: DefaultsValueModel<Bool>
@State private var remoteTmux: DefaultsValueModel<Bool>
@State private var workspaceTasks: DefaultsValueModel<Bool>
@State private var workspaceControls: DefaultsValueModel<Bool>

public init(defaultsStore: UserDefaultsSettingsStore, catalog: SettingCatalog) {
_feed = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.betaFeatures.rightSidebarFeed))
_dock = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.betaFeatures.rightSidebarDock))
_extensions = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.betaFeatures.extensions))
_customSidebars = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.betaFeatures.customSidebars))
_remoteTmux = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.betaFeatures.remoteTmux))
_workspaceTasks = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.betaFeatures.workspaceTasks))
_workspaceControls = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.betaFeatures.workspaceControls))
}

public var body: some View {
Expand All @@ -38,6 +42,10 @@ public struct BetaFeaturesSection: View {
customSidebarsRow
SettingsCardDivider()
remoteTmuxRow
SettingsCardDivider()
workspaceTasksRow
SettingsCardDivider()
workspaceControlsRow
}
}
.task { startObservingSettings() }
Expand All @@ -50,6 +58,8 @@ public struct BetaFeaturesSection: View {
extensions,
customSidebars,
remoteTmux,
workspaceTasks,
workspaceControls,
]
models.forEach { $0.startObserving() }
}
Expand Down Expand Up @@ -138,6 +148,40 @@ public struct BetaFeaturesSection: View {
.accessibilityIdentifier("SettingsBetaRemoteTmuxToggle")
}
}

@ViewBuilder
private var workspaceTasksRow: some View {
SettingsCardRow(
configurationReview: .settingsOnly,
searchAnchorID: "setting:betaFeatures:workspaceTasks",
String(localized: "settings.betaFeatures.workspaceTasks", defaultValue: "Workspace Tasks"),
subtitle: workspaceTasks.current
? String(localized: "settings.betaFeatures.workspaceTasks.subtitleOn", defaultValue: "Shows native per-workspace task lists, the task surface, task popovers, and task CLI/socket commands.")
: String(localized: "settings.betaFeatures.workspaceTasks.subtitleOff", defaultValue: "Hides workspace task lists until you enable them here.")
) {
Toggle("", isOn: Binding(get: { workspaceTasks.current }, set: { workspaceTasks.set($0) }))
.labelsHidden()
.controlSize(.small)
.accessibilityIdentifier("SettingsBetaWorkspaceTasksToggle")
}
}

@ViewBuilder
private var workspaceControlsRow: some View {
SettingsCardRow(
configurationReview: .settingsOnly,
searchAnchorID: "setting:betaFeatures:workspaceControls",
String(localized: "settings.betaFeatures.workspaceControls", defaultValue: "Workspace Controls"),
subtitle: workspaceControls.current
? String(localized: "settings.betaFeatures.workspaceControls.subtitleOn", defaultValue: "Lets sidebar workspace rows expose configurable cmux hover controls.")
: String(localized: "settings.betaFeatures.workspaceControls.subtitleOff", defaultValue: "Keeps sidebar workspace rows on the existing close-only hover control.")
) {
Toggle("", isOn: Binding(get: { workspaceControls.current }, set: { workspaceControls.set($0) }))
.labelsHidden()
.controlSize(.small)
.accessibilityIdentifier("SettingsBetaWorkspaceControlsToggle")
}
}
}

/// Small warning callout with a yellow triangle, used at the top of
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ public struct SidebarSection: View {
@State private var showMetadata: DefaultsValueModel<Bool>
@State private var rightMaxWidth: DefaultsValueModel<Double>
@State private var rememberedRightMaxWidth: DefaultsValueModel<Double>
@State private var workspaceControlsBeta: DefaultsValueModel<Bool>
@State private var workspaceTasksBeta: DefaultsValueModel<Bool>
@State private var workspaceControls: DefaultsValueModel<[WorkspaceRowControlOption]>

public init(defaultsStore: UserDefaultsSettingsStore, catalog: SettingCatalog, hostActions: SettingsHostActions) {
self.catalog = catalog
Expand All @@ -63,6 +66,9 @@ public struct SidebarSection: View {
_showMetadata = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.sidebar.showCustomMetadata))
_rightMaxWidth = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.sidebar.rightMaxWidth))
_rememberedRightMaxWidth = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.sidebar.rememberedRightMaxWidth))
_workspaceControlsBeta = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.betaFeatures.workspaceControls))
_workspaceTasksBeta = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.betaFeatures.workspaceTasks))
_workspaceControls = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.sidebar.workspaceControls))
}

public var body: some View {
Expand Down Expand Up @@ -96,6 +102,9 @@ public struct SidebarSection: View {
showMetadata,
rightMaxWidth,
rememberedRightMaxWidth,
workspaceControlsBeta,
workspaceTasksBeta,
workspaceControls,
]
models.forEach { $0.startObserving() }
}
Expand Down Expand Up @@ -167,6 +176,23 @@ public struct SidebarSection: View {
rightSidebarWidthSettings.clampedSettingsEditorMaximumWidth(value)
}

private var sanitizedWorkspaceControls: [WorkspaceRowControlOption] {
WorkspaceRowControlOption.sanitized(workspaceControls.current)
}

private func workspaceControlBinding(_ option: WorkspaceRowControlOption) -> Binding<Bool> {
Binding(
get: { sanitizedWorkspaceControls.contains(option) },
set: { enabled in
var next = sanitizedWorkspaceControls.filter { $0 != option }
if enabled {
next.append(option)
}
workspaceControls.set(WorkspaceRowControlOption.sanitized(next))
}
)
}

@ViewBuilder
private var mainCard: some View {
SettingsCard {
Expand Down Expand Up @@ -250,6 +276,43 @@ public struct SidebarSection: View {
}
SettingsCardDivider()

SettingsCardRow(
configurationReview: .json("sidebar.workspaceControls"),
searchAnchorID: "setting:sidebarAppearance:workspace-controls",
String(localized: "settings.sidebar.workspaceControls", defaultValue: "Workspace Controls"),
subtitle: workspaceControlsBeta.current
? String(localized: "settings.sidebar.workspaceControls.subtitleOn", defaultValue: "Choose up to 3 cmux controls for sidebar workspace rows. Close is always included.")
: String(localized: "settings.sidebar.workspaceControls.subtitleOff", defaultValue: "Enable Workspace Controls in Beta Features to customize workspace-row hover controls."),
controlWidth: 250
) {
VStack(alignment: .trailing, spacing: 6) {
Toggle(
String(localized: "settings.sidebar.workspaceControls.close", defaultValue: "Close"),
isOn: .constant(true)
)
.toggleStyle(.checkbox)
.disabled(true)

Toggle(
String(localized: "settings.sidebar.workspaceControls.tasks", defaultValue: "Tasks"),
isOn: workspaceControlBinding(.tasks)
)
.toggleStyle(.checkbox)
.disabled(!workspaceControlsBeta.current || !workspaceTasksBeta.current)
.accessibilityIdentifier("SettingsSidebarWorkspaceControlsTasksToggle")

if workspaceControlsBeta.current, !workspaceTasksBeta.current {
Text(String(localized: "settings.sidebar.workspaceControls.tasksDisabled", defaultValue: "Enable Workspace Tasks to add the Tasks control."))
.cmuxFont(.caption)
.foregroundStyle(.secondary)
.multilineTextAlignment(.trailing)
.fixedSize(horizontal: false, vertical: true)
}
}
}
.disabled(!workspaceControlsBeta.current)
SettingsCardDivider()

SettingsCardRow(
configurationReview: .json("sidebar.hideAllDetails"),
String(localized: "settings.app.hideAllSidebarDetails", defaultValue: "Hide All Sidebar Details"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ struct SettingsRowAnchorResolutionTests {
"setting:betaFeatures:dock",
"setting:betaFeatures:customSidebars",
"setting:betaFeatures:remoteTmux",
"setting:betaFeatures:workspaceTasks",
"setting:betaFeatures:workspaceControls",
"setting:sidebarAppearance:workspace-controls",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"setting:customSidebars:enabled",
"setting:browser:history",
"setting:browser:http-allowlist",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,6 @@ public struct SurfaceKind: RawRepresentable, Hashable, Sendable {
public static let project = SurfaceKind(rawValue: "project")
/// A browser pane owned by a sidebar extension.
public static let extensionBrowser = SurfaceKind(rawValue: "extensionBrowser")
/// A per-workspace task-list pane.
public static let workspaceTasks = SurfaceKind(rawValue: "workspaceTasks")
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import Testing
#expect(SurfaceKind.agentSession.rawValue == "agentSession")
#expect(SurfaceKind.project.rawValue == "project")
#expect(SurfaceKind.extensionBrowser.rawValue == "extensionBrowser")
#expect(SurfaceKind.workspaceTasks.rawValue == "workspaceTasks")
}
}

Expand Down
Loading
Loading