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. diff --git a/apps/macos/Sources/OpenClaw/CockpitData.swift b/apps/macos/Sources/OpenClaw/CockpitData.swift index 50dc049f8..2ef988c2d 100644 --- a/apps/macos/Sources/OpenClaw/CockpitData.swift +++ b/apps/macos/Sources/OpenClaw/CockpitData.swift @@ -187,6 +187,16 @@ struct CockpitLaneSummary: Codable, Identifiable, Sendable { } } +struct CockpitReviewArtifacts: Codable, Sendable { + let workerId: String + let worktreePath: String + let baseBranch: String + let diff: String + let commitLog: String + let testOutput: String + let generatedAt: String +} + struct CockpitWorkerLogs: Codable, Sendable { let workerId: String let latestRun: CockpitRunSummary? @@ -421,6 +431,39 @@ extension CockpitWorkspaceSummary { ]) } +extension CockpitReviewArtifacts { + static func preview(workerId: String) -> CockpitReviewArtifacts { + CockpitReviewArtifacts( + workerId: workerId, + worktreePath: "/Users/tessaro/openclaw/.worktrees/code/shell-lane", + baseBranch: "main", + diff: """ + src/code-cockpit/runtime.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ + src/code-cockpit/store.ts | 9 +++++++++ + 2 files changed, 51 insertions(+) + + diff --git a/src/code-cockpit/runtime.ts b/src/code-cockpit/runtime.ts + --- a/src/code-cockpit/runtime.ts + +++ b/src/code-cockpit/runtime.ts + @@ -1585,6 +1585,48 @@ + + async readWorkerReviewArtifacts(params: { + + workerId: string; + + }): Promise { + + // ... review artifacts collection + + } + """, + commitLog: """ + abc1234 feat: add review artifacts endpoint + def5678 feat: add diff/test/log review lane UI + """, + testOutput: """ + Tests: 42 passed, 0 failed + Test Suites: 8 passed, 0 failed + """, + generatedAt: "2026-03-19T13:00:00.000Z") + } +} + extension CockpitWorkerLogs { static func preview(workerId: String) -> CockpitWorkerLogs { CockpitWorkerLogs( diff --git a/apps/macos/Sources/OpenClaw/CockpitStore.swift b/apps/macos/Sources/OpenClaw/CockpitStore.swift index 94df591b0..96b5307dd 100644 --- a/apps/macos/Sources/OpenClaw/CockpitStore.swift +++ b/apps/macos/Sources/OpenClaw/CockpitStore.swift @@ -7,8 +7,25 @@ typealias CockpitGatewayStatusLoader = @Sendable () async throws -> CockpitGatew typealias CockpitWorkerLogsLoader = @Sendable (_ workerId: String) async throws -> CockpitWorkerLogs typealias CockpitSupervisorTickPerformer = @Sendable (_ repoRoot: String?) async throws -> CockpitSupervisorTickResult typealias CockpitWorkerActionPerformer = @Sendable (_ action: CockpitWorkerAction, _ workerId: String) async throws -> Void +typealias CockpitReviewArtifactsLoader = @Sendable (_ workerId: String) async throws -> CockpitReviewArtifacts typealias CockpitRemoteReconnectAction = @Sendable () async throws -> Void +enum ReviewLaneTab: String, CaseIterable, Identifiable { + case diff = "Diff" + case tests = "Tests" + case logs = "Logs" + + var id: String { self.rawValue } + + var systemImage: String { + switch self { + case .diff: "doc.text.magnifyingglass" + case .tests: "checkmark.circle" + case .logs: "terminal" + } + } +} + enum CockpitLoadError: LocalizedError { case gatewayUnavailable(String) @@ -32,6 +49,7 @@ final class CockpitStore { store.selectedWorkerId = CockpitWorkspaceSummary.preview.activeLanes.first?.workerId if let workerId = store.selectedWorkerId { store.selectedWorkerLogs = .preview(workerId: workerId) + store.selectedWorkerReviewArtifacts = .preview(workerId: workerId) } return store } @@ -43,6 +61,9 @@ final class CockpitStore { var selectedWorkerId: String? var selectedWorkerLogs: CockpitWorkerLogs? var isLoadingWorkerLogs = false + var selectedWorkerReviewArtifacts: CockpitReviewArtifacts? + var isLoadingReviewArtifacts = false + var reviewLaneTab: ReviewLaneTab = .diff var isStartingNextWorker = false var isPerformingWorkerAction = false var activeWorkerAction: CockpitWorkerAction? @@ -53,6 +74,7 @@ final class CockpitStore { private let loadGatewayStatus: CockpitGatewayStatusLoader private let loadSummary: CockpitSummaryLoader private let loadWorkerLogs: CockpitWorkerLogsLoader + private let loadReviewArtifacts: CockpitReviewArtifactsLoader private let performSupervisorTickImpl: CockpitSupervisorTickPerformer private let performWorkerActionImpl: CockpitWorkerActionPerformer private let reconnectRemoteGatewayImpl: CockpitRemoteReconnectAction @@ -78,6 +100,7 @@ final class CockpitStore { loadGatewayStatus: CockpitGatewayStatusLoader? = nil, loadSummary: CockpitSummaryLoader? = nil, loadWorkerLogs: CockpitWorkerLogsLoader? = nil, + loadReviewArtifacts: CockpitReviewArtifactsLoader? = nil, performSupervisorTick: CockpitSupervisorTickPerformer? = nil, performWorkerAction: CockpitWorkerActionPerformer? = nil, reconnectRemoteGateway: CockpitRemoteReconnectAction? = nil) @@ -94,6 +117,9 @@ final class CockpitStore { self.loadWorkerLogs = loadWorkerLogs ?? { workerId in try await GatewayConnection.shared.codeWorkerLogs(workerId: workerId) } + self.loadReviewArtifacts = loadReviewArtifacts ?? { workerId in + try await GatewayConnection.shared.codeWorkerReviewArtifacts(workerId: workerId) + } self.performSupervisorTickImpl = performSupervisorTick ?? { repoRoot in try await GatewayConnection.shared.codeSupervisorTick(repoRoot: repoRoot) } @@ -188,6 +214,30 @@ final class CockpitStore { func selectWorker(_ workerId: String) async { self.selectedWorkerId = workerId await self.refreshSelectedWorkerLogs() + await self.refreshSelectedWorkerReviewArtifacts() + } + + func refreshSelectedWorkerReviewArtifacts() async { + guard let workerId = self.selectedWorkerId else { + self.selectedWorkerReviewArtifacts = nil + return + } + if self.isPreview { + self.selectedWorkerReviewArtifacts = .preview(workerId: workerId) + return + } + + self.isLoadingReviewArtifacts = true + defer { self.isLoadingReviewArtifacts = false } + + do { + self.selectedWorkerReviewArtifacts = try await self.loadReviewArtifacts(workerId) + } catch { + let message = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription + self.logger.error("code cockpit review artifacts failed \(message, privacy: .public)") + // Don't overwrite lastError for review artifact failures; they're non-critical + self.selectedWorkerReviewArtifacts = nil + } } func performWorkerAction(_ action: CockpitWorkerAction, workerId: String) async { @@ -251,6 +301,7 @@ final class CockpitStore { guard let snapshot = self.snapshot else { self.selectedWorkerId = nil self.selectedWorkerLogs = nil + self.selectedWorkerReviewArtifacts = nil return } if let selectedWorkerId = self.selectedWorkerId, diff --git a/apps/macos/Sources/OpenClaw/CockpitWindow.swift b/apps/macos/Sources/OpenClaw/CockpitWindow.swift index 88f7769c0..c31f0fc04 100644 --- a/apps/macos/Sources/OpenClaw/CockpitWindow.swift +++ b/apps/macos/Sources/OpenClaw/CockpitWindow.swift @@ -73,6 +73,7 @@ struct CockpitWindow: View { }) CockpitSelectedWorkerSection(store: self.store) } + CockpitReviewLane(store: self.store) HStack(alignment: .top, spacing: 16) { CockpitReviewSection(reviews: snapshot.pendingReviews) CockpitRunsSection(runs: snapshot.recentRuns) @@ -454,6 +455,231 @@ private struct CockpitSelectedWorkerSection: View { } } +private struct CockpitReviewLane: View { + @Bindable var store: CockpitStore + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + HStack { + Text("Review Lane") + .font(.title3.weight(.semibold)) + Spacer() + if self.store.isLoadingReviewArtifacts { + ProgressView() + .controlSize(.small) + } + if self.store.selectedLane != nil { + Button { + Task { await self.store.refreshSelectedWorkerReviewArtifacts() } + } label: { + Label("Refresh", systemImage: "arrow.clockwise") + } + .buttonStyle(.bordered) + .controlSize(.small) + .disabled(self.store.isLoadingReviewArtifacts) + } + } + + if self.store.selectedLane != nil { + VStack(alignment: .leading, spacing: 0) { + // Tab bar + HStack(spacing: 0) { + ForEach(ReviewLaneTab.allCases) { tab in + Button { + self.store.reviewLaneTab = tab + } label: { + Label(tab.rawValue, systemImage: tab.systemImage) + .font(.callout.weight( + self.store.reviewLaneTab == tab ? .semibold : .regular)) + .padding(.horizontal, 14) + .padding(.vertical, 8) + .frame(maxWidth: .infinity) + .background( + self.store.reviewLaneTab == tab + ? Color.accentColor.opacity(0.12) + : Color.clear) + } + .buttonStyle(.plain) + } + } + .background(Color.primary.opacity(0.03)) + + Divider() + + // Tab content + if let artifacts = self.store.selectedWorkerReviewArtifacts { + CockpitReviewLaneContent( + tab: self.store.reviewLaneTab, + artifacts: artifacts, + logs: self.store.selectedWorkerLogs) + } else if self.store.isLoadingReviewArtifacts { + VStack { + ProgressView("Loading review artifacts…") + } + .frame(maxWidth: .infinity, minHeight: 120) + } else { + Text("Select a worker and refresh to load review artifacts.") + .font(.callout) + .foregroundStyle(.secondary) + .padding(14) + .frame(maxWidth: .infinity, minHeight: 80, alignment: .center) + } + } + .background( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(Color.primary.opacity(0.04))) + .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) + } else { + sectionPlaceholder("Select a worker to review its diff, tests, and logs.") + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +private struct CockpitReviewLaneContent: View { + let tab: ReviewLaneTab + let artifacts: CockpitReviewArtifacts + let logs: CockpitWorkerLogs? + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 10) { + switch self.tab { + case .diff: + self.diffView + case .tests: + self.testsView + case .logs: + self.logsView + } + } + .padding(14) + } + .frame(maxWidth: .infinity, minHeight: 160, maxHeight: 400, alignment: .topLeading) + } + + @ViewBuilder + private var diffView: some View { + HStack(spacing: 12) { + LabeledContent("Base") { + Text(self.artifacts.baseBranch) + .font(.caption.monospaced()) + } + LabeledContent("Worktree") { + Text(self.artifacts.worktreePath) + .font(.caption.monospaced()) + .lineLimit(1) + .truncationMode(.middle) + } + } + + if !self.artifacts.commitLog.isEmpty { + VStack(alignment: .leading, spacing: 4) { + Text("Commits") + .font(.caption.weight(.semibold)) + Text(self.artifacts.commitLog) + .font(.caption.monospaced()) + .foregroundStyle(.secondary) + .textSelection(.enabled) + } + } + + if !self.artifacts.diff.isEmpty { + VStack(alignment: .leading, spacing: 4) { + Text("Diff") + .font(.caption.weight(.semibold)) + Text(self.artifacts.diff) + .font(.system(size: 11, design: .monospaced)) + .foregroundStyle(.secondary) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(10) + .background( + RoundedRectangle(cornerRadius: 8, style: .continuous) + .fill(Color.primary.opacity(0.03))) + } + } else { + Text("No diff against \(self.artifacts.baseBranch).") + .font(.callout) + .foregroundStyle(.secondary) + } + } + + @ViewBuilder + private var testsView: some View { + if !self.artifacts.testOutput.isEmpty { + VStack(alignment: .leading, spacing: 4) { + Text("Test Output") + .font(.caption.weight(.semibold)) + Text(self.artifacts.testOutput) + .font(.system(size: 11, design: .monospaced)) + .foregroundStyle(.secondary) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(10) + .background( + RoundedRectangle(cornerRadius: 8, style: .continuous) + .fill(Color.primary.opacity(0.03))) + } + } else { + Text("No test output captured for this worker.") + .font(.callout) + .foregroundStyle(.secondary) + } + } + + @ViewBuilder + private var logsView: some View { + if let logs { + let stdout = logs.stdoutTail.trimmingCharacters(in: .whitespacesAndNewlines) + let stderr = logs.stderrTail.trimmingCharacters(in: .whitespacesAndNewlines) + + if !stdout.isEmpty { + VStack(alignment: .leading, spacing: 4) { + Text("stdout") + .font(.caption.weight(.semibold)) + Text(stdout) + .font(.system(size: 11, design: .monospaced)) + .foregroundStyle(.secondary) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(10) + .background( + RoundedRectangle(cornerRadius: 8, style: .continuous) + .fill(Color.primary.opacity(0.03))) + } + } + + if !stderr.isEmpty { + VStack(alignment: .leading, spacing: 4) { + Text("stderr") + .font(.caption.weight(.semibold)) + Text(stderr) + .font(.system(size: 11, design: .monospaced)) + .foregroundStyle(.red.opacity(0.7)) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(10) + .background( + RoundedRectangle(cornerRadius: 8, style: .continuous) + .fill(Color.red.opacity(0.04))) + } + } + + if stdout.isEmpty && stderr.isEmpty { + Text("No log output yet.") + .font(.callout) + .foregroundStyle(.secondary) + } + } else { + Text("No logs loaded yet.") + .font(.callout) + .foregroundStyle(.secondary) + } + } +} + private struct CockpitReviewSection: View { let reviews: [CockpitReviewSummary] diff --git a/apps/macos/Sources/OpenClaw/GatewayConnection.swift b/apps/macos/Sources/OpenClaw/GatewayConnection.swift index 454e17dd2..007f0a5a1 100644 --- a/apps/macos/Sources/OpenClaw/GatewayConnection.swift +++ b/apps/macos/Sources/OpenClaw/GatewayConnection.swift @@ -104,6 +104,7 @@ actor GatewayConnection { case codeWorkerResume = "code.worker.resume" case codeWorkerCancel = "code.worker.cancel" case codeWorkerLogs = "code.worker.logs" + case codeWorkerReviewArtifacts = "code.worker.review-artifacts" } private let configProvider: @Sendable () async throws -> Config @@ -848,6 +849,13 @@ extension GatewayConnection { timeoutMs: 10000) } + func codeWorkerReviewArtifacts(workerId: String) async throws -> CockpitReviewArtifacts { + try await self.requestDecoded( + method: .codeWorkerReviewArtifacts, + params: ["workerId": AnyCodable(workerId)], + timeoutMs: 15000) + } + nonisolated static func decodeCronListResponse(_ data: Data) throws -> [CronJob] { let decoded = try JSONDecoder().decode(LossyCronListResponse.self, from: data) let jobs = decoded.jobs.compactMap(\.value) diff --git a/docs/cockpit/FAST-TODO.md b/docs/cockpit/FAST-TODO.md index 21ce5983c..5f7da7c21 100644 --- a/docs/cockpit/FAST-TODO.md +++ b/docs/cockpit/FAST-TODO.md @@ -23,7 +23,7 @@ Arc becomes the default daily surface when these are all done: - [ ] Add blocked / needs-input queue - [ ] Add review-ready queue - [ ] Expose run summaries and review-ready artifacts from the gateway -- [ ] Add diff/test/log review lane +- [x] Add diff/test/log review lane - [ ] Add workspace persistence ## Phase B: Embedded Execution Surface diff --git a/src/code-cockpit/gateway-handlers.test.ts b/src/code-cockpit/gateway-handlers.test.ts index 6f6c0c60d..1fd6835b8 100644 --- a/src/code-cockpit/gateway-handlers.test.ts +++ b/src/code-cockpit/gateway-handlers.test.ts @@ -81,6 +81,15 @@ const runtimeMethods = vi.hoisted(() => ({ stdoutTail: "", stderrTail: "", })), + readWorkerReviewArtifacts: vi.fn(async ({ workerId }: { workerId: string }) => ({ + workerId, + worktreePath: "/tmp/worktree", + baseBranch: "main", + diff: "diff --git a/file.ts b/file.ts\n+added line", + commitLog: "abc1234 feat: add review lane", + testOutput: "Tests: 1 passed", + generatedAt: "2026-03-19T12:00:00.000Z", + })), supervisorTick: vi.fn(async ({ repoRoot }: { repoRoot?: string }) => ({ action: "started", task: { id: "task_123", title: "Ship blocked queue", repoRoot }, @@ -155,6 +164,7 @@ beforeEach(() => { runtimeMethods.runtime.cancelWorker.mockClear(); runtimeMethods.runtime.showWorker.mockClear(); runtimeMethods.runtime.readWorkerLogs.mockClear(); + runtimeMethods.runtime.readWorkerReviewArtifacts.mockClear(); runtimeMethods.runtime.supervisorTick.mockClear(); runtimeMethods.runtime.getWorkspaceSummary.mockClear(); }); @@ -325,6 +335,40 @@ describe("code cockpit gateway handlers", () => { ); }); + it("delegates worker.review-artifacts to the gateway-owned runtime", async () => { + const { codeCockpitHandlers } = await import("../gateway/server-methods/code-cockpit.js"); + const respond = vi.fn(); + + await codeCockpitHandlers["code.worker.review-artifacts"]({ + req: { + method: "code.worker.review-artifacts", + id: "1", + params: { workerId: "worker_123" }, + }, + params: { workerId: "worker_123" }, + client: null, + isWebchatConnect: () => false, + respond, + context: {} as never, + }); + + expect(runtimeMethods.getCodeCockpitRuntime).toHaveBeenCalledTimes(1); + expect(runtimeMethods.runtime.readWorkerReviewArtifacts).toHaveBeenCalledWith({ + workerId: "worker_123", + }); + expect(respond).toHaveBeenCalledWith( + true, + expect.objectContaining({ + workerId: "worker_123", + baseBranch: "main", + diff: expect.stringContaining("diff --git"), + commitLog: expect.stringContaining("abc1234"), + testOutput: expect.stringContaining("passed"), + }), + undefined, + ); + }); + it("builds a dashboard snapshot through the gateway-owned runtime", async () => { const { codeCockpitHandlers } = await import("../gateway/server-methods/code-cockpit.js"); const respond = vi.fn(); diff --git a/src/code-cockpit/runtime.ts b/src/code-cockpit/runtime.ts index d1288aed8..a9971b565 100644 --- a/src/code-cockpit/runtime.ts +++ b/src/code-cockpit/runtime.ts @@ -38,6 +38,7 @@ import { type CodeTask, type CodeTaskStatus, type CodeWorkerSession, + type CodeWorkerReviewArtifacts, resolveCodeCockpitStorePath, resolveCodeReviewRequestStatus, updateCodeRun, @@ -1585,6 +1586,85 @@ class CodeCockpitRuntime { }; } + async readWorkerReviewArtifacts(params: { + workerId: string; + }): Promise { + await this.ensureInitialized(); + const store = await loadCodeCockpitStore(); + const worker = store.workers.find((entry) => entry.id === params.workerId); + if (!worker) { + throw new Error(`Worker "${params.workerId}" not found`); + } + const repoRoot = worker.repoRoot; + if (!repoRoot) { + throw new Error(`Worker "${params.workerId}" has no repoRoot`); + } + const worktreePath = worker.worktreePath ?? buildDefaultWorktreePath(repoRoot, worker); + if (!(await worktreeLooksInitialized(worktreePath))) { + throw new Error(`Worker "${params.workerId}" worktree is not initialized`); + } + + const baseBranch = await resolveBaseBranch(this.runCommandWithTimeout, repoRoot); + const mergeBase = await readGitValue(this.runCommandWithTimeout, worktreePath, [ + "merge-base", + `origin/${baseBranch}`, + "HEAD", + ]); + + // Diff: changes since divergence from base branch + const diffBase = mergeBase ?? `origin/${baseBranch}`; + const diffResult = await this.runCommandWithTimeout( + ["git", "-C", worktreePath, "diff", "--stat", "--patch", diffBase], + { timeoutMs: 15_000 }, + ).catch(() => null); + const diff = trimLogTail(diffResult?.stdout ?? "", MAX_LOG_TAIL_CHARS) ?? ""; + + // Commit log: commits since divergence from base branch + const logResult = await this.runCommandWithTimeout( + ["git", "-C", worktreePath, "log", "--oneline", "--no-decorate", `${diffBase}..HEAD`], + { timeoutMs: 10_000 }, + ).catch(() => null); + const commitLog = logResult?.stdout?.trim() ?? ""; + + // Test output: read from the latest run's stdout if available + const runs = store.runs + .filter((entry) => entry.workerId === worker.id) + .toSorted((left, right) => right.updatedAt.localeCompare(left.updatedAt)); + const latestRun = runs[0] ?? null; + let testOutput = ""; + if (latestRun?.stdoutLogPath) { + try { + const fullLog = await fs.readFile(latestRun.stdoutLogPath, "utf8"); + // Extract test-related output lines (vitest/jest patterns) + const testLines = fullLog + .split("\n") + .filter( + (line) => + /^\s*(PASS|FAIL|✓|✗|✕|Tests?:|Test Suites?:|test[s ]|RUNS?)/i.test(line) || + /^\s*(✓|×|√|✘)\s/.test(line) || + /^\s*\d+ (passed|failed|skipped|pending)/i.test(line), + ); + testOutput = testLines.length > 0 ? testLines.join("\n") : ""; + } catch { + // Log file may not exist or be unreadable + } + } + // Fall back to summary if no structured test output found + if (!testOutput && latestRun?.summary) { + testOutput = latestRun.summary; + } + + return { + workerId: worker.id, + worktreePath, + baseBranch, + diff, + commitLog, + testOutput, + generatedAt: new Date().toISOString(), + }; + } + async addTask(params: CreateCodeTaskInput): Promise { await this.ensureInitialized(); return await createCodeTask(params); diff --git a/src/code-cockpit/store.ts b/src/code-cockpit/store.ts index 90f8d744f..17107ed23 100644 --- a/src/code-cockpit/store.ts +++ b/src/code-cockpit/store.ts @@ -235,6 +235,16 @@ export type CodeResolvedReviewResult = { worker: CodeWorkerSession | null; }; +export type CodeWorkerReviewArtifacts = { + workerId: string; + worktreePath: string; + baseBranch: string; + diff: string; + commitLog: string; + testOutput: string; + generatedAt: string; +}; + export type CodeCockpitStoreOptions = { env?: NodeJS.ProcessEnv; homedir?: () => string; diff --git a/src/gateway/server-methods/code-cockpit.ts b/src/gateway/server-methods/code-cockpit.ts index 73f9f1e1f..0bf77f152 100644 --- a/src/gateway/server-methods/code-cockpit.ts +++ b/src/gateway/server-methods/code-cockpit.ts @@ -244,6 +244,15 @@ export const codeCockpitHandlers: GatewayRequestHandlers = { }), ); }, + "code.worker.review-artifacts": async ({ params, respond }) => { + await withRuntimeResult( + respond, + async () => + await getCodeCockpitRuntime().readWorkerReviewArtifacts({ + workerId: requireWorkerId(params.workerId), + }), + ); + }, "code.supervisor.tick": async ({ params, respond }) => { await withRuntimeResult( respond,