diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneCloudStart+Cloud.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneCloudStart+Cloud.swift index 4fbe0acf1520..6a36c128a59a 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneCloudStart+Cloud.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneCloudStart+Cloud.swift @@ -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". diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneCloudStart.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneCloudStart.swift index 0d60259da442..b097a15d1ad7 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneCloudStart.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneCloudStart.swift @@ -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));"]) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Cloud/CloudMoveCLI.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Cloud/CloudMoveCLI.swift index 546e9dae58ce..8bfb3a9db83d 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Cloud/CloudMoveCLI.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Cloud/CloudMoveCLI.swift @@ -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). @@ -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) } } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Cloud/CloudStartChat.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Cloud/CloudStartChat.swift index ab7945e0fb3f..feec9ea27b75 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Cloud/CloudStartChat.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Cloud/CloudStartChat.swift @@ -122,6 +122,13 @@ 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. @@ -129,7 +136,8 @@ struct CloudStartChat { _ = 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) } diff --git a/webviews/src/agent-session/acpmux/CloudStartView.gallery.tsx b/webviews/src/agent-session/acpmux/CloudStartView.gallery.tsx index dbea0ce8b36e..cfff72d613be 100644 --- a/webviews/src/agent-session/acpmux/CloudStartView.gallery.tsx +++ b/webviews/src/agent-session/acpmux/CloudStartView.gallery.tsx @@ -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>({ @@ -63,6 +64,34 @@ export default componentEntry>({ }, }, }, + "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 } }, + }, + "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 } } }, + }, + "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 } } }, + }, + "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 } } }, + }, + "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 } }, + }, }, // 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. diff --git a/webviews/src/agent-session/acpmux/CloudStartView.test.tsx b/webviews/src/agent-session/acpmux/CloudStartView.test.tsx index cd49f7d12518..42a8454e1247 100644 --- a/webviews/src/agent-session/acpmux/CloudStartView.test.tsx +++ b/webviews/src/agent-session/acpmux/CloudStartView.test.tsx @@ -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 = { @@ -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 } }); + 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!" }); +}); + +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(); + expect(html).toContain("Add a setup script to install dependencies and configure your environment."); + expect(html).not.toContain('class="acpmux-cloud-start__link"'); +}); diff --git a/webviews/src/agent-session/acpmux/CloudStartView.tsx b/webviews/src/agent-session/acpmux/CloudStartView.tsx index fbad2f307f80..f9b9e3c5eb8b 100644 --- a/webviews/src/agent-session/acpmux/CloudStartView.tsx +++ b/webviews/src/agent-session/acpmux/CloudStartView.tsx @@ -1,129 +1,113 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import { experimentArm } from "../../experiments/experiment"; import { cloudStartLook } from "./cloudStart.experiment"; -import { CLOUD_START_STEPS, type CloudStartProgress, type CloudStartStep } from "./cloudStartProgress"; -import { Check, Folder, Spinner } from "./conversation/icons"; -import { useT, type StringKey, type Translate } from "./i18n"; +import { CLOUD_START_STEPS, type CloudStartProgress, type CloudStartupOperation, type CloudStartupSetup } from "./cloudStartProgress"; +import { AlertCircle, Check, ChevronDown, ChevronRight, Folder, Spinner } from "./conversation/icons"; +import { useT, type Translate } from "./i18n"; import "./cloudStart.css"; -const STEP_KEYS: Record = { - machine: "cloudStart.step.machine", - folder: "cloudStart.step.folder", - opening: "cloudStart.step.opening", +type ChecklistStatus = "done" | "running" | "pending" | "skipped" | "error"; +type ChecklistStep = { id: "machine" | "clone" | "setup" | "harness"; label: string; status: ChecklistStatus; duration?: string; detail?: string; logTail?: string; action?: boolean }; + +function elapsed(startedAt: number | undefined, finishedAt: number | undefined, now = Date.now()): string | undefined { + if (startedAt === undefined) return undefined; + const ms = Math.max(0, (finishedAt ?? now) - startedAt); + if (ms < 1000) return "<1s"; + if (ms < 60_000) return `${Math.round(ms / 100) / 10}s`; + const seconds = Math.round(ms / 1000); + return `${Math.floor(seconds / 60)}m ${seconds % 60}s`; +} + +function operationStatus(operation: CloudStartupOperation | undefined): ChecklistStatus | undefined { + if (!operation) return undefined; + return operation.state === "done" ? "done" : operation.state === "failed" ? "error" : operation.state === "running" ? "running" : "pending"; +} + +function setupStatus(setup: CloudStartupSetup | undefined): ChecklistStatus | undefined { + if (!setup) return undefined; + if (setup.state === "skipped") return "skipped"; + if (setup.status === "ready" || setup.state === "done") return "done"; + if (setup.status === "failed" || setup.state === "failed") return "error"; + if (setup.status === "building" || setup.state === "running") return "running"; + return "pending"; +} + +function cloneDetail(operation: CloudStartupOperation | undefined): string | undefined { + if (!operation) return undefined; + const parts = [operation.phase]; + const progress = operation.progress; + if (progress?.objects !== undefined && progress.totalObjects !== undefined) parts.push(`${progress.objects}/${progress.totalObjects} objects`); + if (progress?.receivedBytes !== undefined) parts.push(`${Math.round(progress.receivedBytes / 1024)} KB`); + return parts.filter(Boolean).join(" · ") || operation.error; +} + +const DEFAULT_STARTUP_STRINGS: Record = { + "cloudStartup.machine": "Set up a Cloud machine", "cloudStartup.clone": "Clone repository", "cloudStartup.setup": "Run setup script", "cloudStartup.setupHint": "Add a setup script to install dependencies and configure your environment.", "cloudStartup.cached": "cached", "cloudStartup.harness": "Started {harness}", }; +export function startupChecklist(start: CloudStartProgress, t: Translate = (key, values = {}) => (DEFAULT_STARTUP_STRINGS[key] ?? key).replace("{harness}", String(values.harness ?? "Harness")), now = Date.now()): ChecklistStep[] { + const oldStep = start.step ? CLOUD_START_STEPS.indexOf(start.step) : -1; + const failedStep = start.state === "failed" + ? (oldStep === 0 ? "machine" : oldStep === 1 ? "clone" : oldStep === 2 ? "harness" : start.clone?.state === "running" || start.clone?.state === "failed" ? "clone" : start.setup?.state === "running" || start.setup?.state === "failed" ? "setup" : "machine") + : undefined; + const machine: ChecklistStep = { id: "machine", label: t("cloudStartup.machine"), status: failedStep === "machine" ? "error" : oldStep > 0 || start.initialized ? "done" : oldStep === 0 ? "running" : "pending", duration: elapsed(start.machineStartedAt, start.machineFinishedAt, now) }; + const hasRepository = start.hasRepository ?? (Boolean(start.repository || start.folder || start.clone) && !start.home); + const cloneStatus = operationStatus(start.clone); + const stoppedCloneStatus = start.state === "failed" && failedStep !== "clone" && cloneStatus === "running" ? "pending" : cloneStatus; + const clone: ChecklistStep = { id: "clone", label: t("cloudStartup.clone"), status: !hasRepository ? "skipped" : failedStep === "clone" ? "error" : stoppedCloneStatus ?? (oldStep > 1 ? "done" : oldStep === 1 ? "running" : "pending"), duration: elapsed(start.clone?.startedAt, start.clone?.finishedAt, now), ...((cloneDetail(start.clone) || start.clone?.error) ? { detail: cloneDetail(start.clone) || start.clone?.error } : {}) }; + const hasScript = start.setup?.hasScript; + const setupState = setupStatus(start.setup); + const stoppedSetupState = start.state === "failed" && failedStep !== "setup" && setupState === "running" ? "pending" : setupState; + const setup: ChecklistStep = { id: "setup", label: t("cloudStartup.setup"), status: hasScript === false ? "skipped" : failedStep === "setup" ? "error" : stoppedSetupState ?? (start.initialized ? "done" : "pending"), duration: elapsed(start.setup?.startedAt, start.setup?.finishedAt, now), ...(start.setup?.status === "ready" ? { detail: t("cloudStartup.cached") } : {}), ...((start.setup?.logTail || start.setup?.error) ? { detail: start.setup.logTail || start.setup.error, ...(start.setup.logTail ? { logTail: start.setup.logTail } : {}) } : {}), ...(hasScript === false ? { detail: t("cloudStartup.setupHint"), action: true } : {}) }; + const harness: ChecklistStep = { id: "harness", label: t("cloudStartup.harness", { harness: start.agent || "Harness" }), status: failedStep === "harness" ? "error" : start.initialized ? "done" : oldStep === 2 ? "running" : "pending", duration: elapsed(start.harnessStartedAt, start.harnessFinishedAt, now) }; + return [machine, clone, setup, harness]; +} -/// A step's words: "Starting your Cloud machine…", "Copying your project…", "Starting Claude Code…". -function stepText(step: CloudStartStep, agent: string | undefined, t: Translate): string { - return step === "opening" && agent ? t("cloudStart.step.openingAgent", { agent }) : t(STEP_KEYS[step]); +function mark(step: ChecklistStep, t: Translate) { + if (step.status === "done") return <>{t("cloudStartup.state.done")}; + if (step.status === "running") return <>{t("cloudStartup.state.running")}; + if (step.status === "error") return <>{t("cloudStartup.state.error")}; + if (step.status === "skipped") return <>{t("cloudStartup.state.skipped")}; + return <>{t("cloudStartup.state.pending")}; } type Fact = { id: string; label: string; value: string }; - -/// What the card says about the start, in order, each only once known. function facts(start: CloudStartProgress, t: Translate): Fact[] { const list: Fact[] = []; if (start.folder) list.push({ id: "folder", label: t("composer.folder"), value: start.folder }); - if (start.machine) - list.push({ - id: "machine", - label: t("sandbox.vm"), - value: start.size ? `${start.machine} · ${start.size}` : start.machine, - }); - if (start.secrets !== undefined) - list.push({ id: "secrets", label: t("cloudStart.secrets"), value: String(start.secrets) }); - if (start.network) - list.push({ - id: "network", - label: t("cloudStart.network"), - value: t(start.network === "full" ? "cloudStart.networkFull" : "cloudStart.networkLimited"), - }); + if (start.machine) list.push({ id: "machine", label: t("sandbox.vm"), value: start.size ? `${start.machine} · ${start.size}` : start.machine }); + if (start.secrets !== undefined) list.push({ id: "secrets", label: t("cloudStart.secrets"), value: String(start.secrets) }); + if (start.network) list.push({ id: "network", label: t("cloudStart.network"), value: t(start.network === "full" ? "cloudStart.networkFull" : "cloudStart.networkLimited") }); return list; } -/// A chat starting in Cloud (cloudStartProgress.ts): the person's message, then how the start goes -/// as metadata under it, laid out by the `cloud-start-look` experiment's arm. Working is a spinner; -/// a failure says the host's words (the message stays in the composer). -/// `bubble` false: no message bubble, where the message still shows in its box (the New Tab page). -export function CloudStartView({ start, bubble = true }: { start: CloudStartProgress; bubble?: boolean }) { +export function CloudStartView({ start, bubble = true, onOpenEnvironment }: { start: CloudStartProgress; bubble?: boolean; onOpenEnvironment?: (options: { environmentId?: string; repository?: string; section: "script" }) => void }) { const t = useT(); const arm = experimentArm(cloudStartLook); - const known = facts(start, t); const failed = start.state === "failed"; - const status = failed - ? (start.message ?? t("sandbox.failed")) - : start.step && arm !== "steps" - ? stepText(start.step, start.agent, t) - : t("cloudStart.title"); - return ( -
- {bubble && ( -
-
{start.task}
-
- )} - - {!failed && } - {status} - - {arm === "steps" && !failed && } - {known.length > 0 && - (arm === "rows" ? ( -
- {known.map((fact) => ( - -
{fact.label}
-
{fact.value}
-
- ))} -
- ) : ( -
    - {known.map((fact) => ( -
  • - {fact.id === "folder" ? ( - - ) : ( - {fact.label} - )} - {fact.value} -
  • - ))} -
- ))} + const [expanded, setExpanded] = useState(!start.initialized); + const [now, setNow] = useState(() => Date.now()); + useEffect(() => { + if (start.initialized || start.state === "failed") return; + const timer = window.setInterval(() => setNow(Date.now()), 1000); + return () => window.clearInterval(timer); + }, [start.initialized, start.state]); + useEffect(() => { setExpanded(start.initialized !== true); }, [start.initialized]); + const rows = startupChecklist(start, t, now); + const known = facts(start, t); + const openEnvironment = () => { + const options = { ...(start.environmentId ? { environmentId: start.environmentId } : {}), ...(start.repository ? { repository: start.repository } : {}), section: "script" as const }; + if (onOpenEnvironment) onOpenEnvironment(options); + else window.dispatchEvent(new CustomEvent("cmux:open-cloud-environment", { detail: options })); + }; + return
+ {bubble && !failed &&
{start.task}
} + {!failed && !start.initialized && {t("cloudStart.title")}} +
+ + {expanded &&
{rows.map((step) =>
{mark(step, t)}{step.label}{step.duration && }{step.id === "setup" && step.action ? {onOpenEnvironment ? : t("cloudStartup.setupHint")} : step.detail ? {step.detail} : null}
)}
}
- ); -} - -/// The `steps` arm: each step done (a check), current (a spinner) or still to come. -function StepList({ - step, - agent, - home, - t, -}: { - step: CloudStartStep | undefined; - agent?: string; - home?: boolean; - t: Translate; -}) { - // No project: nothing is copied, so there is no copy step. - const steps: readonly CloudStartStep[] = home - ? CLOUD_START_STEPS.filter((name) => name !== "folder") - : CLOUD_START_STEPS; - const current = step ? steps.indexOf(step) : -1; - return ( -
    - {steps.map((name, index) => { - const state = index < current ? "done" : index === current ? "current" : "next"; - return ( -
  1. - - {state === "done" ? : state === "current" ? : null} - - {stepText(name, agent, t)} -
  2. - ); - })} -
