@btst/stack 2.3.0 → 2.5.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 (208) 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 +87 -54
  6. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +87 -54
  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/client/plugin.cjs +14 -21
  16. package/dist/packages/stack/src/plugins/ai-chat/client/plugin.mjs +15 -22
  17. package/dist/packages/stack/src/plugins/ai-chat/schemas.cjs +17 -1
  18. package/dist/packages/stack/src/plugins/ai-chat/schemas.mjs +17 -1
  19. package/dist/packages/stack/src/plugins/blog/api/plugin.cjs +28 -45
  20. package/dist/packages/stack/src/plugins/blog/api/plugin.mjs +22 -39
  21. package/dist/packages/stack/src/plugins/blog/client/components/forms/post-forms.cjs +15 -2
  22. package/dist/packages/stack/src/plugins/blog/client/components/forms/post-forms.mjs +16 -3
  23. package/dist/packages/stack/src/plugins/blog/client/components/pages/edit-post-page.internal.cjs +24 -1
  24. package/dist/packages/stack/src/plugins/blog/client/components/pages/edit-post-page.internal.mjs +24 -1
  25. package/dist/packages/stack/src/plugins/blog/client/components/pages/fill-blog-form-handler.cjs +26 -0
  26. package/dist/packages/stack/src/plugins/blog/client/components/pages/fill-blog-form-handler.mjs +24 -0
  27. package/dist/packages/stack/src/plugins/blog/client/components/pages/new-post-page.internal.cjs +30 -1
  28. package/dist/packages/stack/src/plugins/blog/client/components/pages/new-post-page.internal.mjs +30 -1
  29. package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.cjs +18 -0
  30. package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.mjs +18 -0
  31. package/dist/packages/stack/src/plugins/blog/client/plugin.cjs +23 -27
  32. package/dist/packages/stack/src/plugins/blog/client/plugin.mjs +24 -28
  33. package/dist/packages/stack/src/plugins/cms/api/mutations.cjs +48 -0
  34. package/dist/packages/stack/src/plugins/cms/api/mutations.mjs +46 -0
  35. package/dist/packages/stack/src/plugins/cms/api/plugin.cjs +21 -18
  36. package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +21 -18
  37. package/dist/packages/stack/src/plugins/cms/client/plugin.cjs +11 -15
  38. package/dist/packages/stack/src/plugins/cms/client/plugin.mjs +12 -16
  39. package/dist/packages/stack/src/plugins/form-builder/api/plugin.cjs +58 -62
  40. package/dist/packages/stack/src/plugins/form-builder/api/plugin.mjs +58 -62
  41. package/dist/packages/stack/src/plugins/form-builder/client/plugin.cjs +12 -12
  42. package/dist/packages/stack/src/plugins/form-builder/client/plugin.mjs +13 -13
  43. package/dist/packages/stack/src/plugins/kanban/api/mutations.cjs +91 -0
  44. package/dist/packages/stack/src/plugins/kanban/api/mutations.mjs +87 -0
  45. package/dist/packages/stack/src/plugins/kanban/api/plugin.cjs +92 -118
  46. package/dist/packages/stack/src/plugins/kanban/api/plugin.mjs +89 -115
  47. package/dist/packages/stack/src/plugins/kanban/client/hooks/kanban-hooks.cjs +7 -3
  48. package/dist/packages/stack/src/plugins/kanban/client/hooks/kanban-hooks.mjs +7 -3
  49. package/dist/packages/stack/src/plugins/kanban/client/plugin.cjs +22 -29
  50. package/dist/packages/stack/src/plugins/kanban/client/plugin.mjs +23 -30
  51. package/dist/packages/stack/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.cjs +89 -0
  52. package/dist/packages/stack/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.mjs +89 -0
  53. package/dist/packages/stack/src/plugins/ui-builder/client/plugin.cjs +8 -8
  54. package/dist/packages/stack/src/plugins/ui-builder/client/plugin.mjs +9 -9
  55. package/dist/packages/stack/src/plugins/utils.cjs +42 -0
  56. package/dist/packages/stack/src/plugins/utils.mjs +41 -1
  57. package/dist/plugins/ai-chat/api/index.d.cts +1 -1
  58. package/dist/plugins/ai-chat/api/index.d.mts +1 -1
  59. package/dist/plugins/ai-chat/api/index.d.ts +1 -1
  60. package/dist/plugins/ai-chat/client/components/index.d.cts +1 -1
  61. package/dist/plugins/ai-chat/client/components/index.d.mts +1 -1
  62. package/dist/plugins/ai-chat/client/components/index.d.ts +1 -1
  63. package/dist/plugins/ai-chat/client/context/page-ai-context.cjs +92 -0
  64. package/dist/plugins/ai-chat/client/context/page-ai-context.d.cts +84 -0
  65. package/dist/plugins/ai-chat/client/context/page-ai-context.d.mts +84 -0
  66. package/dist/plugins/ai-chat/client/context/page-ai-context.d.ts +84 -0
  67. package/dist/plugins/ai-chat/client/context/page-ai-context.mjs +88 -0
  68. package/dist/plugins/ai-chat/client/hooks/index.d.cts +1 -1
  69. package/dist/plugins/ai-chat/client/hooks/index.d.mts +1 -1
  70. package/dist/plugins/ai-chat/client/hooks/index.d.ts +1 -1
  71. package/dist/plugins/ai-chat/client/index.d.cts +10 -10
  72. package/dist/plugins/ai-chat/client/index.d.mts +10 -10
  73. package/dist/plugins/ai-chat/client/index.d.ts +10 -10
  74. package/dist/plugins/ai-chat/query-keys.d.cts +1 -1
  75. package/dist/plugins/ai-chat/query-keys.d.mts +1 -1
  76. package/dist/plugins/ai-chat/query-keys.d.ts +1 -1
  77. package/dist/plugins/blog/api/index.d.cts +2 -2
  78. package/dist/plugins/blog/api/index.d.mts +2 -2
  79. package/dist/plugins/blog/api/index.d.ts +2 -2
  80. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  81. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  82. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  83. package/dist/plugins/blog/client/index.d.cts +13 -13
  84. package/dist/plugins/blog/client/index.d.mts +13 -13
  85. package/dist/plugins/blog/client/index.d.ts +13 -13
  86. package/dist/plugins/blog/query-keys.d.cts +2 -2
  87. package/dist/plugins/blog/query-keys.d.mts +2 -2
  88. package/dist/plugins/blog/query-keys.d.ts +2 -2
  89. package/dist/plugins/client/index.cjs +1 -0
  90. package/dist/plugins/client/index.d.cts +8 -1
  91. package/dist/plugins/client/index.d.mts +8 -1
  92. package/dist/plugins/client/index.d.ts +8 -1
  93. package/dist/plugins/client/index.mjs +1 -1
  94. package/dist/plugins/cms/api/index.cjs +2 -0
  95. package/dist/plugins/cms/api/index.d.cts +2 -2
  96. package/dist/plugins/cms/api/index.d.mts +2 -2
  97. package/dist/plugins/cms/api/index.d.ts +2 -2
  98. package/dist/plugins/cms/api/index.mjs +1 -0
  99. package/dist/plugins/cms/client/hooks/index.d.cts +1 -1
  100. package/dist/plugins/cms/client/hooks/index.d.mts +1 -1
  101. package/dist/plugins/cms/client/hooks/index.d.ts +1 -1
  102. package/dist/plugins/cms/client/index.d.cts +6 -6
  103. package/dist/plugins/cms/client/index.d.mts +6 -6
  104. package/dist/plugins/cms/client/index.d.ts +6 -6
  105. package/dist/plugins/cms/query-keys.d.cts +2 -2
  106. package/dist/plugins/cms/query-keys.d.mts +2 -2
  107. package/dist/plugins/cms/query-keys.d.ts +2 -2
  108. package/dist/plugins/form-builder/api/index.d.cts +2 -2
  109. package/dist/plugins/form-builder/api/index.d.mts +2 -2
  110. package/dist/plugins/form-builder/api/index.d.ts +2 -2
  111. package/dist/plugins/form-builder/client/components/index.d.cts +1 -1
  112. package/dist/plugins/form-builder/client/components/index.d.mts +1 -1
  113. package/dist/plugins/form-builder/client/components/index.d.ts +1 -1
  114. package/dist/plugins/form-builder/client/hooks/index.d.cts +1 -1
  115. package/dist/plugins/form-builder/client/hooks/index.d.mts +1 -1
  116. package/dist/plugins/form-builder/client/hooks/index.d.ts +1 -1
  117. package/dist/plugins/form-builder/client/index.d.cts +6 -6
  118. package/dist/plugins/form-builder/client/index.d.mts +6 -6
  119. package/dist/plugins/form-builder/client/index.d.ts +6 -6
  120. package/dist/plugins/form-builder/query-keys.d.cts +2 -2
  121. package/dist/plugins/form-builder/query-keys.d.mts +2 -2
  122. package/dist/plugins/form-builder/query-keys.d.ts +2 -2
  123. package/dist/plugins/kanban/api/index.cjs +4 -0
  124. package/dist/plugins/kanban/api/index.d.cts +1 -1
  125. package/dist/plugins/kanban/api/index.d.mts +1 -1
  126. package/dist/plugins/kanban/api/index.d.ts +1 -1
  127. package/dist/plugins/kanban/api/index.mjs +1 -0
  128. package/dist/plugins/kanban/client/index.d.cts +12 -12
  129. package/dist/plugins/kanban/client/index.d.mts +12 -12
  130. package/dist/plugins/kanban/client/index.d.ts +12 -12
  131. package/dist/plugins/kanban/query-keys.d.cts +1 -1
  132. package/dist/plugins/kanban/query-keys.d.mts +1 -1
  133. package/dist/plugins/kanban/query-keys.d.ts +1 -1
  134. package/dist/plugins/ui-builder/client/hooks/index.d.cts +1 -1
  135. package/dist/plugins/ui-builder/client/hooks/index.d.mts +1 -1
  136. package/dist/plugins/ui-builder/client/hooks/index.d.ts +1 -1
  137. package/dist/plugins/ui-builder/client/index.d.cts +3 -3
  138. package/dist/plugins/ui-builder/client/index.d.mts +3 -3
  139. package/dist/plugins/ui-builder/client/index.d.ts +3 -3
  140. package/dist/plugins/ui-builder/index.d.cts +2 -2
  141. package/dist/plugins/ui-builder/index.d.mts +2 -2
  142. package/dist/plugins/ui-builder/index.d.ts +2 -2
  143. package/dist/shared/{stack.C-WUPMT6.d.cts → stack.B2xZTSiO.d.cts} +4 -4
  144. package/dist/shared/{stack.B1EeBt1b.d.ts → stack.B58oHdqm.d.mts} +33 -3
  145. package/dist/shared/{stack.CVDTkMoO.d.mts → stack.B8QD11QU.d.cts} +7 -7
  146. package/dist/shared/{stack.CVDTkMoO.d.cts → stack.B8QD11QU.d.mts} +7 -7
  147. package/dist/shared/{stack.CVDTkMoO.d.ts → stack.B8QD11QU.d.ts} +7 -7
  148. package/dist/shared/{stack.CIP6QS9l.d.ts → stack.BDVEpue1.d.ts} +1 -1
  149. package/dist/shared/{stack.C5dtIncc.d.mts → stack.BTvbxZvw.d.cts} +1 -1
  150. package/dist/shared/{stack.DaOcgmrM.d.ts → stack.BV9hnvu4.d.cts} +31 -7
  151. package/dist/shared/{stack.DaOcgmrM.d.cts → stack.BV9hnvu4.d.mts} +31 -7
  152. package/dist/shared/{stack.DaOcgmrM.d.mts → stack.BV9hnvu4.d.ts} +31 -7
  153. package/dist/shared/{stack.DdI5W6MB.d.mts → stack.BozPgbrZ.d.cts} +19 -19
  154. package/dist/shared/{stack.DdI5W6MB.d.ts → stack.BozPgbrZ.d.mts} +19 -19
  155. package/dist/shared/{stack.DdI5W6MB.d.cts → stack.BozPgbrZ.d.ts} +19 -19
  156. package/dist/shared/{stack.CP68pFEH.d.mts → stack.C9Mg2Q46.d.cts} +33 -3
  157. package/dist/shared/{stack.BeSm90va.d.ts → stack.CTDVxbrA.d.ts} +72 -14
  158. package/dist/shared/{stack.C-Ptrz8s.d.ts → stack.Cj_zKww4.d.ts} +4 -4
  159. package/dist/shared/{stack.TIBF2AOx.d.ts → stack.CxaFNQCV.d.mts} +89 -34
  160. package/dist/shared/{stack.CMh_EdxW.d.cts → stack.D-b5zbPm.d.cts} +72 -14
  161. package/dist/shared/{stack.Dw0Ly2TM.d.cts → stack.DTtmJPQO.d.mts} +1 -1
  162. package/dist/shared/{stack.BKfolAyK.d.ts → stack.DXnclTG7.d.ts} +11 -11
  163. package/dist/shared/{stack.snB1EDP7.d.cts → stack.DaZM10cp.d.cts} +11 -11
  164. package/dist/shared/{stack.Dg09R0oB.d.mts → stack.FVWf2JhZ.d.mts} +72 -14
  165. package/dist/shared/{stack.BIXEI6v_.d.mts → stack.cfCkioTe.d.mts} +11 -11
  166. package/dist/shared/{stack.6fUOjLs9.d.mts → stack.dH7u-TJH.d.mts} +4 -4
  167. package/dist/shared/{stack.BpolpQpf.d.cts → stack.j75TpKh2.d.ts} +89 -34
  168. package/dist/shared/{stack.rTy7-wQU.d.mts → stack.n1_i1p2B.d.cts} +89 -34
  169. package/dist/shared/{stack.IdtKDRka.d.cts → stack.sO33ZDhK.d.ts} +33 -3
  170. package/package.json +14 -1
  171. package/src/client/components/compose.tsx +7 -4
  172. package/src/plugins/ai-chat/api/page-tools.ts +111 -0
  173. package/src/plugins/ai-chat/api/plugin.ts +228 -72
  174. package/src/plugins/ai-chat/client/components/chat-input.tsx +2 -2
  175. package/src/plugins/ai-chat/client/components/chat-interface.tsx +154 -58
  176. package/src/plugins/ai-chat/client/components/chat-layout.tsx +166 -32
  177. package/src/plugins/ai-chat/client/components/chat-sidebar.tsx +1 -1
  178. package/src/plugins/ai-chat/client/context/page-ai-context.tsx +240 -0
  179. package/src/plugins/ai-chat/client/plugin.tsx +23 -31
  180. package/src/plugins/ai-chat/schemas.ts +16 -0
  181. package/src/plugins/blog/api/plugin.ts +31 -47
  182. package/src/plugins/blog/client/components/forms/post-forms.tsx +29 -2
  183. package/src/plugins/blog/client/components/pages/edit-post-page.internal.tsx +28 -0
  184. package/src/plugins/blog/client/components/pages/fill-blog-form-handler.ts +38 -0
  185. package/src/plugins/blog/client/components/pages/new-post-page.internal.tsx +33 -1
  186. package/src/plugins/blog/client/components/pages/post-page.internal.tsx +20 -0
  187. package/src/plugins/blog/client/plugin.tsx +36 -39
  188. package/src/plugins/client/index.ts +5 -1
  189. package/src/plugins/cms/api/index.ts +4 -0
  190. package/src/plugins/cms/api/mutations.ts +84 -0
  191. package/src/plugins/cms/api/plugin.ts +23 -17
  192. package/src/plugins/cms/client/plugin.tsx +18 -21
  193. package/src/plugins/cms/types.ts +7 -7
  194. package/src/plugins/form-builder/api/plugin.ts +64 -64
  195. package/src/plugins/form-builder/client/plugin.tsx +19 -18
  196. package/src/plugins/form-builder/types.ts +19 -24
  197. package/src/plugins/kanban/api/index.ts +6 -0
  198. package/src/plugins/kanban/api/mutations.ts +169 -0
  199. package/src/plugins/kanban/api/plugin.ts +123 -136
  200. package/src/plugins/kanban/client/hooks/kanban-hooks.tsx +4 -0
  201. package/src/plugins/kanban/client/plugin.tsx +35 -41
  202. package/src/plugins/ui-builder/client/components/pages/page-builder-page.internal.tsx +132 -0
  203. package/src/plugins/ui-builder/client/plugin.tsx +11 -10
  204. package/src/plugins/ui-builder/types.ts +4 -4
  205. package/src/plugins/utils.ts +92 -1
  206. package/dist/shared/{stack.CBON0dWL.d.mts → stack.BQmuNl5p.d.cts} +2 -2
  207. package/dist/shared/{stack.CBON0dWL.d.ts → stack.BQmuNl5p.d.mts} +2 -2
  208. package/dist/shared/{stack.CBON0dWL.d.cts → stack.BQmuNl5p.d.ts} +2 -2
