From 54d6faf72e3b28c505667b4af3fabc16674eda94 Mon Sep 17 00:00:00 2001 From: Arc Date: Sat, 21 Mar 2026 05:15:27 +0000 Subject: [PATCH 1/2] feat: add default 3-worker + 1-review cockpit layout --- apps/macos/Sources/OpenClaw/CockpitData.swift | 34 +++ .../macos/Sources/OpenClaw/CockpitStore.swift | 16 ++ .../Sources/OpenClaw/CockpitWindow.swift | 217 ++++++++++-------- docs/cockpit/FAST-TODO.md | 2 +- 4 files changed, 166 insertions(+), 103 deletions(-) diff --git a/apps/macos/Sources/OpenClaw/CockpitData.swift b/apps/macos/Sources/OpenClaw/CockpitData.swift index 50dc049f8..f4dac7680 100644 --- a/apps/macos/Sources/OpenClaw/CockpitData.swift +++ b/apps/macos/Sources/OpenClaw/CockpitData.swift @@ -216,6 +216,40 @@ struct CockpitWorkspaceSummary: Codable, Sendable { let activeLanes: [CockpitLaneSummary] } +// MARK: - Layout + +enum CockpitLayoutPreset: String, CaseIterable, Codable, Sendable, Equatable, Identifiable { + /// 3 worker slots across the top, 1 review slot spanning the bottom-right. + case threeWorkerOneReview = "3w1r" + + var id: String { self.rawValue } + + var label: String { + switch self { + case .threeWorkerOneReview: + "3 Workers + 1 Review" + } + } + + var workerSlotCount: Int { + switch self { + case .threeWorkerOneReview: + 3 + } + } + + var hasReviewSlot: Bool { + switch self { + case .threeWorkerOneReview: + true + } + } +} + +extension CockpitLayoutPreset { + static let `default`: CockpitLayoutPreset = .threeWorkerOneReview +} + extension CockpitGatewayStatus { static let previewLocal = CockpitGatewayStatus( mode: .local, diff --git a/apps/macos/Sources/OpenClaw/CockpitStore.swift b/apps/macos/Sources/OpenClaw/CockpitStore.swift index 94df591b0..7602a3e62 100644 --- a/apps/macos/Sources/OpenClaw/CockpitStore.swift +++ b/apps/macos/Sources/OpenClaw/CockpitStore.swift @@ -47,6 +47,7 @@ final class CockpitStore { var isPerformingWorkerAction = false var activeWorkerAction: CockpitWorkerAction? var isRepairingRemoteConnection = false + var layoutPreset: CockpitLayoutPreset = .default private let logger = Logger(subsystem: "ai.openclaw", category: "cockpit.ui") private let isPreview: Bool @@ -65,6 +66,21 @@ final class CockpitStore { return snapshot.activeLanes.first(where: { $0.workerId == selectedWorkerId }) ?? snapshot.activeLanes.first } + /// Worker lanes padded or truncated to fill the layout's worker slot count. + var workerSlots: [CockpitLaneSummary?] { + let workers = (self.snapshot?.activeLanes ?? []).filter { $0.lane == "worker" || $0.lane == "code" || $0.lane.isEmpty } + let count = self.layoutPreset.workerSlotCount + if workers.count >= count { + return Array(workers.prefix(count)) + } + return workers.map { Optional($0) } + Array(repeating: nil, count: count - workers.count) + } + + /// The first review-lane worker, if any. + var reviewSlot: CockpitLaneSummary? { + (self.snapshot?.activeLanes ?? []).first(where: { $0.lane == "review" }) + } + var projectRootLabel: String? { Self.resolveProjectRoot(snapshot: self.snapshot, selectedLane: self.selectedLane) } diff --git a/apps/macos/Sources/OpenClaw/CockpitWindow.swift b/apps/macos/Sources/OpenClaw/CockpitWindow.swift index 88f7769c0..89d7b92a7 100644 --- a/apps/macos/Sources/OpenClaw/CockpitWindow.swift +++ b/apps/macos/Sources/OpenClaw/CockpitWindow.swift @@ -64,17 +64,25 @@ struct CockpitWindow: View { ScrollView { VStack(alignment: .leading, spacing: 18) { CockpitMetricStrip(snapshot: snapshot) - HStack(alignment: .top, spacing: 16) { - CockpitLaneSection( - lanes: snapshot.activeLanes, - selectedWorkerId: self.store.selectedWorkerId, - onSelect: { workerId in - Task { await self.store.selectWorker(workerId) } - }) - CockpitSelectedWorkerSection(store: self.store) + + HStack(alignment: .center, spacing: 4) { + Text("Layout") + .font(.caption) + .foregroundStyle(.secondary) + Picker("", selection: self.$store.layoutPreset) { + ForEach(CockpitLayoutPreset.allCases) { preset in + Text(preset.label).tag(preset) + } + } + .pickerStyle(.menu) + .fixedSize() } + + CockpitLayoutGrid(store: self.store) + + CockpitSelectedWorkerSection(store: self.store) + HStack(alignment: .top, spacing: 16) { - CockpitReviewSection(reviews: snapshot.pendingReviews) CockpitRunsSection(runs: snapshot.recentRuns) } CockpitTasksSection(tasks: snapshot.recentTasks) @@ -268,65 +276,39 @@ private struct CockpitMetricCard: View { } } -private struct CockpitLaneSection: View { - let lanes: [CockpitLaneSummary] - let selectedWorkerId: String? - let onSelect: (String) -> Void - - private let columns = [ - GridItem(.flexible(minimum: 280), spacing: 12), - GridItem(.flexible(minimum: 280), spacing: 12), - ] +/// Renders the structured layout grid: worker slots across the top row, review slot at the end. +private struct CockpitLayoutGrid: View { + @Bindable var store: CockpitStore var body: some View { VStack(alignment: .leading, spacing: 10) { - Text("Workers") + Text("Lanes") .font(.title3.weight(.semibold)) - if self.lanes.isEmpty { - sectionPlaceholder("No workers yet. Start the next worker to populate the cockpit.") - } else { - LazyVGrid(columns: self.columns, alignment: .leading, spacing: 12) { - ForEach(self.lanes) { lane in - Button { - self.onSelect(lane.workerId) - } label: { - VStack(alignment: .leading, spacing: 8) { - HStack { - Text(lane.workerName) - .font(.headline) - Spacer() - Text(lane.status.replacingOccurrences(of: "_", with: " ")) - .font(.caption.weight(.medium)) - .foregroundStyle(.secondary) - } - Text(lane.taskTitle) - .font(.subheadline) - .multilineTextAlignment(.leading) - if let branch = lane.branch { - Text(branch) - .font(.caption.monospaced()) - .foregroundStyle(.secondary) - } - if let summary = lane.latestRun?.summary, !summary.isEmpty { - Text(summary) - .font(.caption) - .foregroundStyle(.secondary) - .multilineTextAlignment(.leading) - } - if let review = lane.pendingReview { - Text("Pending review: \(review.title)") - .font(.caption) - .foregroundStyle(.orange) - .multilineTextAlignment(.leading) - } - } - .padding(14) - .frame(maxWidth: .infinity, alignment: .leading) - .background( - RoundedRectangle(cornerRadius: 12, style: .continuous) - .fill(self.selectedWorkerId == lane.workerId ? Color.accentColor.opacity(0.14) : Color.primary.opacity(0.04))) - } - .buttonStyle(.plain) + + let workerSlots = self.store.workerSlots + let reviewSlot = self.store.reviewSlot + + HStack(alignment: .top, spacing: 12) { + ForEach(Array(workerSlots.enumerated()), id: \.offset) { index, lane in + if let lane { + CockpitLaneCard( + lane: lane, + isSelected: self.store.selectedWorkerId == lane.workerId, + onSelect: { Task { await self.store.selectWorker(lane.workerId) } }) + } else { + CockpitEmptySlotCard(label: "Worker \(index + 1)", hint: "Start a worker to fill this slot.") + } + } + + if self.store.layoutPreset.hasReviewSlot { + if let lane = reviewSlot { + CockpitLaneCard( + lane: lane, + isSelected: self.store.selectedWorkerId == lane.workerId, + onSelect: { Task { await self.store.selectWorker(lane.workerId) } }, + accentColor: .orange) + } else { + CockpitEmptySlotCard(label: "Review", hint: "Pending reviews appear here.") } } } @@ -335,6 +317,75 @@ private struct CockpitLaneSection: View { } } +private struct CockpitLaneCard: View { + let lane: CockpitLaneSummary + let isSelected: Bool + let onSelect: () -> Void + var accentColor: Color = .accentColor + + var body: some View { + Button(action: self.onSelect) { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text(self.lane.workerName) + .font(.headline) + Spacer() + Text(self.lane.status.replacingOccurrences(of: "_", with: " ")) + .font(.caption.weight(.medium)) + .foregroundStyle(.secondary) + } + Text(self.lane.taskTitle) + .font(.subheadline) + .multilineTextAlignment(.leading) + if let branch = self.lane.branch { + Text(branch) + .font(.caption.monospaced()) + .foregroundStyle(.secondary) + } + if let summary = self.lane.latestRun?.summary, !summary.isEmpty { + Text(summary) + .font(.caption) + .foregroundStyle(.secondary) + .multilineTextAlignment(.leading) + } + if let review = self.lane.pendingReview { + Text("Pending review: \(review.title)") + .font(.caption) + .foregroundStyle(.orange) + .multilineTextAlignment(.leading) + } + } + .padding(14) + .frame(maxWidth: .infinity, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(self.isSelected ? self.accentColor.opacity(0.14) : Color.primary.opacity(0.04))) + } + .buttonStyle(.plain) + } +} + +private struct CockpitEmptySlotCard: View { + let label: String + let hint: String + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text(self.label) + .font(.headline) + .foregroundStyle(.tertiary) + Text(self.hint) + .font(.caption) + .foregroundStyle(.quaternary) + } + .padding(14) + .frame(maxWidth: .infinity, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .strokeBorder(Color.primary.opacity(0.08), style: StrokeStyle(lineWidth: 1, dash: [6, 4]))) + } +} + private struct CockpitSelectedWorkerSection: View { @Bindable var store: CockpitStore @@ -454,44 +505,6 @@ private struct CockpitSelectedWorkerSection: View { } } -private struct CockpitReviewSection: View { - let reviews: [CockpitReviewSummary] - - var body: some View { - VStack(alignment: .leading, spacing: 10) { - Text("Pending Reviews") - .font(.title3.weight(.semibold)) - if self.reviews.isEmpty { - sectionPlaceholder("No pending reviews.") - } else { - VStack(alignment: .leading, spacing: 8) { - ForEach(self.reviews.prefix(6)) { review in - VStack(alignment: .leading, spacing: 4) { - Text(review.title) - .font(.headline) - if let summary = review.summary, !summary.isEmpty { - Text(summary) - .font(.caption) - .foregroundStyle(.secondary) - } - Text(review.status) - .font(.caption2) - .foregroundStyle(.orange) - } - .padding(.bottom, 6) - } - } - .padding(14) - .frame(maxWidth: .infinity, alignment: .leading) - .background( - RoundedRectangle(cornerRadius: 12, style: .continuous) - .fill(Color.primary.opacity(0.04))) - } - } - .frame(maxWidth: .infinity, alignment: .leading) - } -} - private struct CockpitRunsSection: View { let runs: [CockpitRunSummary] diff --git a/docs/cockpit/FAST-TODO.md b/docs/cockpit/FAST-TODO.md index 21ce5983c..e9e18c2d1 100644 --- a/docs/cockpit/FAST-TODO.md +++ b/docs/cockpit/FAST-TODO.md @@ -30,7 +30,7 @@ Arc becomes the default daily surface when these are all done: - [ ] Add embedded PTY terminal lanes - [ ] Bind terminal lanes to worktrees -- [ ] Add default 3-worker + 1-review layout +- [x] Add default 3-worker + 1-review layout - [ ] Save and restore project layouts ## Phase C: Always-On Arc From 525a118266bffe76c0e8121e76b77dfe0b77cee8 Mon Sep 17 00:00:00 2001 From: Arc Self Drive Date: Sat, 21 Mar 2026 05:15:45 +0000 Subject: [PATCH 2/2] arc: Add default 3-worker + 1-review layout --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a04a13293..1ba5a65d2 100644 --- a/README.md +++ b/README.md @@ -40,21 +40,21 @@ openclaw onboard Arc has one runtime with two surfaces: -| Surface | Role | -| --- | --- | +| Surface | Role | +| ------------------- | ------------------------------------------------------ | | **Swift macOS app** | Flagship review workstation — diffs, queues, decisions | -| **VPS TUI** | Fast remote operator console — queue, inspect, unblock | +| **VPS TUI** | Fast remote operator console — queue, inspect, unblock | ### The Layer Model Arc only makes sense if the layers stay clean: -| Layer | Role | -| --- | --- | -| **Arc** | product, workflow, workstation, project cockpit | -| **OpenClaw** | runtime, gateway, worktrees, worker lifecycle, durable state | -| **Claude + Codex** | worker engines that do the coding work | -| **Obsidian** | planning, notes, specs, architecture, project memory | +| Layer | Role | +| ------------------ | ------------------------------------------------------------ | +| **Arc** | product, workflow, workstation, project cockpit | +| **OpenClaw** | runtime, gateway, worktrees, worker lifecycle, durable state | +| **Claude + Codex** | worker engines that do the coding work | +| **Obsidian** | planning, notes, specs, architecture, project memory | Obsidian should hold thinking. Arc should hold execution.