diff --git a/docs/specs/recipe-construction/rollout-plan.md b/docs/specs/recipe-construction/rollout-plan.md index 80fb5fbcd..e7600259d 100644 --- a/docs/specs/recipe-construction/rollout-plan.md +++ b/docs/specs/recipe-construction/rollout-plan.md @@ -2,7 +2,7 @@ - [x] Disable ShadowRef/unsafe_ and see what breaks, ideally remove it (will merge later as it'll break a few patterns) -- [ ] Update Cell API types to already unify them +- [x] Update Cell API types to already unify them - [x] Create an `BrandedCell<>` type with a symbol based brand, with the value be `string` - [x] Factor out parts of the cell interfaces along reading, writing, .send @@ -19,9 +19,9 @@ - [ ] Simplify most wrap/unwrap types to use `CellLike`. We need - [x] "Accept any T where any sub part of T can be wrapped in one or more `BrandedCell`" (for inputs to node factories) - - [ ] "Strip any `BrandedCell` from T and then wrap it in OpaqueRef<>" (for + - [x] "Strip any `BrandedCell` from T and then wrap it in OpaqueRef<>" (for outputs of node factories, where T is the output of the inner function) - - [ ] Make passing the output of the second into the first work. Tricky + - [x] Make passing the output of the second into the first work. Tricky because we're doing almost opposite expansions on the type. - [ ] Add ability to create a cell without a link yet. - [x] Merge StreamCell into RegularCell and rename RegularCell to CellImpl diff --git a/packages/api/index.ts b/packages/api/index.ts index ae7c62ab6..895397906 100644 --- a/packages/api/index.ts +++ b/packages/api/index.ts @@ -515,6 +515,15 @@ type MaybeCellWrapped = : never); export declare const CELL_LIKE: unique symbol; +/** + * Helper type to transform Cell to Opaque in pattern/lift/handler inputs + */ +export type StripCell = T extends AnyBrandedCell ? StripCell + : T extends ArrayBuffer | ArrayBufferView | URL | Date ? T + : T extends Array ? StripCell[] + : T extends object ? { [K in keyof T]: StripCell } + : T; + /** * Opaque accepts T or any cell wrapping T, recursively at any nesting level. * Used in APIs that accept inputs from developers - can be static values @@ -969,11 +978,11 @@ export interface BuiltInCompileAndRunState { export type PatternFunction = { ( fn: (input: OpaqueRef>) => Opaque, - ): RecipeFactory; + ): RecipeFactory, StripCell>; ( fn: (input: OpaqueRef>) => unknown, - ): RecipeFactory>; + ): RecipeFactory, StripCell>>; ( fn: ( @@ -981,7 +990,7 @@ export type PatternFunction = { ) => Opaque>, argumentSchema: IS, resultSchema: OS, - ): RecipeFactory, Schema>; + ): RecipeFactory, SchemaWithoutCell>; }; /** @deprecated Use pattern() instead */ @@ -989,21 +998,21 @@ export type RecipeFunction = { // Function-only overload ( fn: (input: OpaqueRef>) => Opaque, - ): RecipeFactory; + ): RecipeFactory, StripCell>; ( fn: (input: OpaqueRef>) => any, - ): RecipeFactory>; + ): RecipeFactory, StripCell>>; ( argumentSchema: S, fn: (input: OpaqueRef>>) => any, - ): RecipeFactory, ReturnType>; + ): RecipeFactory, StripCell>>; ( argumentSchema: S, fn: (input: OpaqueRef>>) => Opaque, - ): RecipeFactory, R>; + ): RecipeFactory, StripCell>; ( argumentSchema: S, @@ -1016,18 +1025,18 @@ export type RecipeFunction = { ( argumentSchema: string | JSONSchema, fn: (input: OpaqueRef>) => any, - ): RecipeFactory>; + ): RecipeFactory, StripCell>>; ( argumentSchema: string | JSONSchema, fn: (input: OpaqueRef>) => Opaque, - ): RecipeFactory; + ): RecipeFactory, StripCell>; ( argumentSchema: string | JSONSchema, resultSchema: JSONSchema, fn: (input: OpaqueRef>) => Opaque, - ): RecipeFactory; + ): RecipeFactory, StripCell>; }; export type PatternToolFunction = < @@ -1047,21 +1056,21 @@ export type LiftFunction = { ( implementation: (input: T) => R, - ): ModuleFactory; + ): ModuleFactory, StripCell>; ( implementation: (input: T) => any, - ): ModuleFactory>; + ): ModuleFactory, StripCell>>; any>( implementation: T, - ): ModuleFactory[0], ReturnType>; + ): ModuleFactory[0]>, StripCell>>; ( argumentSchema?: JSONSchema, resultSchema?: JSONSchema, implementation?: (input: T) => R, - ): ModuleFactory; + ): ModuleFactory, StripCell>; }; // Helper type to make non-Cell and non-Stream properties readonly in handler state @@ -1085,17 +1094,17 @@ export type HandlerFunction = { eventSchema: JSONSchema, stateSchema: JSONSchema, handler: (event: E, props: HandlerState) => any, - ): ModuleFactory, E>; + ): ModuleFactory, StripCell>; // Without schemas ( handler: (event: E, props: T) => any, options: { proxy: true }, - ): ModuleFactory, E>; + ): ModuleFactory, StripCell>; ( handler: (event: E, props: HandlerState) => any, - ): ModuleFactory, E>; + ): ModuleFactory, StripCell>; }; /** @@ -1308,12 +1317,6 @@ export type Mutable = T extends ReadonlyArray ? Mutable[] : T extends object ? ({ -readonly [P in keyof T]: Mutable }) : T; -// Helper type to transform Cell to Opaque in handler inputs -export type StripCell = T extends Cell ? StripCell - : T extends Array ? StripCell[] - : T extends object ? { [K in keyof T]: StripCell } - : T; - export type WishKey = `/${string}` | `#${string}`; // ===== JSON Pointer Path Resolution Utilities ===== diff --git a/packages/html/test/html-recipes.test.ts b/packages/html/test/html-recipes.test.ts index 5bfbc71cd..0b33389ac 100644 --- a/packages/html/test/html-recipes.test.ts +++ b/packages/html/test/html-recipes.test.ts @@ -5,7 +5,6 @@ import { type Cell, createBuilder, type IExtendedStorageTransaction, - type OpaqueRef, Runtime, } from "@commontools/runner"; import { StorageManager } from "@commontools/runner/storage/cache.deno"; @@ -215,7 +214,7 @@ describe("recipes with HTML", () => { h( "ul", null, - entries(row).map((input: OpaqueRef<[string, unknown]>) => + entries(row).map((input) => h("li", null, [input[0], ": ", str`${input[1]}`]) ) as VNode[], ) diff --git a/packages/patterns/chatbot-list-view.tsx b/packages/patterns/chatbot-list-view.tsx index 05beea12b..59429e5a8 100644 --- a/packages/patterns/chatbot-list-view.tsx +++ b/packages/patterns/chatbot-list-view.tsx @@ -30,9 +30,9 @@ type Input = { selectedCharm: Default<{ charm: any }, { charm: undefined }>; charmsList: Default; theme?: { - accentColor: Default; - fontFace: Default; - borderRadius: Default; + accentColor: Cell>; + fontFace: Cell>; + borderRadius: Cell>; }; }; diff --git a/packages/patterns/chatbot-note-composed.tsx b/packages/patterns/chatbot-note-composed.tsx index 79745f3b1..cf8a9c520 100644 --- a/packages/patterns/chatbot-note-composed.tsx +++ b/packages/patterns/chatbot-note-composed.tsx @@ -31,8 +31,8 @@ function schemaifyWish(path: string, def: T) { } type ChatbotNoteInput = { - title: Default; - messages: Default, []>; + title?: Cell>; + messages?: Cell, []>>; }; type ChatbotNoteResult = { @@ -57,11 +57,11 @@ const newNote = handler< try { const n = Note({ title: args.title, - content: args.content || "", + content: args.content ?? "", }); args.result.set( - `Created note ${args.title}!`, + `Created note ${args.title}`, ); // TODO(bf): we have to navigate here until DX1 lands diff --git a/packages/patterns/chatbot-outliner.tsx b/packages/patterns/chatbot-outliner.tsx index 32e03559e..3181f2fe4 100644 --- a/packages/patterns/chatbot-outliner.tsx +++ b/packages/patterns/chatbot-outliner.tsx @@ -79,10 +79,10 @@ export const Page = recipe( ); type LLMTestInput = { - title: Default; - messages: Default, []>; - expandChat: Default; - outline: Default< + title?: Cell>; + messages?: Cell, []>>; + expandChat?: Cell>; + outline?: Default< Outliner, { root: { body: "Untitled Page"; children: []; attachments: [] } } >; diff --git a/packages/patterns/chatbot.tsx b/packages/patterns/chatbot.tsx index 74f1818e6..ac6c6efbc 100644 --- a/packages/patterns/chatbot.tsx +++ b/packages/patterns/chatbot.tsx @@ -118,8 +118,8 @@ const clearChat = handler( ); type ChatInput = { - messages: Default, []>; - tools: any; + messages?: Cell, []>>; + tools?: any; theme?: any; system?: string; }; diff --git a/packages/patterns/ct-checkbox-cell.tsx b/packages/patterns/ct-checkbox-cell.tsx index 5928170e1..09ec71718 100644 --- a/packages/patterns/ct-checkbox-cell.tsx +++ b/packages/patterns/ct-checkbox-cell.tsx @@ -2,8 +2,8 @@ import { Cell, Default, handler, ifElse, NAME, recipe, UI } from "commontools"; interface CheckboxDemoInput { - simpleEnabled: Default; - trackedEnabled: Default; + simpleEnabled: Cell>; + trackedEnabled: Cell>; } interface CheckboxDemoOutput extends CheckboxDemoInput {} diff --git a/packages/patterns/ct-checkbox-handler.tsx b/packages/patterns/ct-checkbox-handler.tsx index de74a0b6e..29c2e3a95 100644 --- a/packages/patterns/ct-checkbox-handler.tsx +++ b/packages/patterns/ct-checkbox-handler.tsx @@ -1,8 +1,8 @@ /// -import { Default, ifElse, NAME, recipe, UI } from "commontools"; +import { Cell, Default, ifElse, NAME, recipe, UI } from "commontools"; interface CheckboxSimpleInput { - enabled: Default; + enabled: Cell>; } interface CheckboxSimpleOutput extends CheckboxSimpleInput {} diff --git a/packages/patterns/ct-list.tsx b/packages/patterns/ct-list.tsx index 9a8470b90..5f7a8ff52 100644 --- a/packages/patterns/ct-list.tsx +++ b/packages/patterns/ct-list.tsx @@ -1,13 +1,13 @@ /// -import { Default, NAME, recipe, UI } from "commontools"; +import { Cell, Default, NAME, recipe, UI } from "commontools"; interface Item { title: string; } interface ListInput { - title: Default; - items: Default; + title: Cell>; + items: Cell>; } interface ListOutput extends ListInput {} diff --git a/packages/patterns/ct-select.tsx b/packages/patterns/ct-select.tsx index bba324f85..6e2bc7df1 100644 --- a/packages/patterns/ct-select.tsx +++ b/packages/patterns/ct-select.tsx @@ -1,11 +1,11 @@ /// -import { Default, NAME, recipe, UI } from "commontools"; +import { Cell, Default, NAME, recipe, UI } from "commontools"; type Input = { - selected: Default; - numericChoice: Default; - category: Default; + selected: Cell>; + numericChoice: Cell>; + category: Cell>; }; type Result = { diff --git a/packages/patterns/fetch-data.tsx b/packages/patterns/fetch-data.tsx index 4750a9531..cced11c92 100644 --- a/packages/patterns/fetch-data.tsx +++ b/packages/patterns/fetch-data.tsx @@ -1,5 +1,6 @@ /// import { + Cell, Default, derive, fetchData, @@ -151,7 +152,7 @@ function parseUrl(url: string): { org: string; user: string } { } export default recipe< - { repoUrl: Default } + { repoUrl: Cell> } >( "Github Fetcher Demo", (state) => { diff --git a/packages/patterns/note.tsx b/packages/patterns/note.tsx index 657969e91..c5c16fc88 100644 --- a/packages/patterns/note.tsx +++ b/packages/patterns/note.tsx @@ -1,6 +1,6 @@ /// import { - type Cell, + Cell, cell, type Default, derive, @@ -17,8 +17,8 @@ import { } from "commontools"; import { type MentionableCharm } from "./backlinks-index.tsx"; type Input = { - title: Default; - content: Default; + title?: Cell>; + content?: Cell>; }; type Output = { diff --git a/packages/patterns/omnibox-fab.tsx b/packages/patterns/omnibox-fab.tsx index 914e94c2e..ea85c9211 100644 --- a/packages/patterns/omnibox-fab.tsx +++ b/packages/patterns/omnibox-fab.tsx @@ -63,7 +63,6 @@ export default recipe( const omnibot = Chatbot({ system: "You are a polite but efficient assistant. Think Star Trek computer - helpful and professional without unnecessary conversation. Let your actions speak for themselves.\n\nTool usage priority:\n- Search this space first: listMentionable → addAttachment to access items\n- Search externally only when clearly needed: searchWeb for current events, external information, or when nothing relevant exists in the space\n\nBe matter-of-fact. Prefer action to explanation.", - messages: [], tools: { searchWeb: { pattern: searchWeb, diff --git a/packages/shell/integration/iframe-counter-recipe.tsx b/packages/shell/integration/iframe-counter-recipe.tsx index b7eb0ac1d..3e3f23dc7 100644 --- a/packages/shell/integration/iframe-counter-recipe.tsx +++ b/packages/shell/integration/iframe-counter-recipe.tsx @@ -1,5 +1,5 @@ /// -import { type JSONSchema, NAME, recipe, UI } from "commontools"; +import { CellLike, type JSONSchema, NAME, recipe, UI } from "commontools"; type IFrameRecipe = { src: string; @@ -58,7 +58,9 @@ const runIframeRecipe = ( ) => recipe(argumentSchema, resultSchema, (data) => ({ [NAME]: name, - [UI]: , + [UI]: ( + }> + ), count: data.count, })); diff --git a/recipes/bgCounter.tsx b/recipes/bgCounter.tsx index c4d09f468..2750b5979 100644 --- a/recipes/bgCounter.tsx +++ b/recipes/bgCounter.tsx @@ -33,7 +33,7 @@ const updateError = handler< ); export default recipe< - { error: Default; counter: Default } + { error: Cell>; counter: Cell> } >( "bgCounter", ({ counter, error }) => { diff --git a/recipes/email-summarizer.tsx b/recipes/email-summarizer.tsx index 04f96ef4a..80c0ca401 100644 --- a/recipes/email-summarizer.tsx +++ b/recipes/email-summarizer.tsx @@ -110,11 +110,13 @@ const EmailSummarizerInputSchema = { enum: ["short", "medium", "long"], default: "medium", description: "Length of the summary", + asCell: true, }, includeTags: { type: "boolean", default: true, description: "Include tags in the summary", + asCell: true, }, }, required: ["summaryLength", "includeTags"], @@ -166,7 +168,7 @@ const updateSummaryLength = handler( properties: { summaryLength: { type: "string", - asCell: true, // Mark as cell + asCell: true, }, }, required: ["summaryLength"], @@ -198,7 +200,7 @@ const updateIncludeTags = handler( properties: { includeTags: { type: "boolean", - asCell: true, // Mark as cell + asCell: true, }, }, required: ["includeTags"], @@ -258,19 +260,23 @@ export default recipe( // Create prompts using the str template literal for proper reactivity // This ensures the prompts update when settings change - const lengthInstructions = str`${ - settings.summaryLength === "short" - ? "in 1-2 sentences" - : settings.summaryLength === "long" - ? "in 5-7 sentences" - : "in 3-4 sentences" - }`; + const lengthInstructions = derive( + settings.summaryLength, + (length: "short" | "medium" | "long") => + length === "short" + ? "in 1-2 sentences" + : length === "long" + ? "in 5-7 sentences" + : "in 3-4 sentences", + ); - const tagInstructions = str`${ - settings.includeTags - ? "Include up to 3 relevant tags or keywords in the format #tag at the end of the summary." - : "" - }`; + const tagInstructions = derive( + settings.includeTags, + (includeTags: boolean) => + includeTags + ? "Include up to 3 relevant tags or keywords in the format #tag at the end of the summary." + : "", + ); // Create system prompt with str to maintain reactivity const systemPrompt = str` diff --git a/recipes/gcal.tsx b/recipes/gcal.tsx index ca7bf94f8..0252604a2 100644 --- a/recipes/gcal.tsx +++ b/recipes/gcal.tsx @@ -105,11 +105,13 @@ const GcalImporterInputs = { type: "string", description: "Calendar ID to fetch events from", default: "primary", + asCell: true, }, limit: { type: "number", description: "number of events to import", default: 250, + asCell: true, }, syncToken: { type: "string", @@ -222,8 +224,8 @@ const calendarUpdater = handler( const settings = state.settings.get(); const result = await fetchCalendar( state.auth, - settings.limit, - settings.calendarId, + settings.limit.get(), + settings.calendarId.get(), settings.syncToken, state, ); diff --git a/recipes/gmail-importer.tsx b/recipes/gmail-importer.tsx index ca5d2ef04..6188f891e 100644 --- a/recipes/gmail-importer.tsx +++ b/recipes/gmail-importer.tsx @@ -77,9 +77,9 @@ type Email = { type Settings = { // Gmail filter query to use for fetching emails - gmailFilterQuery: Default; + gmailFilterQuery: Cell>; // Maximum number of emails to fetch - limit: Default; + limit: Cell>; // Gmail history ID for incremental sync historyId: Default; }; @@ -809,11 +809,7 @@ const updateGmailFilterQuery = handler< ); export default recipe<{ - settings: Default; + settings: Settings; auth: Auth; }>( "gmail-importer", diff --git a/recipes/input.tsx b/recipes/input.tsx index 9ba714ba7..ed4529dc2 100644 --- a/recipes/input.tsx +++ b/recipes/input.tsx @@ -7,6 +7,7 @@ const InputSchema = { content: { type: "string", default: "", + asCell: true, }, }, required: ["content"], diff --git a/recipes/research-report.tsx b/recipes/research-report.tsx index 71beebedf..f48961392 100644 --- a/recipes/research-report.tsx +++ b/recipes/research-report.tsx @@ -6,13 +6,14 @@ const InputSchema = { title: { type: "string", default: "Untitled Research Report", + asCell: true, }, content: { type: "string", default: "", + asCell: true, }, }, - required: ["title", "content"], } as const satisfies JSONSchema; const OutputSchema = InputSchema; @@ -22,7 +23,7 @@ export default recipe( OutputSchema, ({ title, content }) => { return { - [NAME]: title || "Untitled Research Report", + [NAME]: title, [UI]: (
diff --git a/recipes/rss.tsx b/recipes/rss.tsx index eb5159ca4..21094ac71 100644 --- a/recipes/rss.tsx +++ b/recipes/rss.tsx @@ -14,7 +14,7 @@ import { import { type FeedItem, parseRSSFeed } from "./rss-utils.ts"; interface Settings { - feedUrl: Default; + feedUrl: Cell>; limit: Default; } @@ -43,7 +43,7 @@ const feedUpdater = handler } + { settings: Settings } >( "rss importer", ({ settings }) => { @@ -76,7 +76,6 @@ export default recipe< placeholder="https://example.com/feed.xml or https://example.com/atom.xml" />
-
diff --git a/recipes/simpleValue.tsx b/recipes/simpleValue.tsx index 437cfc4cc..eb90cecba 100644 --- a/recipes/simpleValue.tsx +++ b/recipes/simpleValue.tsx @@ -50,7 +50,7 @@ const updaterSchema = { const inputSchema = schema({ type: "object", properties: { - values: { type: "array", items: { type: "string" }, asCell: true }, + values: { type: "array", items: { type: "string" } }, }, default: { values: [] }, });