@@ -17,6 +17,11 @@ import {
17
17
  } from "../schemas";
18
18
  import type { Conversation, ConversationWithMessages, Message } from "../types";
19
19
  import { getAllConversations, getConversationById } from "./getters";
20
+ import {
21
+ BUILT_IN_PAGE_TOOL_ROUTE_ALLOWLIST,
22
+ BUILT_IN_PAGE_TOOL_SCHEMAS,
23
+ } from "./page-tools";
24
+ import { runHookWithShim } from "../../utils";
20
25
 
21
26
  /**
22
27
  * Context passed to AI Chat API hooks
@@ -36,48 +41,46 @@ export interface ChatApiContext<TBody = any, TParams = any, TQuery = any> {
36
41
  */
37
42
  export interface AiChatBackendHooks {
38
43
  // ============== Authorization Hooks ==============
39
- // Return false to deny access
44
+ // Throw an error to deny access
40
45
 
41
46
  /**
42
- * Called before processing a chat message. Return false to deny access.
47
+ * Called before processing a chat message. Throw an error to deny access.
43
48
  * @param messages - Array of messages being sent
44
49
  * @param context - Request context with headers, etc.
45
50
  */
46
51
  onBeforeChat?: (
47
52
  messages: Array<{ role: string; content: string }>,
48
53
  context: ChatApiContext,
49
- ) => Promise<boolean> | boolean;
54
+ ) => Promise<void> | void;
50
55
 
