diff --git a/supacode/App/AppShortcuts.swift b/supacode/App/AppShortcuts.swift index e6b31873..f654be1c 100644 --- a/supacode/App/AppShortcuts.swift +++ b/supacode/App/AppShortcuts.swift @@ -93,6 +93,7 @@ enum AppShortcuts { static let openRepository = "open_repository" static let openPullRequest = "open_pull_request" static let toggleLeftSidebar = "toggle_left_sidebar" + static let toggleActiveAgentsPanel = "toggle_active_agents_panel" static let refreshWorktrees = "refresh_worktrees" static let jumpToLatestUnread = "jump_to_latest_unread" static let runScript = "run_script" @@ -182,6 +183,7 @@ enum AppShortcuts { static let openRepository = AppShortcut(key: "o", modifiers: [.command, .shift]) static let openPullRequest = AppShortcut(key: "g", modifiers: [.command, .control]) static let toggleLeftSidebar = AppShortcut(key: "s", modifiers: [.command, .control]) + static let toggleActiveAgentsPanel = AppShortcut(key: "p", modifiers: [.command, .option]) static let refreshWorktrees = AppShortcut(key: "r", modifiers: [.command, .shift]) static let jumpToLatestUnread = AppShortcut(key: "u", modifiers: [.command, .option]) static let runScript = AppShortcut(key: "r", modifiers: .command) @@ -329,6 +331,7 @@ enum AppShortcuts { private static let reservedCustomCommandBindings: [ReservedCustomCommandBinding] = [ .init(actionTitle: "Open Settings", shortcut: openSettings), .init(actionTitle: "Toggle Left Sidebar", shortcut: toggleLeftSidebar), + .init(actionTitle: "Toggle Active Agents Panel", shortcut: toggleActiveAgentsPanel), .init(actionTitle: "Jump to Latest Unread", shortcut: jumpToLatestUnread), .init(actionTitle: "Run Script", shortcut: runScript), .init(actionTitle: "Stop Script", shortcut: stopRunScript), @@ -398,6 +401,12 @@ enum AppShortcuts { scope: .configurableAppAction, shortcut: toggleLeftSidebar ), + .init( + id: CommandID.toggleActiveAgentsPanel, + title: "Toggle Active Agents Panel", + scope: .configurableAppAction, + shortcut: toggleActiveAgentsPanel + ), .init( id: CommandID.refreshWorktrees, title: "Refresh Worktrees", @@ -863,6 +872,7 @@ enum AppShortcuts { openRepository, openPullRequest, toggleLeftSidebar, + toggleActiveAgentsPanel, revealInSidebar, refreshWorktrees, jumpToLatestUnread, diff --git a/supacode/Commands/SidebarCommands.swift b/supacode/Commands/SidebarCommands.swift index 2cdf6cc7..1b531a91 100644 --- a/supacode/Commands/SidebarCommands.swift +++ b/supacode/Commands/SidebarCommands.swift @@ -21,6 +21,13 @@ struct SidebarCommands: Commands { .help(helpText(title: "Reveal in Sidebar", commandID: AppShortcuts.CommandID.revealInSidebar)) .disabled(revealInSidebarAction == nil) Divider() + Button("Active Agents") { + store.send(.repositories(.activeAgents(.togglePanelVisibility))) + } + .modifier( + KeyboardShortcutModifier(shortcut: keyboardShortcut(for: AppShortcuts.CommandID.toggleActiveAgentsPanel)) + ) + .help(helpText(title: "Active Agents", commandID: AppShortcuts.CommandID.toggleActiveAgentsPanel)) Button("Canvas") { store.send(.repositories(.toggleCanvas)) } diff --git a/supacode/Features/ActiveAgents/Reducer/ActiveAgentsFeature.swift b/supacode/Features/ActiveAgents/Reducer/ActiveAgentsFeature.swift index 899f0e58..3095752c 100644 --- a/supacode/Features/ActiveAgents/Reducer/ActiveAgentsFeature.swift +++ b/supacode/Features/ActiveAgents/Reducer/ActiveAgentsFeature.swift @@ -17,7 +17,7 @@ struct ActiveAgentsFeature { } enum Action: Equatable { - case agentEntryChanged(ActiveAgentEntry) + case agentEntryChanged(ActiveAgentEntry, autoShowPanel: Bool) case agentEntryRemoved(ActiveAgentEntry.ID) case entryTapped(ActiveAgentEntry.ID) case togglePanelVisibility @@ -27,8 +27,11 @@ struct ActiveAgentsFeature { var body: some Reducer { Reduce { state, action in switch action { - case .agentEntryChanged(let entry): + case .agentEntryChanged(let entry, let autoShowPanel): state.entries[id: entry.id] = entry + if autoShowPanel, state.isPanelHidden { + state.$isPanelHidden.withLock { $0 = false } + } return .none case .agentEntryRemoved(let id): diff --git a/supacode/Features/ActiveAgents/Views/ActiveAgentRow.swift b/supacode/Features/ActiveAgents/Views/ActiveAgentRow.swift index 270d658f..20889cbd 100644 --- a/supacode/Features/ActiveAgents/Views/ActiveAgentRow.swift +++ b/supacode/Features/ActiveAgents/Views/ActiveAgentRow.swift @@ -7,6 +7,7 @@ struct ActiveAgentRow: View { let branchName: String let repositoryColor: RepositoryColorChoice? let isDimmed: Bool + @Environment(\.accessibilityReduceMotion) private var reduceMotion var body: some View { HStack(spacing: 8) { @@ -57,16 +58,53 @@ struct ActiveAgentRow: View { private var statusPill: some View { HStack(spacing: 4) { if entry.displayState == .working { - ProgressView() - .controlSize(.small) - .frame(width: 10, height: 10) + if reduceMotion { + statusText + } else { + BaguaWorkingIndicator() + } + } else { + statusText } - Text(entry.displayState.label) - .font(.caption2.weight(.semibold)) - .lineLimit(1) } .foregroundStyle(entry.displayState.foregroundStyle) } + + private var statusText: some View { + Text(entry.displayState.label) + .font(.caption2.weight(.semibold)) + .lineLimit(1) + } +} + +struct BaguaWorkingIndicator: View { + static let frames = ["☰", "☱", "☲", "☳", "☴", "☵", "☶", "☷"] + static let frameDuration = 0.12 + + var body: some View { + TimelineView(.periodic(from: .now, by: Self.frameDuration)) { context in + frameText(Self.frame(at: context.date)) + } + } + + static func frame(at date: Date) -> String { + frames[frameIndex(at: date)] + } + + static func frameIndex(at date: Date) -> Int { + let tick = Int(date.timeIntervalSinceReferenceDate / frameDuration) + let cycleLength = (frames.count * 2) - 2 + let cycleIndex = ((tick % cycleLength) + cycleLength) % cycleLength + return cycleIndex < frames.count ? cycleIndex : cycleLength - cycleIndex + } + + private func frameText(_ frame: String) -> some View { + Text(frame) + .font(.system(size: 17, weight: .bold, design: .monospaced)) + .lineLimit(1) + .frame(width: 20, height: 18) + .accessibilityHidden(true) + } } extension AgentDisplayState { diff --git a/supacode/Features/ActiveAgents/Views/ActiveAgentsPanel.swift b/supacode/Features/ActiveAgents/Views/ActiveAgentsPanel.swift index 32cf56f1..b460a40f 100644 --- a/supacode/Features/ActiveAgents/Views/ActiveAgentsPanel.swift +++ b/supacode/Features/ActiveAgents/Views/ActiveAgentsPanel.swift @@ -48,7 +48,7 @@ struct ActiveAgentsPanel: View { ) } .buttonStyle(.plain) - .help("Focus \(entry.agent.displayName) in \(repositoryName(for: entry))") + .help(helpText(for: entry)) } } } @@ -129,6 +129,11 @@ struct ActiveAgentsPanel: View { return false } + private func helpText(for entry: ActiveAgentEntry) -> String { + let trimmed = entry.tabTitle.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmed.isEmpty ? "Untitled tab" : trimmed + } + private var panelBackgroundShape: UnevenRoundedRectangle { UnevenRoundedRectangle( cornerRadii: .init( diff --git a/supacode/Features/App/Reducer/AppFeature.swift b/supacode/Features/App/Reducer/AppFeature.swift index c62d19e7..02cbe69c 100644 --- a/supacode/Features/App/Reducer/AppFeature.swift +++ b/supacode/Features/App/Reducer/AppFeature.swift @@ -1040,7 +1040,13 @@ struct AppFeature { return .none case .terminalEvent(.agentEntryChanged(let entry)): - return .send(.repositories(.activeAgents(.agentEntryChanged(entry)))) + return .send( + .repositories( + .activeAgents( + .agentEntryChanged(entry, autoShowPanel: state.settings.autoShowActiveAgentsPanel) + ) + ) + ) case .terminalEvent(.agentEntryRemoved(let id)): return .send(.repositories(.activeAgents(.agentEntryRemoved(id)))) diff --git a/supacode/Features/Repositories/Views/SidebarFooterView.swift b/supacode/Features/Repositories/Views/SidebarFooterView.swift index ba69d95f..e141e0c6 100644 --- a/supacode/Features/Repositories/Views/SidebarFooterView.swift +++ b/supacode/Features/Repositories/Views/SidebarFooterView.swift @@ -44,7 +44,13 @@ struct SidebarFooterView: View { Image(systemName: Self.activeAgentsPanelIconName(isPanelHidden: store.state.activeAgents.isPanelHidden)) .accessibilityLabel(store.state.activeAgents.isPanelHidden ? "Show Active Agents" : "Hide Active Agents") } - .help(store.state.activeAgents.isPanelHidden ? "Show Active Agents" : "Hide Active Agents") + .help( + AppShortcuts.helpText( + title: store.state.activeAgents.isPanelHidden ? "Show Active Agents" : "Hide Active Agents", + commandID: AppShortcuts.CommandID.toggleActiveAgentsPanel, + in: resolvedKeybindings + ) + ) Button { store.send(.refreshWorktrees) } label: { @@ -101,6 +107,6 @@ struct SidebarFooterView: View { } static func activeAgentsPanelIconName(isPanelHidden: Bool) -> String { - isPanelHidden ? "eye" : "eye.slash" + isPanelHidden ? "person.crop.rectangle.stack" : "person.crop.rectangle.stack.fill" } } diff --git a/supacode/Features/Repositories/Views/SidebarListView.swift b/supacode/Features/Repositories/Views/SidebarListView.swift index b2562eca..27b692a6 100644 --- a/supacode/Features/Repositories/Views/SidebarListView.swift +++ b/supacode/Features/Repositories/Views/SidebarListView.swift @@ -77,7 +77,8 @@ struct SidebarListView: View { ) let panelHeight = min(resizingPanelHeight ?? state.activeAgents.panelHeight, maximumPanelHeight) let panelOffset = state.activeAgents.isPanelHidden ? panelHeight : 0 - let listBottomPadding = state.activeAgents.isPanelHidden ? 0 : panelHeight + let activeAgentsPanelTopGap = 4.0 + let listBottomPadding = state.activeAgents.isPanelHidden ? 0 : panelHeight + activeAgentsPanelTopGap ScrollViewReader { scrollProxy in ScrollView { diff --git a/supacode/Features/Settings/Models/GlobalSettings.swift b/supacode/Features/Settings/Models/GlobalSettings.swift index 9a43b675..533b6517 100644 --- a/supacode/Features/Settings/Models/GlobalSettings.swift +++ b/supacode/Features/Settings/Models/GlobalSettings.swift @@ -28,6 +28,7 @@ nonisolated struct GlobalSettings: Codable, Equatable, Sendable { var keybindingUserOverrides: KeybindingUserOverrideStore var defaultViewMode: DefaultViewMode var dimUnfocusedSplits: Bool + var autoShowActiveAgentsPanel: Bool static let `default` = GlobalSettings( appearanceMode: .dark, @@ -58,7 +59,8 @@ nonisolated struct GlobalSettings: Codable, Equatable, Sendable { terminalFontSize: nil, keybindingUserOverrides: .empty, defaultViewMode: .normal, - dimUnfocusedSplits: true + dimUnfocusedSplits: true, + autoShowActiveAgentsPanel: false ) init( @@ -90,7 +92,8 @@ nonisolated struct GlobalSettings: Codable, Equatable, Sendable { terminalFontSize: Float32? = nil, keybindingUserOverrides: KeybindingUserOverrideStore = .empty, defaultViewMode: DefaultViewMode = .normal, - dimUnfocusedSplits: Bool = true + dimUnfocusedSplits: Bool = true, + autoShowActiveAgentsPanel: Bool = false ) { self.appearanceMode = appearanceMode self.defaultEditorID = defaultEditorID @@ -121,6 +124,7 @@ nonisolated struct GlobalSettings: Codable, Equatable, Sendable { self.keybindingUserOverrides = keybindingUserOverrides self.defaultViewMode = defaultViewMode self.dimUnfocusedSplits = dimUnfocusedSplits + self.autoShowActiveAgentsPanel = autoShowActiveAgentsPanel } func encode(to encoder: any Encoder) throws { @@ -154,6 +158,7 @@ nonisolated struct GlobalSettings: Codable, Equatable, Sendable { try container.encode(keybindingUserOverrides, forKey: .keybindingUserOverrides) try container.encode(defaultViewMode, forKey: .defaultViewMode) try container.encode(dimUnfocusedSplits, forKey: .dimUnfocusedSplits) + try container.encode(autoShowActiveAgentsPanel, forKey: .autoShowActiveAgentsPanel) } private enum CodingKeys: String, CodingKey { @@ -186,6 +191,7 @@ nonisolated struct GlobalSettings: Codable, Equatable, Sendable { case keybindingUserOverrides case defaultViewMode case dimUnfocusedSplits + case autoShowActiveAgentsPanel // Legacy key for migration case automaticallyArchiveMergedWorktrees } @@ -281,5 +287,8 @@ nonisolated struct GlobalSettings: Codable, Equatable, Sendable { dimUnfocusedSplits = try container.decodeIfPresent(Bool.self, forKey: .dimUnfocusedSplits) ?? Self.default.dimUnfocusedSplits + autoShowActiveAgentsPanel = + try container.decodeIfPresent(Bool.self, forKey: .autoShowActiveAgentsPanel) + ?? Self.default.autoShowActiveAgentsPanel } } diff --git a/supacode/Features/Settings/Reducer/SettingsFeature.swift b/supacode/Features/Settings/Reducer/SettingsFeature.swift index f12d22e1..f8e9a31c 100644 --- a/supacode/Features/Settings/Reducer/SettingsFeature.swift +++ b/supacode/Features/Settings/Reducer/SettingsFeature.swift @@ -34,6 +34,7 @@ struct SettingsFeature { var keybindingUserOverrides: KeybindingUserOverrideStore var defaultViewMode: DefaultViewMode var dimUnfocusedSplits: Bool + var autoShowActiveAgentsPanel: Bool var cliInstallStatus: CLIInstallStatus = .notInstalled var cliInstallShowAlert: Bool = true var selection: SettingsSection? = .general @@ -72,6 +73,7 @@ struct SettingsFeature { keybindingUserOverrides = settings.keybindingUserOverrides defaultViewMode = settings.defaultViewMode dimUnfocusedSplits = settings.dimUnfocusedSplits + autoShowActiveAgentsPanel = settings.autoShowActiveAgentsPanel } var globalSettings: GlobalSettings { @@ -106,7 +108,8 @@ struct SettingsFeature { terminalFontSize: terminalFontSize, keybindingUserOverrides: keybindingUserOverrides, defaultViewMode: defaultViewMode, - dimUnfocusedSplits: dimUnfocusedSplits + dimUnfocusedSplits: dimUnfocusedSplits, + autoShowActiveAgentsPanel: autoShowActiveAgentsPanel ) } } @@ -208,6 +211,7 @@ struct SettingsFeature { state.keybindingUserOverrides = normalizedSettings.keybindingUserOverrides state.defaultViewMode = normalizedSettings.defaultViewMode state.dimUnfocusedSplits = normalizedSettings.dimUnfocusedSplits + state.autoShowActiveAgentsPanel = normalizedSettings.autoShowActiveAgentsPanel state.syncGlobalDefaults(from: normalizedSettings) return .send(.delegate(.settingsChanged(normalizedSettings))) diff --git a/supacode/Features/Settings/Views/AppearanceSettingsView.swift b/supacode/Features/Settings/Views/AppearanceSettingsView.swift index fadb84b5..bc255217 100644 --- a/supacode/Features/Settings/Views/AppearanceSettingsView.swift +++ b/supacode/Features/Settings/Views/AppearanceSettingsView.swift @@ -53,6 +53,16 @@ struct AppearanceSettingsView: View { ) .help("Fade split panes that aren't focused so the active one stands out.") } + Section("Active Agents") { + Toggle( + "Show Active Agents panel automatically", + isOn: $store.autoShowActiveAgentsPanel + ) + .help("Open the Active Agents panel when an agent is detected.") + Text("When enabled, hidden panels reopen as soon as an agent starts or updates.") + .foregroundStyle(.secondary) + .font(.callout) + } Section("Default View") { Picker("Launch in", selection: $store.defaultViewMode) { ForEach(DefaultViewMode.allCases) { mode in diff --git a/supacodeTests/ActiveAgentsFeatureTests.swift b/supacodeTests/ActiveAgentsFeatureTests.swift index a3e7d7b0..7a9058ee 100644 --- a/supacodeTests/ActiveAgentsFeatureTests.swift +++ b/supacodeTests/ActiveAgentsFeatureTests.swift @@ -19,23 +19,37 @@ struct ActiveAgentsFeatureTests { let done = entry(id: UUID(3), state: .done, changedAt: new) let updatedIdle = entry(id: UUID(0), state: .blocked, changedAt: Date(timeIntervalSince1970: 30)) - await store.send(.agentEntryChanged(idle)) { + await store.send(.agentEntryChanged(idle, autoShowPanel: false)) { $0.entries = [idle] } - await store.send(.agentEntryChanged(blocked)) { + await store.send(.agentEntryChanged(blocked, autoShowPanel: false)) { $0.entries = [idle, blocked] } - await store.send(.agentEntryChanged(working)) { + await store.send(.agentEntryChanged(working, autoShowPanel: false)) { $0.entries = [idle, blocked, working] } - await store.send(.agentEntryChanged(done)) { + await store.send(.agentEntryChanged(done, autoShowPanel: false)) { $0.entries = [idle, blocked, working, done] } - await store.send(.agentEntryChanged(updatedIdle)) { + await store.send(.agentEntryChanged(updatedIdle, autoShowPanel: false)) { $0.entries = [updatedIdle, blocked, working, done] } } + @Test func autoShowRevealsHiddenPanelOnAgentEntry() async { + var state = ActiveAgentsFeature.State() + state.$isPanelHidden.withLock { $0 = true } + let store = TestStore(initialState: state) { + ActiveAgentsFeature() + } + let agent = entry(id: UUID(0), state: .working, changedAt: Date(timeIntervalSince1970: 10)) + + await store.send(.agentEntryChanged(agent, autoShowPanel: true)) { + $0.entries = [agent] + $0.$isPanelHidden.withLock { $0 = false } + } + } + @Test func panelHeightIsClamped() async { let store = TestStore(initialState: ActiveAgentsFeature.State()) { ActiveAgentsFeature() diff --git a/supacodeTests/AppFeatureSettingsChangedTests.swift b/supacodeTests/AppFeatureSettingsChangedTests.swift index 387e726c..6c7d782d 100644 --- a/supacodeTests/AppFeatureSettingsChangedTests.swift +++ b/supacodeTests/AppFeatureSettingsChangedTests.swift @@ -1,11 +1,16 @@ import ComposableArchitecture import CustomDump import DependenciesTestSupport +import Foundation import Testing @testable import supacode @MainActor +@Suite( + .serialized, + .dependency(\.defaultAppStorage, .appFeatureSettingsChangedTests) +) struct AppFeatureSettingsChangedTests { @Test(.dependencies) func settingsChangedPropagatesRepositorySettings() async { var settings = GlobalSettings.default @@ -58,6 +63,42 @@ struct AppFeatureSettingsChangedTests { #expect(watcherCommands.value.isEmpty) } + @Test(.dependencies) func agentEntryAutoShowsActiveAgentsPanelWhenEnabled() async { + var settings = SettingsFeature.State() + settings.autoShowActiveAgentsPanel = true + UserDefaults.appFeatureSettingsChangedTests.set(true, forKey: "activeAgentsPanelHidden") + let state = AppFeature.State(settings: settings) + let entry = activeAgentEntry() + + let store = TestStore(initialState: state) { + AppFeature() + } + + await store.send(.terminalEvent(.agentEntryChanged(entry))) { + $0.repositories.activeAgents.$isPanelHidden.withLock { $0 = false } + } + await store.receive(\.repositories.activeAgents.agentEntryChanged) { + $0.repositories.activeAgents.entries = [entry] + } + } + + @Test(.dependencies) func agentEntryKeepsActiveAgentsPanelHiddenWhenAutoShowDisabled() async { + var settings = SettingsFeature.State() + settings.autoShowActiveAgentsPanel = false + UserDefaults.appFeatureSettingsChangedTests.set(true, forKey: "activeAgentsPanelHidden") + let state = AppFeature.State(settings: settings) + let entry = activeAgentEntry() + + let store = TestStore(initialState: state) { + AppFeature() + } + + await store.send(.terminalEvent(.agentEntryChanged(entry))) + await store.receive(\.repositories.activeAgents.agentEntryChanged) { + $0.repositories.activeAgents.entries = [entry] + } + } + @Test(.dependencies) func settingsChangedRecomputesResolvedKeybindings() async { var settings = GlobalSettings.default settings.keybindingUserOverrides = KeybindingUserOverrideStore( @@ -114,4 +155,30 @@ struct AppFeatureSettingsChangedTests { $0.repositories.statusToast = .success("Saved terminal layout cleared") } } + + private func activeAgentEntry() -> ActiveAgentEntry { + ActiveAgentEntry( + id: fixedUUID(0), + worktreeID: "/repo/wt", + worktreeName: "wt", + tabID: TerminalTabID(rawValue: fixedUUID(1)), + tabTitle: "codex", + surfaceID: fixedUUID(0), + paneIndex: 1, + agent: .codex, + rawState: .working, + displayState: .working, + lastChangedAt: Date(timeIntervalSince1970: 10) + ) + } + + private func fixedUUID(_ value: UInt8) -> UUID { + UUID(uuid: (value, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)) + } +} + +extension UserDefaults { + fileprivate nonisolated(unsafe) static let appFeatureSettingsChangedTests = UserDefaults( + suiteName: "com.onevcat.Prowl.AppFeatureSettingsChangedTests" + )! } diff --git a/supacodeTests/AppShortcutsTests.swift b/supacodeTests/AppShortcutsTests.swift index 011afa1b..00063691 100644 --- a/supacodeTests/AppShortcutsTests.swift +++ b/supacodeTests/AppShortcutsTests.swift @@ -92,6 +92,7 @@ struct AppShortcutsTests { [ "openSettings=\(AppShortcuts.openSettings.display)", "toggleLeftSidebar=\(AppShortcuts.toggleLeftSidebar.display)", + "toggleActiveAgentsPanel=\(AppShortcuts.toggleActiveAgentsPanel.display)", "runScript=\(AppShortcuts.runScript.display)", "stopRunScript=\(AppShortcuts.stopRunScript.display)", "checkForUpdates=\(AppShortcuts.checkForUpdates.display)", @@ -119,6 +120,7 @@ struct AppShortcutsTests { [ "openSettings=⌘,", "toggleLeftSidebar=⌘⌃S", + "toggleActiveAgentsPanel=⌘⌥P", "runScript=⌘R", "stopRunScript=⌘.", "checkForUpdates=⌘⇧U", @@ -154,6 +156,10 @@ struct AppShortcutsTests { idToDisplay["command_palette"], AppShortcuts.commandPalette.display ) + expectNoDifference( + idToDisplay["toggle_active_agents_panel"], + AppShortcuts.toggleActiveAgentsPanel.display + ) expectNoDifference( idToDisplay["quit_application"], AppShortcuts.quitApplication.display @@ -168,6 +174,7 @@ struct AppShortcutsTests { ) #expect(idToScope["command_palette"] == .configurableAppAction) + #expect(idToScope["toggle_active_agents_panel"] == .configurableAppAction) #expect(idToScope["quit_application"] == .systemFixedAppAction) #expect(idToScope["rename_branch"] == .localInteraction) #expect(idToScope["select_all_canvas_cards"] == .localInteraction) diff --git a/supacodeTests/BaguaWorkingIndicatorTests.swift b/supacodeTests/BaguaWorkingIndicatorTests.swift new file mode 100644 index 00000000..e221d2bd --- /dev/null +++ b/supacodeTests/BaguaWorkingIndicatorTests.swift @@ -0,0 +1,21 @@ +import Foundation +import Testing + +@testable import supacode + +struct BaguaWorkingIndicatorTests { + @Test func framesUseFullTrigramSequence() { + #expect(BaguaWorkingIndicator.frames == ["☰", "☱", "☲", "☳", "☴", "☵", "☶", "☷"]) + } + + @Test func frameSelectionLoopsByDuration() { + let duration = BaguaWorkingIndicator.frameDuration + + #expect(BaguaWorkingIndicator.frame(at: Date(timeIntervalSinceReferenceDate: 0)) == "☰") + #expect(BaguaWorkingIndicator.frame(at: Date(timeIntervalSinceReferenceDate: duration)) == "☱") + #expect(BaguaWorkingIndicator.frame(at: Date(timeIntervalSinceReferenceDate: duration * 7)) == "☷") + #expect(BaguaWorkingIndicator.frame(at: Date(timeIntervalSinceReferenceDate: duration * 8)) == "☶") + #expect(BaguaWorkingIndicator.frame(at: Date(timeIntervalSinceReferenceDate: duration * 13)) == "☱") + #expect(BaguaWorkingIndicator.frame(at: Date(timeIntervalSinceReferenceDate: duration * 14)) == "☰") + } +} diff --git a/supacodeTests/SettingsFeatureTests.swift b/supacodeTests/SettingsFeatureTests.swift index e689a3e6..a71ed446 100644 --- a/supacodeTests/SettingsFeatureTests.swift +++ b/supacodeTests/SettingsFeatureTests.swift @@ -364,6 +364,24 @@ struct SettingsFeatureTests { #expect(settingsFile.global.keybindingUserOverrides == overrides) } + @Test(.dependencies) func autoShowActiveAgentsPanelPersistsChanges() async { + var initialSettings = GlobalSettings.default + initialSettings.autoShowActiveAgentsPanel = false + @Shared(.settingsFile) var settingsFile + $settingsFile.withLock { $0.global = initialSettings } + + let store = TestStore(initialState: SettingsFeature.State(settings: initialSettings)) { + SettingsFeature() + } + + await store.send(.binding(.set(\.autoShowActiveAgentsPanel, true))) { + $0.autoShowActiveAgentsPanel = true + } + await store.receive(\.delegate.settingsChanged) + + #expect(settingsFile.global.autoShowActiveAgentsPanel == true) + } + @Test(.dependencies) func disablingAnalyticsResetsClient() async { var initialSettings = GlobalSettings.default initialSettings.analyticsEnabled = true diff --git a/supacodeTests/SidebarFooterViewTests.swift b/supacodeTests/SidebarFooterViewTests.swift index 760d45d6..95e4318d 100644 --- a/supacodeTests/SidebarFooterViewTests.swift +++ b/supacodeTests/SidebarFooterViewTests.swift @@ -4,7 +4,7 @@ import Testing struct SidebarFooterViewTests { @Test func activeAgentsPanelToggleUsesStableSymbols() { - #expect(SidebarFooterView.activeAgentsPanelIconName(isPanelHidden: true) == "eye") - #expect(SidebarFooterView.activeAgentsPanelIconName(isPanelHidden: false) == "eye.slash") + #expect(SidebarFooterView.activeAgentsPanelIconName(isPanelHidden: true) == "person.crop.rectangle.stack") + #expect(SidebarFooterView.activeAgentsPanelIconName(isPanelHidden: false) == "person.crop.rectangle.stack.fill") } }