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
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ struct LoopWorkspaceFeature {
/// `@State` because the *toggle* lives in the window toolbar, which `AppView` owns —
/// and a control and the thing it controls cannot hold the answer separately.
var isRailVisible = LoopWorkspaceRail.loadVisible()
var hasCustomRailVisibility = LoopWorkspaceRail.hasStoredVisibility()
/// How wide the rail is, after any drag on its edge. Persisted like its visibility:
/// someone who widened it to read beats did not mean only this session.
var railWidth = LoopWorkspaceRail.loadWidth()
Expand Down Expand Up @@ -109,6 +110,7 @@ struct LoopWorkspaceFeature {
/// and handled up there, the way `.nodeTapped` already is.
/// ⌥G, and the toolbar's trailing panel toggle.
case railToggled
case mailroomContentChanged(hasContent: Bool)
/// The rail's leading edge was dragged. Sent once, on release — a per-frame action
/// would put a reducer run and a `UserDefaults` write behind every pixel.
case railWidthChanged(CGFloat)
Expand Down Expand Up @@ -273,9 +275,15 @@ struct LoopWorkspaceFeature {

case .railToggled:
state.isRailVisible.toggle()
state.hasCustomRailVisibility = true
LoopWorkspaceRail.saveVisible(state.isRailVisible)
return .none

case .mailroomContentChanged(let hasContent):
guard hasContent, !state.hasCustomRailVisibility else { return .none }
state.isRailVisible = true
return .none

case .railWidthChanged(let width):
state.railWidth = LoopWorkspaceRail.clamped(width)
LoopWorkspaceRail.saveWidth(state.railWidth)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,11 @@ struct LoopWorkspaceRail: View {
UserDefaults.standard.set(folded, forKey: summaryFoldedDefaultsKey)
}

/// **Off** until someone asks for it. It used to default on, which meant every loop
/// that feeds nothing opened with 212 points of empty panel beside its terminal.
static func hasStoredVisibility() -> Bool {
UserDefaults.standard.object(forKey: visibleDefaultsKey) as? Bool != nil
}

/// Starts hidden; Mailroom content may reveal it until a person chooses otherwise.
static func loadVisible() -> Bool {
UserDefaults.standard.object(forKey: visibleDefaultsKey) as? Bool ?? false
}
Expand Down
10 changes: 10 additions & 0 deletions graphcode/Sources/Features/LoopWorkspace/LoopWorkspaceView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ struct LoopWorkspaceView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity)
.overlay { expandedBoard }
.onReceive(CanvasClock.tick) { now = $0 }
.onChange(of: mailroomHasContent, initial: true) { _, hasContent in
store.send(.mailroomContentChanged(hasContent: hasContent))
}
.onChange(of: store.id) { _, _ in
store.send(.mailroomContentChanged(hasContent: mailroomHasContent))
}
.onDisappear { store.send(.workspaceLeft) }
// The folder header goes in the toolbar, not in the `VStack` above, and the pane
// does *not* claim the titlebar inset. Both were tried: `.ignoresSafeArea(.top)`
Expand Down Expand Up @@ -142,6 +148,10 @@ struct LoopWorkspaceView: View {
LoopWorkspaceRail.hasContent(node: store.node, graph: store.graph)
}

private var mailroomHasContent: Bool {
MailroomPresentation.hasContent(graph: store.graph, enabled: mailroomEnabled)
}

private var railShortcut: some View {
Button("") { store.send(.railToggled) }
.keyboardShortcut("g", modifiers: .option)
Expand Down
69 changes: 67 additions & 2 deletions graphcode/Tests/LoopWorkspaceFeatureTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Testing
/// persists via `TerminalLayoutStore`, so these also incidentally cover that the right
/// save happens at the right time — a temp-directory-backed store is injected so tests
/// never touch the app's real Application Support folder.
@Suite
@Suite(.serialized)
struct LoopWorkspaceFeatureTests {
/// Records what the reducer asks `TerminalSurfaceClient` to end: the attach going
/// away (`retired`) and the zmx sessions being terminated behind it (`killed`).
Expand Down Expand Up @@ -443,7 +443,10 @@ struct LoopWorkspaceFeatureTests {
) { LoopWorkspaceFeature() }
store.exhaustivity = .off

await store.send(.railToggled) { $0.isRailVisible = true }
await store.send(.railToggled) {
$0.isRailVisible = true
$0.hasCustomRailVisibility = true
}
#expect(LoopWorkspaceRail.loadVisible())
}

Expand All @@ -459,3 +462,65 @@ struct LoopWorkspaceFeatureTests {
#expect(LoopWorkspaceRail.loadVisible() == false)
}
}

extension LoopWorkspaceFeatureTests {
@Test(arguments: [false, true])
@MainActor
func mailroomContentRevealsTheRailWithoutSavingAUserPreference(existingContent: Bool) async {
var state = makeState()
state.isRailVisible = false
state.hasCustomRailVisibility = false
let store = makeStore(state)

if !existingContent {
await store.send(.mailroomContentChanged(hasContent: false))
#expect(!store.state.isRailVisible)
}
await store.send(.mailroomContentChanged(hasContent: true)) {
$0.isRailVisible = true
}
#expect(!store.state.hasCustomRailVisibility)
await store.send(.mailroomContentChanged(hasContent: true))
await store.send(.mailroomContentChanged(hasContent: false))
#expect(store.state.isRailVisible)
}

@Test
@MainActor
func hidingTheRailPreventsMailFromReopeningItAcrossWorkspaces() async {
let key = LoopWorkspaceRail.visibleDefaultsKey
let saved = UserDefaults.standard.object(forKey: key)
UserDefaults.standard.removeObject(forKey: key)
defer {
if let saved {
UserDefaults.standard.set(saved, forKey: key)
} else {
UserDefaults.standard.removeObject(forKey: key)
}
}
#expect(!LoopWorkspaceRail.hasStoredVisibility())
let store = makeStore(makeState())
await store.send(.mailroomContentChanged(hasContent: true)) {
$0.isRailVisible = true
}
#expect(!LoopWorkspaceRail.hasStoredVisibility())
await store.send(.railToggled) {
$0.isRailVisible = false
$0.hasCustomRailVisibility = true
}
await store.send(.mailroomContentChanged(hasContent: false))
await store.send(.mailroomContentChanged(hasContent: true))
#expect(!store.state.isRailVisible)
#expect(LoopWorkspaceRail.hasStoredVisibility())
#expect(!LoopWorkspaceRail.loadVisible())

let reopened = makeStore(makeState())
#expect(reopened.state.hasCustomRailVisibility)
await reopened.send(.mailroomContentChanged(hasContent: true))
#expect(!reopened.state.isRailVisible)
await reopened.send(.railToggled) {
$0.isRailVisible = true
}
#expect(LoopWorkspaceRail.loadVisible())
}
}
Loading