51
56
  /**
52
- * Called before listing conversations. Return false to deny access.
57
+ * Called before listing conversations. Throw an error to deny access.
53
58
  * @param context - Request context with headers, etc.
54
59
  */
55
- onBeforeListConversations?: (
56
- context: ChatApiContext,
57
- ) => Promise<boolean> | boolean;
60
+ onBeforeListConversations?: (context: ChatApiContext) => Promise<void> | void;
58
61
 
59
62
  /**
60
- * Called before getting a single conversation. Return false to deny access.
63
+ * Called before getting a single conversation. Throw an error to deny access.
61
64
  * @param conversationId - ID of the conversation being accessed
62
65
  * @param context - Request context with headers, etc.
63
66
  */
64
67
  onBeforeGetConversation?: (
65
68
  conversationId: string,
66
69
  context: ChatApiContext,
67
- ) => Promise<boolean> | boolean;
70
+ ) => Promise<void> | void;
68
71
 
69
72
  /**
70
- * Called before creating a conversation. Return false to deny access.
73
+ * Called before creating a conversation. Throw an error to deny access.
71
74
  * @param data - Conversation data being created
72
75
  * @param context - Request context with headers, etc.
73
76
  */
74
77
  onBeforeCreateConversation?: (
75
78
  data: { id?: string; title?: string },
76
79
  context: ChatApiContext,
77
- ) => Promise<boolean> | boolean;
80
+ ) => Promise<void> | void;
78
81
 
