Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 24 additions & 0 deletions GhosttyTabs.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -64327,6 +64327,13 @@
}
}
},
"settings.betaFeatures.dock": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Dock" } }, "ja": { "stringUnit": { "state": "translated", "value": "Dock" } } } },
"settings.betaFeatures.dock.subtitleOff": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Hides Dock from the right sidebar until you enable it here." } }, "ja": { "stringUnit": { "state": "translated", "value": "ここで有効にするまで、Dock を右サイドバーから隠します。" } } } },
"settings.betaFeatures.dock.subtitleOn": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Shows Dock in the right sidebar mode switcher for custom terminal controls." } }, "ja": { "stringUnit": { "state": "translated", "value": "Dock を右サイドバーのモード切り替えに表示し、カスタム端末コントロールを使えるようにします。" } } } },
"settings.betaFeatures.feed": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Feed" } }, "ja": { "stringUnit": { "state": "translated", "value": "Feed" } } } },
"settings.betaFeatures.feed.subtitleOff": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Hides Feed from the right sidebar until you enable it here." } }, "ja": { "stringUnit": { "state": "translated", "value": "ここで有効にするまで、Feed を右サイドバーから隠します。" } } } },
"settings.betaFeatures.feed.subtitleOn": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Shows Feed in the right sidebar mode switcher for inline agent decisions." } }, "ja": { "stringUnit": { "state": "translated", "value": "Feed を右サイドバーのモード切り替えに表示し、エージェントの判断をインラインで扱えるようにします。" } } } },
"settings.betaFeatures.warning": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "These features are unstable and may change or break. Enable them only when you are testing them." } }, "ja": { "stringUnit": { "state": "translated", "value": "これらの機能は不安定で、変更されたり壊れたりする可能性があります。テストする場合だけ有効にしてください。" } } } },
"settings.browser.enabled": {
"extractionState": "manual",
"localizations": {
Expand Down Expand Up @@ -68186,6 +68193,7 @@
}
}
},
"settings.section.betaFeatures": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Beta Features" } }, "ja": { "stringUnit": { "state": "translated", "value": "ベータ機能" } } } },
"settings.section.browser": {
"extractionState": "manual",
"localizations": {
Expand Down
18 changes: 18 additions & 0 deletions Sources/App/WorkspaceRuntimeSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,24 @@ enum TerminalScrollBarSettings {
}
}

enum RightSidebarBetaFeatureSettings {
static let feedEnabledKey = "rightSidebar.beta.feed.enabled"
static let dockEnabledKey = "rightSidebar.beta.dock.enabled"

static let defaultFeedEnabled = false
static let defaultDockEnabled = false

nonisolated static func isFeedEnabled(defaults: UserDefaults = .standard) -> Bool {
guard defaults.object(forKey: feedEnabledKey) != nil else { return defaultFeedEnabled }
return defaults.bool(forKey: feedEnabledKey)
}

nonisolated static func isDockEnabled(defaults: UserDefaults = .standard) -> Bool {
guard defaults.object(forKey: dockEnabledKey) != nil else { return defaultDockEnabled }
return defaults.bool(forKey: dockEnabledKey)
}
}
Comment on lines +112 to +128

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 RightSidebarBetaFeatureSettings static helpers should be nonisolated

The new static methods isFeedEnabled(defaults:) and isDockEnabled(defaults:) read from UserDefaults — a pure, thread-safe value operation with no UI coupling. Without an explicit nonisolated annotation, they inherit the module's default actor isolation (likely @MainActor in a Swift 6 target), unnecessarily coupling them to the main actor and preventing calls from non-isolated async contexts. Per the actor-isolation rule, static utility helpers that do not interact with UI state should be marked nonisolated.

Rule Used: Flag new or materially worsened Swift 6 actor isol... (source)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by marking the pure RightSidebarBetaFeatureSettings lookup helpers nonisolated.

— Claude Code


enum UITestLaunchManifest {
static let argumentName = "-cmuxUITestLaunchManifest"

Expand Down
104 changes: 104 additions & 0 deletions Sources/BetaFeaturesSettingsView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import SwiftUI

struct BetaFeaturesSettingsView: View {
@Binding var feedEnabled: Bool
@Binding var dockEnabled: Bool

private var feedSubtitle: String {
if feedEnabled {
return String(
localized: "settings.betaFeatures.feed.subtitleOn",
defaultValue: "Shows Feed in the right sidebar mode switcher for inline agent decisions."
)
}
return String(
localized: "settings.betaFeatures.feed.subtitleOff",
defaultValue: "Hides Feed from the right sidebar until you enable it here."
)
}

private var dockSubtitle: String {
if dockEnabled {
return String(
localized: "settings.betaFeatures.dock.subtitleOn",
defaultValue: "Shows Dock in the right sidebar mode switcher for custom terminal controls."
)
}
return String(
localized: "settings.betaFeatures.dock.subtitleOff",
defaultValue: "Hides Dock from the right sidebar until you enable it here."
)
}

var body: some View {
SettingsSectionHeader(title: String(localized: "settings.section.betaFeatures", defaultValue: "Beta Features"))
.settingsSearchAnchor(SettingsSearchIndex.sectionID(for: .betaFeatures))
SettingsCard {
BetaFeaturesWarningNote(
String(
localized: "settings.betaFeatures.warning",
defaultValue: "These features are unstable and may change or break. Enable them only when you are testing them."
)
)

SettingsCardDivider()

SettingsCardRow(
configurationReview: .settingsOnly,
String(localized: "settings.betaFeatures.feed", defaultValue: "Feed"),
subtitle: feedSubtitle,
searchAnchorID: SettingsSearchIndex.settingID(for: .betaFeatures, idSuffix: "feed")
) {
Toggle("", isOn: $feedEnabled)
.labelsHidden()
.controlSize(.small)
.accessibilityIdentifier("SettingsBetaFeedToggle")
.accessibilityLabel(
String(localized: "settings.betaFeatures.feed", defaultValue: "Feed")
)
}

SettingsCardDivider()

SettingsCardRow(
configurationReview: .settingsOnly,
String(localized: "settings.betaFeatures.dock", defaultValue: "Dock"),
subtitle: dockSubtitle,
searchAnchorID: SettingsSearchIndex.settingID(for: .betaFeatures, idSuffix: "dock")
) {
Toggle("", isOn: $dockEnabled)
.labelsHidden()
.controlSize(.small)
.accessibilityIdentifier("SettingsBetaDockToggle")
.accessibilityLabel(
String(localized: "settings.betaFeatures.dock", defaultValue: "Dock")
)
}
}
}
}

private struct BetaFeaturesWarningNote: View {
let text: String

init(_ text: String) {
self.text = text
}

var body: some View {
HStack(alignment: .top, spacing: 8) {
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(.yellow)
.accessibilityHidden(true)

Text(text)
.font(.caption)
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
.padding(.horizontal, 14)
.padding(.vertical, 8)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
23 changes: 20 additions & 3 deletions Sources/ContentView+RightSidebarCommandPalette.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Foundation
import AppKit

extension ContentView {
static func commandPaletteShortcutAction(forCommandID commandId: String) -> KeyboardShortcutSettings.Action? {
Expand Down Expand Up @@ -83,7 +83,7 @@ extension ContentView {
{ _ in value }
}

return RightSidebarMode.allCases.map { mode in
return RightSidebarMode.availableModes().map { mode in
CommandPaletteCommandContribution(
commandId: Self.commandPaletteRightSidebarModeCommandID(mode),
title: constant(mode.shortcutAction.label),
Expand All @@ -108,10 +108,27 @@ extension ContentView {
}
}

func handleCommandPaletteRightSidebarMode(_ mode: RightSidebarMode, observedWindow: NSWindow?) {
guard mode.isAvailable() else {
NSSound.beep()
return
}
if AppDelegate.shared?.focusRightSidebarInActiveMainWindow(
mode: mode,
focusFirstItem: true,
preferredWindow: observedWindow ?? NSApp.keyWindow ?? NSApp.mainWindow
) != true {
fileExplorerState.setVisible(true)
if fileExplorerState.mode != mode {
fileExplorerState.mode = mode
}
}
}

private static func commandPaletteRightSidebarModeShortcutAction(
forCommandID commandID: String
) -> KeyboardShortcutSettings.Action? {
RightSidebarMode.allCases.first { mode in
RightSidebarMode.availableModes().first { mode in
Self.commandPaletteRightSidebarModeCommandID(mode) == commandID
}?.shortcutAction
}
Expand Down
11 changes: 1 addition & 10 deletions Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7882,16 +7882,7 @@ struct ContentView: View {
}
for mode in RightSidebarMode.allCases {
registry.register(commandId: Self.commandPaletteRightSidebarModeCommandID(mode)) {
if AppDelegate.shared?.focusRightSidebarInActiveMainWindow(
mode: mode,
focusFirstItem: true,
preferredWindow: observedWindow ?? NSApp.keyWindow ?? NSApp.mainWindow
) != true {
fileExplorerState.setVisible(true)
if fileExplorerState.mode != mode {
fileExplorerState.mode = mode
}
}
handleCommandPaletteRightSidebarMode(mode, observedWindow: observedWindow)
}
}
registry.register(commandId: "palette.toggleMatchTerminalBackground") {
Expand Down
2 changes: 1 addition & 1 deletion Sources/Feed/FeedPanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ struct FeedPanelView: View {

private var controlBarContent: some View {
HStack(spacing: 6) {
ForEach(Filter.allCases) { f in
ForEach([Filter.actionable]) { f in
FeedSecondaryFilterButton(
filter: f,
isSelected: filter == f
Expand Down
94 changes: 94 additions & 0 deletions Sources/FileExplorerState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import AppKit
import SwiftUI

// MARK: - State (visibility toggle)

final class FileExplorerState: ObservableObject {
private static let modeKey = "rightSidebar.mode"

@Published var isVisible: Bool {
didSet { UserDefaults.standard.set(isVisible, forKey: "fileExplorer.isVisible") }
}
@Published var width: CGFloat {
didSet { UserDefaults.standard.set(Double(width), forKey: "fileExplorer.width") }
}

/// Proportion of sidebar height allocated to the tab list (0.0-1.0).
/// The file explorer gets the remaining space below.
@Published var dividerPosition: CGFloat {
didSet { UserDefaults.standard.set(Double(dividerPosition), forKey: "fileExplorer.dividerPosition") }
}

/// Whether hidden files (dotfiles) are shown in the tree.
@Published var showHiddenFiles: Bool {
didSet { UserDefaults.standard.set(showHiddenFiles, forKey: "fileExplorer.showHidden") }
}

@Published private var storedMode: RightSidebarMode

/// Active mode for the right sidebar (file tree, search, sessions, or enabled beta modes).
var mode: RightSidebarMode {
get { storedMode }
set { setMode(newValue) }
}

init() {
let defaults = UserDefaults.standard
self.isVisible = defaults.bool(forKey: "fileExplorer.isVisible")
let storedWidth = defaults.double(forKey: "fileExplorer.width")
self.width = storedWidth > 0 ? CGFloat(storedWidth) : 220
let storedPosition = defaults.double(forKey: "fileExplorer.dividerPosition")
self.dividerPosition = storedPosition > 0 ? CGFloat(storedPosition) : 0.6
let storedShowHidden = defaults.object(forKey: "fileExplorer.showHidden")
self.showHiddenFiles = storedShowHidden == nil ? true : defaults.bool(forKey: "fileExplorer.showHidden")
let storedMode = RightSidebarMode(rawValue: defaults.string(forKey: Self.modeKey) ?? "") ?? .files
self.storedMode = Self.availableMode(storedMode, defaults: defaults)
defaults.set(self.storedMode.rawValue, forKey: Self.modeKey)
}

func refreshModeAvailability(defaults: UserDefaults = .standard) {
setMode(storedMode, defaults: defaults)
}

func toggle() {
setVisible(!isVisible)
}

func setVisible(_ nextValue: Bool) {
guard isVisible != nextValue else { return }

// Suppress both SwiftUI transactions and AppKit/Core Animation implicit layout changes.
NSAnimationContext.beginGrouping()
CATransaction.begin()
defer {
CATransaction.commit()
NSAnimationContext.endGrouping()
}

NSAnimationContext.current.duration = 0
NSAnimationContext.current.allowsImplicitAnimation = false
CATransaction.setDisableActions(true)

var transaction = Transaction(animation: nil)
transaction.disablesAnimations = true
withTransaction(transaction) {
isVisible = nextValue
}
}

private func setMode(_ mode: RightSidebarMode, defaults: UserDefaults = .standard) {
let nextMode = Self.availableMode(mode, defaults: defaults)
guard storedMode != nextMode else {
if defaults.string(forKey: Self.modeKey) != nextMode.rawValue {
defaults.set(nextMode.rawValue, forKey: Self.modeKey)
}
return
}
storedMode = nextMode
defaults.set(nextMode.rawValue, forKey: Self.modeKey)
}

private static func availableMode(_ mode: RightSidebarMode, defaults: UserDefaults) -> RightSidebarMode {
mode.isAvailable(defaults: defaults) ? mode : .files
}
}
Loading
Loading