@btst/stack 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/dist/packages/stack/src/client/components/compose.cjs +1 -2
  2. package/dist/packages/stack/src/client/components/compose.mjs +1 -2
  3. package/dist/packages/stack/src/plugins/ai-chat/api/page-tools.cjs +71 -0
  4. package/dist/packages/stack/src/plugins/ai-chat/api/page-tools.mjs +68 -0
  5. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.cjs +54 -7
  6. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +54 -7
  7. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-input.cjs +2 -2
  8. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-input.mjs +2 -2
  9. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.cjs +89 -22
  10. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.mjs +90 -23
  11. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.cjs +110 -33
  12. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.mjs +112 -35
  13. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-sidebar.cjs +1 -1
  14. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-sidebar.mjs +1 -1
  15. package/dist/packages/stack/src/plugins/ai-chat/schemas.cjs +17 -1
  16. package/dist/packages/stack/src/plugins/ai-chat/schemas.mjs +17 -1
  17. package/dist/packages/stack/src/plugins/blog/client/components/forms/post-forms.cjs +15 -2
  18. package/dist/packages/stack/src/plugins/blog/client/components/forms/post-forms.mjs +16 -3
  19. package/dist/packages/stack/src/plugins/blog/client/components/pages/edit-post-page.internal.cjs +24 -1
  20. package/dist/packages/stack/src/plugins/blog/client/components/pages/edit-post-page.internal.mjs +24 -1
  21. package/dist/packages/stack/src/plugins/blog/client/components/pages/fill-blog-form-handler.cjs +26 -0
  22. package/dist/packages/stack/src/plugins/blog/client/components/pages/fill-blog-form-handler.mjs +24 -0
  23. package/dist/packages/stack/src/plugins/blog/client/components/pages/new-post-page.internal.cjs +30 -1
  24. package/dist/packages/stack/src/plugins/blog/client/components/pages/new-post-page.internal.mjs +30 -1
  25. package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.cjs +18 -0
  26. package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.mjs +18 -0
  27. package/dist/packages/stack/src/plugins/cms/api/mutations.cjs +48 -0
  28. package/dist/packages/stack/src/plugins/cms/api/mutations.mjs +46 -0
  29. package/dist/packages/stack/src/plugins/cms/api/plugin.cjs +7 -1
  30. package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +7 -1
  31. package/dist/packages/stack/src/plugins/kanban/api/mutations.cjs +91 -0
  32. package/dist/packages/stack/src/plugins/kanban/api/mutations.mjs +87 -0
  33. package/dist/packages/stack/src/plugins/kanban/api/plugin.cjs +6 -1
  34. package/dist/packages/stack/src/plugins/kanban/api/plugin.mjs +6 -1
  35. package/dist/packages/stack/src/plugins/kanban/client/hooks/kanban-hooks.cjs +7 -3
  36. package/dist/packages/stack/src/plugins/kanban/client/hooks/kanban-hooks.mjs +7 -3
  37. package/dist/packages/stack/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.cjs +89 -0
  38. package/dist/packages/stack/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.mjs +89 -0
  39. package/dist/plugins/ai-chat/api/index.d.cts +1 -1
  40. package/dist/plugins/ai-chat/api/index.d.mts +1 -1
  41. package/dist/plugins/ai-chat/api/index.d.ts +1 -1
  42. package/dist/plugins/ai-chat/client/components/index.d.cts +1 -1
  43. package/dist/plugins/ai-chat/client/components/index.d.mts +1 -1
  44. package/dist/plugins/ai-chat/client/components/index.d.ts +1 -1
  45. package/dist/plugins/ai-chat/client/context/page-ai-context.cjs +92 -0
  46. package/dist/plugins/ai-chat/client/context/page-ai-context.d.cts +84 -0
  47. package/dist/plugins/ai-chat/client/context/page-ai-context.d.mts +84 -0
  48. package/dist/plugins/ai-chat/client/context/page-ai-context.d.ts +84 -0
  49. package/dist/plugins/ai-chat/client/context/page-ai-context.mjs +88 -0
  50. package/dist/plugins/ai-chat/client/hooks/index.d.cts +1 -1
  51. package/dist/plugins/ai-chat/client/hooks/index.d.mts +1 -1
  52. package/dist/plugins/ai-chat/client/hooks/index.d.ts +1 -1
  53. package/dist/plugins/ai-chat/client/index.d.cts +2 -2
  54. package/dist/plugins/ai-chat/client/index.d.mts +2 -2
  55. package/dist/plugins/ai-chat/client/index.d.ts +2 -2
  56. package/dist/plugins/ai-chat/query-keys.d.cts +1 -1
  57. package/dist/plugins/ai-chat/query-keys.d.mts +1 -1
  58. package/dist/plugins/ai-chat/query-keys.d.ts +1 -1
  59. package/dist/plugins/blog/api/index.d.cts +2 -2
  60. package/dist/plugins/blog/api/index.d.mts +2 -2
  61. package/dist/plugins/blog/api/index.d.ts +2 -2
  62. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  63. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  64. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  65. package/dist/plugins/blog/client/index.d.cts +1 -1
  66. package/dist/plugins/blog/client/index.d.mts +1 -1
  67. package/dist/plugins/blog/client/index.d.ts +1 -1
  68. package/dist/plugins/blog/query-keys.d.cts +2 -2
  69. package/dist/plugins/blog/query-keys.d.mts +2 -2
  70. package/dist/plugins/blog/query-keys.d.ts +2 -2
  71. package/dist/plugins/cms/api/index.cjs +2 -0
  72. package/dist/plugins/cms/api/index.d.cts +1 -1
  73. package/dist/plugins/cms/api/index.d.mts +1 -1
  74. package/dist/plugins/cms/api/index.d.ts +1 -1
  75. package/dist/plugins/cms/api/index.mjs +1 -0
  76. package/dist/plugins/cms/query-keys.d.cts +1 -1
  77. package/dist/plugins/cms/query-keys.d.mts +1 -1
  78. package/dist/plugins/cms/query-keys.d.ts +1 -1
  79. package/dist/plugins/form-builder/api/index.d.cts +1 -1
  80. package/dist/plugins/form-builder/api/index.d.mts +1 -1
  81. package/dist/plugins/form-builder/api/index.d.ts +1 -1
  82. package/dist/plugins/form-builder/query-keys.d.cts +1 -1
  83. package/dist/plugins/form-builder/query-keys.d.mts +1 -1
  84. package/dist/plugins/form-builder/query-keys.d.ts +1 -1
  85. package/dist/plugins/kanban/api/index.cjs +4 -0
  86. package/dist/plugins/kanban/api/index.d.cts +1 -1
  87. package/dist/plugins/kanban/api/index.d.mts +1 -1
  88. package/dist/plugins/kanban/api/index.d.ts +1 -1
  89. package/dist/plugins/kanban/api/index.mjs +1 -0
  90. package/dist/plugins/kanban/query-keys.d.cts +1 -1
  91. package/dist/plugins/kanban/query-keys.d.mts +1 -1
  92. package/dist/plugins/kanban/query-keys.d.ts +1 -1
  93. package/dist/shared/{stack.BeSm90va.d.ts → stack.BEn34wW6.d.ts} +60 -2
  94. package/dist/shared/{stack.IdtKDRka.d.cts → stack.BUkC2EsZ.d.cts} +32 -2
  95. package/dist/shared/{stack.DaOcgmrM.d.ts → stack.BV9hnvu4.d.cts} +31 -7
  96. package/dist/shared/{stack.DaOcgmrM.d.cts → stack.BV9hnvu4.d.mts} +31 -7
  97. package/dist/shared/{stack.DaOcgmrM.d.mts → stack.BV9hnvu4.d.ts} +31 -7
  98. package/dist/shared/{stack.rTy7-wQU.d.mts → stack.BepFXT3w.d.mts} +70 -15
  99. package/dist/shared/{stack.BKfolAyK.d.ts → stack.CL8ts1Mu.d.ts} +3 -3
  100. package/dist/shared/{stack.CP68pFEH.d.mts → stack.CczspVn2.d.mts} +32 -2
  101. package/dist/shared/{stack.TIBF2AOx.d.ts → stack.CgWzG5jH.d.ts} +70 -15
  102. package/dist/shared/{stack.BpolpQpf.d.cts → stack.D3GB6wKv.d.cts} +70 -15
  103. package/dist/shared/{stack.B1EeBt1b.d.ts → stack.DASmUVjX.d.ts} +32 -2
  104. package/dist/shared/{stack.Dg09R0oB.d.mts → stack.DTDxgFj8.d.mts} +60 -2
  105. package/dist/shared/{stack.CMh_EdxW.d.cts → stack.DWoCZff7.d.cts} +60 -2
  106. package/dist/shared/{stack.snB1EDP7.d.cts → stack.Dk5r4W1F.d.mts} +3 -3
  107. package/dist/shared/{stack.BIXEI6v_.d.mts → stack.heOA9gzA.d.cts} +3 -3
  108. package/package.json +14 -1
  109. package/src/client/components/compose.tsx +7 -4
  110. package/src/plugins/ai-chat/api/page-tools.ts +111 -0
  111. package/src/plugins/ai-chat/api/plugin.ts +180 -9
  112. package/src/plugins/ai-chat/client/components/chat-input.tsx +2 -2
  113. package/src/plugins/ai-chat/client/components/chat-interface.tsx +154 -58
  114. package/src/plugins/ai-chat/client/components/chat-layout.tsx +166 -32
  115. package/src/plugins/ai-chat/client/components/chat-sidebar.tsx +1 -1
  116. package/src/plugins/ai-chat/client/context/page-ai-context.tsx +240 -0
  117. package/src/plugins/ai-chat/schemas.ts +16 -0
  118. package/src/plugins/blog/client/components/forms/post-forms.tsx +29 -2
  119. package/src/plugins/blog/client/components/pages/edit-post-page.internal.tsx +28 -0
  120. package/src/plugins/blog/client/components/pages/fill-blog-form-handler.ts +38 -0
  121. package/src/plugins/blog/client/components/pages/new-post-page.internal.tsx +33 -1
  122. package/src/plugins/blog/client/components/pages/post-page.internal.tsx +20 -0
  123. package/src/plugins/cms/api/index.ts +4 -0
  124. package/src/plugins/cms/api/mutations.ts +84 -0
  125. package/src/plugins/cms/api/plugin.ts +9 -0
  126. package/src/plugins/kanban/api/index.ts +6 -0
  127. package/src/plugins/kanban/api/mutations.ts +169 -0
  128. package/src/plugins/kanban/api/plugin.ts +12 -0
  129. package/src/plugins/kanban/client/hooks/kanban-hooks.tsx +4 -0
  130. package/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.tsx +132 -0
  131. package/dist/shared/{stack.C5dtIncc.d.mts → stack.B7ONvlD_.d.mts} +1 -1
  132. package/dist/shared/{stack.CBON0dWL.d.cts → stack.BQmuNl5p.d.cts} +2 -2
  133. package/dist/shared/{stack.CBON0dWL.d.mts → stack.BQmuNl5p.d.mts} +2 -2
  134. package/dist/shared/{stack.CBON0dWL.d.ts → stack.BQmuNl5p.d.ts} +2 -2
  135. package/dist/shared/{stack.CIP6QS9l.d.ts → stack.Kq2-QzOC.d.ts} +1 -1
  136. package/dist/shared/{stack.Dw0Ly2TM.d.cts → stack.kcdnD4gA.d.cts} +1 -1
