Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/agent-worker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ matched imperative such as “build a website” or “create a mobile app” al
app-builder path before model execution. That high-confidence fallback materializes the project,
scaffolds its canonical workspace, and registers the managed preview even when the selected model
would otherwise attempt generic shell work and finish without a Computer target.
The starter page is an internal server-readiness target, not user-facing generated content. A
fresh template run emits the typed `app-preview-status` transition from `building` to `ready` only
after model execution (and the mobile preview restart, when applicable), so the web client can keep
the branded loading surface over the scaffold without relying on timers or iframe inspection.
The canonical app source remains on the durable `/workspace` volume. Managed Next.js previews
compile from a sandbox-local one-way mirror because Daytona's object-store FUSE mount can stall
webpack compilation even after the listening socket opens. A baked Python synchronizer performs a
Expand Down
32 changes: 25 additions & 7 deletions apps/agent-worker/src/durable-objects/agent-run-app-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,14 @@ interface RunAppBuilderOptions {
setRunStage: (stage: string) => void;
}

interface AppBuilderSetup {
agentContextNote?: string;
waitsForGeneratedPreview: boolean;
}

type WorkspaceOptions = RunAppBuilderOptions & { workspace: AppBuilderWorkspace };

export async function runAppBuilder(
options: RunAppBuilderOptions,
): Promise<{ agentContextNote?: string }> {
export async function runAppBuilder(options: RunAppBuilderOptions): Promise<AppBuilderSetup> {
const { input, logger, sandbox } = options;
throwIfRunCanceled(options.abortSignal);
const workspace = await resolveAppWorkspace(sandbox, input, logger);
Expand All @@ -166,20 +169,35 @@ export async function runAppBuilder(
// follow-up run. See D7 — the marker, not the template-shape check, is the
// one-shot guarantee.
if (await hasImportedAppWorkspace(sandbox, workspace.dir)) {
return restoreImportedWorkspace(workspaceOptions);
return {
...(await restoreImportedWorkspace(workspaceOptions)),
waitsForGeneratedPreview: false,
};
}
const shouldBootstrap = !(await hasExistingAppBuilderWorkspace(sandbox, workspace.dir));
if (shouldBootstrap && input.importRepoUrl) {
return importRepoWorkspace({ ...workspaceOptions, repoUrl: input.importRepoUrl });
return {
...(await importRepoWorkspace({ ...workspaceOptions, repoUrl: input.importRepoUrl })),
waitsForGeneratedPreview: false,
};
}
return runTemplateAppBuilder({ ...workspaceOptions, shouldBootstrap });
return {
...(await runTemplateAppBuilder({ ...workspaceOptions, shouldBootstrap })),
waitsForGeneratedPreview: shouldBootstrap,
};
}

async function runTemplateAppBuilder(
options: WorkspaceOptions & { shouldBootstrap: boolean },
): Promise<{ agentContextNote: string }> {
const { sandbox, workspace } = options;
const { append, sandbox, shouldBootstrap, workspace } = options;
throwIfRunCanceled(options.abortSignal);
if (shouldBootstrap) {
await append({
type: "data-app-preview-status",
data: { v: 1, status: "building" },
});
}
await prepareTemplateWorkspace(options);
throwIfRunCanceled(options.abortSignal);
await clearBuildCache(sandbox, workspace.dir, workspace.mobile);
Expand Down
8 changes: 7 additions & 1 deletion apps/agent-worker/src/durable-objects/agent-run-path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ async function executeAppBuilderPath(
if (options.isCanceled()) {
return "completed";
}
const { agentContextNote } = await runAppBuilder(options);
const { agentContextNote, waitsForGeneratedPreview } = await runAppBuilder(options);
if (options.isCanceled()) {
return "completed";
}
Expand All @@ -69,6 +69,12 @@ async function executeAppBuilderPath(
return "completed";
}
await restartMobilePreviewIfNeeded(options);
if (waitsForGeneratedPreview) {
await options.append({
type: "data-app-preview-status",
data: { v: 1, status: "ready" },
});
}
return "continue";
}

Expand Down
5 changes: 5 additions & 0 deletions apps/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ does not create or wake Daytona merely because the user opens it.
Computer preview wakeups rotate the preview session and reload the visible iframe
once after an actual sandbox/process recovery. Silent capability rotation keeps the
live iframe mounted so application state is preserved during ordinary use.
Fresh app-builder projects keep the animated Cheatcode mark visible while the internal starter
scaffold boots and the model generates the requested app. The persisted `app-preview-status`
stream part reveals the iframe only when generated content is ready, and an iframe-load guard keeps
the same mark in place until the final document has loaded. Existing project edits remain visible
and continue hot-reloading normally.

## Public exports

Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/components/chat/chat-panel-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ function useChatPanelRuntime(input: ChatPanelProps) {
const surfaceApplier = useWorkspaceSurfaceApplier({
projectId: input.project?.id ?? null,
setActivePreviewTab: store.setActivePreviewTab,
setAppPreviewStatus: store.setAppPreviewStatus,
setPreviewPanelOpen: store.setPreviewPanelOpen,
setSandboxStatus: store.setSandboxStatus,
threadId: input.threadId,
Expand Down Expand Up @@ -250,6 +251,7 @@ function useChatPanelStore(threadId: string) {
resetConsole: useAppStore((state) => state.resetConsole),
resetPreviewNavigation: useAppStore((state) => state.resetPreviewNavigation),
setActivePreviewTab: useAppStore((state) => state.setActivePreviewTab),
setAppPreviewStatus: useAppStore((state) => state.setAppPreviewStatus),
setDraft: useAppStore((state) => state.setDraft),
setExpoUrl: useAppStore((state) => state.setExpoUrl),
setPreviewPanelOpen: useAppStore((state) => state.setPreviewPanelOpen),
Expand Down Expand Up @@ -455,6 +457,7 @@ function useChatPanelEffects(
resetConsole: store.resetConsole,
resetPreviewNavigation: store.resetPreviewNavigation,
setActivePreviewTab: store.setActivePreviewTab,
setAppPreviewStatus: store.setAppPreviewStatus,
setExpoUrl: store.setExpoUrl,
setPreviewPanelOpen: store.setPreviewPanelOpen,
setPreviewUrl: store.setPreviewUrl,
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/chat/message-timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export function buildMessageTimeline(

export function isHiddenTranscriptPart(part: MessagePart): boolean {
return (
part.type === "data-app-preview-status" ||
part.type === "data-artifact" ||
part.type === "data-sandbox-status" ||
/* retained for historical transcripts */
Expand Down
62 changes: 51 additions & 11 deletions apps/web/src/components/chat/use-sandbox-surface-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,14 @@ type SandboxStatusData = Extract<
{ type: "data-sandbox-status" }
>["data"];

type AppPreviewStatusData = Extract<
CheatcodeUIMessage["parts"][number],
{ type: "data-app-preview-status" }
>["data"];

export interface SandboxStatusActions {
setActivePreviewTab: (tab: PreviewTab) => void;
setAppPreviewStatus: (status: AppPreviewStatusData["status"] | "idle") => void;
setPreviewPanelOpen: (open: boolean) => void;
setSandboxStatus: (status: SandboxState) => void;
}
Expand All @@ -33,6 +39,7 @@ interface SandboxSurfaceSyncInput extends SandboxStatusActions {
}

export type SurfaceCommand =
| { kind: "preview-status"; status: AppPreviewStatusData["status"] }
| { kind: "status"; status: SandboxStatusData["status"] }
| { kind: "open-browser-preview"; toolCallId: string };

Expand All @@ -53,6 +60,7 @@ interface SurfaceCommandState {

interface HydratedSurfaceCommands {
browser: SurfaceCommand | null;
preview: SurfaceCommand | null;
status: SurfaceCommand | null;
}

Expand All @@ -67,10 +75,16 @@ export function useWorkspaceSurfaceApplier(
const actions = useMemo(
() => ({
setActivePreviewTab: input.setActivePreviewTab,
setAppPreviewStatus: input.setAppPreviewStatus,
setPreviewPanelOpen: input.setPreviewPanelOpen,
setSandboxStatus: input.setSandboxStatus,
}),
[input.setActivePreviewTab, input.setPreviewPanelOpen, input.setSandboxStatus],
[
input.setActivePreviewTab,
input.setAppPreviewStatus,
input.setPreviewPanelOpen,
input.setSandboxStatus,
],
);
const apply = useCallback(
(command: SurfaceCommand) =>
Expand All @@ -91,14 +105,21 @@ export function useSandboxSurfaceSync(input: SandboxSurfaceSyncInput): void {
const actions = useMemo(
() => ({
setActivePreviewTab: input.setActivePreviewTab,
setAppPreviewStatus: input.setAppPreviewStatus,
setPreviewPanelOpen: input.setPreviewPanelOpen,
setSandboxStatus: input.setSandboxStatus,
}),
[input.setActivePreviewTab, input.setPreviewPanelOpen, input.setSandboxStatus],
[
input.setActivePreviewTab,
input.setAppPreviewStatus,
input.setPreviewPanelOpen,
input.setSandboxStatus,
],
);

useResetSandboxSurface(input);
useHydratedSurfaceCommand(commands.status, input.surfaceApplier);
useHydratedSurfaceCommand(commands.preview, input.surfaceApplier);
useProjectFilesDefault(input.project, status, actions, defaultedProjectFilesRef);
useHydratedSurfaceCommand(commands.browser, input.surfaceApplier);
useCompletionPreview(input.chatStatus, previousStatusRef);
Expand All @@ -112,6 +133,10 @@ export function workspaceSurfaceEffect(part: unknown): SurfaceCommand | null {
const parsed = CHEATCODE_DATA_SCHEMAS["sandbox-status"].safeParse(part["data"]);
return parsed.success ? { kind: "status", status: parsed.data.status } : null;
}
if (part["type"] === "data-app-preview-status") {
const parsed = CHEATCODE_DATA_SCHEMAS["app-preview-status"].safeParse(part["data"]);
return parsed.success ? { kind: "preview-status", status: parsed.data.status } : null;
}
if (part["type"] !== "data-tool") {
return null;
}
Expand All @@ -126,6 +151,7 @@ function useResetSandboxSurface(input: SandboxSurfaceSyncInput): void {
input.surfaceApplier.reset();
input.resetConsole();
input.resetPreviewNavigation();
input.setAppPreviewStatus("idle");
input.setPreviewUrl(null);
input.setExpoUrl(null);
input.setPreviewPanelOpen(false);
Expand All @@ -134,6 +160,7 @@ function useResetSandboxSurface(input: SandboxSurfaceSyncInput): void {
input.resetConsole,
input.resetPreviewNavigation,
input.setExpoUrl,
input.setAppPreviewStatus,
input.setPreviewPanelOpen,
input.setPreviewUrl,
input.setSandboxStatus,
Expand Down Expand Up @@ -186,8 +213,7 @@ function useCompletionPreview(
}

function hydratedSurfaceCommands(messages: readonly CheatcodeUIMessage[]): HydratedSurfaceCommands {
let browser: SurfaceCommand | null = null;
let status: SurfaceCommand | null = null;
const commands: HydratedSurfaceCommands = { browser: null, preview: null, status: null };
for (let messageIndex = 0; messageIndex < messages.length; messageIndex += 1) {
const message = messages[messageIndex];
if (!message) {
Expand All @@ -199,15 +225,23 @@ function hydratedSurfaceCommands(messages: readonly CheatcodeUIMessage[]): Hydra
if (!part) {
continue;
}
const command = workspaceSurfaceEffect(part);
if (command?.kind === "status") {
status = command;
} else if (command?.kind === "open-browser-preview") {
browser = command;
}
recordHydratedSurfaceCommand(commands, workspaceSurfaceEffect(part));
}
}
return { browser, status };
return commands;
}

function recordHydratedSurfaceCommand(
commands: HydratedSurfaceCommands,
command: SurfaceCommand | null,
): void {
if (command?.kind === "status") {
commands.status = command;
} else if (command?.kind === "preview-status") {
commands.preview = command;
} else if (command?.kind === "open-browser-preview") {
commands.browser = command;
}
}

function applyWorkspaceSurfaceCommand(
Expand All @@ -222,6 +256,12 @@ function applyWorkspaceSurfaceCommand(
}
return;
}
if (command.kind === "preview-status") {
if (useAppStore.getState().appPreviewStatus !== command.status) {
actions.setAppPreviewStatus(command.status);
}
return;
}
const onceKey = `${threadId}:${command.toolCallId}`;
if (state.openedBrowserToolKeys.has(onceKey)) {
return;
Expand Down
Loading