Show the phone's task list in the Mac's right pane from a list button

This commit is contained in:
Ian Gulliver
2026-10-22 10:31:49 -07:00
parent b54a1a04b3
commit 406f67a1a0
5 changed files with 94 additions and 65 deletions
+1 -1
View File
@@ -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
+1
View File
@@ -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
+24 -2
View File
@@ -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)
}
+2 -62
View File
@@ -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)
}
}
+66
View File
@@ -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