79
82
  /**
80
- * Called before updating a conversation. Return false to deny access.
83
+ * Called before updating a conversation. Throw an error to deny access.
81
84
  * @param conversationId - ID of the conversation being updated
82
85
  * @param data - Updated conversation data
83
86
  * @param context - Request context with headers, etc.
@@ -86,17 +89,34 @@ export interface AiChatBackendHooks {
86
89
  conversationId: string,
87
90
  data: { title?: string },
88
91
  context: ChatApiContext,
89
- ) => Promise<boolean> | boolean;
92
+ ) => Promise<void> | void;
90
93
 
91
94
  /**
92
- * Called before deleting a conversation. Return false to deny access.
95
+ * Called before deleting a conversation. Throw an error to deny access.
93
96
  * @param conversationId - ID of the conversation being deleted
94
97
  * @param context - Request context with headers, etc.
95
98
  */
96
99
  onBeforeDeleteConversation?: (
97
100
  conversationId: string,
98
101
  context: ChatApiContext,
99
- ) => Promise<boolean> | boolean;
102
+ ) => Promise<void> | void;
103
+
104
+ /**
105
+ * Called after the structural routeName/allowlist validation, with the list
106
+ * of tool names that passed. Return a filtered subset to further restrict
107
+ * which tools the LLM sees, or return [] to suppress all page tools.
108
+ * Throw an Error to abort the entire chat request with a 403 response.
109
+ * Not called when no tools passed the structural validation step.
110
+ *
111
+ * @param toolNames - Names that passed the routeName allowlist check
112
+ * @param routeName - routeName claimed by the request (may be undefined)
113
+ * @param context - Full request context (headers, body, etc.)
114
+ */
115
+ onBeforeToolsActivated?: (
116
+ toolNames: string[],
117
+ routeName: string | undefined,
118
+ context: ChatApiContext,
119
+ ) => Promise<string[]> | string[];
100
120
 
