From 81c55be19b564a6fbf592d592041f8977c0595c3 Mon Sep 17 00:00:00 2001 From: sheeek Date: Fri, 9 Jan 2026 10:10:17 +0100 Subject: [PATCH] refactor(sandbox): extract display logic into separate module Move all display functions to sandbox-display.ts: - displayContainers, displayBrowsers with generic displayItems helper - displaySummary with image mismatch warnings - displayRecreatePreview, displayRecreateResult Uses DisplayConfig pattern to reduce duplication between container and browser display logic. 156 LOC. --- src/commands/sandbox-display.ts | 156 ++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 src/commands/sandbox-display.ts diff --git a/src/commands/sandbox-display.ts b/src/commands/sandbox-display.ts new file mode 100644 index 000000000..0bda314f5 --- /dev/null +++ b/src/commands/sandbox-display.ts @@ -0,0 +1,156 @@ +/** + * Display utilities for sandbox CLI + */ + +import type { + SandboxBrowserInfo, + SandboxContainerInfo, +} from "../agents/sandbox.js"; +import type { RuntimeEnv } from "../runtime.js"; +import { + formatAge, + formatImageMatch, + formatSimpleStatus, + formatStatus, +} from "./sandbox-formatters.js"; + +type DisplayConfig = { + emptyMessage: string; + title: string; + renderItem: (item: T, runtime: RuntimeEnv) => void; +}; + +function displayItems( + items: T[], + config: DisplayConfig, + runtime: RuntimeEnv, +): void { + if (items.length === 0) { + runtime.log(config.emptyMessage); + return; + } + + runtime.log(`\n${config.title}\n`); + for (const item of items) { + config.renderItem(item, runtime); + } +} + +export function displayContainers( + containers: SandboxContainerInfo[], + runtime: RuntimeEnv, +): void { + displayItems( + containers, + { + emptyMessage: "No sandbox containers found.", + title: "šŸ“¦ Sandbox Containers:", + renderItem: (container, rt) => { + rt.log(` ${container.containerName}`); + rt.log(` Status: ${formatStatus(container.running)}`); + rt.log(` Image: ${container.image} ${formatImageMatch(container.imageMatch)}`); + rt.log(` Age: ${formatAge(Date.now() - container.createdAtMs)}`); + rt.log(` Idle: ${formatAge(Date.now() - container.lastUsedAtMs)}`); + rt.log(` Session: ${container.sessionKey}`); + rt.log(""); + }, + }, + runtime, + ); +} + +export function displayBrowsers( + browsers: SandboxBrowserInfo[], + runtime: RuntimeEnv, +): void { + displayItems( + browsers, + { + emptyMessage: "No sandbox browser containers found.", + title: "🌐 Sandbox Browser Containers:", + renderItem: (browser, rt) => { + rt.log(` ${browser.containerName}`); + rt.log(` Status: ${formatStatus(browser.running)}`); + rt.log(` Image: ${browser.image} ${formatImageMatch(browser.imageMatch)}`); + rt.log(` CDP: ${browser.cdpPort}`); + if (browser.noVncPort) { + rt.log(` noVNC: ${browser.noVncPort}`); + } + rt.log(` Age: ${formatAge(Date.now() - browser.createdAtMs)}`); + rt.log(` Idle: ${formatAge(Date.now() - browser.lastUsedAtMs)}`); + rt.log(` Session: ${browser.sessionKey}`); + rt.log(""); + }, + }, + runtime, + ); +} + +export function displaySummary( + containers: SandboxContainerInfo[], + browsers: SandboxBrowserInfo[], + runtime: RuntimeEnv, +): void { + const totalCount = containers.length + browsers.length; + const runningCount = + containers.filter((c) => c.running).length + + browsers.filter((b) => b.running).length; + const mismatchCount = + containers.filter((c) => !c.imageMatch).length + + browsers.filter((b) => !b.imageMatch).length; + + runtime.log(`Total: ${totalCount} (${runningCount} running)`); + + if (mismatchCount > 0) { + runtime.log( + `\nāš ļø ${mismatchCount} container(s) with image mismatch detected.`, + ); + runtime.log( + ` Run 'clawdbot sandbox recreate --all' to update all containers.`, + ); + } +} + +export function displayRecreatePreview( + containers: SandboxContainerInfo[], + browsers: SandboxBrowserInfo[], + runtime: RuntimeEnv, +): void { + runtime.log("\nContainers to be recreated:\n"); + + if (containers.length > 0) { + runtime.log("šŸ“¦ Sandbox Containers:"); + for (const container of containers) { + runtime.log( + ` - ${container.containerName} (${formatSimpleStatus(container.running)})`, + ); + } + } + + if (browsers.length > 0) { + runtime.log("\n🌐 Browser Containers:"); + for (const browser of browsers) { + runtime.log( + ` - ${browser.containerName} (${formatSimpleStatus(browser.running)})`, + ); + } + } + + const total = containers.length + browsers.length; + runtime.log(`\nTotal: ${total} container(s)`); +} + +export function displayRecreateResult( + result: { successCount: number; failCount: number }, + runtime: RuntimeEnv, +): void { + runtime.log( + `\nDone: ${result.successCount} removed, ${result.failCount} failed`, + ); + + if (result.successCount > 0) { + runtime.log( + "\nContainers will be automatically recreated when the agent is next used.", + ); + } +}