Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
650e977
Cloud: show startup checklist in chat
teamleaderleo Nov 1, 2026
1b7e56b
Cloud: accept environment install step status
teamleaderleo Nov 1, 2026
f84428e
Cloud: preserve legacy startup event snapshots
teamleaderleo Nov 1, 2026
1357cf7
Cloud: isolate checklist status markers
teamleaderleo Nov 1, 2026
33c0699
Cloud: show repository seed progress
teamleaderleo Nov 1, 2026
961bd86
Cloud: use quiet initialized disclosure
teamleaderleo Nov 1, 2026
8fb4e85
Cloud: infer repository from seed status
teamleaderleo Nov 1, 2026
55091e8
Cloud: repair startup checklist flow
teamleaderleo Nov 1, 2026
a9df367
Cloud: show setup failure detail
teamleaderleo Nov 1, 2026
c9403f3
Cloud: forward startup seed and setup progress
teamleaderleo Nov 1, 2026
975eedf
Cloud: mark explicit setup failures
teamleaderleo Nov 1, 2026
98174c3
Cloud: forward host startup timings
teamleaderleo Nov 1, 2026
8009b7b
Cloud: forward harness timing on initialization
teamleaderleo Nov 1, 2026
73fd563
Cloud: stop failed startup rows cleanly
teamleaderleo Nov 1, 2026
f7093c3
Cloud: clear pending rows after startup failure
teamleaderleo Nov 1, 2026
b624e25
Cloud: include startup labels in the generated catalog
teamleaderleo Nov 1, 2026
c8012ba
Fix Cloud startup gallery variant names
teamleaderleo Nov 1, 2026
9db6f6d
Merge remote-tracking branch 'origin/feat-cmux-next' into lane/cloud-…
teamleaderleo Nov 1, 2026
1de8175
Merge remote-tracking branch 'origin/feat-cmux-next' into lane/cloud-…
teamleaderleo Nov 1, 2026
409d77a
fix cloud startup checklist token dimensions
teamleaderleo Nov 1, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,41 @@ extension AgentPaneCloudStart {
public var folder: String?
public var machine: String?
public var size: String?
public var cloneState: String?
public var clonePhase: String?
public var cloneReceivedBytes: Int?
public var cloneObjects: Int?
public var cloneTotalObjects: Int?
public var setupStatus: String?
public var setupLogTail: String?
public var machineStartedAt: Double?
public var machineFinishedAt: Double?
public var harnessStartedAt: Double?
public var harnessFinishedAt: Double?

public init(name: String, folder: String? = nil, machine: String? = nil, size: String? = nil) {
public init(name: String, folder: String? = nil, machine: String? = nil, size: String? = nil,
cloneState: String? = nil, clonePhase: String? = nil, cloneReceivedBytes: Int? = nil,
cloneObjects: Int? = nil, cloneTotalObjects: Int? = nil, setupStatus: String? = nil,
setupLogTail: String? = nil, machineStartedAt: Double? = nil, machineFinishedAt: Double? = nil,
harnessStartedAt: Double? = nil, harnessFinishedAt: Double? = nil) {
(self.name, self.folder, self.machine, self.size) = (name, folder, machine, size)
(self.cloneState, self.clonePhase, self.cloneReceivedBytes, self.cloneObjects, self.cloneTotalObjects) =
(cloneState, clonePhase, cloneReceivedBytes, cloneObjects, cloneTotalObjects)
(self.setupStatus, self.setupLogTail) = (setupStatus, setupLogTail)
(self.machineStartedAt, self.machineFinishedAt, self.harnessStartedAt, self.harnessFinishedAt) =
(machineStartedAt, machineFinishedAt, harnessStartedAt, harnessFinishedAt)
}
}

/// Tells the page's card `step` as it begins.
@MainActor public func progress(_ step: Step, to view: AgentPaneView) {
progress(step.name, folder: step.folder, machine: step.machine, size: step.size, to: view)
progress(step.name, folder: step.folder, machine: step.machine, size: step.size,
cloneState: step.cloneState, clonePhase: step.clonePhase,
cloneReceivedBytes: step.cloneReceivedBytes, cloneObjects: step.cloneObjects,
cloneTotalObjects: step.cloneTotalObjects, setupStatus: step.setupStatus,
setupLogTail: step.setupLogTail, machineStartedAt: step.machineStartedAt,
machineFinishedAt: step.machineFinishedAt, harnessStartedAt: step.harnessStartedAt,
harnessFinishedAt: step.harnessFinishedAt, to: view)
}

/// A machine's size in the card's words: "4 CPU · 16 GB".
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,11 +205,36 @@ extension AgentPaneCloudStart {
/// step it is on (`machine`, `folder`, `opening`), the folder it copies, and the machine and its
/// size once known, keyed by the page's send so a card for another start ignores it.
@MainActor public func progress(_ step: String, folder: String? = nil, machine: String? = nil, size: String? = nil,
cloneState: String? = nil, clonePhase: String? = nil, cloneReceivedBytes: Int? = nil,
cloneObjects: Int? = nil, cloneTotalObjects: Int? = nil, setupStatus: String? = nil,
setupLogTail: String? = nil,
machineStartedAt: Double? = nil, machineFinishedAt: Double? = nil,
harnessStartedAt: Double? = nil, harnessFinishedAt: Double? = nil,
to view: AgentPaneView) {
var value: [String: JSONValue] = ["key": .string(key), "step": .string(step)]
if let folder { value["folder"] = .string(folder) }
if let machine { value["machine"] = .string(machine) }
if let size { value["size"] = .string(size) }
var cloneFields: [String: JSONValue] = [:]
if let cloneState { cloneFields["state"] = .string(cloneState) }
if let clonePhase { cloneFields["phase"] = .string(clonePhase) }
var cloneProgress: [String: JSONValue] = [:]
if let cloneReceivedBytes { cloneProgress["received_bytes"] = .number(Double(cloneReceivedBytes)) }
if let cloneObjects { cloneProgress["objects"] = .number(Double(cloneObjects)) }
if let cloneTotalObjects { cloneProgress["total_objects"] = .number(Double(cloneTotalObjects)) }
if !cloneProgress.isEmpty { cloneFields["progress"] = .object(cloneProgress) }
if !cloneFields.isEmpty { value["clone"] = .object(cloneFields) }
var setupFields: [String: JSONValue] = [:]
if let setupStatus { setupFields["status"] = .string(setupStatus) }
if let setupLogTail { setupFields["log_tail"] = .string(setupLogTail) }
if !setupFields.isEmpty { value["setup"] = .object(setupFields) }
if let machineStartedAt { value["machineStartedAt"] = .number(machineStartedAt) }
if let machineFinishedAt { value["machineFinishedAt"] = .number(machineFinishedAt) }
if let harnessStartedAt { value["harnessStartedAt"] = .number(harnessStartedAt) }
if let harnessFinishedAt { value["harnessFinishedAt"] = .number(harnessFinishedAt) }
// The harness timing comes from the host's start response. Swift only forwards it;
// do not stamp Date() here because the page owns elapsed-time rendering.
if step == "opening" { value["initialized"] = .bool(true) }
let push = JSONValue.object(value)
view.deliver([AgentPageEvent(kind: "cloudStartProgress", value: push)],
scripts: ["window.cmuxAcpmuxBridge?.cloudStartProgress?.(\(push.compactText));"])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ nonisolated struct CloudMoveCLI: Sendable {
/// `link machine update --if-older`: the machine runs another build that is not older than
/// this app's (a newer one, or the same source tree), so it was left alone.
var notOlder = false
/// Optional git-seed/environment progress returned by Cloud start-chat copy.
var seedState: String?
var seedPhase: String?
var seedReceivedBytes: Int?
var seedObjects: Int?
var seedTotalObjects: Int?
var environmentStatus: String?
var environmentLogTail: String?
var machineStartedAt: Double?
var machineFinishedAt: Double?
var harnessStartedAt: Double?
var harnessFinishedAt: Double?
}

/// The `cmux` binary (the bundled cmux-tui).
Expand Down Expand Up @@ -177,7 +189,18 @@ nonisolated struct CloudMoveCLI: Sendable {
sessionID: object["session_id"] as? String, transferID: object["transfer_id"] as? String,
conflicts: object["conflicts"] as? [String] ?? [], side: object["side"] as? String,
updated: object["updated"] as? Bool ?? false, build: object["build"] as? String,
notOlder: object["not_older"] as? Bool ?? false)
notOlder: object["not_older"] as? Bool ?? false,
seedState: object["seed_state"] as? String ?? (object["state"] as? String),
seedPhase: object["seed_phase"] as? String ?? (object["phase"] as? String),
seedReceivedBytes: object["received_bytes"] as? Int,
seedObjects: object["objects"] as? Int,
seedTotalObjects: object["total_objects"] as? Int,
environmentStatus: object["environment_status"] as? String,
environmentLogTail: object["environment_log_tail"] as? String,
machineStartedAt: object["machine_started_at"] as? Double ?? object["machineStartedAt"] as? Double,
machineFinishedAt: object["machine_finished_at"] as? Double ?? object["machineFinishedAt"] as? Double,
harnessStartedAt: object["harness_started_at"] as? Double ?? object["harnessStartedAt"] as? Double,
harnessFinishedAt: object["harness_finished_at"] as? Double ?? object["harnessFinishedAt"] as? Double)
return (answer, object, result.status)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,22 @@ struct CloudStartChat {
throw CloudMove.Failure(message: CloudMoveStrings.syncFailed(name, CloudStrings.errorText(error)), retry: again)
}
guard copy.0.ok else { throw CloudMove.failure(syncing: copy.0, machine: name, retry: again) }
report(.init(name: "folder", folder: folderName, machine: name, size: size,
cloneState: copy.0.seedState, clonePhase: copy.0.seedPhase,
cloneReceivedBytes: copy.0.seedReceivedBytes, cloneObjects: copy.0.seedObjects,
cloneTotalObjects: copy.0.seedTotalObjects, setupStatus: copy.0.environmentStatus,
setupLogTail: copy.0.environmentLogTail, machineStartedAt: copy.0.machineStartedAt,
machineFinishedAt: copy.0.machineFinishedAt, harnessStartedAt: copy.0.harnessStartedAt,
harnessFinishedAt: copy.0.harnessFinishedAt))
let remoteChat = copy.1.chat
// The copy may take long: a chat whose account changed meanwhile opens nowhere, and this
// Mac's tab stays.
try Self.checkAccount(account, now: services.cloud.account, listCurrent: services.cloud.hasLoadedCurrentMachines)
_ = await loginPassed
// The login may have waited on a confirm and a sign-in: check the account again after it.
try Self.checkAccount(account, now: services.cloud.account, listCurrent: services.cloud.hasLoadedCurrentMachines)
report(.init(name: "opening", folder: folderName, machine: name, size: size))
report(.init(name: "opening", folder: folderName, machine: name, size: size,
harnessStartedAt: copy.0.harnessStartedAt, harnessFinishedAt: copy.0.harnessFinishedAt))
return try await open(start, tab: tab, on: machine, cwd: remoteChat, tabKey: plan.tabKey)
}

Expand Down
31 changes: 30 additions & 1 deletion webviews/src/agent-session/acpmux/CloudStartView.gallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ const begun: CloudStartProgress = {
folder: "paint",
};
const known = { machine: "paint-dev", size: "4 CPU · 16 GB", secrets: 3, network: "limited" } as const;
const timing = { startedAt: 10_000, finishedAt: 12_400 } as const;

const showStart: Play = async (ctx) => {
await ctx.waitFor(() => ctx.document.querySelector(".acpmux-cloud-start__status"));
await ctx.waitFor(() => ctx.document.querySelector(".acpmux-cloud-start__summary"));
};

export default componentEntry<ComponentProps<typeof CloudStartView>>({
Expand Down Expand Up @@ -63,6 +64,34 @@ export default componentEntry<ComponentProps<typeof CloudStartView>>({
},
},
},
"checklist-machine": {
note: "Cloud machine is being created; the machine step spins while later steps remain quiet.",
props: { start: { ...begun, machineStartedAt: 10_000, machine: known.machine } },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: These fixtures do not render the checklist states described by their notes: the machine row remains pending because the fixtures omit step. Set step: "machine" for the machine scene and step: "folder" for later-stage scenes so the machine row becomes running or done.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/src/agent-session/acpmux/CloudStartView.gallery.tsx, line 69:

<comment>These fixtures do not render the checklist states described by their notes: the machine row remains pending because the fixtures omit `step`. Set `step: "machine"` for the machine scene and `step: "folder"` for later-stage scenes so the machine row becomes running or done.</comment>

<file context>
@@ -63,6 +64,34 @@ export default componentEntry<ComponentProps<typeof CloudStartView>>({
     },
+    checklistMachine: {
+      note: "Cloud machine is being created; the machine step spins while later steps remain quiet.",
+      props: { start: { ...begun, machineStartedAt: 10_000, machine: known.machine } },
+    },
+    checklistClone: {
</file context>

},
"checklist-clone": {
note: "A repository is being cloned into the chat folder.",
props: { start: { ...begun, hasRepository: true, repository: "manaflow-ai/cmux", machine: known.machine, machineFinishedAt: 12_400, clone: { state: "running", ...timing } } },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The clone fixture marks the operation running but supplies finishedAt through ...timing, freezing its duration at 2.4 seconds. Use a running-only timing object without finishedAt for this scene.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/src/agent-session/acpmux/CloudStartView.gallery.tsx, line 73:

<comment>The clone fixture marks the operation running but supplies `finishedAt` through `...timing`, freezing its duration at 2.4 seconds. Use a running-only timing object without `finishedAt` for this scene.</comment>

<file context>
@@ -63,6 +64,34 @@ export default componentEntry<ComponentProps<typeof CloudStartView>>({
+    },
+    checklistClone: {
+      note: "A repository is being cloned into the chat folder.",
+      props: { start: { ...begun, hasRepository: true, repository: "manaflow-ai/cmux", machine: known.machine, machineFinishedAt: 12_400, clone: { state: "running", ...timing } } },
+    },
+    checklistSetupCached: {
</file context>

},
"checklist-setup-cached": {
note: "The environment snapshot is ready, so setup completes from cache.",
props: { start: { ...begun, hasRepository: true, repository: "manaflow-ai/cmux", clone: { state: "done", ...timing }, setup: { state: "done", status: "ready", ...timing } } },
},
"checklist-setup-running": {
note: "The derived environment is still building its setup snapshot.",
props: { start: { ...begun, hasRepository: true, repository: "manaflow-ai/cmux", clone: { state: "done", ...timing }, setup: { state: "running", status: "building", startedAt: 10_000 } } },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The running fixtures pass an epoch timestamp of 10_000, so the gallery shows an absurd multi-decade duration for unfinished work. Use a current epoch base, such as Date.now() - 2_400, for running fixture timestamps.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/src/agent-session/acpmux/CloudStartView.gallery.tsx, line 81:

<comment>The running fixtures pass an epoch timestamp of `10_000`, so the gallery shows an absurd multi-decade duration for unfinished work. Use a current epoch base, such as `Date.now() - 2_400`, for running fixture timestamps.</comment>

<file context>
@@ -63,6 +64,34 @@ export default componentEntry<ComponentProps<typeof CloudStartView>>({
+    },
+    checklistSetupRunning: {
+      note: "The derived environment is still building its setup snapshot.",
+      props: { start: { ...begun, hasRepository: true, repository: "manaflow-ai/cmux", clone: { state: "done", ...timing }, setup: { state: "running", status: "building", startedAt: 10_000 } } },
+    },
+    checklistSetupFailed: {
</file context>

},
"checklist-setup-failed": {
note: "Setup failed and the card keeps the short log tail inline.",
props: { start: { ...begun, hasRepository: true, repository: "manaflow-ai/cmux", clone: { state: "done", ...timing }, setup: { state: "failed", status: "failed", logTail: "npm ERR! missing script: build", ...timing } } },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The expanded checklist scenes exceed the entry's fixed 300px gallery surface, so the new rows and setup log tail are clipped by the gallery window. Give the checklist variants a larger height or raise the entry height.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/src/agent-session/acpmux/CloudStartView.gallery.tsx, line 85:

<comment>The expanded checklist scenes exceed the entry's fixed 300px gallery surface, so the new rows and setup log tail are clipped by the gallery window. Give the checklist variants a larger height or raise the entry height.</comment>

<file context>
@@ -63,6 +64,34 @@ export default componentEntry<ComponentProps<typeof CloudStartView>>({
+    },
+    checklistSetupFailed: {
+      note: "Setup failed and the card keeps the short log tail inline.",
+      props: { start: { ...begun, hasRepository: true, repository: "manaflow-ai/cmux", clone: { state: "done", ...timing }, setup: { state: "failed", status: "failed", logTail: "npm ERR! missing script: build", ...timing } } },
+    },
+    checklistSetupSkipped: {
</file context>

},
"checklist-setup-skipped": {
note: "No setup script is configured; the link opens Edit cloud environment at Script.",
props: { start: { ...begun, hasRepository: true, repository: "manaflow-ai/cmux", clone: { state: "done", ...timing }, setup: { state: "skipped", hasScript: false } } },
},
initialized: {
note: "Once the harness starts, the checklist folds to one quiet line.",
props: { start: { ...begun, initialized: true, agent: "Claude Code", harnessStartedAt: 12_400, harnessFinishedAt: 12_900 } },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The initialized fixture has a repository folder but no completed clone state, so expanding its collapsed summary shows Clone repository pending after initialization. Add a { state: "done" } clone fixture value to represent the completed session.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/src/agent-session/acpmux/CloudStartView.gallery.tsx, line 93:

<comment>The initialized fixture has a repository folder but no completed clone state, so expanding its collapsed summary shows `Clone repository` pending after initialization. Add a `{ state: "done" }` clone fixture value to represent the completed session.</comment>

<file context>
@@ -63,6 +64,34 @@ export default componentEntry<ComponentProps<typeof CloudStartView>>({
+    },
+    initialized: {
+      note: "Once the harness starts, the checklist folds to one quiet line.",
+      props: { start: { ...begun, initialized: true, agent: "Claude Code", harnessStartedAt: 12_400, harnessFinishedAt: 12_900 } },
+    },
   },
</file context>
Suggested change
props: { start: { ...begun, initialized: true, agent: "Claude Code", harnessStartedAt: 12_400, harnessFinishedAt: 12_900 } },
props: { start: { ...begun, initialized: true, agent: "Claude Code", clone: { state: "done", ...timing }, harnessStartedAt: 12_400, harnessFinishedAt: 12_900 } },

},
},
// The gallery loads no experiment without a step (format.ts validateEntries): the card is static,
// so its one step waits for the card the arm draws.
Expand Down
33 changes: 32 additions & 1 deletion webviews/src/agent-session/acpmux/CloudStartView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// `cloud-start-look` experiment draws the same facts.
import { afterEach, expect, test } from "bun:test";
import { renderToStaticMarkup } from "react-dom/server";
import { CloudStartView } from "./CloudStartView";
import { CloudStartView, startupChecklist } from "./CloudStartView";
import type { CloudStartProgress } from "./cloudStartProgress";

const start: CloudStartProgress = {
Expand Down Expand Up @@ -54,3 +54,34 @@ test("a failure says the host's words with no spinner, and the New Tab page draw
expect(html).not.toContain("cv-spinner");
expect(html).not.toContain("fix the build");
});

test("the startup checklist skips clone without a repository and explains a missing script", () => {
const rows = startupChecklist({ key: "k", task: "hi", state: "starting", home: true, setup: { state: "skipped", hasScript: false } });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This new checklist input exceeds the repository's 120-column Oxfmt width, so vp fmt --check will reject the file. Wrap the object literal across lines.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/src/agent-session/acpmux/CloudStartView.test.tsx, line 59:

<comment>This new checklist input exceeds the repository's 120-column Oxfmt width, so `vp fmt --check` will reject the file. Wrap the object literal across lines.</comment>

<file context>
@@ -54,3 +54,17 @@ test("a failure says the host's words with no spinner, and the New Tab page draw
 });
+
+test("the startup checklist skips clone without a repository and explains a missing script", () => {
+  const rows = startupChecklist({ key: "k", task: "hi", state: "starting", home: true, setup: { state: "skipped", hasScript: false } });
+  expect(rows.map((row) => row.status)).toEqual(["pending", "skipped", "skipped", "pending"]);
+  expect(rows[2]?.action).toBe(true);
</file context>
Suggested change
const rows = startupChecklist({ key: "k", task: "hi", state: "starting", home: true, setup: { state: "skipped", hasScript: false } });
const rows = startupChecklist({
key: "k",
task: "hi",
state: "starting",
home: true,
setup: { state: "skipped", hasScript: false },
});

expect(rows.map((row) => row.status)).toEqual(["pending", "skipped", "skipped", "pending"]);
expect(rows[2]?.action).toBe(true);
expect(rows[2]?.detail).toContain("Add a setup script");
});

test("environment setup status maps cached, running and failed states", () => {
const base = { key: "k", task: "hi", state: "starting" as const };
expect(startupChecklist({ ...base, setup: { state: "done", status: "ready" } })[2]?.detail).toBe("cached");
expect(startupChecklist({ ...base, setup: { state: "running", status: "building" } })[2]?.status).toBe("running");
expect(startupChecklist({ ...base, setup: { state: "failed", status: "failed", logTail: "npm ERR!" } })[2]).toMatchObject({ status: "error", logTail: "npm ERR!" });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This new assertion also exceeds the repository's 120-column Oxfmt width, so the formatting check will still fail after the other long line is fixed. Break the nested call before the matcher.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/src/agent-session/acpmux/CloudStartView.test.tsx, line 69:

<comment>This new assertion also exceeds the repository's 120-column Oxfmt width, so the formatting check will still fail after the other long line is fixed. Break the nested call before the matcher.</comment>

<file context>
@@ -54,3 +54,17 @@ test("a failure says the host's words with no spinner, and the New Tab page draw
+  const base = { key: "k", task: "hi", state: "starting" as const };
+  expect(startupChecklist({ ...base, setup: { state: "done", status: "ready" } })[2]?.detail).toBe("cached");
+  expect(startupChecklist({ ...base, setup: { state: "running", status: "building" } })[2]?.status).toBe("running");
+  expect(startupChecklist({ ...base, setup: { state: "failed", status: "failed", logTail: "npm ERR!" } })[2]).toMatchObject({ status: "error", logTail: "npm ERR!" });
+});
</file context>
Suggested change
expect(startupChecklist({ ...base, setup: { state: "failed", status: "failed", logTail: "npm ERR!" } })[2]).toMatchObject({ status: "error", logTail: "npm ERR!" });
expect(
startupChecklist({ ...base, setup: { state: "failed", status: "failed", logTail: "npm ERR!" } })[2],
).toMatchObject({ status: "error", logTail: "npm ERR!" });

});

test("clone status keeps seed phase and object progress in the row", () => {
const rows = startupChecklist({ key: "k", task: "hi", state: "starting", hasRepository: true, clone: { state: "running", phase: "Receiving objects", progress: { objects: 2, totalObjects: 4, receivedBytes: 2048 } } });
expect(rows[1]?.detail).toBe("Receiving objects · 2/4 objects · 2 KB");
});

test("a machine failure stops a clone spinner", () => {
const rows = startupChecklist({ key: "k", task: "hi", state: "failed", step: "machine", hasRepository: true, clone: { state: "running" } });
expect(rows[0]?.status).toBe("error");
expect(rows[1]?.status).toBe("pending");
});

test("a missing setup script is plain text without an environment callback", () => {
const html = renderToStaticMarkup(<CloudStartView start={{ key: "k", task: "hi", state: "starting", home: true, setup: { state: "skipped", hasScript: false } }} />);
expect(html).toContain("Add a setup script to install dependencies and configure your environment.");
expect(html).not.toContain('class="acpmux-cloud-start__link"');
});
Loading
Loading