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
7 changes: 7 additions & 0 deletions native/ios/PsycheApp/Sources/PsycheApp/CockpitView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ enum RootTab: String, Hashable, CaseIterable {
/// throw away where you were.
struct CockpitView: View {
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@EnvironmentObject private var remoteActionStore: RemoteActionStore
@EnvironmentObject private var store: WorkspaceStore

Expand All @@ -52,6 +53,12 @@ struct CockpitView: View {
.sheet(isPresented: remoteActionSheetBinding) {
ActionSheetView(store: remoteActionStore)
}
.transaction { transaction in
if reduceMotion || ProcessInfo.processInfo.arguments.contains("-uiReduceMotion") {
transaction.disablesAnimations = true
transaction.animation = nil
}
}
.accessibilityIdentifier("main-cockpit")
}

Expand Down
15 changes: 12 additions & 3 deletions native/ios/PsycheApp/Sources/PsycheApp/Design/PsycheTheme.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,18 @@ enum PaneIndicator: Equatable {
case .quiet: .gray
}
}

var text: String {
switch self {
case .needsAttention: "Needs you"
case .running: "Running"
case .quiet: "Idle"
}
}
}

/// The dot beside a pane. Hidden from VoiceOver on purpose: the row's combined
/// label already states the status, so exposing it again would say it twice.
/// The dot beside a pane. It carries its own status text so the indicator is
/// never only colour, even when it is reused outside a combined row.
struct WorkspaceStatusDot: View {
let status: String
let needsAttention: Bool
Expand All @@ -66,6 +74,7 @@ struct WorkspaceStatusDot: View {
Circle()
.fill(indicator.color)
.frame(width: 9, height: 9)
.accessibilityHidden(true)
.accessibilityLabel("Status")
.accessibilityValue(indicator.text)
}
}
45 changes: 36 additions & 9 deletions native/ios/PsycheApp/Sources/PsycheApp/PsycheApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,42 @@ struct PsycheApp: App {

var body: some Scene {
WindowGroup {
CockpitView()
.environmentObject(model)
.environmentObject(model.workspaceStore)
.environmentObject(model.remoteActionStore)
.environmentObject(model.terminalRegistry)
.preferredColorScheme(.dark)
.task {
await model.start()
}
configuredCockpit
}
}

@ViewBuilder
private var configuredCockpit: some View {
let content = CockpitView()
.environmentObject(model)
.environmentObject(model.workspaceStore)
.environmentObject(model.remoteActionStore)
.environmentObject(model.terminalRegistry)
.preferredColorScheme(.dark)
.task {
await model.start()
}

if let dynamicTypeSize = Self.dynamicTypeSize(in: ProcessInfo.processInfo.arguments) {
content.environment(\.dynamicTypeSize, dynamicTypeSize)
} else {
content
}
}

private static func dynamicTypeSize(in arguments: [String]) -> DynamicTypeSize? {
guard let index = arguments.firstIndex(of: "-uiDynamicTypeSize"),
arguments.indices.contains(index + 1)
else {
return nil
}
switch arguments[index + 1] {
case "accessibility1": return .accessibility1
case "accessibility2": return .accessibility2
case "accessibility3": return .accessibility3
case "accessibility4": return .accessibility4
case "accessibility5": return .accessibility5
default: return nil
}
}
}
24 changes: 19 additions & 5 deletions native/ios/PsycheApp/Sources/PsycheApp/Views/NowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,23 @@ struct NowPaneRow: View {
Text(item.title)
.font(.body.weight(.semibold))
.lineLimit(2)
Text(contextLine)
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(1)
ViewThatFits(in: .horizontal) {
Text(contextLine)
VStack(alignment: .leading, spacing: 2) {
Text(item.projectTitle)
if let agent = item.agent {
Text(agent)
} else {
Text(item.status)
}
if let hostName, !hostName.isEmpty {
Text("Host \(hostName)")
}
}
}
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(2)
if let lastActivity = item.lastActivity {
Text(lastActivity, style: .relative)
.font(.caption)
Expand All @@ -90,7 +103,8 @@ struct NowPaneRow: View {
PaneAccessibility.contextLine(
projectTitle: item.projectTitle,
agent: item.agent,
status: item.status
status: item.status,
hostName: hostName
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ enum PaneAccessibility {
.joined(separator: " · ")
}

static func contextLine(
projectTitle: String,
agent: String?,
status: String,
hostName: String?
) -> String {
[projectTitle, agent ?? status, hostName.map { "host \($0)" }]
.compactMap { $0 }
.filter { !$0.isEmpty }
.joined(separator: " · ")
}

static func label(
title: String,
projectTitle: String,
Expand Down Expand Up @@ -63,11 +75,13 @@ enum PaneAccessibility {

static func projectSubtitle(
branch: String?,
hostName: String?,
runningCount: Int,
attentionCount: Int
) -> String {
var parts: [String] = []
if let branch, !branch.isEmpty { parts.append(branch) }
if let hostName, !hostName.isEmpty { parts.append("host \(hostName)") }
parts.append("\(runningCount) running")
if attentionCount > 0 {
parts.append("\(attentionCount) needs you")
Expand All @@ -78,11 +92,13 @@ enum PaneAccessibility {
static func projectLabel(
title: String,
branch: String?,
hostName: String?,
runningCount: Int,
attentionCount: Int
) -> String {
var parts = [title]
if let branch, !branch.isEmpty { parts.append("branch \(branch)") }
if let hostName, !hostName.isEmpty { parts.append("host \(hostName)") }
parts.append("\(runningCount) running")
parts.append("\(attentionCount) needing attention")
return parts.joined(separator: ", ")
Expand Down
19 changes: 17 additions & 2 deletions native/ios/PsycheApp/Sources/PsycheApp/Views/PaneControls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ struct PaneControlsMenu: View {
} label: {
Label(PaneControlsMenuAction.files.label, systemImage: PaneControlsMenuAction.files.systemImage)
}
.accessibilityIdentifier("pane-action-files")
.disabled(PaneControlsPresentation.filesDisabled(
hasInspectableWorktree: hasInspectableWorktree,
isStale: store.isStale,
Expand All @@ -53,8 +54,9 @@ struct PaneControlsMenu: View {
Button(role: .destructive) {
isConfirmingStop = true
} label: {
Label(PaneControlsMenuAction.stop.label, systemImage: PaneControlsMenuAction.stop.systemImage)
Label(PaneControlsMenuAction.stop.consequenceLabel, systemImage: PaneControlsMenuAction.stop.systemImage)
}
.accessibilityIdentifier("pane-action-stop")
.disabled(PaneControlsPresentation.hostActionDisabled(
isStale: store.isStale,
isBusy: localActionBusy
Expand All @@ -63,8 +65,9 @@ struct PaneControlsMenu: View {
Button(role: .destructive) {
isConfirmingCleanup = true
} label: {
Label(PaneControlsMenuAction.cleanup.label, systemImage: PaneControlsMenuAction.cleanup.systemImage)
Label(PaneControlsMenuAction.cleanup.consequenceLabel, systemImage: PaneControlsMenuAction.cleanup.systemImage)
}
.accessibilityIdentifier("pane-action-cleanup")
.disabled(PaneControlsPresentation.hostActionDisabled(
isStale: store.isStale,
isBusy: remoteActionBusy
Expand Down Expand Up @@ -190,6 +193,7 @@ struct PaneControlsMenu: View {
} label: {
Label(ActionSheetPresentation.actionLabel(for: action), systemImage: systemImage)
}
.accessibilityIdentifier("pane-action-\(action.rawValue)")
.disabled(PaneControlsPresentation.hostActionDisabled(
isStale: store.isStale,
isBusy: remoteActionBusy
Expand Down Expand Up @@ -260,6 +264,17 @@ enum PaneControlsMenuAction: CaseIterable, Equatable {
}
}

var consequenceLabel: String {
switch self {
case .stop:
"Stop pane and keep work"
case .cleanup:
"Close pane and choose cleanup"
default:
label
}
}

var systemImage: String {
switch self {
case .merge:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import SwiftUI
/// would attach a stream for something nobody is looking at, which is exactly
/// what the two-session cap exists to prevent.
struct PaneSwitcher: View {
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
let panes: [PaneChoice]
let primaryPaneID: String?
let secondaryPaneID: String?
Expand Down Expand Up @@ -76,9 +77,13 @@ struct PaneSwitcher: View {
} label: {
HStack(spacing: 6) {
WorkspaceStatusDot(status: pane.status, needsAttention: pane.needsAttention)
Text(pane.title)
.font(.caption.weight(.semibold))
.lineLimit(1)
VStack(alignment: .leading, spacing: 1) {
Text(pane.title)
Text(pane.status)
.foregroundStyle(.secondary)
}
.font(.caption.weight(.semibold))
.lineLimit(dynamicTypeSize.isAccessibilitySize ? 2 : 1)
}
.padding(.horizontal, 12)
.frame(minHeight: PsycheTheme.minimumTapTarget)
Expand Down
56 changes: 48 additions & 8 deletions native/ios/PsycheApp/Sources/PsycheApp/Views/ProjectsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import SwiftUI
/// you need to look: how much is running, and how much is waiting on you.
struct ProjectsView: View {
@EnvironmentObject private var store: WorkspaceStore
@EnvironmentObject private var model: AppModel

private var projects: [WorkspaceProjectSnapshot] {
store.workspace?.projects ?? []
Expand All @@ -17,6 +18,7 @@ struct ProjectsView: View {
ProjectDetailView(projectID: project.id)
} label: {
ProjectRow(project: project)
.environmentObject(model)
}
.accessibilityIdentifier("project-\(project.id)")
}
Expand All @@ -38,6 +40,7 @@ struct ProjectsView: View {
}

struct ProjectRow: View {
@EnvironmentObject private var model: AppModel
let project: WorkspaceProjectSnapshot

private var branch: String? {
Expand All @@ -48,10 +51,24 @@ struct ProjectRow: View {
VStack(alignment: .leading, spacing: 4) {
Text(project.title)
.font(.body.weight(.semibold))
Text(subtitle)
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(1)
ViewThatFits(in: .horizontal) {
Text(subtitle)
VStack(alignment: .leading, spacing: 2) {
if let branch, !branch.isEmpty {
Text(branch)
}
if let hostName = model.hostName, !hostName.isEmpty {
Text("Host \(hostName)")
}
Text("\(project.runningCount) running")
if project.attentionCount > 0 {
Text("\(project.attentionCount) needs you")
}
}
}
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(2)
}
.padding(.vertical, 4)
.frame(minHeight: PsycheTheme.minimumTapTarget, alignment: .leading)
Expand All @@ -62,6 +79,7 @@ struct ProjectRow: View {
private var subtitle: String {
PaneAccessibility.projectSubtitle(
branch: branch,
hostName: model.hostName,
runningCount: project.runningCount,
attentionCount: project.attentionCount
)
Expand All @@ -71,6 +89,7 @@ struct ProjectRow: View {
PaneAccessibility.projectLabel(
title: project.title,
branch: branch,
hostName: model.hostName,
runningCount: project.runningCount,
attentionCount: project.attentionCount
)
Expand Down Expand Up @@ -192,10 +211,22 @@ struct WorkspacePaneRow: View {
Text(pane.title ?? pane.id)
.font(.body.weight(.semibold))
.lineLimit(2)
Text([pane.agent, pane.status].compactMap { $0 }.joined(separator: " · "))
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(1)
ViewThatFits(in: .horizontal) {
Text(contextLine)
VStack(alignment: .leading, spacing: 2) {
Text(projectTitle)
if let agent = pane.agent {
Text(agent)
}
Text(pane.status)
if let hostName, !hostName.isEmpty {
Text("Host \(hostName)")
}
}
}
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(2)
}
}
.padding(.vertical, 6)
Expand All @@ -208,4 +239,13 @@ struct WorkspacePaneRow: View {
private var accessibilityLabel: String {
PaneAccessibility.label(for: pane, projectTitle: projectTitle, hostName: hostName)
}

private var contextLine: String {
PaneAccessibility.contextLine(
projectTitle: projectTitle,
agent: pane.agent,
status: pane.status,
hostName: hostName
)
}
}
Loading
Loading