101
121
  // ============== Lifecycle Hooks ==============
102
122
 
@@ -232,6 +252,32 @@ export type AiChatMode = "authenticated" | "public";
232
252
  /**
233
253
  * Configuration for AI Chat backend plugin
234
254
  */
255
+ /**
256
+ * Extracts only the literal (non-index-signature) keys from a type.
257
+ * For `Record<string, T>` this resolves to `never`, so collision checks are
258
+ * skipped when the tools map is typed with a broad string index.
259
+ */
260
+ type KnownKeys<T> = {
261
+ [K in keyof T]: string extends K ? never : K;
262
+ }[keyof T];
263
+
264
+ /**
265
+ * Ensures `TClientTools` has no keys that are also literal keys in `TTools`.
266
+ * Colliding keys are mapped to `never`, which produces a compile-time error
267
+ * at the point of the duplicate key. When `TTools` uses a string index
268
+ * signature the check is skipped to avoid false positives.
269
+ */
270
+ type NoKeyCollision<
271
+ TTools,
272
+ TClientTools extends Record<string, Tool>,
273
+ > = KnownKeys<TTools> & keyof TClientTools extends never
274
+ ? TClientTools
275
+ : {
276
+ [K in keyof TClientTools]: K extends KnownKeys<TTools>
277
+ ? never // duplicate of a server-side tool — remove from clientToolSchemas
278
+ : TClientTools[K];
279
+ };
280
+
235
281
  export interface AiChatBackendConfig {
236
282
  /**
237
283
  * The language model to use for chat completions.
@@ -269,6 +315,31 @@ export interface AiChatBackendConfig {
269
315
  */
270
316
  tools?: Record<string, Tool>;
271
317
 
318
+ /**
319
+ * Enable route-aware page tools.
320
+ * When true, the server will include tool schemas for client-side page tools
321
+ * (e.g. fillBlogForm, updatePageLayers) based on the availableTools list
322
+ * sent with each request.
323
+ * @default false
324
+ */
325
+ enablePageTools?: boolean;
326
+
327
+ /**
328
+ * Custom client-side tool schemas for non-BTST pages.
329
+ * Merged with built-in page tool schemas (fillBlogForm, updatePageLayers).
330
+ * Only included when enablePageTools is true and the tool name appears in
331
+ * the availableTools list sent with the request.
332
+ *
333
+ * @example
334
+ * clientToolSchemas: {
335
+ * addToCart: tool({
336
+ * description: "Add current product to cart",
337
+ * parameters: z.object({ quantity: z.number().int().min(1) }),
338
+ * }),
339
+ * }
340
+ */
341
+ clientToolSchemas?: Record<string, Tool>;
342
+
272
343
  /**
273
344
  * Optional hooks for customizing plugin behavior
274
345
  */
@@ -282,7 +353,15 @@ export interface AiChatBackendConfig {
282
353
  *
283
354
  * @param config - Configuration including model, tools, and optional hooks
284
355
  */
285
- export const aiChatBackendPlugin = (config: AiChatBackendConfig) =>
356
+ export const aiChatBackendPlugin = <
357
+ TTools extends Record<string, Tool> = Record<never, Tool>,
358
+ TClientTools extends Record<string, Tool> = Record<never, Tool>,
359
+ >(
360
+ config: Omit<AiChatBackendConfig, "tools" | "clientToolSchemas"> & {
361
+ tools?: TTools;
362
+ clientToolSchemas?: NoKeyCollision<TTools, TClientTools>;
363
+ },
364
+ ) =>
286
365
  defineBackendPlugin({
287
366
  name: "ai-chat",
288
367
  // Always include db schema - in public mode we just don't use it
@@ -350,7 +429,13 @@ export const aiChatBackendPlugin = (config: AiChatBackendConfig) =>
350
429
  body: chatRequestSchema,
351
430
  },
352
431
  async (ctx) => {
353
- const { messages: rawMessages, conversationId } = ctx.body;
432
+ const {
433
+ messages: rawMessages,
434
+ conversationId,
435
+ pageContext,
436
+ availableTools,
437
+ routeName,
438
+ } = ctx.body;
354
439
  const uiMessages = rawMessages as UIMessage[];
355
440
 
356
441
  const context: ChatApiContext = {
@@ -366,15 +451,11 @@ export const aiChatBackendPlugin = (config: AiChatBackendConfig) =>
366
451
  role: msg.role,
367
452
  content: getMessageTextContent(msg),
368
453
  }));
369
- const canChat = await config.hooks.onBeforeChat(
370
- messagesForHook,
371
- context,
454
+ await runHookWithShim(
455
+ () => config.hooks!.onBeforeChat!(messagesForHook, context),
456
+ ctx.error,
457
+ "Unauthorized: Cannot start chat",
372
458
  );
373
- if (!canChat) {
374
- throw ctx.error(403, {
375
- message: "Unauthorized: Cannot start chat",
376
- });
377
- }
378
459
  }
379
460
 
380
461
  const firstMessage = uiMessages[0];
@@ -388,22 +469,107 @@ export const aiChatBackendPlugin = (config: AiChatBackendConfig) =>
388
469
  // Convert UIMessages to CoreMessages for streamText
389
470
  const modelMessages = convertToModelMessages(uiMessages);
390
471
 
391
- // Add system prompt if configured
392
- const messagesWithSystem = config.systemPrompt
472
+ // Build system prompt: base config + optional page context
473
+ const pageContextContent =
474
+ pageContext && pageContext.trim()
475
+ ? `\n\nCurrent page context:\n${pageContext}`
476
+ : "";
477
+ const systemContent = config.systemPrompt
478
+ ? `${config.systemPrompt}${pageContextContent}`
479
+ : pageContextContent || undefined;
480
+
481
+ const messagesWithSystem = systemContent
393
482
  ? [
394
- { role: "system" as const, content: config.systemPrompt },
483
+ { role: "system" as const, content: systemContent },
395
484
  ...modelMessages,
396
485
  ]
397
486
  : modelMessages;
398
487
 
488
+ // Merge page tool schemas when enablePageTools is on.
489
+ // Built-in schemas are only included when the request's routeName is in
490
+ // the tool's allowlist — this prevents a page from claiming tools that
491
+ // are intended for a different route (e.g. requesting updatePageLayers
492
+ // from a blog page). Consumer clientToolSchemas are trusted as-is.
493
+ const activePageTools: Record<string, Tool> =
494
+ config.enablePageTools &&
495
+ availableTools &&
496
+ availableTools.length > 0
497
+ ? (() => {
498
+ const consumerSchemas: Record<string, Tool> =
499
+ (config.clientToolSchemas as Record<string, Tool>) ?? {};
500
+ return Object.fromEntries(
501
+ availableTools
502
+ .filter((name) => {
503
+ // Built-in tool: require routeName to be in its allowlist
504
+ if (name in BUILT_IN_PAGE_TOOL_SCHEMAS) {
505
+ const allowed =
506
+ BUILT_IN_PAGE_TOOL_ROUTE_ALLOWLIST[name];
507
+ return (
508
+ allowed &&
509
+ routeName &&
510
+ allowed.includes(routeName)
511
+ );
512
+ }
513
+ // Consumer-defined tool: allow if schema is registered
514
+ return name in consumerSchemas;
515
+ })
516
+ .map((name) => {
517
+ const schema =
518
+ BUILT_IN_PAGE_TOOL_SCHEMAS[name] ??
519
+ consumerSchemas[name]!;
520
+ return [name, schema];
521
+ }),
522
+ );
523
+ })()
524
+ : {};
525
+
526
+ // Consumer hook: user-level tool authorization.
527
+ // Runs after the structural routeName allowlist check.
528
+ // A thrown Error is caught and returned as a 403 response,
529
+ // consistent with how onBeforeChat handles return false → 403.
530
+ if (
531
+ config.hooks?.onBeforeToolsActivated &&
532
+ Object.keys(activePageTools).length > 0
533
+ ) {
534
+ try {
535
+ const allowed = await config.hooks.onBeforeToolsActivated(
536
+ Object.keys(activePageTools),
537
+ routeName,
538
+ context,
539
+ );
540
+ const allowedSet = new Set(allowed);
541
+ for (const key of Object.keys(activePageTools)) {
542
+ if (!allowedSet.has(key)) {
543
+ delete activePageTools[key];
544
+ }
545
+ }
546
+ } catch (hookError) {
547
+ throw ctx.error(403, {
548
+ message:
549
+ hookError instanceof Error
550
+ ? hookError.message
551
+ : "Unauthorized: Tool activation denied",
552
+ });
553
+ }
554
+ }
555
+
556
+ // Page tools are layered under server-side tools so that a
557
+ // clientToolSchemas entry with the same name as a tool in
558
+ // config.tools never silently drops its `execute` function.
559
+ // Server-side tools always win on collision.
560
+ const mergedTools =
561
+ Object.keys(activePageTools).length > 0
562
+ ? { ...activePageTools, ...config.tools }
563
+ : config.tools;
564
+
399
565
  // PUBLIC MODE: Stream without persistence
400
566
  if (isPublicMode) {
401
567
  const result = streamText({
402
568
  model: config.model,
403
569
  messages: messagesWithSystem,
404
- tools: config.tools,
570
+ tools: mergedTools,
405
571
  // Enable multi-step tool calls if tools are configured
406
- ...(config.tools ? { stopWhen: stepCountIs(5) } : {}),
572
+ ...(mergedTools ? { stopWhen: stepCountIs(5) } : {}),
407
573
  });
408
574
 
409
575
  return result.toUIMessageStreamResponse({
@@ -557,9 +723,9 @@ export const aiChatBackendPlugin = (config: AiChatBackendConfig) =>
557
723
  const result = streamText({
558
724
  model: config.model,
559
725
  messages: messagesWithSystem,
560
- tools: config.tools,
726
+ tools: mergedTools,
561
727
  // Enable multi-step tool calls if tools are configured
562
- ...(config.tools ? { stopWhen: stepCountIs(5) } : {}),
728
+ ...(mergedTools ? { stopWhen: stepCountIs(5) } : {}),
563
729
  onFinish: async (completion: { text: string }) => {
564
730
  // Wrap in try-catch since this runs after the response is sent
565
731
  // and errors would otherwise become unhandled promise rejections
@@ -677,15 +843,15 @@ export const aiChatBackendPlugin = (config: AiChatBackendConfig) =>
677
843
 
678
844
  // Authorization hook
679
845
  if (config.hooks?.onBeforeCreateConversation) {
680
- const canCreate = await config.hooks.onBeforeCreateConversation(
681
- { id, title },
682
- context,
846
+ await runHookWithShim(
847
+ () =>
848
+ config.hooks!.onBeforeCreateConversation!(
849
+ { id, title },
850
+ context,
851
+ ),
852
+ ctx.error,
853
+ "Unauthorized: Cannot create conversation",
683
854
  );
684
- if (!canCreate) {
685
- throw ctx.error(403, {
686
- message: "Unauthorized: Cannot create conversation",
687
- });
688
- }
689
855
  }
690
856
 
691
857
  const newConv = await adapter.create<Conversation>({
@@ -743,13 +909,11 @@ export const aiChatBackendPlugin = (config: AiChatBackendConfig) =>
743
909
 
744
910
  // Authorization hook
745
911
  if (config.hooks?.onBeforeListConversations) {
746
- const canList =
747
- await config.hooks.onBeforeListConversations(context);
748
- if (!canList) {
749
- throw ctx.error(403, {
750
- message: "Unauthorized: Cannot list conversations",
751
- });
752
- }
912
+ await runHookWithShim(
913
+ () => config.hooks!.onBeforeListConversations!(context),
914
+ ctx.error,
915
+ "Unauthorized: Cannot list conversations",
916
+ );
753
917
  }
754
918
 
755
919
  // Build where conditions - filter by userId if set
@@ -820,15 +984,11 @@ export const aiChatBackendPlugin = (config: AiChatBackendConfig) =>
820
984
 
821
985
  // Authorization hook
822
986
  if (config.hooks?.onBeforeGetConversation) {
823
- const canGet = await config.hooks.onBeforeGetConversation(
824
- id,
825
- context,
987
+ await runHookWithShim(
988
+ () => config.hooks!.onBeforeGetConversation!(id, context),
989
+ ctx.error,
990
+ "Unauthorized: Cannot get conversation",
826
991
  );
827
- if (!canGet) {
828
- throw ctx.error(403, {
829
- message: "Unauthorized: Cannot get conversation",
830
- });
831
- }
832
992
  }
833
993
 
834
994
  // Fetch conversation with messages in a single query using join
@@ -945,16 +1105,16 @@ export const aiChatBackendPlugin = (config: AiChatBackendConfig) =>
945
1105
 
946
1106
  // Authorization hook
947
1107
  if (config.hooks?.onBeforeUpdateConversation) {
948
- const canUpdate = await config.hooks.onBeforeUpdateConversation(
949
- id,
950
- { title },
951
- context,
1108
+ await runHookWithShim(
1109
+ () =>
1110
+ config.hooks!.onBeforeUpdateConversation!(
1111
+ id,
1112
+ { title },
1113
+ context,
1114
+ ),
1115
+ ctx.error,
1116
+ "Unauthorized: Cannot update conversation",
952
1117
  );
953
- if (!canUpdate) {
954
- throw ctx.error(403, {
955
- message: "Unauthorized: Cannot update conversation",
956
- });
957
- }
958
1118
  }
959
1119
 
960
1120
  const updated = await adapter.update<Conversation>({
@@ -1040,15 +1200,11 @@ export const aiChatBackendPlugin = (config: AiChatBackendConfig) =>
1040
1200
 
1041
1201
  // Authorization hook
1042
1202
  if (config.hooks?.onBeforeDeleteConversation) {
1043
- const canDelete = await config.hooks.onBeforeDeleteConversation(
1044
- id,
1045
- context,
1203
+ await runHookWithShim(
1204
+ () => config.hooks!.onBeforeDeleteConversation!(id, context),
1205
+ ctx.error,
1206
+ "Unauthorized: Cannot delete conversation",
1046
1207
  );
1047
- if (!canDelete) {
1048
- throw ctx.error(403, {
1049
- message: "Unauthorized: Cannot delete conversation",
1050
- });
1051
- }
1052
1208
  }
1053
1209
 
1054
1210
  // Messages are automatically deleted via cascade (onDelete: "cascade")
@@ -260,14 +260,14 @@ export function ChatInput({
260
260
  )}
261
261
 
262
262
  {/* Text Input */}
263
- <div className="relative flex-1">
263
+ <div className="relative flex-1 min-w-0">
264
264
  <Textarea
265
265
  value={input}
266
266
  onChange={handleInputChange}
267
267
  onKeyDown={handleKeyDown}
268
268
  placeholder={placeholder || localization.CHAT_PLACEHOLDER}
269
269
  className={cn(
270
- "resize-none pr-12",
270
+ "resize-none pr-12 max-w-full",
271
271
  isCompact
272
272
  ? "min-h-[40px] max-h-[120px] py-2"
273
273
  : "min-h-[50px] max-h-[200px] py-3",