diff --git a/AGENTS.md b/AGENTS.md index 7595f75..f9f8bd6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,7 +51,7 @@ When the owner tells the lead "deploy byre", or anything that means the same, th - Exactly two panes, side by side, each exactly half the width, at every window size. Never three or more columns, never a sidebar, never a resizable or collapsible split. - No navbar. The panes fill the window, below only the identity banner when it shows. -- The left pane has six tabs: Lead (the user's lead agent), Status, Notes (what the lead has noted about the user), Workflows (the saved workflows), Server (the byre service on this Mac) and Settings (the user's and this client's preferences), with the connection dot and the user's name at the end of its tab strip. Nothing else of the user's goes in it. The right pane always shows one task's conversation (its transcript, or a workflow stage task's run), or a project's dashboard when no task is selected; the lead sets the task, and the human can switch it (⌘K). The task's other views (its other tabs, and images and diagrams zoomed from its transcript) open only when the human opens them, in a sheet over the left pane: attached to the center line, in the task's color and named by it, leaving a strip of the left pane visible that closes it. Images and diagrams zoomed from the lead chat open the same way over the right pane. A new task view is a new tab that opens in that sheet, never a new column or region. +- The left pane has six tabs: Lead (the user's lead agent), Status, Notes (what the lead has noted about the user), Workflows (the saved workflows), Server (the byre service on this Mac) and Settings (the user's and this client's preferences), with the connection dot and the user's name at the end of its tab strip. Nothing else of the user's goes in it. The right pane always shows one task's conversation (its transcript, or a workflow stage task's run), or a project's dashboard when no task is selected, or the list of every project's active tasks while the task bar's list button has it open; the lead sets the task, and the human can switch it (⌘K). The task's other views (its other tabs, and images and diagrams zoomed from its transcript) open only when the human opens them, in a sheet over the left pane: attached to the center line, in the task's color and named by it, leaving a strip of the left pane visible that closes it. Images and diagrams zoomed from the lead chat open the same way over the right pane. A new task view is a new tab that opens in that sheet, never a new column or region. - Overlays (the task switcher, confirmation dialogs) are allowed; they sit above the panes and don't change the layout. ## UI look diff --git a/mac/Sources/Byre/Dialogs.swift b/mac/Sources/Byre/Dialogs.swift index b48b2bb..5b46c43 100644 --- a/mac/Sources/Byre/Dialogs.swift +++ b/mac/Sources/Byre/Dialogs.swift @@ -53,6 +53,7 @@ final class Overlays { var leadFocus = 0 var tabFocus = 0 var projectFocus = false + var taskList = false @ObservationIgnored var pickerFrame: CGRect = .zero @ObservationIgnored var menuHover = false var paneFrame: CGRect = .zero diff --git a/mac/Sources/Byre/Panes.swift b/mac/Sources/Byre/Panes.swift index 95e049b..f121c3a 100644 --- a/mac/Sources/Byre/Panes.swift +++ b/mac/Sources/Byre/Panes.swift @@ -192,7 +192,9 @@ private struct TaskPane: View { VStack(spacing: 0) { TaskBar() VStack(spacing: 0) { - if store.selectedTaskId == nil { + if overlays.taskList { + TaskList() + } else if store.selectedTaskId == nil { ProjectView() } else { TabStrip() @@ -208,6 +210,12 @@ private struct TaskPane: View { } .environment(\.paneAccent, accent) .environment(\.inTask, true) + .onChange(of: store.reveals) { + overlays.taskList = false + } + .onChange(of: store.focus) { + overlays.taskList = false + } .onGeometryChange(for: CGRect.self) { g in g.frame(in: .global) } action: { frame in @@ -260,7 +268,7 @@ struct TaskBar: View { var body: some View { let task = store.selectedTaskId.flatMap { store.findTask($0) } let project = store.focusedProject() - let label = task.map { "\(project?.name ?? "") ▸ \($0.name)" } ?? project?.name ?? "No task" + let label = overlays.taskList ? "All tasks" : task.map { "\(project?.name ?? "") ▸ \($0.name)" } ?? project?.name ?? "No task" HStack(spacing: 0) { Color.clear.frame(maxWidth: .infinity, maxHeight: 1) Button { @@ -290,6 +298,20 @@ struct TaskBar: View { PreviewLink(preview: p) } } + #if os(macOS) + Button { + overlays.taskList.toggle() + } label: { + Image(systemName: "list.bullet") + .font(Fonts.font(13, weight: 600)) + .foregroundStyle(accent) + .padding(.vertical, 6) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .help(overlays.taskList ? "Back to the task" : "Task list") + .accessibilityLabel("Task list") + #endif } .frame(maxWidth: .infinity) } diff --git a/mac/Sources/Byre/Phone.swift b/mac/Sources/Byre/Phone.swift index b174245..e4be11a 100644 --- a/mac/Sources/Byre/Phone.swift +++ b/mac/Sources/Byre/Phone.swift @@ -279,69 +279,9 @@ private struct ConnectionMark: View { } private struct TaskListScreen: View { - @Environment(Store.self) private var store - @Environment(Overlays.self) private var overlays - var body: some View { - ScrollView { - VStack(alignment: .leading, spacing: 0) { - ForEach(store.projects, id: \.id) { project in - let active = (store.tasks[project.id] ?? []).filter { $0.status == .active } - Button { - store.sheet = nil - store.focusProject(project.id) - } label: { - HStack(alignment: .firstTextBaseline, spacing: 6) { - Text(project.name) - .font(Fonts.font(13, weight: 700)) - Text(project.defaultBranch) - .font(Fonts.font(11)) - .foregroundStyle(Palette.muted) - Spacer(minLength: 0) - Image(systemName: "chevron.right") - .font(Fonts.font(11)) - .foregroundStyle(Palette.muted) - } - .padding(.horizontal, 10) - .padding(.top, 16) - .padding(.bottom, 6) - .contentShape(Rectangle()) - } - .buttonStyle(.plain) - if active.isEmpty { - Text("No active tasks.") - .foregroundStyle(Palette.muted) - .padding(.horizontal, 10) - .padding(.vertical, 6) - } - TaskRows(active: active) - } - } - .padding(.horizontal, 8) - .padding(.bottom, 16) - } - .safeAreaInset(edge: .top, spacing: 0) { - HStack { - Spacer(minLength: 0) - Button("New task") { - overlays.openSwitcher(store) - } - .buttonStyle(WebButtonStyle()) - .onGeometryChange(for: CGRect.self) { g in - g.frame(in: .named("window")) - } action: { frame in - overlays.pickerFrame = frame - } - } - .padding(.horizontal, 16) - .padding(.vertical, 6) - .background(Palette.bg) - .overlay(alignment: .bottom) { - Rectangle().fill(Palette.border).frame(height: 1) - } - } - .background(Palette.bg) - .toolbar(.hidden, for: .navigationBar) + TaskList() + .toolbar(.hidden, for: .navigationBar) } } diff --git a/mac/Sources/Byre/Project.swift b/mac/Sources/Byre/Project.swift index fcedbeb..e3a1631 100644 --- a/mac/Sources/Byre/Project.swift +++ b/mac/Sources/Byre/Project.swift @@ -215,6 +215,72 @@ private func rowIndent(_ task: ByreTask, _ depth: Int) -> CGFloat { task.parentTaskId != nil ? 20 * CGFloat(depth - 1) : 0 } +struct TaskList: View { + @Environment(Store.self) private var store + @Environment(Overlays.self) private var overlays + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + ForEach(store.projects, id: \.id) { project in + let active = (store.tasks[project.id] ?? []).filter { $0.status == .active } + Button { + store.sheet = nil + store.focusProject(project.id) + } label: { + HStack(alignment: .firstTextBaseline, spacing: 6) { + Text(project.name) + .font(Fonts.font(13, weight: 700)) + Text(project.defaultBranch) + .font(Fonts.font(11)) + .foregroundStyle(Palette.muted) + Spacer(minLength: 0) + Image(systemName: "chevron.right") + .font(Fonts.font(11)) + .foregroundStyle(Palette.muted) + } + .padding(.horizontal, 10) + .padding(.top, 16) + .padding(.bottom, 6) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + if active.isEmpty { + Text("No active tasks.") + .foregroundStyle(Palette.muted) + .padding(.horizontal, 10) + .padding(.vertical, 6) + } + TaskRows(active: active) + } + } + .padding(.horizontal, 8) + .padding(.bottom, 16) + } + .safeAreaInset(edge: .top, spacing: 0) { + HStack { + Spacer(minLength: 0) + Button("New task") { + overlays.openSwitcher(store) + } + .buttonStyle(WebButtonStyle()) + .onGeometryChange(for: CGRect.self) { g in + g.frame(in: .named("window")) + } action: { frame in + overlays.pickerFrame = frame + } + } + .padding(.horizontal, 16) + .padding(.vertical, 6) + .background(Palette.bg) + .overlay(alignment: .bottom) { + Rectangle().fill(Palette.border).frame(height: 1) + } + } + .background(Palette.bg) + } +} + struct TaskRows: View { let active: [ByreTask] @Environment(Store.self) private var store