- ); + {failed && {start.message ?? t("sandbox.failed")}} + {known.length > 0 &&
    {known.map((fact) =>
  • {fact.id === "folder" ? : {fact.label}}{fact.value}
  • )}
} + {arm === "steps" && !failed &&
    {CLOUD_START_STEPS.map((name, index) => { const current = start.step ? CLOUD_START_STEPS.indexOf(start.step) : -1; const state = index < current ? "done" : index === current ? "current" : "next"; return
  1. {name}
  2. ; })}
} +
; } diff --git a/webviews/src/agent-session/acpmux/Localizable.xcstrings b/webviews/src/agent-session/acpmux/Localizable.xcstrings index 6b554316a9a5..d549c878ff46 100644 --- a/webviews/src/agent-session/acpmux/Localizable.xcstrings +++ b/webviews/src/agent-session/acpmux/Localizable.xcstrings @@ -246064,7 +246064,20 @@ "tools.cmux.closedTabs": {"comment":"Agent pane: a cmux_exec script's (or a cmux MCP tool's) transcript row title, after it closed {n} tabs","extractionState":"manual","localizations":{"ar":{"variations":{"plural":{"zero":{"stringUnit":{"state":"needs_review","value":"تم إغلاق علامات تبويب: {n}"}},"one":{"stringUnit":{"state":"needs_review","value":"تم إغلاق علامات تبويب: {n}"}},"two":{"stringUnit":{"state":"needs_review","value":"تم إغلاق علامات تبويب: {n}"}},"few":{"stringUnit":{"state":"needs_review","value":"تم إغلاق علامات تبويب: {n}"}},"many":{"stringUnit":{"state":"needs_review","value":"تم إغلاق علامات تبويب: {n}"}},"other":{"stringUnit":{"state":"needs_review","value":"تم إغلاق علامات تبويب: {n}"}}}}},"bs":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Zatvorena {n} kartica"}},"few":{"stringUnit":{"state":"needs_review","value":"Zatvorene {n} kartice"}},"other":{"stringUnit":{"state":"needs_review","value":"Zatvoreno {n} kartica"}}}}},"da":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Lukkede {n} fane"}},"other":{"stringUnit":{"state":"needs_review","value":"Lukkede {n} faner"}}}}},"de":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"{n} Tab geschlossen"}},"other":{"stringUnit":{"state":"needs_review","value":"{n} Tabs geschlossen"}}}}},"en":{"variations":{"plural":{"one":{"stringUnit":{"state":"translated","value":"Closed {n} tab"}},"other":{"stringUnit":{"state":"translated","value":"Closed {n} tabs"}}}}},"es":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Se cerró {n} pestaña"}},"many":{"stringUnit":{"state":"needs_review","value":"Se cerraron {n} pestañas"}},"other":{"stringUnit":{"state":"needs_review","value":"Se cerraron {n} pestañas"}}}}},"fr":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"{n} onglet fermé"}},"many":{"stringUnit":{"state":"needs_review","value":"{n} onglets fermés"}},"other":{"stringUnit":{"state":"needs_review","value":"{n} onglets fermés"}}}}},"it":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Chiusa {n} scheda"}},"many":{"stringUnit":{"state":"needs_review","value":"Chiuse {n} schede"}},"other":{"stringUnit":{"state":"needs_review","value":"Chiuse {n} schede"}}}}},"ja":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"{n} 個のタブを閉じました"}}}}},"km":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"បានបិទផ្ទាំងរង {n}"}}}}},"ko":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"탭 {n}개 닫음"}}}}},"nb":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Lukket {n} fane"}},"other":{"stringUnit":{"state":"needs_review","value":"Lukket {n} faner"}}}}},"pl":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Zamknięto {n} kartę"}},"few":{"stringUnit":{"state":"needs_review","value":"Zamknięto {n} karty"}},"many":{"stringUnit":{"state":"needs_review","value":"Zamknięto {n} kart"}},"other":{"stringUnit":{"state":"needs_review","value":"Zamknięto {n} karty"}}}}},"pt-BR":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"{n} aba fechada"}},"many":{"stringUnit":{"state":"needs_review","value":"{n} abas fechadas"}},"other":{"stringUnit":{"state":"needs_review","value":"{n} abas fechadas"}}}}},"ru":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Закрыта {n} вкладка"}},"few":{"stringUnit":{"state":"needs_review","value":"Закрыто {n} вкладки"}},"many":{"stringUnit":{"state":"needs_review","value":"Закрыто {n} вкладок"}},"other":{"stringUnit":{"state":"needs_review","value":"Закрыто {n} вкладки"}}}}},"th":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"ปิดแท็บ {n} แท็บแล้ว"}}}}},"tr":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"{n} sekme kapatıldı"}},"other":{"stringUnit":{"state":"needs_review","value":"{n} sekme kapatıldı"}}}}},"uk":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Закрито {n} вкладку"}},"few":{"stringUnit":{"state":"needs_review","value":"Закрито {n} вкладки"}},"many":{"stringUnit":{"state":"needs_review","value":"Закрито {n} вкладок"}},"other":{"stringUnit":{"state":"needs_review","value":"Закрито {n} вкладки"}}}}},"vi":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"Đã đóng {n} tab"}}}}},"zh-Hans":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"已关闭 {n} 个标签页"}}}}},"zh-Hant":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"已關閉 {n} 個分頁"}}}}}}}, "tools.cmux.sentNotifications": {"comment":"Agent pane: a cmux_exec script's (or a cmux MCP tool's) transcript row title, after it sent {n} notifications","extractionState":"manual","localizations":{"ar":{"variations":{"plural":{"zero":{"stringUnit":{"state":"needs_review","value":"تم إرسال إشعارات: {n}"}},"one":{"stringUnit":{"state":"needs_review","value":"تم إرسال إشعارات: {n}"}},"two":{"stringUnit":{"state":"needs_review","value":"تم إرسال إشعارات: {n}"}},"few":{"stringUnit":{"state":"needs_review","value":"تم إرسال إشعارات: {n}"}},"many":{"stringUnit":{"state":"needs_review","value":"تم إرسال إشعارات: {n}"}},"other":{"stringUnit":{"state":"needs_review","value":"تم إرسال إشعارات: {n}"}}}}},"bs":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Poslana {n} obavijest"}},"few":{"stringUnit":{"state":"needs_review","value":"Poslane {n} obavijesti"}},"other":{"stringUnit":{"state":"needs_review","value":"Poslano {n} obavijesti"}}}}},"da":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Sendte {n} notifikation"}},"other":{"stringUnit":{"state":"needs_review","value":"Sendte {n} notifikationer"}}}}},"de":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"{n} Mitteilung gesendet"}},"other":{"stringUnit":{"state":"needs_review","value":"{n} Mitteilungen gesendet"}}}}},"en":{"variations":{"plural":{"one":{"stringUnit":{"state":"translated","value":"Sent {n} notification"}},"other":{"stringUnit":{"state":"translated","value":"Sent {n} notifications"}}}}},"es":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Se envió {n} notificación"}},"many":{"stringUnit":{"state":"needs_review","value":"Se enviaron {n} notificaciones"}},"other":{"stringUnit":{"state":"needs_review","value":"Se enviaron {n} notificaciones"}}}}},"fr":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"{n} notification envoyée"}},"many":{"stringUnit":{"state":"needs_review","value":"{n} notifications envoyées"}},"other":{"stringUnit":{"state":"needs_review","value":"{n} notifications envoyées"}}}}},"it":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Inviata {n} notifica"}},"many":{"stringUnit":{"state":"needs_review","value":"Inviate {n} notifiche"}},"other":{"stringUnit":{"state":"needs_review","value":"Inviate {n} notifiche"}}}}},"ja":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"{n} 件の通知を送信しました"}}}}},"km":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"បានផ្ញើការជូនដំណឹង {n}"}}}}},"ko":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"알림 {n}개 보냄"}}}}},"nb":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Sendte {n} varsel"}},"other":{"stringUnit":{"state":"needs_review","value":"Sendte {n} varsler"}}}}},"pl":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Wysłano {n} powiadomienie"}},"few":{"stringUnit":{"state":"needs_review","value":"Wysłano {n} powiadomienia"}},"many":{"stringUnit":{"state":"needs_review","value":"Wysłano {n} powiadomień"}},"other":{"stringUnit":{"state":"needs_review","value":"Wysłano {n} powiadomienia"}}}}},"pt-BR":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"{n} notificação enviada"}},"many":{"stringUnit":{"state":"needs_review","value":"{n} notificações enviadas"}},"other":{"stringUnit":{"state":"needs_review","value":"{n} notificações enviadas"}}}}},"ru":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Отправлено {n} уведомление"}},"few":{"stringUnit":{"state":"needs_review","value":"Отправлено {n} уведомления"}},"many":{"stringUnit":{"state":"needs_review","value":"Отправлено {n} уведомлений"}},"other":{"stringUnit":{"state":"needs_review","value":"Отправлено {n} уведомления"}}}}},"th":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"ส่งการแจ้งเตือน {n} รายการแล้ว"}}}}},"tr":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"{n} bildirim gönderildi"}},"other":{"stringUnit":{"state":"needs_review","value":"{n} bildirim gönderildi"}}}}},"uk":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Надіслано {n} сповіщення"}},"few":{"stringUnit":{"state":"needs_review","value":"Надіслано {n} сповіщення"}},"many":{"stringUnit":{"state":"needs_review","value":"Надіслано {n} сповіщень"}},"other":{"stringUnit":{"state":"needs_review","value":"Надіслано {n} сповіщення"}}}}},"vi":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"Đã gửi {n} thông báo"}}}}},"zh-Hans":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"已发送 {n} 条通知"}}}}},"zh-Hant":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"已傳送 {n} 則通知"}}}}}}}, "tools.cmux.andMore": {"comment":"Agent pane: a cmux_exec script's (or a cmux MCP tool's) transcript row title, a script's main step and how many other steps of its kind it took; {action} is the main step's title","extractionState":"manual","localizations":{"ar":{"variations":{"plural":{"zero":{"stringUnit":{"state":"needs_review","value":"{action} و{n} خطوات أخرى"}},"one":{"stringUnit":{"state":"needs_review","value":"{action} و{n} خطوات أخرى"}},"two":{"stringUnit":{"state":"needs_review","value":"{action} و{n} خطوات أخرى"}},"few":{"stringUnit":{"state":"needs_review","value":"{action} و{n} خطوات أخرى"}},"many":{"stringUnit":{"state":"needs_review","value":"{action} و{n} خطوات أخرى"}},"other":{"stringUnit":{"state":"needs_review","value":"{action} و{n} خطوات أخرى"}}}}},"bs":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"{action} i još {n} korak"}},"few":{"stringUnit":{"state":"needs_review","value":"{action} i još {n} koraka"}},"other":{"stringUnit":{"state":"needs_review","value":"{action} i još {n} koraka"}}}}},"da":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"{action} og {n} trin mere"}},"other":{"stringUnit":{"state":"needs_review","value":"{action} og {n} trin mere"}}}}},"de":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"{action} und {n} weiterer Schritt"}},"other":{"stringUnit":{"state":"needs_review","value":"{action} und {n} weitere Schritte"}}}}},"en":{"variations":{"plural":{"one":{"stringUnit":{"state":"translated","value":"{action} and {n} more step"}},"other":{"stringUnit":{"state":"translated","value":"{action} and {n} more steps"}}}}},"es":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"{action} y {n} paso más"}},"many":{"stringUnit":{"state":"needs_review","value":"{action} y {n} pasos más"}},"other":{"stringUnit":{"state":"needs_review","value":"{action} y {n} pasos más"}}}}},"fr":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"{action} et {n} autre étape"}},"many":{"stringUnit":{"state":"needs_review","value":"{action} et {n} autres étapes"}},"other":{"stringUnit":{"state":"needs_review","value":"{action} et {n} autres étapes"}}}}},"it":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"{action} e {n} altro passaggio"}},"many":{"stringUnit":{"state":"needs_review","value":"{action} e altri {n} passaggi"}},"other":{"stringUnit":{"state":"needs_review","value":"{action} e altri {n} passaggi"}}}}},"ja":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"{action}、ほか {n} 件のステップ"}}}}},"km":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"{action} និងជំហាន {n} ទៀត"}}}}},"ko":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"{action} 외 {n}단계"}}}}},"nb":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"{action} og {n} trinn til"}},"other":{"stringUnit":{"state":"needs_review","value":"{action} og {n} trinn til"}}}}},"pl":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"{action} i {n} inny krok"}},"few":{"stringUnit":{"state":"needs_review","value":"{action} i {n} inne kroki"}},"many":{"stringUnit":{"state":"needs_review","value":"{action} i {n} innych kroków"}},"other":{"stringUnit":{"state":"needs_review","value":"{action} i {n} innego kroku"}}}}},"pt-BR":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"{action} e mais {n} etapa"}},"many":{"stringUnit":{"state":"needs_review","value":"{action} e mais {n} etapas"}},"other":{"stringUnit":{"state":"needs_review","value":"{action} e mais {n} etapas"}}}}},"ru":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"{action} и ещё {n} шаг"}},"few":{"stringUnit":{"state":"needs_review","value":"{action} и ещё {n} шага"}},"many":{"stringUnit":{"state":"needs_review","value":"{action} и ещё {n} шагов"}},"other":{"stringUnit":{"state":"needs_review","value":"{action} и ещё {n} шага"}}}}},"th":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"{action} และอีก {n} ขั้นตอน"}}}}},"tr":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"{action} ve {n} adım daha"}},"other":{"stringUnit":{"state":"needs_review","value":"{action} ve {n} adım daha"}}}}},"uk":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"{action} і ще {n} крок"}},"few":{"stringUnit":{"state":"needs_review","value":"{action} і ще {n} кроки"}},"many":{"stringUnit":{"state":"needs_review","value":"{action} і ще {n} кроків"}},"other":{"stringUnit":{"state":"needs_review","value":"{action} і ще {n} кроку"}}}}},"vi":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"{action} và {n} bước khác"}}}}},"zh-Hans":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"{action},另有 {n} 个步骤"}}}}},"zh-Hant":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"{action},另有 {n} 個步驟"}}}}}}}, - "tools.cmux.script": {"comment":"Agent pane: a cmux_exec script's (or a cmux MCP tool's) transcript row title, a finished script that called tools the row has no words for; {n} is how many calls it made, {first} the first tool's name","extractionState":"manual","localizations":{"ar":{"variations":{"plural":{"zero":{"stringUnit":{"state":"needs_review","value":"تم تشغيل نص cmux ({n} استدعاء): {first}"}},"one":{"stringUnit":{"state":"needs_review","value":"تم تشغيل نص cmux ({n} استدعاء): {first}"}},"two":{"stringUnit":{"state":"needs_review","value":"تم تشغيل نص cmux ({n} استدعاء): {first}"}},"few":{"stringUnit":{"state":"needs_review","value":"تم تشغيل نص cmux ({n} استدعاء): {first}"}},"many":{"stringUnit":{"state":"needs_review","value":"تم تشغيل نص cmux ({n} استدعاء): {first}"}},"other":{"stringUnit":{"state":"needs_review","value":"تم تشغيل نص cmux ({n} استدعاء): {first}"}}}}},"bs":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Pokrenuta cmux skripta ({n} poziv): {first}"}},"few":{"stringUnit":{"state":"needs_review","value":"Pokrenuta cmux skripta ({n} poziva): {first}"}},"other":{"stringUnit":{"state":"needs_review","value":"Pokrenuta cmux skripta ({n} poziva): {first}"}}}}},"da":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Kørte cmux-script ({n} kald): {first}"}},"other":{"stringUnit":{"state":"needs_review","value":"Kørte cmux-script ({n} kald): {first}"}}}}},"de":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"cmux-Skript ausgeführt ({n} Aufruf): {first}"}},"other":{"stringUnit":{"state":"needs_review","value":"cmux-Skript ausgeführt ({n} Aufrufe): {first}"}}}}},"en":{"variations":{"plural":{"one":{"stringUnit":{"state":"translated","value":"Ran cmux script ({n} call): {first}"}},"other":{"stringUnit":{"state":"translated","value":"Ran cmux script ({n} calls): {first}"}}}}},"es":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Se ejecutó un script de cmux ({n} llamada): {first}"}},"many":{"stringUnit":{"state":"needs_review","value":"Se ejecutó un script de cmux ({n} llamadas): {first}"}},"other":{"stringUnit":{"state":"needs_review","value":"Se ejecutó un script de cmux ({n} llamadas): {first}"}}}}},"fr":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Script cmux exécuté ({n} appel) : {first}"}},"many":{"stringUnit":{"state":"needs_review","value":"Script cmux exécuté ({n} appels) : {first}"}},"other":{"stringUnit":{"state":"needs_review","value":"Script cmux exécuté ({n} appels) : {first}"}}}}},"it":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Eseguito script cmux ({n} chiamata): {first}"}},"many":{"stringUnit":{"state":"needs_review","value":"Eseguito script cmux ({n} chiamate): {first}"}},"other":{"stringUnit":{"state":"needs_review","value":"Eseguito script cmux ({n} chiamate): {first}"}}}}},"ja":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"cmux スクリプトを実行しました ({n} 件の呼び出し): {first}"}}}}},"km":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"បានដំណើរការស្គ្រីប cmux ({n} ការហៅ)៖ {first}"}}}}},"ko":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"cmux 스크립트 실행함 (호출 {n}개): {first}"}}}}},"nb":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Kjørte cmux-skript ({n} kall): {first}"}},"other":{"stringUnit":{"state":"needs_review","value":"Kjørte cmux-skript ({n} kall): {first}"}}}}},"pl":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Uruchomiono skrypt cmux ({n} wywołanie): {first}"}},"few":{"stringUnit":{"state":"needs_review","value":"Uruchomiono skrypt cmux ({n} wywołania): {first}"}},"many":{"stringUnit":{"state":"needs_review","value":"Uruchomiono skrypt cmux ({n} wywołań): {first}"}},"other":{"stringUnit":{"state":"needs_review","value":"Uruchomiono skrypt cmux ({n} wywołania): {first}"}}}}},"pt-BR":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Script do cmux executado ({n} chamada): {first}"}},"many":{"stringUnit":{"state":"needs_review","value":"Script do cmux executado ({n} chamadas): {first}"}},"other":{"stringUnit":{"state":"needs_review","value":"Script do cmux executado ({n} chamadas): {first}"}}}}},"ru":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Выполнен скрипт cmux ({n} вызов): {first}"}},"few":{"stringUnit":{"state":"needs_review","value":"Выполнен скрипт cmux ({n} вызова): {first}"}},"many":{"stringUnit":{"state":"needs_review","value":"Выполнен скрипт cmux ({n} вызовов): {first}"}},"other":{"stringUnit":{"state":"needs_review","value":"Выполнен скрипт cmux ({n} вызова): {first}"}}}}},"th":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"เรียกใช้สคริปต์ cmux แล้ว ({n} การเรียก): {first}"}}}}},"tr":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"cmux betiği çalıştırıldı ({n} çağrı): {first}"}},"other":{"stringUnit":{"state":"needs_review","value":"cmux betiği çalıştırıldı ({n} çağrı): {first}"}}}}},"uk":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Виконано скрипт cmux ({n} виклик): {first}"}},"few":{"stringUnit":{"state":"needs_review","value":"Виконано скрипт cmux ({n} виклики): {first}"}},"many":{"stringUnit":{"state":"needs_review","value":"Виконано скрипт cmux ({n} викликів): {first}"}},"other":{"stringUnit":{"state":"needs_review","value":"Виконано скрипт cmux ({n} виклику): {first}"}}}}},"vi":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"Đã chạy tập lệnh cmux ({n} lệnh gọi): {first}"}}}}},"zh-Hans":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"已运行 cmux 脚本({n} 次调用):{first}"}}}}},"zh-Hant":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"已執行 cmux 指令碼({n} 次呼叫):{first}"}}}}}}} + "tools.cmux.script": {"comment":"Agent pane: a cmux_exec script's (or a cmux MCP tool's) transcript row title, a finished script that called tools the row has no words for; {n} is how many calls it made, {first} the first tool's name","extractionState":"manual","localizations":{"ar":{"variations":{"plural":{"zero":{"stringUnit":{"state":"needs_review","value":"تم تشغيل نص cmux ({n} استدعاء): {first}"}},"one":{"stringUnit":{"state":"needs_review","value":"تم تشغيل نص cmux ({n} استدعاء): {first}"}},"two":{"stringUnit":{"state":"needs_review","value":"تم تشغيل نص cmux ({n} استدعاء): {first}"}},"few":{"stringUnit":{"state":"needs_review","value":"تم تشغيل نص cmux ({n} استدعاء): {first}"}},"many":{"stringUnit":{"state":"needs_review","value":"تم تشغيل نص cmux ({n} استدعاء): {first}"}},"other":{"stringUnit":{"state":"needs_review","value":"تم تشغيل نص cmux ({n} استدعاء): {first}"}}}}},"bs":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Pokrenuta cmux skripta ({n} poziv): {first}"}},"few":{"stringUnit":{"state":"needs_review","value":"Pokrenuta cmux skripta ({n} poziva): {first}"}},"other":{"stringUnit":{"state":"needs_review","value":"Pokrenuta cmux skripta ({n} poziva): {first}"}}}}},"da":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Kørte cmux-script ({n} kald): {first}"}},"other":{"stringUnit":{"state":"needs_review","value":"Kørte cmux-script ({n} kald): {first}"}}}}},"de":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"cmux-Skript ausgeführt ({n} Aufruf): {first}"}},"other":{"stringUnit":{"state":"needs_review","value":"cmux-Skript ausgeführt ({n} Aufrufe): {first}"}}}}},"en":{"variations":{"plural":{"one":{"stringUnit":{"state":"translated","value":"Ran cmux script ({n} call): {first}"}},"other":{"stringUnit":{"state":"translated","value":"Ran cmux script ({n} calls): {first}"}}}}},"es":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Se ejecutó un script de cmux ({n} llamada): {first}"}},"many":{"stringUnit":{"state":"needs_review","value":"Se ejecutó un script de cmux ({n} llamadas): {first}"}},"other":{"stringUnit":{"state":"needs_review","value":"Se ejecutó un script de cmux ({n} llamadas): {first}"}}}}},"fr":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Script cmux exécuté ({n} appel) : {first}"}},"many":{"stringUnit":{"state":"needs_review","value":"Script cmux exécuté ({n} appels) : {first}"}},"other":{"stringUnit":{"state":"needs_review","value":"Script cmux exécuté ({n} appels) : {first}"}}}}},"it":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Eseguito script cmux ({n} chiamata): {first}"}},"many":{"stringUnit":{"state":"needs_review","value":"Eseguito script cmux ({n} chiamate): {first}"}},"other":{"stringUnit":{"state":"needs_review","value":"Eseguito script cmux ({n} chiamate): {first}"}}}}},"ja":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"cmux スクリプトを実行しました ({n} 件の呼び出し): {first}"}}}}},"km":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"បានដំណើរការស្គ្រីប cmux ({n} ការហៅ)៖ {first}"}}}}},"ko":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"cmux 스크립트 실행함 (호출 {n}개): {first}"}}}}},"nb":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Kjørte cmux-skript ({n} kall): {first}"}},"other":{"stringUnit":{"state":"needs_review","value":"Kjørte cmux-skript ({n} kall): {first}"}}}}},"pl":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Uruchomiono skrypt cmux ({n} wywołanie): {first}"}},"few":{"stringUnit":{"state":"needs_review","value":"Uruchomiono skrypt cmux ({n} wywołania): {first}"}},"many":{"stringUnit":{"state":"needs_review","value":"Uruchomiono skrypt cmux ({n} wywołań): {first}"}},"other":{"stringUnit":{"state":"needs_review","value":"Uruchomiono skrypt cmux ({n} wywołania): {first}"}}}}},"pt-BR":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Script do cmux executado ({n} chamada): {first}"}},"many":{"stringUnit":{"state":"needs_review","value":"Script do cmux executado ({n} chamadas): {first}"}},"other":{"stringUnit":{"state":"needs_review","value":"Script do cmux executado ({n} chamadas): {first}"}}}}},"ru":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Выполнен скрипт cmux ({n} вызов): {first}"}},"few":{"stringUnit":{"state":"needs_review","value":"Выполнен скрипт cmux ({n} вызова): {first}"}},"many":{"stringUnit":{"state":"needs_review","value":"Выполнен скрипт cmux ({n} вызовов): {first}"}},"other":{"stringUnit":{"state":"needs_review","value":"Выполнен скрипт cmux ({n} вызова): {first}"}}}}},"th":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"เรียกใช้สคริปต์ cmux แล้ว ({n} การเรียก): {first}"}}}}},"tr":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"cmux betiği çalıştırıldı ({n} çağrı): {first}"}},"other":{"stringUnit":{"state":"needs_review","value":"cmux betiği çalıştırıldı ({n} çağrı): {first}"}}}}},"uk":{"variations":{"plural":{"one":{"stringUnit":{"state":"needs_review","value":"Виконано скрипт cmux ({n} виклик): {first}"}},"few":{"stringUnit":{"state":"needs_review","value":"Виконано скрипт cmux ({n} виклики): {first}"}},"many":{"stringUnit":{"state":"needs_review","value":"Виконано скрипт cmux ({n} викликів): {first}"}},"other":{"stringUnit":{"state":"needs_review","value":"Виконано скрипт cmux ({n} виклику): {first}"}}}}},"vi":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"Đã chạy tập lệnh cmux ({n} lệnh gọi): {first}"}}}}},"zh-Hans":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"已运行 cmux 脚本({n} 次调用):{first}"}}}}},"zh-Hant":{"variations":{"plural":{"other":{"stringUnit":{"state":"needs_review","value":"已執行 cmux 指令碼({n} 次呼叫):{first}"}}}}}}}, + "cloudStartup.title": {"comment":"Agent pane: Cloud startup checklist","extractionState":"manual","localizations":{"en":{"stringUnit":{"state":"translated","value":"Initializing session"}},"ar":{"stringUnit":{"state":"needs_review","value":"Initializing session"}},"bs":{"stringUnit":{"state":"needs_review","value":"Initializing session"}},"da":{"stringUnit":{"state":"needs_review","value":"Initializing session"}},"de":{"stringUnit":{"state":"needs_review","value":"Initializing session"}},"es":{"stringUnit":{"state":"needs_review","value":"Initializing session"}},"fr":{"stringUnit":{"state":"needs_review","value":"Initializing session"}},"it":{"stringUnit":{"state":"needs_review","value":"Initializing session"}},"ja":{"stringUnit":{"state":"needs_review","value":"Initializing session"}},"km":{"stringUnit":{"state":"needs_review","value":"Initializing session"}},"ko":{"stringUnit":{"state":"needs_review","value":"Initializing session"}},"nb":{"stringUnit":{"state":"needs_review","value":"Initializing session"}},"pl":{"stringUnit":{"state":"needs_review","value":"Initializing session"}},"pt-BR":{"stringUnit":{"state":"needs_review","value":"Initializing session"}},"ru":{"stringUnit":{"state":"needs_review","value":"Initializing session"}},"th":{"stringUnit":{"state":"needs_review","value":"Initializing session"}},"tr":{"stringUnit":{"state":"needs_review","value":"Initializing session"}},"uk":{"stringUnit":{"state":"needs_review","value":"Initializing session"}},"vi":{"stringUnit":{"state":"needs_review","value":"Initializing session"}},"zh-Hans":{"stringUnit":{"state":"needs_review","value":"Initializing session"}},"zh-Hant":{"stringUnit":{"state":"needs_review","value":"Initializing session"}}}}, + "cloudStartup.initialized": {"comment":"Agent pane: Cloud startup checklist","extractionState":"manual","localizations":{"en":{"stringUnit":{"state":"translated","value":"Initialized session"}},"ar":{"stringUnit":{"state":"needs_review","value":"Initialized session"}},"bs":{"stringUnit":{"state":"needs_review","value":"Initialized session"}},"da":{"stringUnit":{"state":"needs_review","value":"Initialized session"}},"de":{"stringUnit":{"state":"needs_review","value":"Initialized session"}},"es":{"stringUnit":{"state":"needs_review","value":"Initialized session"}},"fr":{"stringUnit":{"state":"needs_review","value":"Initialized session"}},"it":{"stringUnit":{"state":"needs_review","value":"Initialized session"}},"ja":{"stringUnit":{"state":"needs_review","value":"Initialized session"}},"km":{"stringUnit":{"state":"needs_review","value":"Initialized session"}},"ko":{"stringUnit":{"state":"needs_review","value":"Initialized session"}},"nb":{"stringUnit":{"state":"needs_review","value":"Initialized session"}},"pl":{"stringUnit":{"state":"needs_review","value":"Initialized session"}},"pt-BR":{"stringUnit":{"state":"needs_review","value":"Initialized session"}},"ru":{"stringUnit":{"state":"needs_review","value":"Initialized session"}},"th":{"stringUnit":{"state":"needs_review","value":"Initialized session"}},"tr":{"stringUnit":{"state":"needs_review","value":"Initialized session"}},"uk":{"stringUnit":{"state":"needs_review","value":"Initialized session"}},"vi":{"stringUnit":{"state":"needs_review","value":"Initialized session"}},"zh-Hans":{"stringUnit":{"state":"needs_review","value":"Initialized session"}},"zh-Hant":{"stringUnit":{"state":"needs_review","value":"Initialized session"}}}}, + "cloudStartup.machine": {"comment":"Agent pane: Cloud startup checklist","extractionState":"manual","localizations":{"en":{"stringUnit":{"state":"translated","value":"Set up a Cloud machine"}},"ar":{"stringUnit":{"state":"needs_review","value":"Set up a Cloud machine"}},"bs":{"stringUnit":{"state":"needs_review","value":"Set up a Cloud machine"}},"da":{"stringUnit":{"state":"needs_review","value":"Set up a Cloud machine"}},"de":{"stringUnit":{"state":"needs_review","value":"Set up a Cloud machine"}},"es":{"stringUnit":{"state":"needs_review","value":"Set up a Cloud machine"}},"fr":{"stringUnit":{"state":"needs_review","value":"Set up a Cloud machine"}},"it":{"stringUnit":{"state":"needs_review","value":"Set up a Cloud machine"}},"ja":{"stringUnit":{"state":"needs_review","value":"Set up a Cloud machine"}},"km":{"stringUnit":{"state":"needs_review","value":"Set up a Cloud machine"}},"ko":{"stringUnit":{"state":"needs_review","value":"Set up a Cloud machine"}},"nb":{"stringUnit":{"state":"needs_review","value":"Set up a Cloud machine"}},"pl":{"stringUnit":{"state":"needs_review","value":"Set up a Cloud machine"}},"pt-BR":{"stringUnit":{"state":"needs_review","value":"Set up a Cloud machine"}},"ru":{"stringUnit":{"state":"needs_review","value":"Set up a Cloud machine"}},"th":{"stringUnit":{"state":"needs_review","value":"Set up a Cloud machine"}},"tr":{"stringUnit":{"state":"needs_review","value":"Set up a Cloud machine"}},"uk":{"stringUnit":{"state":"needs_review","value":"Set up a Cloud machine"}},"vi":{"stringUnit":{"state":"needs_review","value":"Set up a Cloud machine"}},"zh-Hans":{"stringUnit":{"state":"needs_review","value":"Set up a Cloud machine"}},"zh-Hant":{"stringUnit":{"state":"needs_review","value":"Set up a Cloud machine"}}}}, + "cloudStartup.clone": {"comment":"Agent pane: Cloud startup checklist","extractionState":"manual","localizations":{"en":{"stringUnit":{"state":"translated","value":"Clone repository"}},"ar":{"stringUnit":{"state":"needs_review","value":"Clone repository"}},"bs":{"stringUnit":{"state":"needs_review","value":"Clone repository"}},"da":{"stringUnit":{"state":"needs_review","value":"Clone repository"}},"de":{"stringUnit":{"state":"needs_review","value":"Clone repository"}},"es":{"stringUnit":{"state":"needs_review","value":"Clone repository"}},"fr":{"stringUnit":{"state":"needs_review","value":"Clone repository"}},"it":{"stringUnit":{"state":"needs_review","value":"Clone repository"}},"ja":{"stringUnit":{"state":"needs_review","value":"Clone repository"}},"km":{"stringUnit":{"state":"needs_review","value":"Clone repository"}},"ko":{"stringUnit":{"state":"needs_review","value":"Clone repository"}},"nb":{"stringUnit":{"state":"needs_review","value":"Clone repository"}},"pl":{"stringUnit":{"state":"needs_review","value":"Clone repository"}},"pt-BR":{"stringUnit":{"state":"needs_review","value":"Clone repository"}},"ru":{"stringUnit":{"state":"needs_review","value":"Clone repository"}},"th":{"stringUnit":{"state":"needs_review","value":"Clone repository"}},"tr":{"stringUnit":{"state":"needs_review","value":"Clone repository"}},"uk":{"stringUnit":{"state":"needs_review","value":"Clone repository"}},"vi":{"stringUnit":{"state":"needs_review","value":"Clone repository"}},"zh-Hans":{"stringUnit":{"state":"needs_review","value":"Clone repository"}},"zh-Hant":{"stringUnit":{"state":"needs_review","value":"Clone repository"}}}}, + "cloudStartup.setup": {"comment":"Agent pane: Cloud startup checklist","extractionState":"manual","localizations":{"en":{"stringUnit":{"state":"translated","value":"Run setup script"}},"ar":{"stringUnit":{"state":"needs_review","value":"Run setup script"}},"bs":{"stringUnit":{"state":"needs_review","value":"Run setup script"}},"da":{"stringUnit":{"state":"needs_review","value":"Run setup script"}},"de":{"stringUnit":{"state":"needs_review","value":"Run setup script"}},"es":{"stringUnit":{"state":"needs_review","value":"Run setup script"}},"fr":{"stringUnit":{"state":"needs_review","value":"Run setup script"}},"it":{"stringUnit":{"state":"needs_review","value":"Run setup script"}},"ja":{"stringUnit":{"state":"needs_review","value":"Run setup script"}},"km":{"stringUnit":{"state":"needs_review","value":"Run setup script"}},"ko":{"stringUnit":{"state":"needs_review","value":"Run setup script"}},"nb":{"stringUnit":{"state":"needs_review","value":"Run setup script"}},"pl":{"stringUnit":{"state":"needs_review","value":"Run setup script"}},"pt-BR":{"stringUnit":{"state":"needs_review","value":"Run setup script"}},"ru":{"stringUnit":{"state":"needs_review","value":"Run setup script"}},"th":{"stringUnit":{"state":"needs_review","value":"Run setup script"}},"tr":{"stringUnit":{"state":"needs_review","value":"Run setup script"}},"uk":{"stringUnit":{"state":"needs_review","value":"Run setup script"}},"vi":{"stringUnit":{"state":"needs_review","value":"Run setup script"}},"zh-Hans":{"stringUnit":{"state":"needs_review","value":"Run setup script"}},"zh-Hant":{"stringUnit":{"state":"needs_review","value":"Run setup script"}}}}, + "cloudStartup.harness": {"comment":"Agent pane: Cloud startup checklist","extractionState":"manual","localizations":{"en":{"stringUnit":{"state":"translated","value":"Started {harness}"}},"ar":{"stringUnit":{"state":"needs_review","value":"Started {harness}"}},"bs":{"stringUnit":{"state":"needs_review","value":"Started {harness}"}},"da":{"stringUnit":{"state":"needs_review","value":"Started {harness}"}},"de":{"stringUnit":{"state":"needs_review","value":"Started {harness}"}},"es":{"stringUnit":{"state":"needs_review","value":"Started {harness}"}},"fr":{"stringUnit":{"state":"needs_review","value":"Started {harness}"}},"it":{"stringUnit":{"state":"needs_review","value":"Started {harness}"}},"ja":{"stringUnit":{"state":"needs_review","value":"Started {harness}"}},"km":{"stringUnit":{"state":"needs_review","value":"Started {harness}"}},"ko":{"stringUnit":{"state":"needs_review","value":"Started {harness}"}},"nb":{"stringUnit":{"state":"needs_review","value":"Started {harness}"}},"pl":{"stringUnit":{"state":"needs_review","value":"Started {harness}"}},"pt-BR":{"stringUnit":{"state":"needs_review","value":"Started {harness}"}},"ru":{"stringUnit":{"state":"needs_review","value":"Started {harness}"}},"th":{"stringUnit":{"state":"needs_review","value":"Started {harness}"}},"tr":{"stringUnit":{"state":"needs_review","value":"Started {harness}"}},"uk":{"stringUnit":{"state":"needs_review","value":"Started {harness}"}},"vi":{"stringUnit":{"state":"needs_review","value":"Started {harness}"}},"zh-Hans":{"stringUnit":{"state":"needs_review","value":"Started {harness}"}},"zh-Hant":{"stringUnit":{"state":"needs_review","value":"Started {harness}"}}}}, + "cloudStartup.setupHint": {"comment":"Agent pane: Cloud startup checklist","extractionState":"manual","localizations":{"en":{"stringUnit":{"state":"translated","value":"Add a setup script to install dependencies and configure your environment."}},"ar":{"stringUnit":{"state":"needs_review","value":"Add a setup script to install dependencies and configure your environment."}},"bs":{"stringUnit":{"state":"needs_review","value":"Add a setup script to install dependencies and configure your environment."}},"da":{"stringUnit":{"state":"needs_review","value":"Add a setup script to install dependencies and configure your environment."}},"de":{"stringUnit":{"state":"needs_review","value":"Add a setup script to install dependencies and configure your environment."}},"es":{"stringUnit":{"state":"needs_review","value":"Add a setup script to install dependencies and configure your environment."}},"fr":{"stringUnit":{"state":"needs_review","value":"Add a setup script to install dependencies and configure your environment."}},"it":{"stringUnit":{"state":"needs_review","value":"Add a setup script to install dependencies and configure your environment."}},"ja":{"stringUnit":{"state":"needs_review","value":"Add a setup script to install dependencies and configure your environment."}},"km":{"stringUnit":{"state":"needs_review","value":"Add a setup script to install dependencies and configure your environment."}},"ko":{"stringUnit":{"state":"needs_review","value":"Add a setup script to install dependencies and configure your environment."}},"nb":{"stringUnit":{"state":"needs_review","value":"Add a setup script to install dependencies and configure your environment."}},"pl":{"stringUnit":{"state":"needs_review","value":"Add a setup script to install dependencies and configure your environment."}},"pt-BR":{"stringUnit":{"state":"needs_review","value":"Add a setup script to install dependencies and configure your environment."}},"ru":{"stringUnit":{"state":"needs_review","value":"Add a setup script to install dependencies and configure your environment."}},"th":{"stringUnit":{"state":"needs_review","value":"Add a setup script to install dependencies and configure your environment."}},"tr":{"stringUnit":{"state":"needs_review","value":"Add a setup script to install dependencies and configure your environment."}},"uk":{"stringUnit":{"state":"needs_review","value":"Add a setup script to install dependencies and configure your environment."}},"vi":{"stringUnit":{"state":"needs_review","value":"Add a setup script to install dependencies and configure your environment."}},"zh-Hans":{"stringUnit":{"state":"needs_review","value":"Add a setup script to install dependencies and configure your environment."}},"zh-Hant":{"stringUnit":{"state":"needs_review","value":"Add a setup script to install dependencies and configure your environment."}}}}, + "cloudStartup.cached": {"comment":"Agent pane: Cloud startup checklist","extractionState":"manual","localizations":{"en":{"stringUnit":{"state":"translated","value":"cached"}},"ar":{"stringUnit":{"state":"needs_review","value":"cached"}},"bs":{"stringUnit":{"state":"needs_review","value":"cached"}},"da":{"stringUnit":{"state":"needs_review","value":"cached"}},"de":{"stringUnit":{"state":"needs_review","value":"cached"}},"es":{"stringUnit":{"state":"needs_review","value":"cached"}},"fr":{"stringUnit":{"state":"needs_review","value":"cached"}},"it":{"stringUnit":{"state":"needs_review","value":"cached"}},"ja":{"stringUnit":{"state":"needs_review","value":"cached"}},"km":{"stringUnit":{"state":"needs_review","value":"cached"}},"ko":{"stringUnit":{"state":"needs_review","value":"cached"}},"nb":{"stringUnit":{"state":"needs_review","value":"cached"}},"pl":{"stringUnit":{"state":"needs_review","value":"cached"}},"pt-BR":{"stringUnit":{"state":"needs_review","value":"cached"}},"ru":{"stringUnit":{"state":"needs_review","value":"cached"}},"th":{"stringUnit":{"state":"needs_review","value":"cached"}},"tr":{"stringUnit":{"state":"needs_review","value":"cached"}},"uk":{"stringUnit":{"state":"needs_review","value":"cached"}},"vi":{"stringUnit":{"state":"needs_review","value":"cached"}},"zh-Hans":{"stringUnit":{"state":"needs_review","value":"cached"}},"zh-Hant":{"stringUnit":{"state":"needs_review","value":"cached"}}}}, + "cloudStartup.state.done": {"comment":"Agent pane: Cloud startup checklist","extractionState":"manual","localizations":{"en":{"stringUnit":{"state":"translated","value":"Complete"}},"ar":{"stringUnit":{"state":"needs_review","value":"Complete"}},"bs":{"stringUnit":{"state":"needs_review","value":"Complete"}},"da":{"stringUnit":{"state":"needs_review","value":"Complete"}},"de":{"stringUnit":{"state":"needs_review","value":"Complete"}},"es":{"stringUnit":{"state":"needs_review","value":"Complete"}},"fr":{"stringUnit":{"state":"needs_review","value":"Complete"}},"it":{"stringUnit":{"state":"needs_review","value":"Complete"}},"ja":{"stringUnit":{"state":"needs_review","value":"Complete"}},"km":{"stringUnit":{"state":"needs_review","value":"Complete"}},"ko":{"stringUnit":{"state":"needs_review","value":"Complete"}},"nb":{"stringUnit":{"state":"needs_review","value":"Complete"}},"pl":{"stringUnit":{"state":"needs_review","value":"Complete"}},"pt-BR":{"stringUnit":{"state":"needs_review","value":"Complete"}},"ru":{"stringUnit":{"state":"needs_review","value":"Complete"}},"th":{"stringUnit":{"state":"needs_review","value":"Complete"}},"tr":{"stringUnit":{"state":"needs_review","value":"Complete"}},"uk":{"stringUnit":{"state":"needs_review","value":"Complete"}},"vi":{"stringUnit":{"state":"needs_review","value":"Complete"}},"zh-Hans":{"stringUnit":{"state":"needs_review","value":"Complete"}},"zh-Hant":{"stringUnit":{"state":"needs_review","value":"Complete"}}}}, + "cloudStartup.state.running": {"comment":"Agent pane: Cloud startup checklist","extractionState":"manual","localizations":{"en":{"stringUnit":{"state":"translated","value":"In progress"}},"ar":{"stringUnit":{"state":"needs_review","value":"In progress"}},"bs":{"stringUnit":{"state":"needs_review","value":"In progress"}},"da":{"stringUnit":{"state":"needs_review","value":"In progress"}},"de":{"stringUnit":{"state":"needs_review","value":"In progress"}},"es":{"stringUnit":{"state":"needs_review","value":"In progress"}},"fr":{"stringUnit":{"state":"needs_review","value":"In progress"}},"it":{"stringUnit":{"state":"needs_review","value":"In progress"}},"ja":{"stringUnit":{"state":"needs_review","value":"In progress"}},"km":{"stringUnit":{"state":"needs_review","value":"In progress"}},"ko":{"stringUnit":{"state":"needs_review","value":"In progress"}},"nb":{"stringUnit":{"state":"needs_review","value":"In progress"}},"pl":{"stringUnit":{"state":"needs_review","value":"In progress"}},"pt-BR":{"stringUnit":{"state":"needs_review","value":"In progress"}},"ru":{"stringUnit":{"state":"needs_review","value":"In progress"}},"th":{"stringUnit":{"state":"needs_review","value":"In progress"}},"tr":{"stringUnit":{"state":"needs_review","value":"In progress"}},"uk":{"stringUnit":{"state":"needs_review","value":"In progress"}},"vi":{"stringUnit":{"state":"needs_review","value":"In progress"}},"zh-Hans":{"stringUnit":{"state":"needs_review","value":"In progress"}},"zh-Hant":{"stringUnit":{"state":"needs_review","value":"In progress"}}}}, + "cloudStartup.state.pending": {"comment":"Agent pane: Cloud startup checklist","extractionState":"manual","localizations":{"en":{"stringUnit":{"state":"translated","value":"Waiting"}},"ar":{"stringUnit":{"state":"needs_review","value":"Waiting"}},"bs":{"stringUnit":{"state":"needs_review","value":"Waiting"}},"da":{"stringUnit":{"state":"needs_review","value":"Waiting"}},"de":{"stringUnit":{"state":"needs_review","value":"Waiting"}},"es":{"stringUnit":{"state":"needs_review","value":"Waiting"}},"fr":{"stringUnit":{"state":"needs_review","value":"Waiting"}},"it":{"stringUnit":{"state":"needs_review","value":"Waiting"}},"ja":{"stringUnit":{"state":"needs_review","value":"Waiting"}},"km":{"stringUnit":{"state":"needs_review","value":"Waiting"}},"ko":{"stringUnit":{"state":"needs_review","value":"Waiting"}},"nb":{"stringUnit":{"state":"needs_review","value":"Waiting"}},"pl":{"stringUnit":{"state":"needs_review","value":"Waiting"}},"pt-BR":{"stringUnit":{"state":"needs_review","value":"Waiting"}},"ru":{"stringUnit":{"state":"needs_review","value":"Waiting"}},"th":{"stringUnit":{"state":"needs_review","value":"Waiting"}},"tr":{"stringUnit":{"state":"needs_review","value":"Waiting"}},"uk":{"stringUnit":{"state":"needs_review","value":"Waiting"}},"vi":{"stringUnit":{"state":"needs_review","value":"Waiting"}},"zh-Hans":{"stringUnit":{"state":"needs_review","value":"Waiting"}},"zh-Hant":{"stringUnit":{"state":"needs_review","value":"Waiting"}}}}, + "cloudStartup.state.skipped": {"comment":"Agent pane: Cloud startup checklist","extractionState":"manual","localizations":{"en":{"stringUnit":{"state":"translated","value":"Skipped"}},"ar":{"stringUnit":{"state":"needs_review","value":"Skipped"}},"bs":{"stringUnit":{"state":"needs_review","value":"Skipped"}},"da":{"stringUnit":{"state":"needs_review","value":"Skipped"}},"de":{"stringUnit":{"state":"needs_review","value":"Skipped"}},"es":{"stringUnit":{"state":"needs_review","value":"Skipped"}},"fr":{"stringUnit":{"state":"needs_review","value":"Skipped"}},"it":{"stringUnit":{"state":"needs_review","value":"Skipped"}},"ja":{"stringUnit":{"state":"needs_review","value":"Skipped"}},"km":{"stringUnit":{"state":"needs_review","value":"Skipped"}},"ko":{"stringUnit":{"state":"needs_review","value":"Skipped"}},"nb":{"stringUnit":{"state":"needs_review","value":"Skipped"}},"pl":{"stringUnit":{"state":"needs_review","value":"Skipped"}},"pt-BR":{"stringUnit":{"state":"needs_review","value":"Skipped"}},"ru":{"stringUnit":{"state":"needs_review","value":"Skipped"}},"th":{"stringUnit":{"state":"needs_review","value":"Skipped"}},"tr":{"stringUnit":{"state":"needs_review","value":"Skipped"}},"uk":{"stringUnit":{"state":"needs_review","value":"Skipped"}},"vi":{"stringUnit":{"state":"needs_review","value":"Skipped"}},"zh-Hans":{"stringUnit":{"state":"needs_review","value":"Skipped"}},"zh-Hant":{"stringUnit":{"state":"needs_review","value":"Skipped"}}}}, + "cloudStartup.state.error": {"comment":"Agent pane: Cloud startup checklist","extractionState":"manual","localizations":{"en":{"stringUnit":{"state":"translated","value":"Error"}},"ar":{"stringUnit":{"state":"needs_review","value":"Error"}},"bs":{"stringUnit":{"state":"needs_review","value":"Error"}},"da":{"stringUnit":{"state":"needs_review","value":"Error"}},"de":{"stringUnit":{"state":"needs_review","value":"Error"}},"es":{"stringUnit":{"state":"needs_review","value":"Error"}},"fr":{"stringUnit":{"state":"needs_review","value":"Error"}},"it":{"stringUnit":{"state":"needs_review","value":"Error"}},"ja":{"stringUnit":{"state":"needs_review","value":"Error"}},"km":{"stringUnit":{"state":"needs_review","value":"Error"}},"ko":{"stringUnit":{"state":"needs_review","value":"Error"}},"nb":{"stringUnit":{"state":"needs_review","value":"Error"}},"pl":{"stringUnit":{"state":"needs_review","value":"Error"}},"pt-BR":{"stringUnit":{"state":"needs_review","value":"Error"}},"ru":{"stringUnit":{"state":"needs_review","value":"Error"}},"th":{"stringUnit":{"state":"needs_review","value":"Error"}},"tr":{"stringUnit":{"state":"needs_review","value":"Error"}},"uk":{"stringUnit":{"state":"needs_review","value":"Error"}},"vi":{"stringUnit":{"state":"needs_review","value":"Error"}},"zh-Hans":{"stringUnit":{"state":"needs_review","value":"Error"}},"zh-Hant":{"stringUnit":{"state":"needs_review","value":"Error"}}}} }, "version": "1.0" } diff --git a/webviews/src/agent-session/acpmux/cloudStart.css b/webviews/src/agent-session/acpmux/cloudStart.css index 9ba2c447cbe0..68ebcbc5970c 100644 --- a/webviews/src/agent-session/acpmux/cloudStart.css +++ b/webviews/src/agent-session/acpmux/cloudStart.css @@ -13,6 +13,40 @@ line-height: var(--text-caption--line-height); letter-spacing: var(--text-caption--letter-spacing); } +.acpmux-cloud-start__card { + border: 1px solid color-mix(in srgb, var(--agent-border) 72%, transparent); + border-radius: 14px; + overflow: hidden; + background: color-mix(in srgb, var(--agent-surface) 54%, transparent); +} +.acpmux-cloud-start__summary { + display: flex; + align-items: center; + gap: 6px; + width: 100%; + padding: 13px 16px; + border: 0; + color: var(--agent-text); + background: transparent; + font: inherit; + text-align: left; + cursor: pointer; +} +.acpmux-cloud-start[data-initialized="true"] .acpmux-cloud-start__summary { color: var(--agent-muted); } +.acpmux-cloud-start__checklist { padding: 2px 16px 14px; } +.acpmux-cloud-start__row { display: grid; grid-template-columns: 20px max-content max-content 1fr; align-items: start; gap: 7px; min-height: 28px; color: var(--agent-text); } +.acpmux-cloud-start__row[data-status="pending"], .acpmux-cloud-start__row[data-status="skipped"] { color: var(--agent-muted); } +.acpmux-cloud-start__row[data-status="error"] { color: var(--agent-danger, #f87171); } +.acpmux-cloud-start__mark { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 20px; color: var(--agent-accent, #4ade80); } +.acpmux-cloud-start__row[data-status="pending"] .acpmux-cloud-start__mark, .acpmux-cloud-start__row[data-status="skipped"] .acpmux-cloud-start__mark { color: var(--agent-muted); } +.acpmux-cloud-start__row[data-status="error"] .acpmux-cloud-start__mark { color: inherit; } +.acpmux-cloud-start__skip-mark { display: block; width: 14px; height: calc(var(--ds-space-3) + 2px); margin: 2px; border: 1.5px solid currentColor; border-radius: 50%; opacity: .55; position: relative; } +.acpmux-cloud-start__skip-mark::after { content: ""; position: absolute; left: 1px; top: 6px; width: 11px; border-top: 1.5px solid currentColor; transform: rotate(45deg); } +.acpmux-cloud-start__pending-mark { display: block; width: 6px; height: calc(var(--ds-space-1) + 2px); margin: 6px; border-radius: 50%; background: currentColor; opacity: .25; } +.acpmux-cloud-start__time { margin-left: 4px; color: var(--agent-muted); font-variant-numeric: tabular-nums; white-space: nowrap; } +.acpmux-cloud-start__detail { grid-column: 2 / -1; margin: -3px 0 5px; color: var(--agent-muted); line-height: 1.35; } +.acpmux-cloud-start__log-tail { white-space: pre-wrap; font-family: var(--font-mono, ui-monospace); font-size: .9em; } +.acpmux-cloud-start__link { padding: 0; border: 0; color: var(--agent-link, #60a5fa); background: none; font: inherit; text-decoration: underline; cursor: pointer; } .acpmux-cloud-start__status { display: flex; align-items: center; diff --git a/webviews/src/agent-session/acpmux/cloudStartProgress.ts b/webviews/src/agent-session/acpmux/cloudStartProgress.ts index cd3b9c78891c..02d9facf5a3d 100644 --- a/webviews/src/agent-session/acpmux/cloudStartProgress.ts +++ b/webviews/src/agent-session/acpmux/cloudStartProgress.ts @@ -39,6 +39,46 @@ export type CloudStartProgress = { agent?: string; /// No project: nothing is copied, so the card shows no copy step. home?: boolean; + /// A repository was selected for this chat. `false` is explicit: the clone step is skipped. + hasRepository?: boolean; + repository?: string; + /// The environment whose setup status is being reported by cmux-platform. + environmentId?: string; + /// Client-side timing for the machine and harness phases. Milliseconds since epoch. + machineStartedAt?: number; + machineFinishedAt?: number; + harnessStartedAt?: number; + harnessFinishedAt?: number; + /// The git-seed status, forwarded from `git.seed.status`. + clone?: CloudStartupOperation; + /// The derived environment setup status, forwarded from GET /v1/environments/{id}. + setup?: CloudStartupSetup; + /// True after the harness has started. The checklist stays available but folds to one quiet line. + initialized?: boolean; +}; + +export type CloudStartupOperation = { + state: "pending" | "running" | "done" | "failed"; + startedAt?: number; + finishedAt?: number; + error?: string; + phase?: string; + progress?: { + receivedBytes?: number; + objects?: number; + totalObjects?: number; + }; +}; + +export type CloudStartupSetup = { + state: "pending" | "running" | "done" | "failed" | "skipped"; + startedAt?: number; + finishedAt?: number; + error?: string; + /// `ready` means the setup snapshot is cached; `building` is still running. + status?: "ready" | "building" | "failed"; + logTail?: string; + hasScript?: boolean; }; /// The start the page just sent: the message, and the folder by its name. @@ -67,6 +107,12 @@ function line(value: unknown, max: number): string | undefined { return text ? (text.length > max ? `${text.slice(0, max - 1)}…` : text) : undefined; } +function logLine(value: unknown, max: number): string | undefined { + if (typeof value !== "string") return undefined; + const text = value.trim(); + return text ? (text.length > max ? `${text.slice(0, max - 1)}…` : text) : undefined; +} + /// `current` with the host's push `push` taken in: only for the same start while it runs, field by /// field, each checked (anything else in it is ignored). A step never goes back. export function cloudStartPushed( @@ -80,6 +126,27 @@ export function cloudStartPushed( const step = CLOUD_START_STEPS.indexOf(fields.step as CloudStartStep); if (step >= 0 && step >= (current.step ? CLOUD_START_STEPS.indexOf(current.step) : -1)) next.step = CLOUD_START_STEPS[step]; + if (fields.initialized === true || fields.harnessStarted === true || fields.harness_started === true) { + next.initialized = true; + } + if (fields.initialized === false) next.initialized = false; + if (typeof fields.hasRepository === "boolean") next.hasRepository = fields.hasRepository; + if (typeof fields.has_repo === "boolean") next.hasRepository = fields.has_repo; + const repository = line(fields.repository ?? fields.repo, 160); + if (repository) { + next.repository = repository; + next.hasRepository = true; + } + const environmentId = line(fields.environmentId ?? fields.environment_id, 128); + if (environmentId) next.environmentId = environmentId; + const directMachineStarted = number(fields.machineStartedAt ?? fields.machine_started_at); + const directMachineFinished = number(fields.machineFinishedAt ?? fields.machine_finished_at); + const directHarnessStarted = number(fields.harnessStartedAt ?? fields.harness_started_at); + const directHarnessFinished = number(fields.harnessFinishedAt ?? fields.harness_finished_at); + if (directMachineStarted !== undefined) next.machineStartedAt = directMachineStarted; + if (directMachineFinished !== undefined) next.machineFinishedAt = directMachineFinished; + if (directHarnessStarted !== undefined) next.harnessStartedAt = directHarnessStarted; + if (directHarnessFinished !== undefined) next.harnessFinishedAt = directHarnessFinished; const folder = line(fields.folder, 64); if (folder && !folder.includes("/")) next.folder = folder; const machine = line(fields.machine, 64); @@ -89,9 +156,99 @@ export function cloudStartPushed( if (typeof fields.secrets === "number" && Number.isSafeInteger(fields.secrets) && fields.secrets >= 0) next.secrets = fields.secrets; if (fields.network === "full" || fields.network === "limited") next.network = fields.network; + const timings = asRecord(fields.timings); + if (timings) { + const machineTiming = timing(timings.machine); + const harnessTiming = timing(timings.harness); + if (machineTiming) Object.assign(next, prefixedTiming("machine", machineTiming)); + if (harnessTiming) Object.assign(next, prefixedTiming("harness", harnessTiming)); + } + const clone = operation(fields.clone ?? fields.gitSeed ?? fields.seed ?? fields["git.seed"]); + if (clone) next.clone = clone; + const setup = setupStatus(fields.setup ?? fields.environment); + if (setup) next.setup = setup; return next; } +function asRecord(value: unknown): Record | undefined { + return typeof value === "object" && value !== null && !Array.isArray(value) ? (value as Record) : undefined; +} + +function number(value: unknown): number | undefined { + return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined; +} + +function timing(value: unknown): { startedAt?: number; finishedAt?: number } | undefined { + const record = asRecord(value); + if (!record) return undefined; + const startedAt = number(record.startedAt ?? record.started_at); + const finishedAt = number(record.finishedAt ?? record.finished_at); + return startedAt === undefined && finishedAt === undefined ? undefined : { ...(startedAt !== undefined && { startedAt }), ...(finishedAt !== undefined && { finishedAt }) }; +} + +function prefixedTiming(prefix: "machine" | "harness", value: { startedAt?: number; finishedAt?: number }): Record { + return { + ...(value.startedAt !== undefined && { [`${prefix}StartedAt`]: value.startedAt }), + ...(value.finishedAt !== undefined && { [`${prefix}FinishedAt`]: value.finishedAt }), + } as Record; +} + +function operation(value: unknown): CloudStartupOperation | undefined { + const record = asRecord(value); + if (!record) return undefined; + const raw = record.state ?? record.status; + const state = raw === "done" || raw === "complete" ? "done" : raw === "failed" || raw === "error" ? "failed" : raw === "running" ? "running" : raw === "pending" ? "pending" : undefined; + if (!state) return undefined; + const startedAt = number(record.startedAt ?? record.started_at); + const finishedAt = number(record.finishedAt ?? record.finished_at); + const error = line(record.error ?? record.message, 240); + const phase = line(record.phase, 80); + const progressRecord = asRecord(record.progress); + const receivedBytes = number(progressRecord?.receivedBytes ?? progressRecord?.received_bytes); + const objects = number(progressRecord?.objects); + const totalObjects = number(progressRecord?.totalObjects ?? progressRecord?.total_objects); + const progress = progressRecord + ? { + ...(receivedBytes !== undefined && { receivedBytes }), + ...(objects !== undefined && { objects }), + ...(totalObjects !== undefined && { totalObjects }), + } + : undefined; + return { state, ...(startedAt !== undefined && { startedAt }), ...(finishedAt !== undefined && { finishedAt }), ...(error && { error }), ...(phase && { phase }), ...(progress && Object.keys(progress).length > 0 && { progress }) }; +} + +function setupStatus(value: unknown): CloudStartupSetup | undefined { + const record = asRecord(value); + if (!record) return undefined; + // The platform endpoint reports phase timings under `steps.install`; hosts may forward that + // object as-is so the page does not need to know the rest of the environment response. + const steps = asRecord(record.steps); + const install = steps && (steps.install ?? steps.setup ?? steps.run); + if (install && asRecord(install)) { + const merged = { ...record }; + delete merged.steps; + Object.assign(merged, asRecord(install)); + if (record.status !== undefined) merged.status = record.status; + return setupStatus(merged); + } + const raw = record.status ?? record.state; + const status = raw === "ready" || raw === "building" || raw === "failed" ? raw : undefined; + const base = operation({ ...record, state: status === "ready" ? "done" : status === "building" ? "running" : status === "failed" ? "failed" : record.state }); + if (!base && raw !== "skipped") return undefined; + const startedAt = number(record.startedAt ?? record.started_at); + const finishedAt = number(record.finishedAt ?? record.finished_at); + const hasScript = typeof record.hasScript === "boolean" ? record.hasScript : typeof record.has_script === "boolean" ? record.has_script : undefined; + const logTail = logLine(record.logTail ?? record.log_tail, 1200); + return { + ...(base ?? { state: "skipped" as const }), + ...(status && { status }), + ...(startedAt !== undefined && { startedAt }), + ...(finishedAt !== undefined && { finishedAt }), + ...(hasScript !== undefined && { hasScript }), + ...(logTail && { logTail }), + }; +} + /// `current` once the host answered start `key`: none when the chat opened on the machine (this tab /// closes), failed with the host's words when it didn't. Another start's answer changes nothing. export function cloudStartEnded(