From 4f30285c924ea92159502a3a1a3886a6044d3c60 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Wed, 21 Oct 2026 13:15:57 +0300 Subject: [PATCH 01/25] docs(platform): the React hook is one page, like the chat module useChat, ChatBackend, ChatItem and ChatQuestion were four pages, with the hook's own types appended to each and an unlabelled Methods section in the middle of useChat. They are now one useChat page in the layout of the chat module page: Overview, then "Actions" (send, create, clearError), "Backend calls" (the four ChatBackend calls), "Type Definitions" and "Questions". ChatOptions has no section of its own: Parameters already documents it. The shared pipeline gets three generic pieces for this, with no effect on the app SDK reference, which generates byte-identical: - In a type definition group, a type's own methods nest under it instead of sitting at the level of the types, and the bare Methods label goes. - A type's properties become fields up to its first method, which keeps its heading. - A last pass points in-page links at headings and explicit anchors that exist: a type whose section was renamed is linked by its new heading, and a link to anything the page no longer has keeps its text without the link. Co-Authored-By: Claude Opus 5.5 --- packages/platform/README.md | 2 +- .../appended-articles.json | 18 ++-- .../page-sections.json | 55 ++++++++++-- .../types-to-delete-after-processing.json | 3 + .../file-processing/file-processing.js | 89 ++++++++++++++++++- .../sdk/tests/unit/typedoc-mintlify.test.ts | 77 +++++++++++++++- 6 files changed, 223 insertions(+), 21 deletions(-) create mode 100644 packages/platform/scripts/mintlify-post-processing/types-to-delete-after-processing.json diff --git a/packages/platform/README.md b/packages/platform/README.md index c379dee..ac45691 100644 --- a/packages/platform/README.md +++ b/packages/platform/README.md @@ -213,7 +213,7 @@ Every exported shape and field has JSDoc. `npm run docs` validates it into `docs and `npm run copy-docs-local -- --target ` copies the result into `developers/references/platform-sdk/docs`. The pipeline's config for this package lives in `scripts/mintlify-post-processing/`, including `reference.ts`, the docs-only entry that combines -the client and server entrypoints into one reference. The Socket messages page is built in mintlify-docs from +the client, server and React entrypoints into one reference. The Socket messages page is built in mintlify-docs from the service's AsyncAPI document, not here. `PlatformSocketError` extends `Error` with `code` and optional `appId`. Original diff --git a/packages/platform/scripts/mintlify-post-processing/appended-articles.json b/packages/platform/scripts/mintlify-post-processing/appended-articles.json index 64f28f2..009935a 100644 --- a/packages/platform/scripts/mintlify-post-processing/appended-articles.json +++ b/packages/platform/scripts/mintlify-post-processing/appended-articles.json @@ -17,21 +17,17 @@ "type-aliases/WebhookConfigurationErrorCode" ], "functions/useChat": [ - "interfaces/ChatOptions", "interfaces/UseChatResult", + "interfaces/ChatBackend", + "interfaces/ChatItem", + "interfaces/ChatStep", + "interfaces/ChatAnswer", "type-aliases/ChatStatus", "type-aliases/ChatConnection", - "interfaces/ChatError" - ], - "interfaces/ChatBackend": [ + "interfaces/ChatError", "interfaces/ChatApp", - "interfaces/ToolCallAnswer" - ], - "interfaces/ChatItem": [ - "interfaces/ChatStep", - "interfaces/ChatAnswer" - ], - "type-aliases/ChatQuestion": [ + "interfaces/ToolCallAnswer", + "type-aliases/ChatQuestion", "interfaces/ChoiceQuestion", "interfaces/ChoiceEntry", "interfaces/SecretsQuestion", diff --git a/packages/platform/scripts/mintlify-post-processing/page-sections.json b/packages/platform/scripts/mintlify-post-processing/page-sections.json index 62cf557..c249fd1 100644 --- a/packages/platform/scripts/mintlify-post-processing/page-sections.json +++ b/packages/platform/scripts/mintlify-post-processing/page-sections.json @@ -1,22 +1,65 @@ { "typesWithOwnMethods": { - "ChatSession": "Session methods" + "ChatSession": "Session methods", + "UseChatResult": "Actions", + "ChatBackend": "Backend calls" }, "typeDefinitionGroups": [ - ["SocketSession", "PlatformEvent", "PlatformSnapshot"], + [ + "SocketSession", + "PlatformEvent", + "PlatformSnapshot" + ], { "title": "Errors", - "types": ["PlatformSocketError", "SessionErrorCode", "AppErrorCode"], - "nested": { "SessionErrorCode": "Session error codes", "AppErrorCode": "App error codes" } + "types": [ + "PlatformSocketError", + "SessionErrorCode", + "AppErrorCode" + ], + "nested": { + "SessionErrorCode": "Session error codes", + "AppErrorCode": "App error codes" + } }, - ["WebhookEvent", "UnknownWebhookEvent"], + [ + "WebhookEvent", + "UnknownWebhookEvent" + ], { "title": "Errors", - "types": ["WebhookVerificationError", "WebhookVerificationErrorCode", "WebhookConfigurationError", "WebhookConfigurationErrorCode"], + "types": [ + "WebhookVerificationError", + "WebhookVerificationErrorCode", + "WebhookConfigurationError", + "WebhookConfigurationErrorCode" + ], "nested": { "WebhookVerificationErrorCode": "Verification error codes", "WebhookConfigurationErrorCode": "Configuration error codes" } + }, + [ + "ChatItem", + "ChatStep", + "ChatAnswer", + "ChatStatus", + "ChatConnection", + "ChatError", + "ChatApp", + "ToolCallAnswer" + ], + { + "title": "Questions", + "types": [ + "ChatQuestion", + "ChoiceQuestion", + "ChoiceEntry", + "SecretsQuestion", + "SecretField", + "ApprovalQuestion", + "ApprovalDetails" + ] } ] } diff --git a/packages/platform/scripts/mintlify-post-processing/types-to-delete-after-processing.json b/packages/platform/scripts/mintlify-post-processing/types-to-delete-after-processing.json new file mode 100644 index 0000000..4df56a7 --- /dev/null +++ b/packages/platform/scripts/mintlify-post-processing/types-to-delete-after-processing.json @@ -0,0 +1,3 @@ +[ + "ChatOptions" +] diff --git a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js index fb56490..5cae63a 100755 --- a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js +++ b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js @@ -1578,6 +1578,7 @@ function groupTypeDefinitions(content) { for (const typeName of group.types) { section = section.replace(new RegExp(`^## ${typeName}$`, "m"), `### ${typeName}`); } + section = nestUnderGroupTypes(section, new Set(group.types)); // Any "##" heading still left in this slice belongs to one of the types // above it (e.g. "## Properties" from an appended interface with members, // since normalizeHeadings() clamps an appended article's own headings to @@ -1590,6 +1591,41 @@ function groupTypeDefinitions(content) { return { content: updatedContent, modified: true }; } +/** + * Inside a group, a type's own methods arrive at the level of the types themselves: a "## Methods" + * label, then "### method()" with its "#### Parameters" and "#### Returns". Nest them under their + * type instead. A "###" heading that isn't one of the group's types belongs to the type above it, + * so it moves down a level with everything under it, and the bare "Methods" label goes. + */ +function nestUnderGroupTypes(section, typeNames) { + const lines = section.split("\n"); + const result = []; + let inFence = false; + let inMember = false; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.trim().startsWith("```")) inFence = !inFence; + const heading = inFence ? null : line.match(/^(#{2,6}) (.+)$/); + if (!heading) { + result.push(line); + continue; + } + const level = heading[1].length; + if (level <= 3) inMember = false; + if (level === 3 && typeNames.has(heading[2])) { + result.push(line); + continue; + } + if (level === 2 && heading[2] === "Methods") { + if (lines[i + 1]?.trim() === "") i++; + continue; + } + if (level === 3) inMember = true; + result.push(inMember && level < 6 ? `#${line}` : line); + } + return result.join("\n"); +} + /** * Rename and merge method sections for better TOC organization * - EntityHandler → Entity Handler Methods @@ -2551,7 +2587,8 @@ function propertiesAsFields(content, classPage) { .replace(/^#### Inherited from\n\n.*\n\n?/gm, "") .replace(/^#### Extends\n\n(- .*\n)+\n?/gm, "") .replace(classPage ? /^## Properties\n([\s\S]*?)(?=^## |(?![\s\S]))/m : /(?!)/, (section, body) => `## Properties\n\n${fieldsOf(body)}\n\n`) - .replace(/^#### Properties\n([\s\S]*?)(?=^#{1,3} |(?![\s\S]))/gm, (section, body) => `${fieldsOf(body)}\n\n`); + // A type's properties end at its first method ("name()"), which keeps its heading. + .replace(/^#### Properties\n([\s\S]*?)(?=^#{1,3} |^#{4,6} \S+\(\)$|(?![\s\S]))/gm, (section, body) => `${fieldsOf(body)}\n\n`); } function applyPropertiesAsFields(dir) { @@ -2706,6 +2743,51 @@ function linkReturnTypes(content) { }); } +/** The anchor Mintlify gives a heading: lowercase, punctuation dropped, spaces as hyphens. */ +function headingAnchor(text) { + return text.toLowerCase().replace(/[^a-z0-9 -]/g, "").trim().replace(/ /g, "-"); +} + +/** + * Point every in-page link at a heading or explicit anchor that exists. A type whose section was renamed + * (page-sections.json `typesWithOwnMethods`) is linked by its new heading. A link to anything + * else the page no longer has, such as a property now shown as a field, keeps its text and loses + * the link, so it can't jump nowhere. + */ +function fixInPageAnchors(content) { + const anchors = new Set(); + let inFence = false; + for (const line of content.split("\n")) { + if (line.trim().startsWith("```")) inFence = !inFence; + if (inFence) continue; + const heading = line.match(/^#{1,6} (.+)$/); + if (heading) anchors.add(headingAnchor(heading[1])); + // An explicit anchor, such as an overload's label + for (const [, id] of line.matchAll(/\bid="([^"]+)"/g)) anchors.add(id); + } + const renamed = new Map( + Object.entries(TYPES_WITH_OWN_METHODS).map(([type, title]) => [headingAnchor(type), headingAnchor(title ?? type)]) + ); + return content.replace(/\[([^\]]+)\]\(#([^)\s]+)\)/g, (link, text, anchor) => { + if (anchors.has(anchor)) return link; + const target = renamed.get(anchor); + if (target && anchors.has(target)) return `[${text}](#${target})`; + return text; + }); +} + +function applyInPageAnchorFixes(dir) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const entryPath = path.join(dir, entry.name); + if (entry.isDirectory()) applyInPageAnchorFixes(entryPath); + else if (entry.name.endsWith(".mdx")) { + const content = fs.readFileSync(entryPath, "utf-8"); + const fixed = fixInPageAnchors(content); + if (fixed !== content) fs.writeFileSync(entryPath, fixed, "utf-8"); + } + } +} + function applyReturnTypeLinks(dir) { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const entryPath = path.join(dir, entry.name); @@ -2795,6 +2877,9 @@ function main() { // that method ordering splits on. applyReturnTypeLinks(DOCS_DIR); + // Last, so every heading the page ends up with counts + applyInPageAnchorFixes(DOCS_DIR); + // Clean up the linked types file try { if (fs.existsSync(LINKED_TYPES_FILE)) { @@ -2820,4 +2905,4 @@ if (process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(__fi main(); } -export { removeNonExposedTypeLinks }; +export { fixInPageAnchors, nestUnderGroupTypes, propertiesAsFields, removeNonExposedTypeLinks }; diff --git a/packages/sdk/tests/unit/typedoc-mintlify.test.ts b/packages/sdk/tests/unit/typedoc-mintlify.test.ts index 59a5032..7caa8f4 100644 --- a/packages/sdk/tests/unit/typedoc-mintlify.test.ts +++ b/packages/sdk/tests/unit/typedoc-mintlify.test.ts @@ -5,7 +5,12 @@ import { Application, ReflectionKind } from "typedoc"; import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { nameCallableProperties } from "../../scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-content.js"; -import { removeNonExposedTypeLinks } from "../../scripts/mintlify-post-processing/file-processing/file-processing.js"; +import { + fixInPageAnchors, + nestUnderGroupTypes, + propertiesAsFields, + removeNonExposedTypeLinks, +} from "../../scripts/mintlify-post-processing/file-processing/file-processing.js"; // Runs TypeDoc with the Mintlify plugin on a small fixture, so each case is checked on the pages // the plugin really writes. @@ -169,3 +174,73 @@ describe("removeNonExposedTypeLinks", () => { expect(removeNonExposedTypeLinks(content, exposed)).toEqual({ content, modified: false }); }); }); + +describe("nestUnderGroupTypes", () => { + test("nests a grouped type's methods under it, and drops the bare Methods label", () => { + const section = [ + "### Question", + "#### Properties", + "## Methods", + "", + "### answer()", + "#### Parameters", + "### Entry", + "#### Properties", + ].join("\n"); + + expect(nestUnderGroupTypes(section, new Set(["Question", "Entry"])).split("\n")).toEqual([ + "### Question", + "#### Properties", + "#### answer()", + "##### Parameters", + "### Entry", + "#### Properties", + ]); + }); +}); + +describe("propertiesAsFields", () => { + test("turns a type's properties into fields and keeps its methods as headings", () => { + const content = [ + "#### Properties", + "", + "#### kind", + "", + "> **kind**: `\"choice\"`", + "", + "Kind of question.", + "", + "***", + "", + "#### answer()", + "", + "Answers.", + ].join("\n"); + + const converted = propertiesAsFields(content, false); + expect(converted).toContain(``); + expect(converted).not.toContain('name="answer()"'); + const method = converted.search(/^#### answer\(\)$/m); + expect(method).toBeGreaterThan(converted.lastIndexOf("")); + }); +}); + +describe("fixInPageAnchors", () => { + test("keeps links to headings and explicit anchors", () => { + const content = ["## Send", "", "See [send](#send) and [records](#list-records)."].join("\n"); + + expect(fixInPageAnchors(content)).toBe(content); + }); + + test("follows a type's section to its new heading", () => { + const content = ["## Entity Handler Methods", "See [EntityHandler](#entityhandler)."].join("\n"); + + expect(fixInPageAnchors(content)).toContain("[EntityHandler](#entity-handler-methods)"); + }); + + test("unlinks an anchor the page doesn't have, and leaves other links alone", () => { + const content = "The [status](#status) and the [guide](/guide#status)."; + + expect(fixInPageAnchors(content)).toBe("The status and the [guide](/guide#status)."); + }); +}); From 1ff6e68e3a9298e256f8099e642983587fe5bf53 Mon Sep 17 00:00:00 2001 From: "Sam (automated drift fix)" Date: Wed, 21 Oct 2026 15:45:27 +0300 Subject: [PATCH 02/25] docs(pipeline): match Mintlify's heading anchors and link unique field mentions Co-Authored-By: Claude Opus 5.5 --- .../file-processing/file-processing.js | 21 +++++++++++++++++-- .../sdk/tests/unit/typedoc-mintlify.test.ts | 17 +++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js index 5cae63a..decfdab 100755 --- a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js +++ b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js @@ -2743,9 +2743,18 @@ function linkReturnTypes(content) { }); } -/** The anchor Mintlify gives a heading: lowercase, punctuation dropped, spaces as hyphens. */ +/** + * The anchor Mintlify gives a heading: lowercase, with backticks and parentheses dropped, dots and + * spaces as hyphens, and underscores kept, so `pull_request.changed` becomes `pull_request-changed`. + */ function headingAnchor(text) { - return text.toLowerCase().replace(/[^a-z0-9 -]/g, "").trim().replace(/ /g, "-"); + return text + .toLowerCase() + .replace(/[`()]/g, "") + .replace(/[.\s]+/g, "-") + .replace(/[^a-z0-9_-]/g, "") + .replace(/-+/g, "-") + .replace(/^-|-$/g, ""); } /** @@ -2765,6 +2774,12 @@ function fixInPageAnchors(content) { // An explicit anchor, such as an overload's label for (const [, id] of line.matchAll(/\bid="([^"]+)"/g)) anchors.add(id); } + // Mintlify anchors a field as `param-`. Only a name that one field on the page uses is + // a safe target, because a repeated name gets numbered anchors in page order. + const fieldCounts = new Map(); + for (const [, name] of content.matchAll(/<(?:ResponseField|ParamField) (?:name|body)="([^"]+)"/g)) { + fieldCounts.set(name, (fieldCounts.get(name) ?? 0) + 1); + } const renamed = new Map( Object.entries(TYPES_WITH_OWN_METHODS).map(([type, title]) => [headingAnchor(type), headingAnchor(title ?? type)]) ); @@ -2772,6 +2787,8 @@ function fixInPageAnchors(content) { if (anchors.has(anchor)) return link; const target = renamed.get(anchor); if (target && anchors.has(target)) return `[${text}](#${target})`; + const field = [...fieldCounts].find(([name, count]) => count === 1 && headingAnchor(name) === anchor); + if (field) return `[${text}](#param-${headingAnchor(field[0])})`; return text; }); } diff --git a/packages/sdk/tests/unit/typedoc-mintlify.test.ts b/packages/sdk/tests/unit/typedoc-mintlify.test.ts index 7caa8f4..d2ce65b 100644 --- a/packages/sdk/tests/unit/typedoc-mintlify.test.ts +++ b/packages/sdk/tests/unit/typedoc-mintlify.test.ts @@ -243,4 +243,21 @@ describe("fixInPageAnchors", () => { expect(fixInPageAnchors(content)).toBe("The status and the [guide](/guide#status)."); }); + + test("matches Mintlify's anchors for headings with dots and underscores", () => { + const content = ["### `pull_request.changed`", "See [the event](#pull_request-changed)."].join("\n"); + + expect(fixInPageAnchors(content)).toBe(content); + }); + + test("links to a field only when one field on the page has that name", () => { + const content = [ + '', + '', + '', + "See [error](#error) and [status](#status).", + ].join("\n"); + + expect(fixInPageAnchors(content)).toContain("See [error](#param-error) and status."); + }); }); From f89b5f33d154009c04406034079cb0c4da0f4f9c Mon Sep 17 00:00:00 2001 From: "Sam (automated drift fix)" Date: Thu, 22 Oct 2026 10:00:26 +0300 Subject: [PATCH 03/25] docs(pipeline): let the category map put one page in its own nav group The Platform SDK's useChat page goes under React instead of Clients, so a regeneration keeps the React group. Co-Authored-By: Claude Opus 5.5 --- .../mintlify-post-processing/category-map.json | 3 ++- .../mintlify-post-processing/copy-to-local-docs.js | 13 +++++++++++-- .../file-processing/file-processing.js | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/platform/scripts/mintlify-post-processing/category-map.json b/packages/platform/scripts/mintlify-post-processing/category-map.json index 809b882..ab1a619 100644 --- a/packages/platform/scripts/mintlify-post-processing/category-map.json +++ b/packages/platform/scripts/mintlify-post-processing/category-map.json @@ -1,5 +1,6 @@ { "functions": "Clients", "interfaces": "Modules", - "type-aliases": "Modules" + "type-aliases": "Modules", + "functions/useChat": "React" } diff --git a/packages/sdk/scripts/mintlify-post-processing/copy-to-local-docs.js b/packages/sdk/scripts/mintlify-post-processing/copy-to-local-docs.js index 9491258..0470d9f 100644 --- a/packages/sdk/scripts/mintlify-post-processing/copy-to-local-docs.js +++ b/packages/sdk/scripts/mintlify-post-processing/copy-to-local-docs.js @@ -130,9 +130,18 @@ function updateDocsJson(repoDir, sdkFiles) { }; const p = (kind, file) => `${prefix}${basePath}/${kind}/${file}`; - // Groups follow the category map's key order. + // Groups follow the category map's key order. A key that names one page, such as + // "functions/useChat", moves that page out of its kind's group into its own. + const pageGroups = Object.entries(categoryMap).filter(([key]) => key.includes("/")); + const movedPages = new Set(pageGroups.map(([key]) => key)); for (const [kind, group] of Object.entries(categoryMap)) { - if (sdkFiles[kind]?.length > 0) addToGroup(group, sdkFiles[kind].map((f) => p(kind, f))); + if (kind.includes("/")) { + const [pageKind, file] = kind.split("/"); + if (sdkFiles[pageKind]?.includes(file)) addToGroup(group, [p(pageKind, file)]); + continue; + } + const files = (sdkFiles[kind] ?? []).filter((f) => !movedPages.has(`${kind}/${f}`)); + if (files.length > 0) addToGroup(group, files.map((f) => p(kind, f))); } return Array.from(groupMap.entries()).map(([group, pages]) => ({ diff --git a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js index decfdab..f23e079 100755 --- a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js +++ b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js @@ -408,6 +408,20 @@ function generateDocsJson(docsContent) { } } + // A category map key that names one page, such as "functions/useChat", moves that page out of + // its kind's group into its own, after the kind groups. + for (const [key, groupName] of Object.entries(categoryMap)) { + if (!key.includes("/")) continue; + for (const group of groups) { + const index = group.pages.findIndex((page) => page.endsWith(`/${key}`)); + if (index === -1) continue; + const [page] = group.pages.splice(index, 1); + const target = groups.find((g) => g.group === groupName) ?? groups[groups.push({ group: groupName, expanded: true, pages: [] }) - 1]; + target.pages.push(page); + } + } + for (let i = groups.length - 1; i >= 0; i--) if (groups[i].pages.length === 0) groups.splice(i, 1); + // Find or create SDK Reference tab let sdkTab = template.navigation.tabs.find( (tab) => tab.tab === "SDK Reference" From fd02508088023ba91cae2508f2d3a4ed4a7fb16f Mon Sep 17 00:00:00 2001 From: "Sam (automated drift fix)" Date: Thu, 22 Oct 2026 10:29:32 +0300 Subject: [PATCH 04/25] docs(platform): useChat overview review edits Co-Authored-By: Claude Opus 5.5 --- packages/platform/src/react/chat.types.ts | 4 +++- packages/platform/src/react/useChat.ts | 21 +++++++-------------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/packages/platform/src/react/chat.types.ts b/packages/platform/src/react/chat.types.ts index a79379b..62a50f0 100644 --- a/packages/platform/src/react/chat.types.ts +++ b/packages/platform/src/react/chat.types.ts @@ -305,7 +305,9 @@ export interface ChatError { export interface ChatOptions { /** * ID of the app to follow, or `null` until an app exists. While it's `null`, the chat follows the - * app {@link UseChatResult.create} made. Any change to `appId` forgets that app. + * app that [`create()`](#create) made, so you don't have to pass the new ID back. + * Changing `appId` stops following that app, so setting it back to `null` starts a new chat. + * Switching to another app closes the old session and starts over. */ appId: string | null; /** The four calls your backend makes to Base44. The chat reads the latest on every action, so an inline object works. */ diff --git a/packages/platform/src/react/useChat.ts b/packages/platform/src/react/useChat.ts index fb3f475..bdd8aeb 100644 --- a/packages/platform/src/react/useChat.ts +++ b/packages/platform/src/react/useChat.ts @@ -11,23 +11,16 @@ const BUILD_FAILED_ERROR: ChatError = { message: "The build failed", code: "buil const NEW_APP = "new-app"; /** - * Runs the AI chat for one app in your own UI. + * Runs an app's AI chat in your own React UI. Requires a browser environment and can't be used in + * the backend. * - * The hook renders nothing. It gives you the conversation and the actions, and you draw them: + * The hook is headless. It gives you the conversation and the actions, and your components render them: * - Follows the app live, and renews its socket session when it expires. - * - Turns Base44's messages into {@link ChatItem | items}, with each open question bound to the step it belongs to. - * - Reports what the AI is doing in {@link UseChatResult.status}, and the live connection in {@link UseChatResult.connection}. - * - Sends prompts, creates apps and answers questions through the {@link ChatBackend | backend} you pass, so no credential reaches the browser. + * - Turns Base44's messages into {@link ChatItem | items}, and shows each open question with the step that asked it. + * - Reports what the AI is doing in {@linkcode UseChatResult.status}, and the live connection in {@linkcode UseChatResult.connection}. + * - Sends prompts, creates apps, and answers questions through the {@link ChatBackend | backend} you pass, so no credential reaches the browser. * - * The actions never throw. A failure sets {@link UseChatResult.error}, and a question whose answer - * failed opens again. The chat declines a question kind this release doesn't know. - * - * Requires a browser environment and can't be used in the backend. - * - * The chat follows `appId` when it's set. While `appId` is `null`, it follows the app - * {@link UseChatResult.create} made, so passing the new ID back is optional. Any change to `appId` - * forgets that app, so setting it back to `null` starts a new chat. Switching apps closes the old - * session and starts over. + * The actions never throw. A failure sets {@linkcode UseChatResult.error}. * * @param options - The app to follow, your backend's calls, and what to do with a created app. * @returns The conversation, its status, connection and error, and the actions `send`, `create` and `clearError`. From 6f4b83728c2241442af3a1c54040e27b7118419d Mon Sep 17 00:00:00 2001 From: "Sam (automated drift fix)" Date: Thu, 22 Oct 2026 10:46:09 +0300 Subject: [PATCH 05/25] docs(platform): useChat options wording Co-Authored-By: Claude Opus 5.5 --- packages/platform/src/react/chat.types.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/platform/src/react/chat.types.ts b/packages/platform/src/react/chat.types.ts index 62a50f0..d9642d3 100644 --- a/packages/platform/src/react/chat.types.ts +++ b/packages/platform/src/react/chat.types.ts @@ -310,11 +310,14 @@ export interface ChatOptions { * Switching to another app closes the old session and starts over. */ appId: string | null; - /** The four calls your backend makes to Base44. The chat reads the latest on every action, so an inline object works. */ + /** + * The four calls your backend makes to Base44. The hook uses the `backend` from the latest render, + * so you can pass an inline object without memoizing it. + */ backend: ChatBackend; /** - * Called with the new app after {@link UseChatResult.create} succeeds. The chat already follows it. - * Pass its ID back as `appId` to keep it in your own state. + * Called with the new app after [`create()`](#create) succeeds. The chat already follows the new + * app. To keep its ID in your own state, pass it back as `appId`. * * @param app - The app that was created. */ From a737b6ee9a8b9d8aadfff87256d164f7048bfa3e Mon Sep 17 00:00:00 2001 From: "Sam (automated drift fix)" Date: Thu, 22 Oct 2026 11:08:01 +0300 Subject: [PATCH 06/25] docs(platform): useChat returns and item field wording Co-Authored-By: Claude Opus 5.5 --- packages/platform/src/react/chat.types.ts | 12 ++++++++---- packages/platform/src/react/useChat.ts | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/platform/src/react/chat.types.ts b/packages/platform/src/react/chat.types.ts index d9642d3..f2dbf43 100644 --- a/packages/platform/src/react/chat.types.ts +++ b/packages/platform/src/react/chat.types.ts @@ -95,9 +95,13 @@ export interface ChatItem { id: string; /** Who wrote the message. */ role: "user" | "assistant"; - /** The message text, as raw markdown. It comes before the steps, and is empty when the message only holds tool calls. */ + /** The message text, as raw markdown. Show it before the message's steps. It's empty when the message only holds tool calls. */ text: string; - /** When the message was created, as an ISO timestamp. Base44's timestamps are UTC with no time zone. A pending prompt carries the browser's time. */ + /** + * When the message was created, as an ISO 8601 timestamp in UTC. A timestamp from Base44 has no `Z` + * or offset, so add a `Z` before parsing it with `new Date()`. A pending prompt has the browser's + * time, which already ends in `Z`. + */ createdAt: string; /** One step per tool call, in order. */ steps: ChatStep[]; @@ -324,9 +328,9 @@ export interface ChatOptions { onAppCreated?: (app: ChatApp) => void; } -/** The chat that {@link useChat} returns. */ +/** The chat state and actions that `useChat()` returns. */ export interface UseChatResult { - /** The conversation, oldest first, with open questions bound to their steps. */ + /** The conversation, oldest first. Each open question appears with the step that asked it. */ items: ChatItem[]; /** What the AI is doing. */ status: ChatStatus; diff --git a/packages/platform/src/react/useChat.ts b/packages/platform/src/react/useChat.ts index bdd8aeb..948baf2 100644 --- a/packages/platform/src/react/useChat.ts +++ b/packages/platform/src/react/useChat.ts @@ -23,7 +23,7 @@ const NEW_APP = "new-app"; * The actions never throw. A failure sets {@linkcode UseChatResult.error}. * * @param options - The app to follow, your backend's calls, and what to do with a created app. - * @returns The conversation, its status, connection and error, and the actions `send`, `create` and `clearError`. + * @returns The conversation, its `status`, `connection`, and `error`, and the [actions](#actions) `send()`, `create()`, and `clearError()`. * * @example * ```typescript From 28e4eeb3fbb0ebb60860dfbfbbb17d2ecf7e60a9 Mon Sep 17 00:00:00 2001 From: "Sam (automated drift fix)" Date: Thu, 22 Oct 2026 11:19:27 +0300 Subject: [PATCH 07/25] docs(platform): useChat step and pending field wording Co-Authored-By: Claude Opus 5.5 --- packages/platform/src/react/chat.types.ts | 25 ++++++++++++++--------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/platform/src/react/chat.types.ts b/packages/platform/src/react/chat.types.ts index f2dbf43..b8678c2 100644 --- a/packages/platform/src/react/chat.types.ts +++ b/packages/platform/src/react/chat.types.ts @@ -106,9 +106,10 @@ export interface ChatItem { /** One step per tool call, in order. */ steps: ChatStep[]; /** - * Whether this is a prompt sent from this chat that Base44 hasn't confirmed yet. It shows at once, - * and Base44's own message replaces it. A failed send removes it. A prompt sent while the AI is - * working waits in Base44's queue and stays pending until it runs. + * Whether this is a stand-in for a prompt you just sent. When you call [`send()`](#send) or + * [`create()`](#create), the prompt appears in `items` right away as a pending item, and Base44's + * copy of the message replaces it once it arrives. A failed send removes it. A prompt sent while the + * AI is working waits in Base44's queue, so it stays pending until it runs. */ pending: boolean; } @@ -121,20 +122,24 @@ export interface ChatStep { tool: string; /** The file paths the tool call touched. */ files: string[]; - /** A line ready to show. It's Base44's description of the step when there is one, then the entity it touched, then the tool name and its files. */ + /** + * A line to show for the step. It's Base44's description of the step when there is one. Otherwise + * it's the tool name followed by the entity the step touched and its record count, such as + * `create_records Task (3)`, or by the step's files. + */ label: string; /** - * Where the tool call is. A `waiting` step waits on the user. A `stopped` step was declined, or the - * turn was stopped. + * The step's progress. A `waiting` step needs the user to answer its question. A `stopped` step was + * declined, or its turn was stopped. A status this release doesn't know counts as `done`. */ status: "running" | "waiting" | "done" | "stopped" | "error"; /** - * The question this step waits on, with the actions that answer it. Present only while the step is - * `waiting` in the last assistant message, which is the one that makes the chat wait. It goes away - * once answered, and is absent for a question kind this release doesn't know. + * The question the step needs answered, with the actions that answer it. It's present only on a + * `waiting` step in the latest assistant reply, and goes away once answered. A `waiting` step in an + * older message has no question. It's also absent for a question kind this release doesn't know. */ question?: ChatQuestion; - /** What the user answered on a choice question, once answered. It stays after the question closes, so the chat's history shows it. */ + /** The user's answers to a choice question, one for each question the step asked. It stays after the question closes, so you can show the answers in the chat's history. */ answered?: ChatAnswer[]; } From 32b864a675b8c4fb18bc6388d72d11919cc333b5 Mon Sep 17 00:00:00 2001 From: "Sam (automated drift fix)" Date: Thu, 22 Oct 2026 11:24:11 +0300 Subject: [PATCH 08/25] docs(pipeline): show a string union's values in the fields that use it A field typed with a union of string literals, such as useChat's status or a purchase's status, only named the type, and its values sat in a separate section, or nowhere when the type wasn't exposed. Post-processing now reads each such union from TypeDoc's pages, adds its comment (after the summary paragraph) to every field that uses it, and drops the union's own section. A comment that doesn't name every value gets a line listing them. Unions that document each value as a field, like the webhook error codes, are left alone. Co-Authored-By: Claude Opus 5.5 --- packages/platform/src/react/chat.types.ts | 4 +- .../file-processing/file-processing.js | 167 +++++++++++++++--- packages/sdk/src/modules/connectors.types.ts | 2 +- .../sdk/tests/unit/typedoc-mintlify.test.ts | 60 +++++++ 4 files changed, 209 insertions(+), 24 deletions(-) diff --git a/packages/platform/src/react/chat.types.ts b/packages/platform/src/react/chat.types.ts index b8678c2..d97d818 100644 --- a/packages/platform/src/react/chat.types.ts +++ b/packages/platform/src/react/chat.types.ts @@ -275,7 +275,9 @@ export interface ApprovalDetails { } /** - * What the AI is doing. When several hold at once, the first in this list wins. + * What the AI is doing. + * + * When several hold at once, the first in this list wins. * * - `error`: Base44 reports that the last build failed. * - `waiting`: a question waits on the user. diff --git a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js index f23e079..0e4e962 100755 --- a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js +++ b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js @@ -687,7 +687,7 @@ function prepareAppendedSection(appendPath) { return { section, headings: headingList }; } -function applyAppendedArticles(appendedArticles) { +function applyAppendedArticles(appendedArticles, inlinedTypeNames = new Set()) { const hosts = Object.keys(appendedArticles); if (hosts.length === 0) { return; @@ -728,6 +728,8 @@ function applyAppendedArticles(appendedArticles) { let effectiveAppendKey = appendKey; const appendParts = appendKey.split("/"); const appendName = appendParts[appendParts.length - 1]; + // Its values are in the fields that use it, so it gets no section of its own. + if (inlinedTypeNames.has(appendName)) continue; mergedTypeNames.push(appendName); const appendModuleRename = getModuleRename(appendName); if (appendModuleRename) { @@ -789,28 +791,33 @@ function applyAppendedArticles(appendedArticles) { hostContent = hostContent.trimEnd() + combinedSections + "\n"; hostContent = updatePanelWithHeadings(hostContent, collectedHeadings); - // Types merged onto this page no longer have their own file, so any link - // that still points at one — a `{@link Type.member}` cross-reference - // TypeDoc resolved to `.../Type.mdx#member`, or a plain `{@link Type}` - // resolved to `.../Type` with no fragment at all — now 404s. Rewrite - // every such link to a same-page anchor: `#member` when TypeDoc kept a - // fragment, otherwise `#` (Mintlify's own heading - // slug for a single-word PascalCase heading has no inserted hyphens). - for (const typeName of mergedTypeNames) { - const linkRegex = new RegExp( - `\\]\\((?:\\.\\./(?:interfaces|type-aliases|classes|functions)/)?${typeName}(?:\\.mdx|\\.md)?(#[\\w-]*)?\\)`, - "g" - ); - hostContent = hostContent.replace( - linkRegex, - (_match, anchor) => `](${anchor || `#${typeName.toLowerCase()}`})` - ); - } - + hostContent = linkMergedTypesInPage(hostContent, mergedTypeNames); fs.writeFileSync(hostPath, hostContent, "utf-8"); } } +/** + * Types merged onto a page no longer have their own file, so any link that still points at one — + * a `{@link Type.member}` cross-reference TypeDoc resolved to `.../Type.mdx#member`, or a plain + * `{@link Type}` resolved to `.../Type` with no fragment at all — now 404s. Rewrites every such + * link to a same-page anchor: `#member` when TypeDoc kept a fragment, otherwise + * `#` (Mintlify's own heading slug for a single-word PascalCase heading has + * no inserted hyphens). + */ +function linkMergedTypesInPage(content, typeNames) { + for (const typeName of typeNames) { + const linkRegex = new RegExp( + `\\]\\((?:\\.\\./(?:interfaces|type-aliases|classes|functions)/)?${typeName}(?:\\.mdx|\\.md)?(#[\\w-]*)?\\)`, + "g" + ); + content = content.replace( + linkRegex, + (_match, anchor) => `](${anchor || `#${typeName.toLowerCase()}`})` + ); + } + return content; +} + /** * Clean up method signatures and type parameter sections: * 1. Replace truncated generics (e.g., Pick<..., ...> → Pick) @@ -2242,6 +2249,112 @@ function cleanupTruncatedParamTypes(dir) { } } +/** + * A type alias page for a union of string literals, such as `type Status = "ready" | "error"`, as + * its values and its comment minus the first paragraph. The first paragraph sums up the type, and a + * field that uses the type has its own summary. Anything else, including a union whose comment + * documents each value as a field and so has no signature line, is null. + */ +function parseStringUnion(content, typeName) { + const signature = content.match(new RegExp(`^> \\*\\*${typeName}\\*\\* = (.+)$`, "m")); + if (!signature) return null; + const values = signature[1].split(" | ").map((part) => part.trim().match(/^`"([^"`]*)"`$/)?.[1]); + if (values.some((value) => value === undefined)) return null; + const comment = content.slice(signature.index + signature[0].length).trim(); + return { values, details: comment.split(/\n\s*\n/).slice(1).join("\n\n").trim() }; +} + +/** + * A field whose type is the union, with the union's comment after the field's own description. The + * type label keeps the union's name, for the import. A comment that doesn't name every value in + * backticks gets a line that lists them. + */ +function inlineStringUnions(content, unions) { + let modified = false; + for (const [typeName, { values, details }] of unions) { + const field = new RegExp( + `(<(ResponseField|ParamField) [^>]*type=")(?:${typeName}|\\[${typeName}\\]\\([^)]*\\))("[^>]*>)([\\s\\S]*?)\\n*()`, + "g" + ); + content = content.replace(field, (_match, open, _tag, attributes, description, close) => { + modified = true; + const namesEvery = values.every((value) => new RegExp(`\`'?${value}'?\``).test(details)); + const quoted = values.map((value) => `\`'${value}'\``); + const valueLine = namesEvery + ? "" + : `It's ${quoted.length > 2 ? `${quoted.slice(0, -1).join(", ")}, or ${quoted.at(-1)}` : quoted.join(" or ")}.`; + const added = [details, valueLine].filter(Boolean).join("\n\n"); + return `${open}${typeName}${attributes}\n\n${[description.trim(), added].filter(Boolean).join("\n\n")}\n\n${close}`; + }); + } + return { content, modified }; +} + +/** + * The string unions that some field uses, read from TypeDoc's pages before anything is removed. + * Their pages go, so their values show only in the fields, and the returned map lets the fields + * take them in once every field on a page has its final form. + */ +function collectInlinedStringUnions(contentDir) { + const aliasDir = path.join(contentDir, "type-aliases"); + if (!fs.existsSync(aliasDir)) return new Map(); + const unions = new Map(); + for (const file of fs.readdirSync(aliasDir)) { + if (!file.endsWith(".mdx")) continue; + const typeName = path.basename(file, ".mdx"); + const union = parseStringUnion(fs.readFileSync(path.join(aliasDir, file), "utf-8"), typeName); + if (union) unions.set(typeName, union); + } + + const used = new Set(); + const scan = (dir) => { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const entryPath = path.join(dir, entry.name); + if (entry.isDirectory()) scan(entryPath); + else if (entry.name.endsWith(".mdx")) { + const content = fs.readFileSync(entryPath, "utf-8"); + for (const typeName of unions.keys()) { + // A field's type attribute, or a property's signature line before it becomes a field. + const usage = new RegExp( + `type="${typeName}"|^> \\*\\*[\\w$]+\\*\\*\\??: (?:\\[\`?${typeName}\`?\\]\\([^)]*\\)|\`${typeName}\`)\\s*$`, + "m" + ); + if (usage.test(content)) used.add(typeName); + } + } + } + }; + scan(contentDir); + + const inlined = new Map([...unions].filter(([typeName]) => used.has(typeName))); + for (const typeName of inlined.keys()) { + fs.unlinkSync(path.join(aliasDir, `${typeName}.mdx`)); + console.log(`Removed (inlined into its fields): content/type-aliases/${typeName}.mdx`); + } + return inlined; +} + +function applyStringUnionInlining(dir, unions, appendedArticles) { + if (unions.size === 0) return; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const entryPath = path.join(dir, entry.name); + if (entry.isDirectory()) applyStringUnionInlining(entryPath, unions, appendedArticles); + else if (entry.name.endsWith(".mdx")) { + const result = inlineStringUnions(fs.readFileSync(entryPath, "utf-8"), unions); + if (!result.modified) continue; + // The comments come from TypeDoc's type-alias pages, so their links still carry the file + // extension and point at types this page may have merged in. + const hostKey = path.relative(CONTENT_DIR, entryPath).replace(/\.mdx$/, ""); + const merged = Object.entries(appendedArticles) + .filter(([host]) => host === hostKey || host.replace(/[^/]+$/, (name) => getModuleRename(name) ?? name) === hostKey) + .flatMap(([, list]) => list.map((key) => key.split("/").pop())); + const content = linkMergedTypesInPage(result.content.replace(/\]\(([^)#]+?)\.mdx?(#[^)]*)?\)/g, "]($1$2)"), merged); + fs.writeFileSync(entryPath, content, "utf-8"); + console.log(`Inlined string unions: ${path.relative(DOCS_DIR, entryPath)}`); + } + } +} + /** * Main function */ @@ -2844,6 +2957,9 @@ function main() { const linkedTypeNames = getLinkedTypeNames(); const exposedTypeNames = getTypesToExpose(); + // Read the string unions that fields use before their pages are removed or appended + const inlinedUnions = collectInlinedStringUnions(CONTENT_DIR); + // First, perform module renames (EntitiesModule -> entities, etc.) performModuleRenames(DOCS_DIR); @@ -2852,7 +2968,7 @@ function main() { // Append configured articles const appendedArticles = loadAppendedArticlesConfig(); - applyAppendedArticles(appendedArticles); + applyAppendedArticles(appendedArticles, new Set(inlinedUnions.keys())); // Add clickable links for types in ParamFields addTypeLinksToParamFields(DOCS_DIR); @@ -2898,8 +3014,15 @@ function main() { // Link type names in Type Declarations sections to their corresponding headings applyTypeDeclarationLinking(DOCS_DIR); + // Put each string union's values in the fields that use it, now that every field has its final form + applyStringUnionInlining(CONTENT_DIR, inlinedUnions, appendedArticles); + // Remove links to types that aren't exposed (would 404) - applyNonExposedTypeLinkRemoval(DOCS_DIR, exposedTypeNames, loadTypesToDelete()); + applyNonExposedTypeLinkRemoval( + DOCS_DIR, + exposedTypeNames, + new Set([...loadTypesToDelete(), ...inlinedUnions.keys()]) + ); // Delete types that should not appear in navigation but were needed for inline rendering deleteTypesAfterProcessing(DOCS_DIR); @@ -2936,4 +3059,4 @@ if (process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(__fi main(); } -export { fixInPageAnchors, nestUnderGroupTypes, propertiesAsFields, removeNonExposedTypeLinks }; +export { fixInPageAnchors, inlineStringUnions, nestUnderGroupTypes, parseStringUnion, propertiesAsFields, removeNonExposedTypeLinks }; diff --git a/packages/sdk/src/modules/connectors.types.ts b/packages/sdk/src/modules/connectors.types.ts index efe0d90..9903745 100644 --- a/packages/sdk/src/modules/connectors.types.ts +++ b/packages/sdk/src/modules/connectors.types.ts @@ -94,7 +94,7 @@ export interface ConnectorApiRequest { export interface ConnectorApiResponse { /** `true` only when the upstream API returned a 2xx status. Proxy and upstream errors are `false`. */ success: boolean; - /** How far the call progressed. Only `not_sent` proves the provider did not execute it. */ + /** How far the call progressed. */ phase: ConnectorApiResponsePhase; /** The upstream HTTP status code, or `null` when no response was received. */ status: number | null; diff --git a/packages/sdk/tests/unit/typedoc-mintlify.test.ts b/packages/sdk/tests/unit/typedoc-mintlify.test.ts index d2ce65b..e2ca035 100644 --- a/packages/sdk/tests/unit/typedoc-mintlify.test.ts +++ b/packages/sdk/tests/unit/typedoc-mintlify.test.ts @@ -7,7 +7,9 @@ import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { nameCallableProperties } from "../../scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-content.js"; import { fixInPageAnchors, + inlineStringUnions, nestUnderGroupTypes, + parseStringUnion, propertiesAsFields, removeNonExposedTypeLinks, } from "../../scripts/mintlify-post-processing/file-processing/file-processing.js"; @@ -225,6 +227,64 @@ describe("propertiesAsFields", () => { }); }); +describe("parseStringUnion", () => { + test("reads the values and the comment after its summary paragraph", () => { + const page = [ + "***", + "", + '> **Status** = `"ready"` | `"error"`', + "", + "What the widget is doing.", + "", + "- `ready`: Nothing is wrong.", + "- `error`: The widget failed.", + ].join("\n"); + + expect(parseStringUnion(page, "Status")).toEqual({ + values: ["ready", "error"], + details: "- `ready`: Nothing is wrong.\n- `error`: The widget failed.", + }); + }); + + test("skips other aliases, and unions that document each value as a field", () => { + expect(parseStringUnion('> **Size** = `"s"` | `number`', "Size")).toBeNull(); + expect(parseStringUnion('Codes.\n\n', "Code")).toBeNull(); + }); +}); + +describe("inlineStringUnions", () => { + test("adds the union's comment to each field that uses it, keeping the type's name", () => { + const unions = new Map([["Status", { values: ["ready", "error"], details: "- `ready`: Fine.\n- `error`: Failed." }]]); + const content = [ + '', + "", + "What the widget is doing.", + "", + "", + "", + '', + "", + "", + ].join("\n"); + + const { content: inlined, modified } = inlineStringUnions(content, unions); + expect(modified).toBe(true); + expect(inlined).toContain( + '\n\nWhat the widget is doing.\n\n- `ready`: Fine.\n- `error`: Failed.\n\n' + ); + expect(inlined).toContain('\n\n'); + }); + + test("lists the values when the comment doesn't name them all", () => { + const unions = new Map([["Unit", { values: ["day", "week", "month"], details: "" }]]); + const content = '\n\nBilling unit.\n\n'; + + expect(inlineStringUnions(content, unions).content).toBe( + "\n\nBilling unit.\n\nIt's `'day'`, `'week'`, or `'month'`.\n\n" + ); + }); +}); + describe("fixInPageAnchors", () => { test("keeps links to headings and explicit anchors", () => { const content = ["## Send", "", "See [send](#send) and [records](#list-records)."].join("\n"); From 098217bc1645ccd8f1ec9274718f3034ebac0e14 Mon Sep 17 00:00:00 2001 From: "Sam (automated drift fix)" Date: Thu, 22 Oct 2026 11:26:57 +0300 Subject: [PATCH 09/25] docs(platform): useChat lists its types once, in the fields that use them ChatItem, ChatStep, ChatAnswer, ChatError, ChatApp, and ToolCallAnswer were already nested in full under the fields that use them, so their Type Definitions sections only repeated them. The page keeps one types section, Questions, which the step's question field now links to. Co-Authored-By: Claude Opus 5.5 --- .../mintlify-post-processing/appended-articles.json | 6 ------ .../mintlify-post-processing/page-sections.json | 10 ---------- .../types-to-delete-after-processing.json | 8 +++++++- packages/platform/src/react/chat.types.ts | 2 +- packages/platform/src/react/useChat.ts | 2 +- 5 files changed, 9 insertions(+), 19 deletions(-) diff --git a/packages/platform/scripts/mintlify-post-processing/appended-articles.json b/packages/platform/scripts/mintlify-post-processing/appended-articles.json index 009935a..4d97018 100644 --- a/packages/platform/scripts/mintlify-post-processing/appended-articles.json +++ b/packages/platform/scripts/mintlify-post-processing/appended-articles.json @@ -19,14 +19,8 @@ "functions/useChat": [ "interfaces/UseChatResult", "interfaces/ChatBackend", - "interfaces/ChatItem", - "interfaces/ChatStep", - "interfaces/ChatAnswer", "type-aliases/ChatStatus", "type-aliases/ChatConnection", - "interfaces/ChatError", - "interfaces/ChatApp", - "interfaces/ToolCallAnswer", "type-aliases/ChatQuestion", "interfaces/ChoiceQuestion", "interfaces/ChoiceEntry", diff --git a/packages/platform/scripts/mintlify-post-processing/page-sections.json b/packages/platform/scripts/mintlify-post-processing/page-sections.json index c249fd1..707425a 100644 --- a/packages/platform/scripts/mintlify-post-processing/page-sections.json +++ b/packages/platform/scripts/mintlify-post-processing/page-sections.json @@ -39,16 +39,6 @@ "WebhookConfigurationErrorCode": "Configuration error codes" } }, - [ - "ChatItem", - "ChatStep", - "ChatAnswer", - "ChatStatus", - "ChatConnection", - "ChatError", - "ChatApp", - "ToolCallAnswer" - ], { "title": "Questions", "types": [ diff --git a/packages/platform/scripts/mintlify-post-processing/types-to-delete-after-processing.json b/packages/platform/scripts/mintlify-post-processing/types-to-delete-after-processing.json index 4df56a7..05744a6 100644 --- a/packages/platform/scripts/mintlify-post-processing/types-to-delete-after-processing.json +++ b/packages/platform/scripts/mintlify-post-processing/types-to-delete-after-processing.json @@ -1,3 +1,9 @@ [ - "ChatOptions" + "ChatOptions", + "ChatItem", + "ChatStep", + "ChatAnswer", + "ChatError", + "ChatApp", + "ToolCallAnswer" ] diff --git a/packages/platform/src/react/chat.types.ts b/packages/platform/src/react/chat.types.ts index d97d818..6334441 100644 --- a/packages/platform/src/react/chat.types.ts +++ b/packages/platform/src/react/chat.types.ts @@ -134,7 +134,7 @@ export interface ChatStep { */ status: "running" | "waiting" | "done" | "stopped" | "error"; /** - * The question the step needs answered, with the actions that answer it. It's present only on a + * The {@link ChatQuestion | question} the step needs answered, with the actions that answer it. It's present only on a * `waiting` step in the latest assistant reply, and goes away once answered. A `waiting` step in an * older message has no question. It's also absent for a question kind this release doesn't know. */ diff --git a/packages/platform/src/react/useChat.ts b/packages/platform/src/react/useChat.ts index 948baf2..d45acba 100644 --- a/packages/platform/src/react/useChat.ts +++ b/packages/platform/src/react/useChat.ts @@ -16,7 +16,7 @@ const NEW_APP = "new-app"; * * The hook is headless. It gives you the conversation and the actions, and your components render them: * - Follows the app live, and renews its socket session when it expires. - * - Turns Base44's messages into {@link ChatItem | items}, and shows each open question with the step that asked it. + * - Turns Base44's messages into [items](#items), and shows each open question with the step that asked it. * - Reports what the AI is doing in {@linkcode UseChatResult.status}, and the live connection in {@linkcode UseChatResult.connection}. * - Sends prompts, creates apps, and answers questions through the {@link ChatBackend | backend} you pass, so no credential reaches the browser. * From e28e3d130cf179a61f585c1d45ce6b2d5b7b29cf Mon Sep 17 00:00:00 2001 From: "Sam (automated drift fix)" Date: Thu, 22 Oct 2026 11:36:48 +0300 Subject: [PATCH 10/25] docs(platform): useChat error field wording Co-Authored-By: Claude Opus 5.5 --- packages/platform/src/react/chat.types.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/platform/src/react/chat.types.ts b/packages/platform/src/react/chat.types.ts index 6334441..aaa5636 100644 --- a/packages/platform/src/react/chat.types.ts +++ b/packages/platform/src/react/chat.types.ts @@ -301,14 +301,16 @@ export type ChatConnection = "connecting" | "connected" | "disconnected"; /** A failure of a call, of the live connection, or of the build. */ export interface ChatError { - /** What went wrong, for people. */ + /** A readable description of what went wrong, such as `The build failed`. */ message: string; /** - * What went wrong, for code. A connection error carries a `PlatformSocketErrorCode`, and a failed - * build carries `build_failed`. A failed call to your backend carries none. + * A value your code can check to handle the error, such as `build_failed` for a failed build. A + * connection error has one of the session or app error codes listed under the chat module's + * [Errors](/developers/references/platform-sdk/docs/interfaces/chat#errors). A failed call to + * your backend has no code. */ code?: string; - /** The original error, for logging. */ + /** The error the failure came from, such as the one your backend threw. Log it to debug the failure. It's absent when the hook found the failure itself, such as a failed build or a send with no app. */ cause?: unknown; } @@ -344,9 +346,11 @@ export interface UseChatResult { /** Whether the chat follows the app live. */ connection: ChatConnection; /** - * The last failure. It's set whenever `status` is `error`, and on any failed call or connection - * error. A failed call's error clears on the next action. A connection error stays until - * {@link UseChatResult.clearError}. A failed build stays while Base44 reports it. + * The failure to show, or `null`. It's set when a call fails, when the live connection fails, and + * whenever `status` is `error`. When more than one of these conditions is true, `error` holds the + * failed call first, then the connection error, then the failed build. A failed call's error + * clears on the next action, and a connection error stays until [`clearError()`](#clearerror). A + * failed build's error stays while Base44 reports it. Switching to another app clears it. */ error: ChatError | null; /** From 2ecbe501e0902d14e07023a9c0685fcafac499de Mon Sep 17 00:00:00 2001 From: "Sam (automated drift fix)" Date: Thu, 22 Oct 2026 11:40:27 +0300 Subject: [PATCH 11/25] docs(platform): useChat example renders markdown, with its imports Co-Authored-By: Claude Opus 5.5 --- packages/platform/src/react/useChat.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/platform/src/react/useChat.ts b/packages/platform/src/react/useChat.ts index d45acba..61f6dd6 100644 --- a/packages/platform/src/react/useChat.ts +++ b/packages/platform/src/react/useChat.ts @@ -26,14 +26,20 @@ const NEW_APP = "new-app"; * @returns The conversation, its `status`, `connection`, and `error`, and the [actions](#actions) `send()`, `create()`, and `clearError()`. * * @example - * ```typescript - * // Draw the chat and answer an approval + * ```tsx + * // Render the chat and answer an approval + * import { useChat } from "@base44/platform/react"; + * import Markdown from "react-markdown"; + * + * // ... + * + * // Inside your component * const chat = useChat({ appId, backend, onAppCreated: selectApp }); * return ( * <> * {chat.items.map((item) => ( *
- * {item.text &&

{item.text}

} + * {item.text && {item.text}} * {item.steps.map((step) => * step.question?.kind === "approval" ? ( * From e82a5a59c15acc4c156d5598f1688607bb5a0bb1 Mon Sep 17 00:00:00 2001 From: "Sam (automated drift fix)" Date: Thu, 22 Oct 2026 12:14:25 +0300 Subject: [PATCH 12/25] docs(pipeline): show useChat's returned object as Returned state and Returned actions useChat returns one object of state to render and functions to call. Its page split them unevenly, with the state in a collapsed Returns accordion and the functions in an Actions section opened by the wrong intro, and none of the state in the page's table of contents. page-sections.json `returnedState` now names a section for a returned type's properties. The function's Returns gives way to that section, with one heading per property that keeps what Returns showed for it, and the type's methods follow in their own section. Each section opens with its @groupDescription. The page keeps Parameters at the top level, and the Returns example moves to the Overview. Co-Authored-By: Claude Opus 5.5 --- .../page-sections.json | 5 +- packages/platform/src/react/chat.types.ts | 12 +- .../file-processing/file-processing.js | 110 +++++++++++++++++- .../sdk/tests/unit/typedoc-mintlify.test.ts | 28 +++++ 4 files changed, 147 insertions(+), 8 deletions(-) diff --git a/packages/platform/scripts/mintlify-post-processing/page-sections.json b/packages/platform/scripts/mintlify-post-processing/page-sections.json index 707425a..a7a2764 100644 --- a/packages/platform/scripts/mintlify-post-processing/page-sections.json +++ b/packages/platform/scripts/mintlify-post-processing/page-sections.json @@ -1,9 +1,12 @@ { "typesWithOwnMethods": { "ChatSession": "Session methods", - "UseChatResult": "Actions", + "UseChatResult": "Returned actions", "ChatBackend": "Backend calls" }, + "returnedState": { + "UseChatResult": "Returned state" + }, "typeDefinitionGroups": [ [ "SocketSession", diff --git a/packages/platform/src/react/chat.types.ts b/packages/platform/src/react/chat.types.ts index aaa5636..31b6dc4 100644 --- a/packages/platform/src/react/chat.types.ts +++ b/packages/platform/src/react/chat.types.ts @@ -337,7 +337,17 @@ export interface ChatOptions { onAppCreated?: (app: ChatApp) => void; } -/** The chat state and actions that `useChat()` returns. */ +/** + * The chat state and actions that `useChat()` returns. + * + * @groupDescription Properties + * The hook returns one object, of type `UseChatResult`. Its state is below, and its functions are + * under [Returned actions](#returned-actions). + * + * @groupDescription Methods + * Functions on the same object, which you call to act on the chat, as in `chat.send(prompt)`. They + * never throw. A failure sets [`error`](#error). + */ export interface UseChatResult { /** The conversation, oldest first. Each open question appears with the step that asked it. */ items: ChatItem[]; diff --git a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js index 0e4e962..9d326d9 100755 --- a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js +++ b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js @@ -1456,8 +1456,13 @@ function applyTypeDeclarationLinking(dir) { // Appended types whose methods get their own section, by section title, and groups of appended // types nested under "## Type Definitions" (the first type of a group starts the section). A group // is a list of type names, or `{ "title": "...", "types": [...] }` for another section heading. -const { typesWithOwnMethods: TYPES_WITH_OWN_METHODS = {}, typeDefinitionGroups: TYPE_DEFINITION_GROUPS = [] } = - fs.existsSync(PAGE_SECTIONS_PATH) ? JSON.parse(fs.readFileSync(PAGE_SECTIONS_PATH, "utf-8")) : {}; +// A function's return type in `returnedState` also gets its properties a section, by title, in +// place of the function's Returns: one heading per property, as its methods have. +const { + typesWithOwnMethods: TYPES_WITH_OWN_METHODS = {}, + returnedState: RETURNED_STATE = {}, + typeDefinitionGroups: TYPE_DEFINITION_GROUPS = [], +} = fs.existsSync(PAGE_SECTIONS_PATH) ? JSON.parse(fs.readFileSync(PAGE_SECTIONS_PATH, "utf-8")) : {}; /** * Group intro sections (like "Built-in User Entity", "Generated Types") under an "Overview" heading @@ -1517,11 +1522,14 @@ function groupIntroSections(content) { return { content, modified: false }; } - // Find all ## headings between insert point and main section (these are intro sections) + // Find all ## headings between insert point and main section (these are intro sections). A + // function page whose return has its own sections keeps its Parameters and Returns beside them. + const hasReturnedState = Object.keys(RETURNED_STATE).some((type) => lines.includes(`## ${type}`)); + const keptHeadings = hasReturnedState ? ["## Parameters", "## Returns"] : []; const introHeadingIndices = []; for (let i = insertIndex; i < mainSectionIndex; i++) { const line = lines[i].trim(); - if (line.startsWith("## ") && !line.startsWith("### ")) { + if (line.startsWith("## ") && !line.startsWith("### ") && !keptHeadings.includes(line)) { introHeadingIndices.push(i); } } @@ -1892,7 +1900,17 @@ function mergeSectionWithMethods(content, filePath) { } } - if (methodsIndex !== -1) { + if (methodsIndex !== -1 && Object.hasOwn(RETURNED_STATE, typeName) && propertiesIndex !== -1) { + // A returned type's two groups become two sections, each opened by its + // @groupDescription: the properties' text, whose fields returnedStateSections() + // fills in from the function's Returns, and the methods. The type's own comment + // stays on the type. + const firstProperty = lines.findIndex((l, j) => j > propertiesIndex && j < methodsIndex && /^#{3,6} /.test(l)); + const propertiesIntro = lines.slice(propertiesIndex + 1, firstProperty === -1 ? methodsIndex : firstProperty); + lines.splice(i, methodsIndex - i + 1, + `## ${RETURNED_STATE[typeName]}`, ...propertiesIntro, `## ${TYPES_WITH_OWN_METHODS[typeName] ?? typeName}`); + modified = true; + } else if (methodsIndex !== -1) { lines[i] = `## ${TYPES_WITH_OWN_METHODS[typeName] ?? typeName}`; // Drop the ## Properties section too: the heading and its content sit // between the type heading and ## Methods. @@ -2718,6 +2736,74 @@ function propertiesAsFields(content, classPage) { .replace(/^#### Properties\n([\s\S]*?)(?=^#{1,3} |^#{4,6} \S+\(\)$|(?![\s\S]))/gm, (section, body) => `${fieldsOf(body)}\n\n`); } +/** The outermost ResponseFields in a piece of a page, as name, type, and the markup inside each. */ +function topLevelResponseFields(text) { + const fields = []; + let depth = 0; + let start = 0; + let attributes = ""; + for (const match of text.matchAll(/]*)>|<\/ResponseField>/g)) { + if (match[1] !== undefined) { + if (depth === 0) { + start = match.index + match[0].length; + attributes = match[1]; + } + depth++; + } else if (--depth === 0) { + fields.push({ + name: attributes.match(/name="([^"]*)"/)?.[1] ?? "", + type: attributes.match(/type="([^"]*)"/)?.[1] ?? "", + inner: text.slice(start, match.index).trim(), + }); + } + } + return fields; +} + +/** + * A function whose return type is in page-sections.json `returnedState` returns an object of state + * and actions, and its page shows them as two sections instead of a Returns section: the + * properties under their own heading each, and the methods, which `typesWithOwnMethods` already + * gives a section. Each property keeps what Returns showed for it, its type and description and + * any fields of its own, and the Returns example moves to the end of the Overview. + */ +function returnedStateSections(content) { + for (const [typeName, title] of Object.entries(RETURNED_STATE)) { + if (!content.includes(`\n## ${title}\n`)) continue; + const returns = content.match(new RegExp(`^## Returns\\n\\n\`${typeName}\`\\n[\\s\\S]*?(?=^## )`, "m")); + if (!returns) continue; + const exampleAt = returns[0].search(/^#{3,4} Examples?$/m); + const example = exampleAt === -1 ? "" : returns[0].slice(exampleAt).trim().replace(/^#{3,4} /, "### "); + const unescaped = (type) => type.replace(/"/g, '"').replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&"); + const properties = topLevelResponseFields(exampleAt === -1 ? returns[0] : returns[0].slice(0, exampleAt)) + .map(({ name, type, inner }) => `### ${name}\n\n\`${unescaped(type)}\`\n\n${inner}`) + .join("\n\n***\n\n"); + + content = content.replace(returns[0], ""); + content = content.replace( + new RegExp(`^## ${title}\\n[\\s\\S]*?(?=^## )`, "m"), + (section) => `${section.trimEnd()}\n\n${properties}\n\n` + ); + if (example) { + content = content.replace(/^## Overview\n[\s\S]*?(?=^## )/m, (overview) => `${overview.trimEnd()}\n\n${example}\n\n`); + } + } + return content; +} + +function applyReturnedStateSections(dir) { + if (Object.keys(RETURNED_STATE).length === 0) return; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const entryPath = path.join(dir, entry.name); + if (entry.isDirectory()) applyReturnedStateSections(entryPath); + else if (entry.name.endsWith(".mdx")) { + const content = fs.readFileSync(entryPath, "utf-8"); + const converted = returnedStateSections(content); + if (converted !== content) fs.writeFileSync(entryPath, converted, "utf-8"); + } + } +} + function applyPropertiesAsFields(dir) { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const entryPath = path.join(dir, entry.name); @@ -3017,6 +3103,10 @@ function main() { // Put each string union's values in the fields that use it, now that every field has its final form applyStringUnionInlining(CONTENT_DIR, inlinedUnions, appendedArticles); + // A returned object of state and actions: its properties take the place of the function's + // Returns. After the inlining, so each property keeps the values its field was given. + applyReturnedStateSections(DOCS_DIR); + // Remove links to types that aren't exposed (would 404) applyNonExposedTypeLinkRemoval( DOCS_DIR, @@ -3059,4 +3149,12 @@ if (process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(__fi main(); } -export { fixInPageAnchors, inlineStringUnions, nestUnderGroupTypes, parseStringUnion, propertiesAsFields, removeNonExposedTypeLinks }; +export { + fixInPageAnchors, + inlineStringUnions, + nestUnderGroupTypes, + parseStringUnion, + propertiesAsFields, + removeNonExposedTypeLinks, + topLevelResponseFields, +}; diff --git a/packages/sdk/tests/unit/typedoc-mintlify.test.ts b/packages/sdk/tests/unit/typedoc-mintlify.test.ts index e2ca035..d6ec7a3 100644 --- a/packages/sdk/tests/unit/typedoc-mintlify.test.ts +++ b/packages/sdk/tests/unit/typedoc-mintlify.test.ts @@ -12,6 +12,7 @@ import { parseStringUnion, propertiesAsFields, removeNonExposedTypeLinks, + topLevelResponseFields, } from "../../scripts/mintlify-post-processing/file-processing/file-processing.js"; // Runs TypeDoc with the Mintlify plugin on a small fixture, so each case is checked on the pages @@ -285,6 +286,33 @@ describe("inlineStringUnions", () => { }); }); +describe("topLevelResponseFields", () => { + test("returns each outermost field with its nested fields inside", () => { + const content = [ + '', + '', + "The items.", + '', + '', + "ID.", + "", + "", + "", + '', + "Status.", + "", + "", + ].join("\n"); + + const fields = topLevelResponseFields(content); + expect(fields.map(({ name, type }) => [name, type])).toEqual([ + ["items", "Item[]"], + ["status", ""ready" | "error""], + ]); + expect(fields[0].inner).toContain(''); + }); +}); + describe("fixInPageAnchors", () => { test("keeps links to headings and explicit anchors", () => { const content = ["## Send", "
", "See [send](#send) and [records](#list-records)."].join("\n"); From fd04539d6ee7bc1a6b781ba6e0ed5f7f530d094c Mon Sep 17 00:00:00 2001 From: "Sam (automated drift fix)" Date: Thu, 22 Oct 2026 12:20:31 +0300 Subject: [PATCH 13/25] docs(platform): useChat overview links and no repeated never-throw note Co-Authored-By: Claude Opus 5.5 --- packages/platform/src/react/useChat.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/platform/src/react/useChat.ts b/packages/platform/src/react/useChat.ts index 61f6dd6..08a4644 100644 --- a/packages/platform/src/react/useChat.ts +++ b/packages/platform/src/react/useChat.ts @@ -14,14 +14,12 @@ const NEW_APP = "new-app"; * Runs an app's AI chat in your own React UI. Requires a browser environment and can't be used in * the backend. * - * The hook is headless. It gives you the conversation and the actions, and your components render them: + * The hook is headless. It gives you the conversation and the [actions](#returned-actions), and your components render them: * - Follows the app live, and renews its socket session when it expires. * - Turns Base44's messages into [items](#items), and shows each open question with the step that asked it. - * - Reports what the AI is doing in {@linkcode UseChatResult.status}, and the live connection in {@linkcode UseChatResult.connection}. + * - Reports what the AI is doing in [`status`](#status), and the live connection in [`connection`](#connection). * - Sends prompts, creates apps, and answers questions through the {@link ChatBackend | backend} you pass, so no credential reaches the browser. * - * The actions never throw. A failure sets {@linkcode UseChatResult.error}. - * * @param options - The app to follow, your backend's calls, and what to do with a created app. * @returns The conversation, its `status`, `connection`, and `error`, and the [actions](#actions) `send()`, `create()`, and `clearError()`. * From e72a2da161a72c72b9410121994639381533b37b Mon Sep 17 00:00:00 2001 From: "Sam (automated drift fix)" Date: Thu, 22 Oct 2026 12:24:12 +0300 Subject: [PATCH 14/25] docs(platform): useChat status and connection value wording Co-Authored-By: Claude Opus 5.5 --- packages/platform/src/react/chat.types.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/platform/src/react/chat.types.ts b/packages/platform/src/react/chat.types.ts index 31b6dc4..9e469f1 100644 --- a/packages/platform/src/react/chat.types.ts +++ b/packages/platform/src/react/chat.types.ts @@ -280,20 +280,20 @@ export interface ApprovalDetails { * When several hold at once, the first in this list wins. * * - `error`: Base44 reports that the last build failed. - * - `waiting`: a question waits on the user. - * - `building`: the AI is working. - * - `submitted`: a prompt, a new app or an answer is on its way. It lasts until Base44 reports work, a reply or a queue change, or the call fails. - * - `ready`: none of the above. + * - `waiting`: A question waits for the user's answer. + * - `building`: The AI is working. + * - `submitted`: A prompt, a new app, or an answer is on its way. It lasts until Base44 reports work, a reply, or a queue change, or until the call fails. + * - `ready`: None of the above. */ export type ChatStatus = "ready" | "submitted" | "building" | "waiting" | "error"; /** * Whether the chat follows the app live. * - * - `connecting`: until the first snapshot arrives. - * - `connected`: after the first snapshot. - * - `disconnected`: live updates stopped for good. The session couldn't open, the retries ran out, - * or Base44 ended the session or the app's subscription. {@link UseChatResult.error} says why. + * - `connecting`: The chat waits for the app's first snapshot. + * - `connected`: The first snapshot arrived, and updates follow live. + * - `disconnected`: Live updates stopped for good. The session couldn't open, the retries ran out, + * or Base44 ended the session or the app's subscription. Check [`error`](#error) to see why. * * Watching another app, or mounting the chat again, starts over. */ From fdd03d3cb07b00e0ceb71060674047e4f7e34b3b Mon Sep 17 00:00:00 2001 From: "Sam (automated drift fix)" Date: Thu, 22 Oct 2026 12:27:44 +0300 Subject: [PATCH 15/25] docs(platform): useChat send() and create() wording Co-Authored-By: Claude Opus 5.5 --- packages/platform/src/react/chat.types.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/platform/src/react/chat.types.ts b/packages/platform/src/react/chat.types.ts index 9e469f1..64250ff 100644 --- a/packages/platform/src/react/chat.types.ts +++ b/packages/platform/src/react/chat.types.ts @@ -364,19 +364,22 @@ export interface UseChatResult { */ error: ChatError | null; /** - * Sends a prompt to the current app. Base44 queues it while the AI is working. The prompt shows - * at once as a pending item, and the reply arrives through `items`. Without an app, it sets - * `error`. Use {@link UseChatResult.create} for the first prompt. + * Sends a prompt to the app the chat follows. The prompt appears in [`items`](#items) right away + * as a pending item, and the reply arrives there too. Base44 queues the prompt while the AI is + * working. If the chat doesn't follow an app yet, it sets [`error`](#error) instead. Use + * [`create()`](#create) for the first prompt. * * @param prompt - The prompt to send. - * @returns Promise resolving once Base44 accepts the prompt. It never rejects. + * @returns Promise resolving once Base44 accepts the prompt, or once the send fails and sets [`error`](#error). It never rejects. */ send(prompt: string): Promise; /** - * Creates an app from its first prompt and reports it through `onAppCreated`. + * Creates an app from its first prompt. The prompt appears in [`items`](#items) right away as a + * pending item. Once the app exists, the hook passes it to [`onAppCreated`](#onappcreated). If + * `appId` is `null`, the chat then follows the new app. Otherwise it stays on the app it follows. * * @param prompt - The first prompt, which describes the app. - * @returns Promise resolving once the app exists. It never rejects. + * @returns Promise resolving once the app exists, or once creating it fails and sets [`error`](#error). It never rejects. */ create(prompt: string): Promise; /** Clears {@link UseChatResult.error}. A failed build's error stays while Base44 reports it. */ From 13e03f6b8309459249b44cc1581c5e280f99eaa0 Mon Sep 17 00:00:00 2001 From: "Sam (automated drift fix)" Date: Thu, 22 Oct 2026 12:34:23 +0300 Subject: [PATCH 16/25] docs(platform): useChat clearError() and backend wording Co-Authored-By: Claude Opus 5.5 --- packages/platform/src/react/chat.types.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/platform/src/react/chat.types.ts b/packages/platform/src/react/chat.types.ts index 64250ff..bbd5332 100644 --- a/packages/platform/src/react/chat.types.ts +++ b/packages/platform/src/react/chat.types.ts @@ -21,7 +21,8 @@ export interface ToolCallAnswer { } /** - * The four calls your backend makes to Base44 for the chat. + * The functions you write for the `backend` option. The hook calls them when it needs Base44, and + * each one usually forwards to a route on your own server, which adds your credentials. * * Each call wraps one Base44 API request with your credentials, so the browser never holds a * workspace API key or an access token. Your backend can be written in any language. @@ -324,8 +325,9 @@ export interface ChatOptions { */ appId: string | null; /** - * The four calls your backend makes to Base44. The hook uses the `backend` from the latest render, - * so you can pass an inline object without memoizing it. + * The functions the hook calls to reach Base44 through your backend, described in + * [Backend calls](#backend-calls). The hook uses the `backend` from the latest render, so you can + * pass an inline object without memoizing it. */ backend: ChatBackend; /** @@ -382,6 +384,6 @@ export interface UseChatResult { * @returns Promise resolving once the app exists, or once creating it fails and sets [`error`](#error). It never rejects. */ create(prompt: string): Promise; - /** Clears {@link UseChatResult.error}. A failed build's error stays while Base44 reports it. */ + /** Clears a failed call's or the connection's [`error`](#error). A failed build's error stays while Base44 reports it. */ clearError(): void; } From 703448936108847fdd32022a3bb1ad49a5ddb3dd Mon Sep 17 00:00:00 2001 From: "Sam (automated drift fix)" Date: Thu, 22 Oct 2026 12:35:42 +0300 Subject: [PATCH 17/25] docs(platform): useChat Backend calls intro wording and links Co-Authored-By: Claude Opus 5.5 --- packages/platform/src/react/chat.types.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/platform/src/react/chat.types.ts b/packages/platform/src/react/chat.types.ts index bbd5332..7024a88 100644 --- a/packages/platform/src/react/chat.types.ts +++ b/packages/platform/src/react/chat.types.ts @@ -24,14 +24,15 @@ export interface ToolCallAnswer { * The functions you write for the `backend` option. The hook calls them when it needs Base44, and * each one usually forwards to a route on your own server, which adds your credentials. * - * Each call wraps one Base44 API request with your credentials, so the browser never holds a - * workspace API key or an access token. Your backend can be written in any language. + * Keeping the Base44 requests on your server means the browser never holds a workspace API key or + * an access token. Your server can be written in any language. * - * - `createApp`: [Create app](/api-reference/create-app), with the first prompt as `initial_message`. - * - `openLiveSession`: [Create socket session](/api-reference/create-socket-session) with `{ app_ids: [appId] }`. + * Each function's route calls one Base44 endpoint: + * - [`createApp()`](#createapp): [Create app](/api-reference/create-app), with the first prompt as `initial_message`. + * - [`openLiveSession()`](#openlivesession): [Create socket session](/api-reference/create-socket-session) with `{ app_ids: [appId] }`. * Return the response body unchanged. It holds a session token, never the workspace API key. - * - `sendMessage`: [Send chat message](/api-reference/send-chat-message). - * - `submitToolCallInput`: [Submit tool-call input](/api-reference/submit-tool-call-input). Send an + * - [`sendMessage()`](#sendmessage): [Send chat message](/api-reference/send-chat-message). + * - [`submitToolCallInput()`](#submittoolcallinput): [Submit tool-call input](/api-reference/submit-tool-call-input). Send an * `X-Request-ID` built from the tool call ID, so a retry resumes the turn only once. * * Base44 keeps the Send chat message and Submit tool-call input requests open until the AI From 4c9165d38443cbab34a635ac9d523a449ef429f6 Mon Sep 17 00:00:00 2001 From: "Sam (automated drift fix)" Date: Thu, 22 Oct 2026 12:38:03 +0300 Subject: [PATCH 18/25] docs(platform): useChat links error in place of linking the page to itself Co-Authored-By: Claude Opus 5.5 --- packages/platform/src/react/chat.types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/platform/src/react/chat.types.ts b/packages/platform/src/react/chat.types.ts index 7024a88..9869c18 100644 --- a/packages/platform/src/react/chat.types.ts +++ b/packages/platform/src/react/chat.types.ts @@ -37,7 +37,7 @@ export interface ToolCallAnswer { * * Base44 keeps the Send chat message and Submit tool-call input requests open until the AI * finishes the turn. Resolve your call once Base44 accepts the request, not when it returns. The - * chat follows the rest live. A rejected promise from any call sets `error` on the {@link useChat | chat}. + * chat follows the rest live. A rejected promise from any call sets [`error`](#error). * * @example * ```typescript @@ -159,7 +159,7 @@ export interface ChatAnswer { * An open question, with the actions that answer this question and no other. * * Each kind carries only the actions that make sense for it, and every kind can be declined. The - * actions never throw. A failure sets `error` on the {@link useChat | chat}, and the question opens again. + * actions never throw. A failure sets [`error`](#error), and the question opens again. * * The chat declines a question kind this release doesn't know, once per tool call, when Base44 * names that kind on the last assistant message. If the decline fails, the chat's `error` says From 86d235f5edb5cfc472b78ead9d0fe913e037ede4 Mon Sep 17 00:00:00 2001 From: "Sam (automated drift fix)" Date: Thu, 22 Oct 2026 12:41:07 +0300 Subject: [PATCH 19/25] docs(platform): return the socket session's three fields, and keep session_id on the server Create socket session's own docs say to keep session_id on your server, but the chat module and useChat told backends to pass the whole response through unchanged. Co-Authored-By: Claude Opus 5.5 --- .../platform/src/client/modules/chat.types.ts | 16 ++++++++-------- packages/platform/src/react/chat.types.ts | 5 +++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/platform/src/client/modules/chat.types.ts b/packages/platform/src/client/modules/chat.types.ts index 1653fbf..5e5e98d 100644 --- a/packages/platform/src/client/modules/chat.types.ts +++ b/packages/platform/src/client/modules/chat.types.ts @@ -2,9 +2,8 @@ import type { PlatformEvent, PlatformSnapshot } from "./chat.events.types.js"; import type { PlatformSocketError } from "../errors.js"; /** - * The response body of [Create socket session](/api-reference/create-socket-session), which your - * `getSession()` callback returns. Pass the response through unchanged. The client reads the three - * fields below. + * The three fields of a [Create socket session](/api-reference/create-socket-session) response that + * your `getSession()` callback returns. Keep the response's `session_id` on your server. */ export interface SocketSession { /** Token that authenticates the socket. The client keeps it in memory and sends it only when the socket connects. */ @@ -20,13 +19,15 @@ export interface ChatInitOptions { /** * Returns a new socket session from your server. * - * Return the response body of [Create socket session](/api-reference/create-socket-session) as a - * [`SocketSession`](#socketsession), and return a new session on every call. The client calls + * Return the `session_token`, `socket_url`, and `socket_path` of a + * [Create socket session](/api-reference/create-socket-session) response as a + * [`SocketSession`](#socketsession), and keep its `session_id` on your server. Return a new + * session on every call. The client calls * `getSession()` on the first {@linkcode ChatSession.connect | connect()}, and again only when * the session expires or Base44 rejects its token. If a new session has a different `socket_url` * or `socket_path`, the client moves to it and rejoins every active subscription. * - * @returns The session, as your server received it. + * @returns The session's `session_token`, `socket_url`, and `socket_path`. */ getSession: () => Promise; /** @@ -55,8 +56,7 @@ export interface ChatModule { * * Pass a `getSession()` callback in the options. The client calls it to get a * [socket session](/developers/references/apps-api/sections/socket-sessions), which your server - * opens with [Create socket session](/api-reference/create-socket-session) and returns as the - * response body. Each session is independent and gets its own socket session. + * opens with [Create socket session](/api-reference/create-socket-session). Each session is independent and gets its own socket session. * * Call {@linkcode ChatSession.connect | connect()} on the returned session to open the socket. * diff --git a/packages/platform/src/react/chat.types.ts b/packages/platform/src/react/chat.types.ts index 9869c18..447387e 100644 --- a/packages/platform/src/react/chat.types.ts +++ b/packages/platform/src/react/chat.types.ts @@ -30,7 +30,8 @@ export interface ToolCallAnswer { * Each function's route calls one Base44 endpoint: * - [`createApp()`](#createapp): [Create app](/api-reference/create-app), with the first prompt as `initial_message`. * - [`openLiveSession()`](#openlivesession): [Create socket session](/api-reference/create-socket-session) with `{ app_ids: [appId] }`. - * Return the response body unchanged. It holds a session token, never the workspace API key. + * Return its `session_token`, `socket_url`, and `socket_path`, and keep `session_id` on your server. + * None of them is the workspace API key. * - [`sendMessage()`](#sendmessage): [Send chat message](/api-reference/send-chat-message). * - [`submitToolCallInput()`](#submittoolcallinput): [Submit tool-call input](/api-reference/submit-tool-call-input). Send an * `X-Request-ID` built from the tool call ID, so a retry resumes the turn only once. @@ -69,7 +70,7 @@ export interface ChatBackend { * whenever the session expires. Sessions last one hour. * * @param appId - ID of the app to follow. - * @returns Promise resolving to the Create socket session response body, unchanged. + * @returns Promise resolving to the session's `session_token`, `socket_url`, and `socket_path`. */ openLiveSession(appId: string): Promise; /** From 19550ace271682147894a95e9d05e466fd56d1ca Mon Sep 17 00:00:00 2001 From: "Sam (automated drift fix)" Date: Thu, 22 Oct 2026 12:42:03 +0300 Subject: [PATCH 20/25] docs(platform): SocketSession comment that fits both pages Co-Authored-By: Claude Opus 5.5 --- packages/platform/src/client/modules/chat.types.ts | 2 +- packages/platform/src/react/chat.types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/platform/src/client/modules/chat.types.ts b/packages/platform/src/client/modules/chat.types.ts index 5e5e98d..6543b6d 100644 --- a/packages/platform/src/client/modules/chat.types.ts +++ b/packages/platform/src/client/modules/chat.types.ts @@ -3,7 +3,7 @@ import type { PlatformSocketError } from "../errors.js"; /** * The three fields of a [Create socket session](/api-reference/create-socket-session) response that - * your `getSession()` callback returns. Keep the response's `session_id` on your server. + * the browser needs. Keep the response's `session_id` on your server. */ export interface SocketSession { /** Token that authenticates the socket. The client keeps it in memory and sends it only when the socket connects. */ diff --git a/packages/platform/src/react/chat.types.ts b/packages/platform/src/react/chat.types.ts index 447387e..9eace30 100644 --- a/packages/platform/src/react/chat.types.ts +++ b/packages/platform/src/react/chat.types.ts @@ -70,7 +70,7 @@ export interface ChatBackend { * whenever the session expires. Sessions last one hour. * * @param appId - ID of the app to follow. - * @returns Promise resolving to the session's `session_token`, `socket_url`, and `socket_path`. + * @returns Promise resolving to the session. */ openLiveSession(appId: string): Promise; /** From 905df9d9397d176dce1550a3c2d9e3dcf2d069a5 Mon Sep 17 00:00:00 2001 From: "Sam (automated drift fix)" Date: Thu, 22 Oct 2026 12:44:41 +0300 Subject: [PATCH 21/25] docs(platform): useChat sendMessage() refusal wording Co-Authored-By: Claude Opus 5.5 --- packages/platform/src/react/chat.types.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/platform/src/react/chat.types.ts b/packages/platform/src/react/chat.types.ts index 9eace30..95a8ee9 100644 --- a/packages/platform/src/react/chat.types.ts +++ b/packages/platform/src/react/chat.types.ts @@ -74,8 +74,10 @@ export interface ChatBackend { */ openLiveSession(appId: string): Promise; /** - * Sends a prompt to an existing app. Base44 queues a prompt sent while the AI is working. When the - * app has no queue, Base44 refuses it with a 409, so reject the promise. + * Sends a prompt to an existing app. Base44 queues a prompt sent while the AI is working. If Base44 + * refuses the prompt, for example because the app is busy with an operation that blocks new + * messages, reject the promise. The hook then sets [`error`](#error), so your UI can show the + * failure. * * @param appId - ID of the app. * @param content - The prompt. From ac26a3ae7d0d2ad474f1531861a7c927559aa373 Mon Sep 17 00:00:00 2001 From: "Sam (automated drift fix)" Date: Thu, 22 Oct 2026 12:45:30 +0300 Subject: [PATCH 22/25] docs(platform): useChat backend example title Co-Authored-By: Claude Opus 5.5 --- packages/platform/src/react/chat.types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/platform/src/react/chat.types.ts b/packages/platform/src/react/chat.types.ts index 95a8ee9..89dc469 100644 --- a/packages/platform/src/react/chat.types.ts +++ b/packages/platform/src/react/chat.types.ts @@ -42,7 +42,7 @@ export interface ToolCallAnswer { * * @example * ```typescript - * // Your four calls, behind your own API routes + * // Your four functions, behind your own API routes * async function post(path: string, body: unknown): Promise { * const response = await fetch(path, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); * if (!response.ok) throw new Error(`${path} failed: ${response.status}`); From f675dc7f4e6520f5a81324790887f15d9c1e8c61 Mon Sep 17 00:00:00 2001 From: "Sam (automated drift fix)" Date: Thu, 22 Oct 2026 12:52:35 +0300 Subject: [PATCH 23/25] docs(pipeline): inline a page's helper types, and open a group with its intro type On the useChat page, ChoiceEntry, SecretField, and ApprovalDetails each had a section of their own though only one field uses each, and nothing linked them. page-sections.json `inlineTypes` now moves such a type into the field that uses it, as a Properties accordion. A typeDefinitionGroups entry's `intro` type opens its section with no heading of its own, with each type its signature names linked to its heading, so ChatQuestion introduces Questions and links the three kinds. Co-Authored-By: Claude Opus 5.5 --- .../page-sections.json | 8 +- packages/platform/src/react/useChat.ts | 2 +- .../file-processing/file-processing.js | 89 ++++++++++++++++++- .../sdk/tests/unit/typedoc-mintlify.test.ts | 44 +++++++++ 4 files changed, 137 insertions(+), 6 deletions(-) diff --git a/packages/platform/scripts/mintlify-post-processing/page-sections.json b/packages/platform/scripts/mintlify-post-processing/page-sections.json index a7a2764..f06a3a8 100644 --- a/packages/platform/scripts/mintlify-post-processing/page-sections.json +++ b/packages/platform/scripts/mintlify-post-processing/page-sections.json @@ -7,6 +7,11 @@ "returnedState": { "UseChatResult": "Returned state" }, + "inlineTypes": [ + "ChoiceEntry", + "SecretField", + "ApprovalDetails" + ], "typeDefinitionGroups": [ [ "SocketSession", @@ -52,7 +57,8 @@ "SecretField", "ApprovalQuestion", "ApprovalDetails" - ] + ], + "intro": "ChatQuestion" } ] } diff --git a/packages/platform/src/react/useChat.ts b/packages/platform/src/react/useChat.ts index 08a4644..97e9985 100644 --- a/packages/platform/src/react/useChat.ts +++ b/packages/platform/src/react/useChat.ts @@ -16,7 +16,7 @@ const NEW_APP = "new-app"; * * The hook is headless. It gives you the conversation and the [actions](#returned-actions), and your components render them: * - Follows the app live, and renews its socket session when it expires. - * - Turns Base44's messages into [items](#items), and shows each open question with the step that asked it. + * - Turns Base44's messages into [items](#items), and shows each open [question](#questions) with the step that asked it. * - Reports what the AI is doing in [`status`](#status), and the live connection in [`connection`](#connection). * - Sends prompts, creates apps, and answers questions through the {@link ChatBackend | backend} you pass, so no credential reaches the browser. * diff --git a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js index 9d326d9..8521bd6 100755 --- a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js +++ b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js @@ -1457,10 +1457,13 @@ function applyTypeDeclarationLinking(dir) { // types nested under "## Type Definitions" (the first type of a group starts the section). A group // is a list of type names, or `{ "title": "...", "types": [...] }` for another section heading. // A function's return type in `returnedState` also gets its properties a section, by title, in -// place of the function's Returns: one heading per property, as its methods have. +// place of the function's Returns: one heading per property, as its methods have. A type in +// `inlineTypes` moves into the field that uses it, and a group's `intro` type opens its section +// with no heading of its own. const { typesWithOwnMethods: TYPES_WITH_OWN_METHODS = {}, returnedState: RETURNED_STATE = {}, + inlineTypes: INLINE_TYPES = [], typeDefinitionGroups: TYPE_DEFINITION_GROUPS = [], } = fs.existsSync(PAGE_SECTIONS_PATH) ? JSON.parse(fs.readFileSync(PAGE_SECTIONS_PATH, "utf-8")) : {}; @@ -2804,6 +2807,76 @@ function applyReturnedStateSections(dir) { } } +/** + * A type's section, from its heading to the next heading at its level or above: the heading + * level, the section's text, and where it starts. + */ +function typeSection(content, typeName) { + const heading = content.match(new RegExp(`^(#{2,3}) ${typeName}\\n`, "m")); + if (!heading) return null; + const level = heading[1].length; + const rest = content.slice(heading.index + heading[0].length); + const end = rest.search(new RegExp(`^#{1,${level}} `, "m")); + return { level, index: heading.index, text: content.slice(heading.index, heading.index + heading[0].length + (end === -1 ? rest.length : end)) }; +} + +/** + * Each type in page-sections.json `inlineTypes` that a field on the page uses, as `type="T"` or + * `type="T[]"`, moves into that field as a Properties accordion, the way Returns shows a nested + * type, and its own section goes. The field describes it, so the type's comment goes too. + */ +function inlineTypesIntoFields(content, typeNames = INLINE_TYPES) { + for (const typeName of typeNames) { + const section = typeSection(content, typeName); + if (!section) continue; + const fieldsAt = section.text.indexOf("]*type="${typeName}(?:\\[\\])?"[^>]*>)([\\s\\S]*?)\\n*()`); + const field = content.match(user); + // Only a field with no fields of its own takes the type in. + if (!field || field[2].includes(" + `${open}${body.trimEnd()}\n\n\n\n${fields}\n\n\n\n${close}`); + } + return content; +} + +/** + * A type definition group's `intro` type opens the group's section: its heading goes, so its text + * follows the section heading, and each type its signature names that has a heading on the page + * is linked to it. + */ +function groupIntroTypes(content, groups = TYPE_DEFINITION_GROUPS) { + for (const group of groups) { + const intro = !Array.isArray(group) && group.intro; + if (!intro) continue; + const section = typeSection(content, intro); + if (!section) continue; + const headings = new Set([...content.matchAll(/^#{2,4} (\w+)$/gm)].map((m) => m[1])); + const text = section.text + .replace(/^#{2,3} .*\n+(?:\*{3}\n+)?/, "") + .replace(new RegExp(`^> \\*\\*${intro}\\*\\* = .*$`, "m"), (signature) => + signature.replace(/`(\w+)`/g, (code, name) => (headings.has(name) && name !== intro ? `[\`${name}\`](#${name.toLowerCase()})` : code))); + content = content.replace(section.text, text); + } + return content; +} + +function applyInlineTypesAndGroupIntros(dir) { + if (INLINE_TYPES.length === 0 && !TYPE_DEFINITION_GROUPS.some((group) => !Array.isArray(group) && group.intro)) return; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const entryPath = path.join(dir, entry.name); + if (entry.isDirectory()) applyInlineTypesAndGroupIntros(entryPath); + else if (entry.name.endsWith(".mdx")) { + const content = fs.readFileSync(entryPath, "utf-8"); + const converted = groupIntroTypes(inlineTypesIntoFields(content)); + if (converted !== content) fs.writeFileSync(entryPath, converted, "utf-8"); + } + } +} + function applyPropertiesAsFields(dir) { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const entryPath = path.join(dir, entry.name); @@ -2993,9 +3066,12 @@ function fixInPageAnchors(content) { for (const [, name] of content.matchAll(/<(?:ResponseField|ParamField) (?:name|body)="([^"]+)"/g)) { fieldCounts.set(name, (fieldCounts.get(name) ?? 0) + 1); } - const renamed = new Map( - Object.entries(TYPES_WITH_OWN_METHODS).map(([type, title]) => [headingAnchor(type), headingAnchor(title ?? type)]) - ); + const renamed = new Map([ + ...Object.entries(TYPES_WITH_OWN_METHODS).map(([type, title]) => [headingAnchor(type), headingAnchor(title ?? type)]), + // A group's intro type has no heading of its own, so a link to it goes to the group's. + ...TYPE_DEFINITION_GROUPS.filter((group) => !Array.isArray(group) && group.intro && group.title) + .map((group) => [headingAnchor(group.intro), headingAnchor(group.title)]), + ]); return content.replace(/\[([^\]]+)\]\(#([^)\s]+)\)/g, (link, text, anchor) => { if (anchors.has(anchor)) return link; const target = renamed.get(anchor); @@ -3107,6 +3183,9 @@ function main() { // Returns. After the inlining, so each property keeps the values its field was given. applyReturnedStateSections(DOCS_DIR); + // Types inlined into the field that uses them, and groups that open with their intro type + applyInlineTypesAndGroupIntros(DOCS_DIR); + // Remove links to types that aren't exposed (would 404) applyNonExposedTypeLinkRemoval( DOCS_DIR, @@ -3151,6 +3230,8 @@ if (process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(__fi export { fixInPageAnchors, + groupIntroTypes, + inlineTypesIntoFields, inlineStringUnions, nestUnderGroupTypes, parseStringUnion, diff --git a/packages/sdk/tests/unit/typedoc-mintlify.test.ts b/packages/sdk/tests/unit/typedoc-mintlify.test.ts index d6ec7a3..dafafa1 100644 --- a/packages/sdk/tests/unit/typedoc-mintlify.test.ts +++ b/packages/sdk/tests/unit/typedoc-mintlify.test.ts @@ -7,6 +7,8 @@ import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { nameCallableProperties } from "../../scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-content.js"; import { fixInPageAnchors, + groupIntroTypes, + inlineTypesIntoFields, inlineStringUnions, nestUnderGroupTypes, parseStringUnion, @@ -313,6 +315,48 @@ describe("topLevelResponseFields", () => { }); }); +describe("inlineTypesIntoFields", () => { + test("moves a type's fields into the field that uses it, and drops its section", () => { + const content = [ + "## Questions", + "### Choice", + '', + "", + "The entries.", + "", + "", + "### Entry", + "", + "One entry of a Choice.", + "", + '', + "", + "The text.", + "", + "", + "### Other", + ].join("\n"); + + const inlined = inlineTypesIntoFields(content, ["Entry"]); + expect(inlined).not.toContain("### Entry"); + expect(inlined).not.toContain("One entry of a Choice."); + expect(inlined).toContain( + 'The entries.\n\n\n\n\n\nThe text.\n\n\n\n\n\n' + ); + expect(inlined).toContain("### Other"); + }); +}); + +describe("groupIntroTypes", () => { + test("opens the group's section with the intro type, and links the types it names", () => { + const content = ["## Questions", "", "### Question", "", "***", "", "> **Question** = `Choice` | `Unknown`", "", "An open question.", "", "### Choice", ""].join("\n"); + + expect(groupIntroTypes(content, [{ title: "Questions", intro: "Question", types: ["Question", "Choice"] }])).toBe( + ["## Questions", "", "> **Question** = [`Choice`](#choice) | `Unknown`", "", "An open question.", "", "### Choice", ""].join("\n") + ); + }); +}); + describe("fixInPageAnchors", () => { test("keeps links to headings and explicit anchors", () => { const content = ["## Send", "", "See [send](#send) and [records](#list-records)."].join("\n"); From fb5e70c888c22e976ed967a540d62c5ea1a59933 Mon Sep 17 00:00:00 2001 From: "Sam (automated drift fix)" Date: Thu, 22 Oct 2026 12:56:54 +0300 Subject: [PATCH 24/25] docs(platform): useChat Questions wording and facts Co-Authored-By: Claude Opus 5.5 --- packages/platform/src/react/chat.types.ts | 38 +++++++++++++---------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/packages/platform/src/react/chat.types.ts b/packages/platform/src/react/chat.types.ts index 89dc469..4cc8836 100644 --- a/packages/platform/src/react/chat.types.ts +++ b/packages/platform/src/react/chat.types.ts @@ -159,14 +159,17 @@ export interface ChatAnswer { } /** - * An open question, with the actions that answer this question and no other. + * An open question the AI waits on. A `waiting` step carries it as its `question`, and + * [`status`](#status) becomes `waiting`. Check `kind` to see which of the three kinds it is. * - * Each kind carries only the actions that make sense for it, and every kind can be declined. The - * actions never throw. A failure sets [`error`](#error), and the question opens again. + * Each kind has only the actions that fit it, and every kind can be declined. Calling an action + * closes the question right away and sends the answer through your + * [`submitToolCallInput()`](#submittoolcallinput). The actions never throw. A failure sets + * [`error`](#error), and the question opens again. * - * The chat declines a question kind this release doesn't know, once per tool call, when Base44 - * names that kind on the last assistant message. If the decline fails, the chat's `error` says - * so, and the chat tries again after it reconnects. + * The hook declines a question kind this release doesn't know, once per tool call, when Base44 + * names that kind on the last assistant message. If the decline fails, [`error`](#error) says + * so, and the hook tries again after it reconnects. */ export type ChatQuestion = ChoiceQuestion | SecretsQuestion | ApprovalQuestion; @@ -191,30 +194,31 @@ export interface ChoiceQuestion { /** The questions, in order. */ questions: ChoiceEntry[]; /** - * Answers with the picked labels, and free text where the user wrote some. + * Answers with the picked labels, and free text where the user wrote some. For a question whose + * `multi` is `false`, only the first picked label counts. * * @param picked - One list of picked labels per question, in the order of `questions`. * @param texts - One free-text answer per question, in the same order. Use it for a `text_input` question, or as a note beside a pick. A question with no pick and no text is skipped. - * @returns Promise resolving once the answer is sent. + * @returns Promise resolving once Base44 accepts the answer, or once sending it fails and sets [`error`](#error). It never rejects. */ answer(picked: string[][], texts?: string[]): Promise; /** * Declines the question. The AI continues without the answer. * - * @returns Promise resolving once the decline is sent. + * @returns Promise resolving once Base44 accepts the decline, or once sending it fails and sets [`error`](#error). It never rejects. */ decline(): Promise; } /** A value the AI needs, such as an API key. */ export interface SecretField { - /** Name of the field, used as its key in `submit`. */ + /** Name of the field, which keys its value in [`submit()`](#submit). */ name: string; /** Where to get the value. */ description?: string; } -/** The AI needs secrets, such as API keys. The values go to Base44 and the chat keeps none of them. */ +/** The AI needs secrets, such as API keys. The values go to Base44, and the hook keeps none of them. */ export interface SecretsQuestion { /** Kind of question. */ kind: "secrets"; @@ -223,14 +227,14 @@ export interface SecretsQuestion { /** * Submits the values. * - * @param values - The values, keyed by field name. - * @returns Promise resolving once the values are sent. + * @param values - The values, keyed by each field's `name`. + * @returns Promise resolving once Base44 accepts the values, or once sending it fails and sets [`error`](#error). It never rejects. */ submit(values: Record): Promise; /** * Declines the question. The AI continues without the values. * - * @returns Promise resolving once the decline is sent. + * @returns Promise resolving once Base44 accepts the decline, or once sending it fails and sets [`error`](#error). It never rejects. */ decline(): Promise; } @@ -241,7 +245,7 @@ export interface ApprovalQuestion { kind: "approval"; /** Name of the tool that wants to run. */ action: string; - /** Why the action needs approval, when Base44 gives a reason. Empty otherwise. */ + /** Why the action needs approval. It's Base44's reason when there is one, otherwise the `summary` in `details`, and empty when neither is given. */ reason: string; /** The check that paused the action, such as `exec_tool_send_email` or `entity_rls_guard`. */ guard: string; @@ -250,13 +254,13 @@ export interface ApprovalQuestion { /** * Lets the action run. * - * @returns Promise resolving once the approval is sent. + * @returns Promise resolving once Base44 accepts the approval, or once sending it fails and sets [`error`](#error). It never rejects. */ approve(): Promise; /** * Declines the action. It never runs, and the AI continues. * - * @returns Promise resolving once the decline is sent. + * @returns Promise resolving once Base44 accepts the decline, or once sending it fails and sets [`error`](#error). It never rejects. */ decline(): Promise; } From 1f49ae9acb6951d980fe7d52557f9b594e217e1a Mon Sep 17 00:00:00 2001 From: "Sam (automated drift fix)" Date: Thu, 22 Oct 2026 13:06:32 +0300 Subject: [PATCH 25/25] docs(platform): useChat @returns without the old Actions link Co-Authored-By: Claude Opus 5.5 --- packages/platform/src/react/useChat.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/platform/src/react/useChat.ts b/packages/platform/src/react/useChat.ts index 97e9985..098f7f6 100644 --- a/packages/platform/src/react/useChat.ts +++ b/packages/platform/src/react/useChat.ts @@ -21,7 +21,7 @@ const NEW_APP = "new-app"; * - Sends prompts, creates apps, and answers questions through the {@link ChatBackend | backend} you pass, so no credential reaches the browser. * * @param options - The app to follow, your backend's calls, and what to do with a created app. - * @returns The conversation, its `status`, `connection`, and `error`, and the [actions](#actions) `send()`, `create()`, and `clearError()`. + * @returns The chat's state, `items`, `status`, `connection`, and `error`, and its actions `send()`, `create()`, and `clearError()`. * * @example * ```tsx