diff --git a/apps/backend/src/services/generate/prompts.ts b/apps/backend/src/services/generate/prompts.ts index ac2e5fc..1e8f708 100644 --- a/apps/backend/src/services/generate/prompts.ts +++ b/apps/backend/src/services/generate/prompts.ts @@ -9,7 +9,7 @@ export const STREAMING_PATCH_PROMPT = `You are cuttlekit, a generative UI engine Users describe what they want and you build it as live HTML. You also handle user actions like button clicks, form inputs, and selections to update the UI accordingly. OUTPUT: JSONL, one JSON per line with "op" field. Stream multiple small lines, NOT one big line. -{"op":"patches","patches":[...]} - 1-3 patches per line MAX, under 800 chars each. Many changes = many lines, one item per line. +{"op":"patches","patches":[...]} - 1-3 patches per line MAX, try under 400 chars each. Many changes = many lines, one item per line. {"op":"full","html":"..."} - ONLY when UI is completely broken or unrecoverable. Patches are strongly preferred. COMPONENTS: Register reusable UI components with define, then use custom tags in patches. @@ -59,7 +59,9 @@ FONTS: Any Fontsource font via style="font-family: 'FontName'". Default Inter. C LOADING: For large UI rebuilds (10+ patches), sandbox operations (API calls, code execution, data fetching), or multi-step workflows — emit a loading/status patch matching current UI style first, then replace it with final content. For simple updates (button clicks, text changes, toggles, counter increments, style tweaks) — emit patches directly, no loading state. -BATCHING: [NOW] list all actions and prompts in chronological order, multiple numbered. Apply ALL in order.`; +BATCHING: [NOW] list all actions and prompts in chronological order, multiple numbered. Apply ALL in order. + +INSPECT: get_page_state returns rendered HTML. Avoid unless lost or verifying after many changes.`; // Sandbox addendum — appended to system prompt only when sandbox is configured export type PackageInfo = { package: string; envVar?: string }; diff --git a/apps/backend/src/services/generate/service.ts b/apps/backend/src/services/generate/service.ts index c0a9ebe..8e5f632 100644 --- a/apps/backend/src/services/generate/service.ts +++ b/apps/backend/src/services/generate/service.ts @@ -1,5 +1,6 @@ -import { Effect, Stream, pipe, DateTime, Duration, Ref, Option } from "effect"; -import { streamText, type TextStreamPart } from "ai"; +import { Effect, Stream, pipe, DateTime, Duration, Ref, Option, Runtime } from "effect"; +import { streamText, tool, type TextStreamPart, type ToolSet } from "ai"; +import { z } from "zod"; import type { LanguageModelConfig } from "@cuttlekit/common/server"; import { MemoryService, type MemorySearchResult } from "../memory/index.js"; import { accumulateLinesWithFlush } from "../../stream/utils.js"; @@ -22,7 +23,7 @@ import { safeAsyncIterable, } from "./index.js"; import type { GenerationError } from "./errors.js"; -import { ToolService, TOOL_STEP_LIMIT, type SandboxTools } from "./tools.js"; +import { ToolService, TOOL_STEP_LIMIT } from "./tools.js"; import type { ManagedSandbox, SandboxContext } from "../sandbox/manager.js"; export class GenerateService extends Effect.Service()( @@ -104,7 +105,7 @@ export class GenerateService extends Effect.Service()( usageRef: Ref.Ref, ttftRef: Ref.Ref, modelConfig: LanguageModelConfig, - requestTools?: SandboxTools, + requestTools: ToolSet, ): Stream.Stream => Stream.unwrap( Effect.gen(function* () { @@ -118,11 +119,9 @@ export class GenerateService extends Effect.Service()( model: modelConfig.model, messages: messages as Message[], providerOptions: modelConfig.providerOptions, - ...(requestTools && { - tools: requestTools, - stopWhen: TOOL_STEP_LIMIT, - toolChoice: "auto", - }), + tools: requestTools, + stopWhen: TOOL_STEP_LIMIT, + toolChoice: "auto", }); // Use fullStream to get both text AND usage events @@ -137,7 +136,7 @@ export class GenerateService extends Effect.Service()( return pipe( fullStream, // Extract text from text-delta, track usage from finish - Stream.mapEffect((part: TextStreamPart) => + Stream.mapEffect((part: TextStreamPart) => Effect.gen(function* () { // Capture usage from finish-step using provider-specific extractor if (part.type === "finish-step") { @@ -258,7 +257,7 @@ export class GenerateService extends Effect.Service()( modeRef: Ref.Ref<"patches" | "full">, attempt: number, modelConfig: LanguageModelConfig, - requestTools?: SandboxTools, + requestTools: ToolSet, ): Stream.Stream => { if (attempt >= MAX_RETRY_ATTEMPTS) { return Stream.fail( @@ -352,11 +351,12 @@ export class GenerateService extends Effect.Service()( const { sessionId, currentHtml, catalog, actions } = options; const modelConfig = yield* modelRegistry.resolve(options.modelId); + const runtime = yield* Effect.runtime(); // Build per-request sandbox tools (only when sandbox is configured) // Reuse session-scoped sandboxCtx (warm mode) or create fresh one (lazy) const packageInfo = toolService.listPackageInfo(); - const requestTools = + const sandboxTools = packageInfo.length > 0 ? yield* Effect.gen(function* () { const sandboxCtx: SandboxContext = options.sandboxCtx ?? { @@ -365,7 +365,6 @@ export class GenerateService extends Effect.Service()( ), lock: yield* Effect.makeSemaphore(1), }; - const runtime = yield* Effect.runtime(); return toolService.makeTools({ sessionId, sandboxCtx, @@ -491,6 +490,18 @@ export class GenerateService extends Effect.Service()( yield* renderCETree(validationCtx.window, validationCtx.registry); } + // Build always-available page state tool + optional sandbox tools + const allTools: ToolSet = { + get_page_state: tool({ + description: "Get the current rendered HTML. Use ONLY when you've lost track after many patches.", + inputSchema: z.object({}), + execute: async () => ({ + html: Runtime.runSync(runtime)(getCompactHtmlFromCtx(validationCtx)), + }), + }), + ...(sandboxTools ?? {}), + }; + // Create Refs to track state across retries const usageRef = yield* Ref.make([]); const ttftRef = yield* Ref.make(0); @@ -508,7 +519,7 @@ export class GenerateService extends Effect.Service()( modeRef, 0, modelConfig, - requestTools, + allTools, ); // Stats stream runs AFTER content stream completes diff --git a/apps/webpage/src/main.ts b/apps/webpage/src/main.ts index 8de1267..33fa4c2 100644 --- a/apps/webpage/src/main.ts +++ b/apps/webpage/src/main.ts @@ -149,9 +149,11 @@ const app = { loadIconsFromHTML(content); } - if ("text" in patch) { - el.textContent = patch.text; - } else if ("attr" in patch) { + if ("remove" in patch) { + el.remove(); + return; + } + if ("attr" in patch) { Object.entries(patch.attr).forEach(([key, value]) => { if (value === null) { el.removeAttribute(key); @@ -159,14 +161,15 @@ const app = { el.setAttribute(key, value); } }); + } + if ("text" in patch) { + el.textContent = patch.text; + } else if ("html" in patch) { + el.innerHTML = patch.html; } else if ("append" in patch) { el.insertAdjacentHTML("beforeend", patch.append); } else if ("prepend" in patch) { el.insertAdjacentHTML("afterbegin", patch.prepend); - } else if ("html" in patch) { - el.innerHTML = patch.html; - } else if ("remove" in patch) { - el.remove(); } },