@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
@@ -40,8 +40,7 @@ function ComposedRoute({
40
40
  }) {
41
41
  if (PageComponent) {
42
42
  const content = /* @__PURE__ */ jsxRuntime.jsx(PageComponent, { ...props });
43
- const isBrowser = typeof window !== "undefined";
44
- const suspenseFallback = isBrowser && LoadingComponent ? /* @__PURE__ */ jsxRuntime.jsx(LoadingComponent, {}) : null;
43
+ const suspenseFallback = LoadingComponent ? /* @__PURE__ */ jsxRuntime.jsx(LoadingComponent, {}) : null;
45
44
  if (ErrorComponent) {
46
45
  return /* @__PURE__ */ jsxRuntime.jsx(React.Suspense, { fallback: suspenseFallback, children: /* @__PURE__ */ jsxRuntime.jsx(
47
46
  errorBoundary.ErrorBoundary,
@@ -38,8 +38,7 @@ function ComposedRoute({
38
38
  }) {
39
39
  if (PageComponent) {
40
40
  const content = /* @__PURE__ */ jsx(PageComponent, { ...props });
41
- const isBrowser = typeof window !== "undefined";
42
- const suspenseFallback = isBrowser && LoadingComponent ? /* @__PURE__ */ jsx(LoadingComponent, {}) : null;
41
+ const suspenseFallback = LoadingComponent ? /* @__PURE__ */ jsx(LoadingComponent, {}) : null;
43
42
  if (ErrorComponent) {
44
43
  return /* @__PURE__ */ jsx(Suspense, { fallback: suspenseFallback, children: /* @__PURE__ */ jsx(
45
44
  ErrorBoundary,
@@ -0,0 +1,71 @@
1
+ 'use strict';
2
+
3
+ const ai = require('ai');
4
+ const z = require('zod');
5
+
6
+ const BUILT_IN_PAGE_TOOL_ROUTE_ALLOWLIST = {
7
+ /** Blog new-post and edit-post pages */
8
+ fillBlogForm: ["blog-new-post", "blog-edit-post", "newPost", "editPost"],
9
+ /** UI builder edit page */
10
+ updatePageLayers: ["ui-builder-edit-page"]
11
+ };
12
+ const BUILT_IN_PAGE_TOOL_SCHEMAS = {
13
+ /**
14
+ * Fill in the blog post editor form fields.
15
+ * Registered by blog new/edit page via useRegisterPageAIContext.
16
+ */
17
+ fillBlogForm: ai.tool({
18
+ description: "Fill in the blog post editor form fields. Call this when the user asks to write, draft, or populate a blog post. You can fill any combination of title, content, excerpt, and tags.",
19
+ inputSchema: z.z.object({
20
+ title: z.z.string().optional().describe("The post title"),
21
+ content: z.z.string().optional().describe(
22
+ "Full markdown content for the post body. Use proper markdown formatting with headings, lists, etc."
23
+ ),
24
+ excerpt: z.z.string().optional().describe("A short summary/excerpt of the post (1-2 sentences)"),
25
+ tags: z.z.array(z.z.string()).optional().describe("Array of tag names to apply to the post")
26
+ })
27
+ }),
28
+ /**
29
+ * Replace the UI builder page layers with new ones.
30
+ * Registered by the UI builder edit page via useRegisterPageAIContext.
31
+ */
32
+ updatePageLayers: ai.tool({
33
+ description: `Replace the UI builder page component layers. Call this when the user asks to change, add, redesign, or update the page layout and components.
34
+
35
+ Rules:
36
+ - Provide the COMPLETE layer tree, not a partial diff. The entire tree will replace the current layers.
37
+ - Only use component types that appear in the "Available Component Types" list in the page context.
38
+ - Every layer must have a unique \`id\` string (e.g. "hero-section", "card-title-1").
39
+ - The \`type\` field must exactly match a name from the component registry (e.g. "div", "Button", "Card", "Flexbox").
40
+ - The \`name\` field is the human-readable label shown in the layers panel.
41
+ - \`props\` contains component-specific props (className uses Tailwind classes).
42
+ - \`children\` is either an array of child ComponentLayer objects, or a plain string for text content.
43
+ - Use \`Flexbox\` or \`Grid\` for layout instead of raw div flex/grid when possible.
44
+ - Preserve any layers the user has not asked to change \u2014 read the current layers from the page context first.
45
+ - ALWAYS use shadcn/ui semantic color tokens in className (e.g. bg-background, bg-card, bg-primary, text-foreground, text-muted-foreground, text-primary-foreground, border-border) instead of hardcoded Tailwind colors like bg-white, bg-gray-*, text-black, etc. This ensures the UI automatically adapts to light and dark themes.`,
46
+ inputSchema: z.z.object({
47
+ layers: z.z.array(
48
+ z.z.object({
49
+ id: z.z.string().describe("Unique identifier for this layer"),
50
+ type: z.z.string().describe(
51
+ "Component type \u2014 must match a key in the component registry (e.g. 'div', 'Button', 'Card', 'Flexbox')"
52
+ ),
53
+ name: z.z.string().describe(
54
+ "Human-readable display name shown in the layers panel"
55
+ ),
56
+ props: z.z.record(z.z.string(), z.z.any()).describe(
57
+ "Component props object. Use Tailwind classes for className. See the component registry for valid props per type."
58
+ ),
59
+ children: z.z.any().optional().describe(
60
+ "Child layers (array of ComponentLayer) or plain text string"
61
+ )
62
+ })
63
+ ).describe(
64
+ "Complete replacement layer tree. Must include ALL layers for the page, not just changed ones."
65
+ )
66
+ })
67
+ })
68
+ };
69
+
70
+ exports.BUILT_IN_PAGE_TOOL_ROUTE_ALLOWLIST = BUILT_IN_PAGE_TOOL_ROUTE_ALLOWLIST;
71
+ exports.BUILT_IN_PAGE_TOOL_SCHEMAS = BUILT_IN_PAGE_TOOL_SCHEMAS;
@@ -0,0 +1,68 @@
1
+ import { tool } from 'ai';
2
+ import { z } from 'zod';
3
+
4
+ const BUILT_IN_PAGE_TOOL_ROUTE_ALLOWLIST = {
5
+ /** Blog new-post and edit-post pages */
6
+ fillBlogForm: ["blog-new-post", "blog-edit-post", "newPost", "editPost"],
7
+ /** UI builder edit page */
8
+ updatePageLayers: ["ui-builder-edit-page"]
9
+ };
10
+ const BUILT_IN_PAGE_TOOL_SCHEMAS = {
11
+ /**
12
+ * Fill in the blog post editor form fields.
13
+ * Registered by blog new/edit page via useRegisterPageAIContext.
14
+ */
15
+ fillBlogForm: tool({
16
+ description: "Fill in the blog post editor form fields. Call this when the user asks to write, draft, or populate a blog post. You can fill any combination of title, content, excerpt, and tags.",
17
+ inputSchema: z.object({
18
+ title: z.string().optional().describe("The post title"),
19
+ content: z.string().optional().describe(
20
+ "Full markdown content for the post body. Use proper markdown formatting with headings, lists, etc."
21
+ ),
22
+ excerpt: z.string().optional().describe("A short summary/excerpt of the post (1-2 sentences)"),
23
+ tags: z.array(z.string()).optional().describe("Array of tag names to apply to the post")
24
+ })
25
+ }),
26
+ /**
27
+ * Replace the UI builder page layers with new ones.
28
+ * Registered by the UI builder edit page via useRegisterPageAIContext.
29
+ */
30
+ updatePageLayers: tool({
31
+ description: `Replace the UI builder page component layers. Call this when the user asks to change, add, redesign, or update the page layout and components.
32
+
33
+ Rules:
34
+ - Provide the COMPLETE layer tree, not a partial diff. The entire tree will replace the current layers.
35
+ - Only use component types that appear in the "Available Component Types" list in the page context.
36
+ - Every layer must have a unique \`id\` string (e.g. "hero-section", "card-title-1").
37
+ - The \`type\` field must exactly match a name from the component registry (e.g. "div", "Button", "Card", "Flexbox").
38
+ - The \`name\` field is the human-readable label shown in the layers panel.
39
+ - \`props\` contains component-specific props (className uses Tailwind classes).
40
+ - \`children\` is either an array of child ComponentLayer objects, or a plain string for text content.
41
+ - Use \`Flexbox\` or \`Grid\` for layout instead of raw div flex/grid when possible.
42
+ - Preserve any layers the user has not asked to change \u2014 read the current layers from the page context first.
43
+ - ALWAYS use shadcn/ui semantic color tokens in className (e.g. bg-background, bg-card, bg-primary, text-foreground, text-muted-foreground, text-primary-foreground, border-border) instead of hardcoded Tailwind colors like bg-white, bg-gray-*, text-black, etc. This ensures the UI automatically adapts to light and dark themes.`,
44
+ inputSchema: z.object({
45
+ layers: z.array(
46
+ z.object({
47
+ id: z.string().describe("Unique identifier for this layer"),
48
+ type: z.string().describe(
49
+ "Component type \u2014 must match a key in the component registry (e.g. 'div', 'Button', 'Card', 'Flexbox')"
50
+ ),
51
+ name: z.string().describe(
52
+ "Human-readable display name shown in the layers panel"
53
+ ),
54
+ props: z.record(z.string(), z.any()).describe(
55
+ "Component props object. Use Tailwind classes for className. See the component registry for valid props per type."
56
+ ),
57
+ children: z.any().optional().describe(
58
+ "Child layers (array of ComponentLayer) or plain text string"
59
+ )
60
+ })
61
+ ).describe(
62
+ "Complete replacement layer tree. Must include ALL layers for the page, not just changed ones."
63
+ )
64
+ })
65
+ })
66
+ };
67
+
68
+ export { BUILT_IN_PAGE_TOOL_ROUTE_ALLOWLIST, BUILT_IN_PAGE_TOOL_SCHEMAS };
@@ -5,6 +5,7 @@ const ai = require('ai');
5
5
  const db = require('../db.cjs');
6
6
  const schemas = require('../schemas.cjs');
7
7
  const getters = require('./getters.cjs');
8
+ const pageTools = require('./page-tools.cjs');
8
9
 
9
10
  const aiChatBackendPlugin = (config) => api.defineBackendPlugin({
10
11
  name: "ai-chat",
@@ -52,7 +53,13 @@ const aiChatBackendPlugin = (config) => api.defineBackendPlugin({
52
53
  body: schemas.chatRequestSchema
53
54
  },
54
55
  async (ctx) => {
55
- const { messages: rawMessages, conversationId } = ctx.body;
56
+ const {
57
+ messages: rawMessages,
58
+ conversationId,
59
+ pageContext,
60
+ availableTools,
61
+ routeName
62
+ } = ctx.body;
56
63
  const uiMessages = rawMessages;
57
64
  const context = {
58
65
  body: ctx.body,
@@ -83,17 +90,57 @@ const aiChatBackendPlugin = (config) => api.defineBackendPlugin({
83
90
  }
84
91
  const firstMessageContent = getMessageTextContent(firstMessage);
85
92
  const modelMessages = ai.convertToModelMessages(uiMessages);
86
- const messagesWithSystem = config.systemPrompt ? [
87
- { role: "system", content: config.systemPrompt },
93
+ const pageContextContent = pageContext && pageContext.trim() ? `
94
+
95
+ Current page context:
96
+ ${pageContext}` : "";
97
+ const systemContent = config.systemPrompt ? `${config.systemPrompt}${pageContextContent}` : pageContextContent || void 0;
98
+ const messagesWithSystem = systemContent ? [
99
+ { role: "system", content: systemContent },
88
100
  ...modelMessages
89
101
  ] : modelMessages;
102
+ const activePageTools = config.enablePageTools && availableTools && availableTools.length > 0 ? (() => {
103
+ const consumerSchemas = config.clientToolSchemas ?? {};
104
+ return Object.fromEntries(
105
+ availableTools.filter((name) => {
106
+ if (name in pageTools.BUILT_IN_PAGE_TOOL_SCHEMAS) {
107
+ const allowed = pageTools.BUILT_IN_PAGE_TOOL_ROUTE_ALLOWLIST[name];
108
+ return allowed && routeName && allowed.includes(routeName);
109
+ }
110
+ return name in consumerSchemas;
111
+ }).map((name) => {
112
+ const schema = pageTools.BUILT_IN_PAGE_TOOL_SCHEMAS[name] ?? consumerSchemas[name];
113
+ return [name, schema];
114
+ })
115
+ );
116
+ })() : {};
117
+ if (config.hooks?.onBeforeToolsActivated && Object.keys(activePageTools).length > 0) {
118
+ try {
119
+ const allowed = await config.hooks.onBeforeToolsActivated(
120
+ Object.keys(activePageTools),
121
+ routeName,
122
+ context
123
+ );
124
+ const allowedSet = new Set(allowed);
125
+ for (const key of Object.keys(activePageTools)) {
126
+ if (!allowedSet.has(key)) {
127
+ delete activePageTools[key];
128
+ }
129
+ }
130
+ } catch (hookError) {
131
+ throw ctx.error(403, {
132
+ message: hookError instanceof Error ? hookError.message : "Unauthorized: Tool activation denied"
133
+ });
134
+ }
135
+ }
136
+ const mergedTools = Object.keys(activePageTools).length > 0 ? { ...activePageTools, ...config.tools } : config.tools;
90
137
  if (isPublicMode) {
91
138
  const result2 = ai.streamText({
92
139
  model: config.model,
93
140
  messages: messagesWithSystem,
94
- tools: config.tools,
141
+ tools: mergedTools,
95
142
  // Enable multi-step tool calls if tools are configured
96
- ...config.tools ? { stopWhen: ai.stepCountIs(5) } : {}
143
+ ...mergedTools ? { stopWhen: ai.stepCountIs(5) } : {}
97
144
  });
98
145
  return result2.toUIMessageStreamResponse({
99
146
  originalMessages: uiMessages
@@ -201,9 +248,9 @@ const aiChatBackendPlugin = (config) => api.defineBackendPlugin({
201
248
  const result = ai.streamText({
202
249
  model: config.model,
203
250
  messages: messagesWithSystem,
204
- tools: config.tools,
251
+ tools: mergedTools,
205
252
  // Enable multi-step tool calls if tools are configured
206
- ...config.tools ? { stopWhen: ai.stepCountIs(5) } : {},
253
+ ...mergedTools ? { stopWhen: ai.stepCountIs(5) } : {},
207
254
  onFinish: async (completion) => {
208
255
  try {
209
256
  const assistantParts = completion.text ? [{ type: "text", text: completion.text }] : [];
@@ -3,6 +3,7 @@ import { convertToModelMessages, streamText, stepCountIs } from 'ai';
3
3
  import { aiChatSchema } from '../db.mjs';
4
4
  import { chatRequestSchema, createConversationSchema, updateConversationSchema } from '../schemas.mjs';
5
5
  import { getConversationById, getAllConversations } from './getters.mjs';
6
+ import { BUILT_IN_PAGE_TOOL_SCHEMAS, BUILT_IN_PAGE_TOOL_ROUTE_ALLOWLIST } from './page-tools.mjs';
6
7
 
7
8
  const aiChatBackendPlugin = (config) => defineBackendPlugin({
8
9
  name: "ai-chat",
@@ -50,7 +51,13 @@ const aiChatBackendPlugin = (config) => defineBackendPlugin({
50
51
  body: chatRequestSchema
51
52
  },
52
53
  async (ctx) => {
53
- const { messages: rawMessages, conversationId } = ctx.body;
54
+ const {
55
+ messages: rawMessages,
56
+ conversationId,
57
+ pageContext,
58
+ availableTools,
59
+ routeName
60
+ } = ctx.body;
54
61
  const uiMessages = rawMessages;
55
62
  const context = {
56
63
  body: ctx.body,
@@ -81,17 +88,57 @@ const aiChatBackendPlugin = (config) => defineBackendPlugin({
81
88
  }
82
89
  const firstMessageContent = getMessageTextContent(firstMessage);
83
90
  const modelMessages = convertToModelMessages(uiMessages);
84
- const messagesWithSystem = config.systemPrompt ? [
85
- { role: "system", content: config.systemPrompt },
91
+ const pageContextContent = pageContext && pageContext.trim() ? `
92
+
93
+ Current page context:
94
+ ${pageContext}` : "";
95
+ const systemContent = config.systemPrompt ? `${config.systemPrompt}${pageContextContent}` : pageContextContent || void 0;
96
+ const messagesWithSystem = systemContent ? [
97
+ { role: "system", content: systemContent },
86
98
  ...modelMessages
87
99
  ] : modelMessages;
100
+ const activePageTools = config.enablePageTools && availableTools && availableTools.length > 0 ? (() => {
101
+ const consumerSchemas = config.clientToolSchemas ?? {};
102
+ return Object.fromEntries(
103
+ availableTools.filter((name) => {
104
+ if (name in BUILT_IN_PAGE_TOOL_SCHEMAS) {
105
+ const allowed = BUILT_IN_PAGE_TOOL_ROUTE_ALLOWLIST[name];
106
+ return allowed && routeName && allowed.includes(routeName);
107
+ }
108
+ return name in consumerSchemas;
109
+ }).map((name) => {
110
+ const schema = BUILT_IN_PAGE_TOOL_SCHEMAS[name] ?? consumerSchemas[name];
111
+ return [name, schema];
112
+ })
113
+ );
114
+ })() : {};
115
+ if (config.hooks?.onBeforeToolsActivated && Object.keys(activePageTools).length > 0) {
116
+ try {
117
+ const allowed = await config.hooks.onBeforeToolsActivated(
118
+ Object.keys(activePageTools),
119
+ routeName,
120
+ context
121
+ );
122
+ const allowedSet = new Set(allowed);
123
+ for (const key of Object.keys(activePageTools)) {
124
+ if (!allowedSet.has(key)) {
125
+ delete activePageTools[key];
126
+ }
127
+ }
128
+ } catch (hookError) {
129
+ throw ctx.error(403, {
130
+ message: hookError instanceof Error ? hookError.message : "Unauthorized: Tool activation denied"
131
+ });
132
+ }
133
+ }
134
+ const mergedTools = Object.keys(activePageTools).length > 0 ? { ...activePageTools, ...config.tools } : config.tools;
88
135
  if (isPublicMode) {
89
136
  const result2 = streamText({
90
137
  model: config.model,
91
138
  messages: messagesWithSystem,
92
- tools: config.tools,
139
+ tools: mergedTools,
93
140
  // Enable multi-step tool calls if tools are configured
94
- ...config.tools ? { stopWhen: stepCountIs(5) } : {}
141
+ ...mergedTools ? { stopWhen: stepCountIs(5) } : {}
95
142
  });
96
143
  return result2.toUIMessageStreamResponse({
97
144
  originalMessages: uiMessages
@@ -199,9 +246,9 @@ const aiChatBackendPlugin = (config) => defineBackendPlugin({
199
246
  const result = streamText({
200
247
  model: config.model,
201
248
  messages: messagesWithSystem,
202
- tools: config.tools,
249
+ tools: mergedTools,
203
250
  // Enable multi-step tool calls if tools are configured
204
- ...config.tools ? { stopWhen: stepCountIs(5) } : {},
251
+ ...mergedTools ? { stopWhen: stepCountIs(5) } : {},
205
252
  onFinish: async (completion) => {
206
253
  try {
207
254
  const assistantParts = completion.text ? [{ type: "text", text: completion.text }] : [];
@@ -184,7 +184,7 @@ function ChatInput({
184
184
  }
185
185
  )
186
186
  ] }),
187
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex-1", children: [
187
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex-1 min-w-0", children: [
188
188
  /* @__PURE__ */ jsxRuntime.jsx(
189
189
  textarea.Textarea,
190
190
  {
@@ -193,7 +193,7 @@ function ChatInput({
193
193
  onKeyDown: handleKeyDown,
194
194
  placeholder: placeholder || localization.CHAT_PLACEHOLDER,
195
195
  className: utils.cn(
196
- "resize-none pr-12",
196
+ "resize-none pr-12 max-w-full",
197
197
  isCompact ? "min-h-[40px] max-h-[120px] py-2" : "min-h-[50px] max-h-[200px] py-3"
198
198
  ),
199
199
  rows: 1
@@ -182,7 +182,7 @@ function ChatInput({
182
182
  }
183
183
  )
184
184
  ] }),
185
- /* @__PURE__ */ jsxs("div", { className: "relative flex-1", children: [
185
+ /* @__PURE__ */ jsxs("div", { className: "relative flex-1 min-w-0", children: [
186
186
  /* @__PURE__ */ jsx(
187
187
  Textarea,
188
188
  {
@@ -191,7 +191,7 @@ function ChatInput({
191
191
  onKeyDown: handleKeyDown,
192
192
  placeholder: placeholder || localization.CHAT_PLACEHOLDER,
193
193
  className: cn(
194
- "resize-none pr-12",
194
+ "resize-none pr-12 max-w-full",
195
195
  isCompact ? "min-h-[40px] max-h-[120px] py-2" : "min-h-[50px] max-h-[200px] py-3"
196
196
  ),
197
197
  rows: 1
@@ -16,6 +16,7 @@ const index = require('../localization/index.cjs');
16
16
  const client = require('@btst/stack/plugins/client');
17
17
  const plugins_aiChat_queryKeys = require('../../../../../../../plugins/ai-chat/query-keys.cjs');
18
18
  const chatHooks = require('../hooks/chat-hooks.cjs');
19
+ const plugins_aiChat_client_context_pageAiContext = require('../../../../../../../plugins/ai-chat/client/context/page-ai-context.cjs');
19
20
 
20
21
  function ChatInterface({
21
22
  apiPath = "/api/chat",
@@ -40,6 +41,7 @@ function ChatInterface({
40
41
  );
41
42
  const basePath = context.useBasePath();
42
43
  const isPublicMode = mode === "public";
44
+ const pageAIContext = plugins_aiChat_client_context_pageAiContext.usePageAIContext();
43
45
  const localization = { ...index.AI_CHAT_LOCALIZATION, ...customLocalization };
44
46
  const queryClient = reactQuery.useQueryClient();
45
47
  const conversationsListQueryKey = React.useMemo(() => {
@@ -83,13 +85,25 @@ function ChatInterface({
83
85
  !initialMessages || initialMessages.length === 0
84
86
  )
85
87
  );
88
+ const pageAIContextRef = React.useRef(pageAIContext);
89
+ React.useEffect(() => {
90
+ pageAIContextRef.current = pageAIContext;
91
+ }, [pageAIContext]);
86
92
  const transport = React.useMemo(
87
93
  () => new ai.DefaultChatTransport({
88
94
  api: apiPath,
89
95
  // In public mode, don't send conversationId
90
96
  body: isPublicMode ? void 0 : () => ({ conversationId: conversationIdRef.current }),
91
- // Handle edit operations by using truncated messages from the ref
97
+ // Handle edit operations and inject page context
92
98
  prepareSendMessagesRequest: ({ messages: hookMessages }) => {
99
+ const currentPageContext = pageAIContextRef.current;
100
+ const pageContextBody = currentPageContext?.pageDescription ? {
101
+ pageContext: currentPageContext.pageDescription,
102
+ availableTools: Object.keys(
103
+ currentPageContext.clientTools ?? {}
104
+ ),
105
+ routeName: currentPageContext.routeName
106
+ } : {};
93
107
  if (editMessagesRef.current !== null) {
94
108
  const newUserMessage = hookMessages[hookMessages.length - 1];
95
109
  const messagesToSend = [...editMessagesRef.current];
@@ -100,22 +114,63 @@ function ChatInterface({
100
114
  return {
101
115
  body: {
102
116
  messages: messagesToSend,
103
- conversationId: conversationIdRef.current
117
+ conversationId: conversationIdRef.current,
118
+ ...pageContextBody
104
119
  }
105
120
  };
106
121
  }
107
122
  return {
108
123
  body: {
109
124
  messages: hookMessages,
110
- conversationId: conversationIdRef.current
125
+ conversationId: conversationIdRef.current,
126
+ ...pageContextBody
111
127
  }
112
128
  };
113
129
  }
114
130
  }),
115
131
  [apiPath, isPublicMode]
116
132
  );
117
- const { messages, sendMessage, status, error, setMessages, regenerate } = react.useChat({
133
+ const addToolOutputRef = React.useRef(null);
134
+ const {
135
+ messages,
136
+ sendMessage,
137
+ status,
138
+ error,
139
+ setMessages,
140
+ regenerate,
141
+ addToolOutput
142
+ } = react.useChat({
118
143
  transport,
144
+ // Automatically resubmit after all client-side tool results are provided
145
+ sendAutomaticallyWhen: ai.lastAssistantMessageIsCompleteWithToolCalls,
146
+ onToolCall: async ({ toolCall }) => {
147
+ const toolName = toolCall.toolName;
148
+ const handler = pageAIContextRef.current?.clientTools?.[toolName];
149
+ if (handler) {
150
+ try {
151
+ const result = await handler(toolCall.input);
152
+ addToolOutputRef.current?.({
153
+ tool: toolName,
154
+ toolCallId: toolCall.toolCallId,
155
+ output: result
156
+ });
157
+ } catch (err) {
158
+ addToolOutputRef.current?.({
159
+ tool: toolName,
160
+ toolCallId: toolCall.toolCallId,
161
+ state: "output-error",
162
+ errorText: err instanceof Error ? err.message : "Tool execution failed"
163
+ });
164
+ }
165
+ } else {
166
+ addToolOutputRef.current?.({
167
+ tool: toolName,
168
+ toolCallId: toolCall.toolCallId,
169
+ state: "output-error",
170
+ errorText: `No client-side handler registered for tool "${toolName}". The page context may have changed while the response was streaming.`
171
+ });
172
+ }
173
+ },
119
174
  onError: (err) => {
120
175
  console.error("useChat onError:", err);
121
176
  if (!id && !hasNavigatedRef.current) {
@@ -138,19 +193,24 @@ function ChatInterface({
138
193
  if (newConversation) {
139
194
  setCurrentConversationId(newConversation.id);
140
195
  conversationIdRef.current = newConversation.id;
141
- const newUrl = `${basePath}/chat/${newConversation.id}`;
142
- if (typeof window !== "undefined") {
143
- window.history.replaceState(
144
- { ...window.history.state },
145
- "",
146
- newUrl
147
- );
196
+ if (variant === "full") {
197
+ const newUrl = `${basePath}/chat/${newConversation.id}`;
198
+ if (typeof window !== "undefined") {
199
+ window.history.replaceState(
200
+ { ...window.history.state },
201
+ "",
202
+ newUrl
203
+ );
204
+ }
148
205
  }
149
206
  }
150
207
  }
151
208
  }
152
209
  }
153
210
  });
211
+ React.useEffect(() => {
212
+ addToolOutputRef.current = addToolOutput;
213
+ }, [addToolOutput]);
154
214
  React.useEffect(() => {
155
215
  if (isEditInProgressRef.current) {
156
216
  return;
@@ -313,17 +373,24 @@ function ChatInterface({
313
373
  ),
314
374
  children: [
315
375
  messages.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col h-full min-h-[300px]", children: [
316
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 flex items-center justify-center text-muted-foreground", children: /* @__PURE__ */ jsxRuntime.jsx("p", { children: localization.CHAT_EMPTY_STATE }) }),
317
- chatSuggestions && chatSuggestions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap justify-center gap-2 pb-4 max-w-md mx-auto", children: chatSuggestions.map((suggestion, index) => /* @__PURE__ */ jsxRuntime.jsx(
318
- "button",
319
- {
320
- type: "button",
321
- onClick: () => setInput(suggestion),
322
- className: "px-3 py-2 text-sm rounded-lg border border-border bg-background hover:bg-accent hover:text-accent-foreground transition-colors text-foreground",
323
- children: suggestion
324
- },
325
- index
326
- )) })
376
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 flex items-center justify-center text-muted-foreground mb-4", children: /* @__PURE__ */ jsxRuntime.jsx("p", { children: localization.CHAT_EMPTY_STATE }) }),
377
+ (() => {
378
+ const pageSuggestions = pageAIContext?.suggestions ?? [];
379
+ const allSuggestions = [
380
+ ...pageSuggestions,
381
+ ...chatSuggestions ?? []
382
+ ];
383
+ return allSuggestions.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap justify-center gap-2 pb-4 max-w-md mx-auto", children: allSuggestions.map((suggestion, index) => /* @__PURE__ */ jsxRuntime.jsx(
384
+ "button",
385
+ {
386
+ type: "button",
387
+ onClick: () => setInput(suggestion),
388
+ className: "px-3 py-2 text-sm rounded-lg border border-border bg-background hover:bg-accent hover:text-accent-foreground transition-colors text-foreground",
389
+ children: suggestion
390
+ },
391
+ index
392
+ )) }) : null;
393
+ })()
327
394
  ] }) : messages.map((m, index) => /* @__PURE__ */ jsxRuntime.jsx(
328
395
  chatMessage.ChatMessage,
329
396
  {