From 9a47dca8064516b4909da859165ad8493a456500 Mon Sep 17 00:00:00 2001 From: "Brian J. Fox" Date: Mon, 2 Nov 2026 02:42:26 +0000 Subject: [PATCH] feat(web): show the first build's progress while the app pane waits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit During the first build the app pane said only "setting up your app" with an elapsed clock, for as long as it took the app to start serving, while the coder wrote a file every half minute. The hub already reports each run's progress: files written, the last of them, and when it last did anything. Show that under the activity line as "12 files written · last: routines · 20 s ago", with the quiet time ticking every second, so the pane moves even while one long step says nothing. Before the first write it reads "No files written yet · last activity 45 s ago". --- apps/web/src/config.ts | 10 ++++++ .../src/pages/workspace/agent-activity.tsx | 4 +-- apps/web/src/pages/workspace/live-app.tsx | 8 +++++ .../pages/workspace/setup-progress.test.ts | 33 ++++++++++++++++++ .../web/src/pages/workspace/setup-progress.ts | 34 +++++++++++++++++++ 5 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/pages/workspace/setup-progress.test.ts create mode 100644 apps/web/src/pages/workspace/setup-progress.ts diff --git a/apps/web/src/config.ts b/apps/web/src/config.ts index 3a1f766d..9d4963e1 100644 --- a/apps/web/src/config.ts +++ b/apps/web/src/config.ts @@ -134,6 +134,16 @@ export const wording = { unknown: "The app builder's progress cannot be read right now; your app shows here once it can.", notWritten: "The app builder has not written any of your app yet.", + /** The build so far: "12 files written · last: routines · 20 s ago". */ + progress: (files: number, last: string | undefined, ago: string) => + `${String(files)} ${files === 1 ? "file" : "files"} written` + + (last === undefined ? "" : ` · last: ${last}`) + + ` · ${ago}`, + nothingYet: (ago: string) => + `No files written yet · last activity ${ago}`, + justNow: "just now", + secondsAgo: (seconds: number) => `${String(seconds)} s ago`, + minutesAgo: (minutes: number) => `${String(minutes)} min ago`, }, phase: { installing: "Installing the app's packages", diff --git a/apps/web/src/pages/workspace/agent-activity.tsx b/apps/web/src/pages/workspace/agent-activity.tsx index 1ee43c6b..77c87c0d 100644 --- a/apps/web/src/pages/workspace/agent-activity.tsx +++ b/apps/web/src/pages/workspace/agent-activity.tsx @@ -17,8 +17,8 @@ const text = wording.activity; const ASKS = new Set(["instruct_coder", "request_approval", "design_app"]); const CHECK_SCRIPT = /\bcheck-app\b/; -// A person sees what the file is, never where it lives: no folders, no internal ids. -function fileLabel(path: string): string { +/** A person sees what the file is, never where it lives: no folders, no internal ids. */ +export function fileLabel(path: string): string { if (path.startsWith("tool-output:")) return text.files.earlierOutput; const name = path.split("/").filter(Boolean).at(-1) ?? path; if (/[0-9a-f]{12,}/.test(name)) return text.files.generic; diff --git a/apps/web/src/pages/workspace/live-app.tsx b/apps/web/src/pages/workspace/live-app.tsx index c392e13b..084c2f8d 100644 --- a/apps/web/src/pages/workspace/live-app.tsx +++ b/apps/web/src/pages/workspace/live-app.tsx @@ -40,6 +40,7 @@ import { import { keys } from "../../queries/keys.ts"; import { wording } from "../../config.ts"; import { activityPhrase, useProjectActivity } from "./agent-activity.tsx"; +import { setupProgressLine } from "./setup-progress.ts"; import { AppSketch } from "./design-concepts.tsx"; import { clock, useElapsedMs } from "./elapsed.tsx"; import { DevicePreview, type PreviewDevice } from "./device-preview.tsx"; @@ -126,6 +127,10 @@ function BuilderSetup({ (item) => item.runId === builder.runId, ); const phrase = agent === undefined ? undefined : activityPhrase(agent).phrase; + // What the build has produced so far, with the time since its last event ticking, so the pane + // moves every second even while one long step says nothing. + const quietMs = useElapsedMs(builder.progress?.lastActivityAt); + const progress = setupProgressLine(builder.progress, quietMs); return (
@@ -135,6 +140,9 @@ function BuilderSetup({ {phrase}

)} + {progress === undefined ? null : ( +

{progress}

+ )}

{text.setup.note}{" "} diff --git a/apps/web/src/pages/workspace/setup-progress.test.ts b/apps/web/src/pages/workspace/setup-progress.test.ts new file mode 100644 index 00000000..8ae3ee73 --- /dev/null +++ b/apps/web/src/pages/workspace/setup-progress.test.ts @@ -0,0 +1,33 @@ +import { expect, test } from "bun:test"; +import { ago, setupProgressLine } from "./setup-progress.ts"; + +const progress = { + turns: 3, + recent: ["write_file client/src/pages/routines.tsx"], + filesWritten: 12, + files: ["client/src/api.ts", "client/src/pages/routines.tsx"], + lastActivityAt: "2026-11-02T02:23:22.000Z", +}; + +test("the first build's line names the count, the last file in plain words, and the quiet time", () => { + expect(setupProgressLine(progress, 20_000)).toBe( + "12 files written · last: routines · 20 s ago", + ); + expect(setupProgressLine({ ...progress, filesWritten: 1 }, 2_000)).toBe( + "1 file written · last: routines · just now", + ); +}); + +test("before the first write the line still says how long it has been", () => { + expect( + setupProgressLine({ ...progress, filesWritten: 0, files: [] }, 45_000), + ).toBe("No files written yet · last activity 45 s ago"); + expect(setupProgressLine(undefined, 45_000)).toBeUndefined(); +}); + +test("quiet time reads in seconds under two minutes, then in minutes", () => { + expect(ago(1_000)).toBe("just now"); + expect(ago(59_000)).toBe("59 s ago"); + expect(ago(119_000)).toBe("119 s ago"); + expect(ago(200_000)).toBe("3 min ago"); +}); diff --git a/apps/web/src/pages/workspace/setup-progress.ts b/apps/web/src/pages/workspace/setup-progress.ts new file mode 100644 index 00000000..856cfe4f --- /dev/null +++ b/apps/web/src/pages/workspace/setup-progress.ts @@ -0,0 +1,34 @@ +/** + * The first build's progress in one line, from what the hub saw of the run's + * own write events: how many files it has written, the last of them, and how + * long since anything happened. The quiet time ticks on the caller's clock, + * so the line changes every second even while one long step says nothing. + */ +import type { RunProgress } from "@corbits-builder/schemas/artifact"; +import { wording } from "../../config.ts"; +import { fileLabel } from "./agent-activity.tsx"; + +const text = wording.liveApp.setup; + +/** How long ago, in a person's words: "just now", "40 s ago", "3 min ago". */ +export function ago(ms: number): string { + const seconds = Math.floor(ms / 1_000); + if (seconds < 5) return text.justNow; + if (seconds < 120) return text.secondsAgo(seconds); + return text.minutesAgo(Math.floor(seconds / 60)); +} + +export function setupProgressLine( + progress: RunProgress | undefined, + quietMs: number, +): string | undefined { + if (progress === undefined) return undefined; + const last = progress.files.at(-1); + return progress.filesWritten === 0 + ? text.nothingYet(ago(quietMs)) + : text.progress( + progress.filesWritten, + last === undefined ? undefined : fileLabel(last), + ago(quietMs), + ); +}