@@ -0,0 +1,169 @@
1
+ import type { Adapter } from "@btst/db";
2
+ import type { Board, Column, Task, Priority } from "../types";
3
+
4
+ /**
5
+ * Input for creating a new Kanban task.
6
+ */
7
+ export interface CreateKanbanTaskInput {
8
+ title: string;
9
+ columnId: string;
10
+ description?: string;
11
+ priority?: Priority;
12
+ assigneeId?: string;
13
+ }
14
+
15
+ /**
16
+ * Create a new task in a Kanban column.
17
+ * Computes the next order value from existing tasks in the column.
18
+ *
19
+ * @remarks **Security:** No authorization hooks (onBeforeCreateTask) are called.
20
+ * The caller is responsible for any access-control checks before invoking this
21
+ * function.
22
+ *
23
+ * @param adapter - The database adapter
24
+ * @param input - Task creation input
25
+ */
26
+ export async function createKanbanTask(
27
+ adapter: Adapter,
28
+ input: CreateKanbanTaskInput,
29
+ ): Promise<Task> {
30
+ const existingTasks = await adapter.findMany<Task>({
31
+ model: "kanbanTask",
32
+ where: [
33
+ {
34
+ field: "columnId",
35
+ value: input.columnId,
36
+ operator: "eq" as const,
37
+ },
38
+ ],
39
+ });
40
+
41
+ const nextOrder =
42
+ existingTasks.length > 0
43
+ ? Math.max(...existingTasks.map((t) => t.order)) + 1
44
+ : 0;
45
+
46
+ return adapter.create<Task>({
47
+ model: "kanbanTask",
48
+ data: {
49
+ title: input.title,
50
+ columnId: input.columnId,
51
+ description: input.description,
52
+ priority: input.priority ?? "MEDIUM",
53
+ order: nextOrder,
54
+ assigneeId: input.assigneeId,
55
+ isArchived: false,
56
+ createdAt: new Date(),
57
+ updatedAt: new Date(),
58
+ },
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Coalesces concurrent `findOrCreateKanbanBoard` calls within the same process.
64
+ * Keyed by slug; entries are removed once the creation promise settles.
65
+ */
66
+ const _pendingBoardCreations = new Map<string, Promise<Board>>();
67
+
68
+ /**
69
+ * Find a board by slug, or create it with the given name and custom column titles.
70
+ *
71
+ * Concurrency-safe at two levels:
72
+ * - **Same process**: concurrent calls with the same slug share a single in-flight
73
+ * Promise (via `_pendingBoardCreations`), so only one DB write is attempted.
74
+ * - **Cross-instance**: the DB `unique` constraint on `slug` causes the losing
75
+ * write to throw; the catch block re-fetches and returns the winner's board.
76
+ *
77
+ * @remarks **Security:** No authorization hooks are called. The caller is
78
+ * responsible for any access-control checks before invoking this function.
79
+ *
80
+ * @param adapter - The database adapter
81
+ * @param slug - Unique URL-safe slug for the board
82
+ * @param name - Display name for the board (used only on creation)
83
+ * @param columnTitles - Ordered list of column names to create (used only on creation)
84
+ */
85
+ export async function findOrCreateKanbanBoard(
86
+ adapter: Adapter,
87
+ slug: string,
88
+ name: string,
89
+ columnTitles: string[],
90
+ ): Promise<Board> {
91
+ const existing = await adapter.findOne<Board>({
92
+ model: "kanbanBoard",
93
+ where: [{ field: "slug", value: slug, operator: "eq" as const }],
94
+ });
95
+
96
+ if (existing) return existing;
97
+
98
+ // Coalesce same-process concurrent calls for this slug
99
+ const inflight = _pendingBoardCreations.get(slug);
100
+ if (inflight) return inflight;
101
+
102
+ const creation = (async () => {
103
+ try {
104
+ const board = await adapter.create<Board>({
105
+ model: "kanbanBoard",
106
+ data: {
107
+ name,
108
+ slug,
109
+ createdAt: new Date(),
110
+ updatedAt: new Date(),
111
+ },
112
+ });
113
+
114
+ await Promise.all(
115
+ columnTitles.map((title, index) =>
116
+ adapter.create<Column>({
117
+ model: "kanbanColumn",
118
+ data: {
119
+ title,
120
+ boardId: board.id,
121
+ order: index,
122
+ createdAt: new Date(),
123
+ updatedAt: new Date(),
124
+ },
125
+ }),
126
+ ),
127
+ );
128
+
129
+ return board;
130
+ } catch (err) {
131
+ // Cross-instance race: another process won the unique-constraint race.
132
+ // Re-fetch so all callers return the same board.
133
+ const winner = await adapter.findOne<Board>({
134
+ model: "kanbanBoard",
135
+ where: [{ field: "slug", value: slug, operator: "eq" as const }],
136
+ });
137
+ if (winner) return winner;
138
+ throw err;
139
+ }
140
+ })();
141
+
142
+ _pendingBoardCreations.set(slug, creation);
143
+ try {
144
+ return await creation;
145
+ } finally {
146
+ _pendingBoardCreations.delete(slug);
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Retrieve all columns for a given board, sorted by order.
152
+ * Co-located with mutations because it is primarily used alongside
153
+ * {@link createKanbanTask} to resolve column IDs before task creation.
154
+ *
155
+ * @remarks **Security:** No authorization hooks are called.
156
+ *
157
+ * @param adapter - The database adapter
158
+ * @param boardId - The board ID
159
+ */
160
+ export async function getKanbanColumnsByBoardId(
161
+ adapter: Adapter,
162
+ boardId: string,
163
+ ): Promise<Column[]> {
164
+ return adapter.findMany<Column>({
165
+ model: "kanbanColumn",
166
+ where: [{ field: "boardId", value: boardId, operator: "eq" as const }],
167
+ sortBy: { field: "order", direction: "asc" },
168
+ });
169
+ }
@@ -24,6 +24,11 @@ import {
24
24
  updateTaskSchema,
25
25
  } from "../schemas";
26
26
  import { getAllBoards, getBoardById } from "./getters";
27
+ import {
28
+ createKanbanTask,
29
+ findOrCreateKanbanBoard,
30
+ getKanbanColumnsByBoardId,
31
+ } from "./mutations";
27
32
  import { KANBAN_QUERY_KEYS } from "./query-key-defs";
28
33
  import { serializeBoard } from "./serializers";
29
34
  import type { QueryClient } from "@tanstack/react-query";
@@ -317,6 +322,13 @@ export const kanbanBackendPlugin = (hooks?: KanbanBackendHooks) =>
317
322
  getAllBoards(adapter, params),
318
323
  getBoardById: (id: string) => getBoardById(adapter, id),
319
324
  prefetchForRoute: createKanbanPrefetchForRoute(adapter),
325
+ // Mutations
326
+ createTask: (input: Parameters<typeof createKanbanTask>[1]) =>
327
+ createKanbanTask(adapter, input),
328
+ findOrCreateBoard: (slug: string, name: string, columnTitles: string[]) =>
329
+ findOrCreateKanbanBoard(adapter, slug, name, columnTitles),
330
+ getColumnsByBoardId: (boardId: string) =>
331
+ getKanbanColumnsByBoardId(adapter, boardId),
320
332
  }),
321
333
 
322
334
  routes: (adapter: Adapter) => {
@@ -85,6 +85,7 @@ export function useBoards(params?: {
85
85
  return useQuery({
86
86
  ...queries.boards.list(params),
87
87
  staleTime: 30_000,
88
+ refetchOnWindowFocus: true,
88
89
  });
89
90
  }
90
91
 
@@ -102,6 +103,7 @@ export function useSuspenseBoards(params?: {
102
103
  const result = useSuspenseQuery({
103
104
  ...queries.boards.list(params),
104
105
  staleTime: 30_000,
106
+ refetchOnWindowFocus: true,
105
107
  });
106
108
 
107
109
  if (result.error && !result.isFetching) {
@@ -121,6 +123,7 @@ export function useBoard(boardId: string) {
121
123
  return useQuery({
122
124
  ...queries.boards.detail(boardId),
123
125
  staleTime: 30_000,
126
+ refetchOnWindowFocus: true,
124
127
  enabled: !!boardId,
125
128
  });
126
129
  }
@@ -135,6 +138,7 @@ export function useSuspenseBoard(boardId: string) {
135
138
  const result = useSuspenseQuery({
136
139
  ...queries.boards.detail(boardId),
137
140
  staleTime: 30_000,
141
+ refetchOnWindowFocus: true,
138
142
  });
139
143
 
140
144
  if (result.error && !result.isFetching) {
@@ -22,9 +22,12 @@ import { toast } from "sonner";
22
22
  import UIBuilder from "@workspace/ui/components/ui-builder";
23
23
  import type {
24
24
  ComponentLayer,
25
+ ComponentRegistry,
25
26
  Variable,
26
27
  } from "@workspace/ui/components/ui-builder/types";
27
28
 
29
+ import { useLayerStore } from "@workspace/ui/lib/ui-builder/store/layer-store";
30
+ import { useRegisterPageAIContext } from "@btst/stack/plugins/ai-chat/client/context";
28
31
  import {
29
32
  useSuspenseUIBuilderPage,
30
33
  useCreateUIBuilderPage,
@@ -39,6 +42,104 @@ export interface PageBuilderPageProps {
39
42
  id?: string;
40
43
  }
41
44
 
45
+ /**
46
+ * Generate a concise AI-readable description of the available components
47
+ * in the component registry, including their prop names.
48
+ */
49
+ function buildRegistryDescription(registry: ComponentRegistry): string {
50
+ const lines: string[] = [];
51
+ for (const [name, entry] of Object.entries(registry) as [
52
+ string,
53
+ { schema?: unknown },
54
+ ][]) {
55
+ let propsLine = "";
56
+ try {
57
+ const shape = (entry.schema as any)?.shape as
58
+ | Record<string, unknown>
59
+ | undefined;
60
+ if (shape) {
61
+ const fields = Object.keys(shape).join(", ");
62
+ propsLine = ` — props: ${fields}`;
63
+ }
64
+ } catch {
65
+ // ignore schema introspection errors
66
+ }
67
+ lines.push(`- ${name}${propsLine}`);
68
+ }
69
+ return lines.join("\n");
70
+ }
71
+
72
+ /**
73
+ * Build the full page description string for the AI context.
74
+ * Stays within the 8,000-character pageContext limit.
75
+ */
76
+ function buildPageDescription(
77
+ id: string | undefined,
78
+ slug: string,
79
+ layers: ComponentLayer[],
80
+ registry: ComponentRegistry,
81
+ ): string {
82
+ const header = id
83
+ ? `UI Builder — editing page (slug: "${slug}")`
84
+ : "UI Builder — creating new page";
85
+
86
+ const layersJson = JSON.stringify(layers, null, 2);
87
+
88
+ const registryDesc = buildRegistryDescription(registry);
89
+
90
+ const layerFormat = `Each layer: { id: string, type: string, name: string, props: Record<string,any>, children?: ComponentLayer[] | string }`;
91
+
92
+ const full = [
93
+ header,
94
+ "",
95
+ `## Current Layers (${layers.length})`,
96
+ layersJson,
97
+ "",
98
+ `## Available Component Types`,
99
+ registryDesc,
100
+ "",
101
+ `## ComponentLayer format`,
102
+ layerFormat,
103
+ ].join("\n");
104
+
105
+ // Trim to fit the 16,000-char server-side limit, cutting the layers JSON if needed
106
+ if (full.length <= 16000) return full;
107
+
108
+ // Re-build with truncated layers JSON
109
+ const overhead =
110
+ [
111
+ header,
112
+ "",
113
+ `## Current Layers (${layers.length})`,
114
+ "",
115
+ "",
116
+ `## Available Component Types`,
117
+ registryDesc,
118
+ "",
119
+ `## ComponentLayer format`,
120
+ layerFormat,
121
+ ].join("\n").length + 30; // 30-char buffer for "...(truncated)"
122
+
123
+ const budget = Math.max(0, 16000 - overhead);
124
+ const truncatedLayers =
125
+ layersJson.length > budget
126
+ ? layersJson.slice(0, budget) + "\n...(truncated)"
127
+ : layersJson;
128
+
129
+ return [
130
+ header,
131
+ "",
132
+ `## Current Layers (${layers.length})`,
133
+ truncatedLayers,
134
+ "",
135
+ `## Available Component Types`,
136
+ registryDesc,
137
+ "",
138
+ `## ComponentLayer format`,
139
+ layerFormat,
140
+ ].join("\n");
141
+ }
142
+
42
143
  /**
43
144
  * Slugify a string for URL-friendly slugs
44
145
  */
@@ -139,6 +240,37 @@ function PageBuilderPageContent({
139
240
  // Auto-generate slug from first page name
140
241
  const [autoSlug, setAutoSlug] = useState(!id);
141
242
 
243
+ // Register AI context so the chat can update the page layout
244
+ useRegisterPageAIContext({
245
+ routeName: id ? "ui-builder-edit-page" : "ui-builder-new-page",
246
+ pageDescription: buildPageDescription(id, slug, layers, componentRegistry),
247
+ suggestions: [
248
+ "Add a hero section",
249
+ "Add a 3-column feature grid",
250
+ "Make the layout full-width",
251
+ "Add a card with a title, description, and button",
252
+ "Replace the layout with a centered single-column design",
253
+ ],
254
+ clientTools: {
255
+ updatePageLayers: async ({ layers: newLayers }) => {
256
+ // Drive the UIBuilder's Zustand store directly so the editor
257
+ // and layers panel update immediately. The store's onChange
258
+ // callback will propagate back to the parent's `layers` state.
259
+ const store = useLayerStore.getState();
260
+ store.initialize(
261
+ newLayers,
262
+ store.selectedPageId || newLayers[0]?.id,
263
+ undefined,
264
+ store.variables,
265
+ );
266
+ return {
267
+ success: true,
268
+ message: `Applied ${newLayers.length} layer(s) to the page`,
269
+ };
270
+ },
271
+ },
272
+ });
273
+
142
274
  // Handle layers change from UIBuilder
143
275
  const handleLayersChange = useCallback(
144
276
  (newLayers: ComponentLayer[]) => {
@@ -191,8 +191,8 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
191
191
  };
192
192
  submittedAt: string;
193
193
  id: string;
194
- formId: string;
195
194
  data: string;
195
+ formId: string;
196
196
  submittedBy?: string | undefined;
197
197
  ipAddress?: string | undefined;
198
198
  userAgent?: string | undefined;
@@ -42,11 +42,11 @@ declare const createPostSchema: z.ZodObject<{
42
42
  name: z.ZodString;
43
43
  slug: z.ZodString;
44
44
  }, z.core.$strip>]>>>>;
45
+ title: z.ZodString;
45
46
  slug: z.ZodOptional<z.ZodString>;
46
- publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
47
47
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
48
48
  updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
49
- title: z.ZodString;
49
+ publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
50
50
  content: z.ZodString;
51
51
  excerpt: z.ZodString;
52
52
  image: z.ZodOptional<z.ZodString>;
@@ -42,11 +42,11 @@ declare const createPostSchema: z.ZodObject<{
42
42
  name: z.ZodString;
43
43
  slug: z.ZodString;
44
44
  }, z.core.$strip>]>>>>;
45
+ title: z.ZodString;
45
46
  slug: z.ZodOptional<z.ZodString>;
46
- publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
47
47
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
48
48
  updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
49
- title: z.ZodString;
49
+ publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
50
50
  content: z.ZodString;
51
51
  excerpt: z.ZodString;
52
52
  image: z.ZodOptional<z.ZodString>;
@@ -42,11 +42,11 @@ declare const createPostSchema: z.ZodObject<{
42
42
  name: z.ZodString;
43
43
  slug: z.ZodString;
44
44
  }, z.core.$strip>]>>>>;
45
+ title: z.ZodString;
45
46
  slug: z.ZodOptional<z.ZodString>;
46
- publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
47
47
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
48
48
  updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
49
- title: z.ZodString;
49
+ publishedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
50
50
  content: z.ZodString;
51
51
  excerpt: z.ZodString;
52
52
  image: z.ZodOptional<z.ZodString>;
@@ -191,8 +191,8 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
191
191
  };
192
192
  submittedAt: string;
193
193
  id: string;
194
- formId: string;
195
194
  data: string;
195
+ formId: string;
196
196
  submittedBy?: string | undefined;
197
197
  ipAddress?: string | undefined;
198
198
  userAgent?: string | undefined;
@@ -191,8 +191,8 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
191
191
  };
192
192
  submittedAt: string;
193
193
  id: string;
194
- formId: string;
195
194
  data: string;
195
+ formId: string;
196
196
  submittedBy?: string | undefined;
197
197
  ipAddress?: string | undefined;
198
198
  userAgent?: string | undefined;