@btst/stack 4.4.0 → 4.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 (92) hide show
  1. package/README.md +2 -2
  2. package/dist/packages/stack/src/plugins/ai-chat/api/compaction.cjs +64 -24
  3. package/dist/packages/stack/src/plugins/ai-chat/api/compaction.mjs +65 -25
  4. package/dist/packages/stack/src/plugins/ai-chat/api/context-checkpoint.cjs +54 -0
  5. package/dist/packages/stack/src/plugins/ai-chat/api/context-checkpoint.mjs +51 -0
  6. package/dist/packages/stack/src/plugins/ai-chat/api/getters.cjs +2 -0
  7. package/dist/packages/stack/src/plugins/ai-chat/api/getters.mjs +2 -0
  8. package/dist/packages/stack/src/plugins/ai-chat/api/operations.cjs +269 -93
  9. package/dist/packages/stack/src/plugins/ai-chat/api/operations.mjs +270 -94
  10. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.cjs +3 -5
  11. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +3 -5
  12. package/dist/packages/stack/src/plugins/ai-chat/api/serializers.cjs +1 -0
  13. package/dist/packages/stack/src/plugins/ai-chat/api/serializers.mjs +1 -0
  14. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.cjs +26 -16
  15. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.mjs +26 -16
  16. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-message.cjs +22 -2
  17. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-message.mjs +22 -2
  18. package/dist/packages/stack/src/plugins/ai-chat/db.cjs +8 -0
  19. package/dist/packages/stack/src/plugins/ai-chat/db.mjs +8 -0
  20. package/dist/packages/stack/src/plugins/ai-chat/history.cjs +47 -0
  21. package/dist/packages/stack/src/plugins/ai-chat/history.mjs +45 -0
  22. package/dist/plugins/ai-chat/api/index.d.cts +2 -2
  23. package/dist/plugins/ai-chat/api/index.d.mts +2 -2
  24. package/dist/plugins/ai-chat/api/index.d.ts +2 -2
  25. package/dist/plugins/ai-chat/client/components/index.d.cts +1 -1
  26. package/dist/plugins/ai-chat/client/components/index.d.mts +1 -1
  27. package/dist/plugins/ai-chat/client/components/index.d.ts +1 -1
  28. package/dist/plugins/ai-chat/client/hooks/index.d.cts +2 -2
  29. package/dist/plugins/ai-chat/client/hooks/index.d.mts +2 -2
  30. package/dist/plugins/ai-chat/client/hooks/index.d.ts +2 -2
  31. package/dist/plugins/ai-chat/client/index.d.cts +3 -3
  32. package/dist/plugins/ai-chat/client/index.d.mts +3 -3
  33. package/dist/plugins/ai-chat/client/index.d.ts +3 -3
  34. package/dist/plugins/ai-chat/query-keys.d.cts +1 -1
  35. package/dist/plugins/ai-chat/query-keys.d.mts +1 -1
  36. package/dist/plugins/ai-chat/query-keys.d.ts +1 -1
  37. package/dist/plugins/blog/api/index.d.cts +2 -2
  38. package/dist/plugins/blog/api/index.d.mts +2 -2
  39. package/dist/plugins/blog/api/index.d.ts +2 -2
  40. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  41. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  42. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  43. package/dist/plugins/blog/client/index.d.cts +2 -2
  44. package/dist/plugins/blog/client/index.d.mts +2 -2
  45. package/dist/plugins/blog/client/index.d.ts +2 -2
  46. package/dist/plugins/blog/query-keys.d.cts +2 -2
  47. package/dist/plugins/blog/query-keys.d.mts +2 -2
  48. package/dist/plugins/blog/query-keys.d.ts +2 -2
  49. package/dist/plugins/form-builder/api/index.d.cts +1 -1
  50. package/dist/plugins/form-builder/api/index.d.mts +1 -1
  51. package/dist/plugins/form-builder/api/index.d.ts +1 -1
  52. package/dist/plugins/form-builder/client/hooks/index.d.cts +1 -1
  53. package/dist/plugins/form-builder/client/hooks/index.d.mts +1 -1
  54. package/dist/plugins/form-builder/client/hooks/index.d.ts +1 -1
  55. package/dist/plugins/form-builder/query-keys.d.cts +1 -1
  56. package/dist/plugins/form-builder/query-keys.d.mts +1 -1
  57. package/dist/plugins/form-builder/query-keys.d.ts +1 -1
  58. package/dist/shared/{stack.CjCSDfWy.d.mts → stack.94kN3pEK.d.mts} +5 -5
  59. package/dist/shared/{stack.C4zspX0T.d.ts → stack.B2EYZyh_.d.ts} +5 -2
  60. package/dist/shared/{stack.BGzgFx3R.d.cts → stack.BTStHVTy.d.cts} +5 -5
  61. package/dist/shared/{stack.DhZtP2JZ.d.mts → stack.BggTHKlo.d.mts} +37 -37
  62. package/dist/shared/{stack.DDQlE2Pm.d.cts → stack.Cei2T2gc.d.cts} +1 -0
  63. package/dist/shared/{stack.DNw6Hv5o.d.cts → stack.CtrnoQgH.d.cts} +37 -37
  64. package/dist/shared/{stack.tAUeRAtd.d.mts → stack.DZRZAzcx.d.mts} +1 -0
  65. package/dist/shared/{stack.Dpg_JjIo.d.ts → stack.DfrRYOIl.d.ts} +5 -5
  66. package/dist/shared/{stack.Ps3MDhTC.d.mts → stack.DivuAnNI.d.mts} +5 -2
  67. package/dist/shared/{stack.DlehgNZZ.d.ts → stack.byjorQi5.d.ts} +1 -0
  68. package/dist/shared/{stack.ymWsP4HZ.d.cts → stack.fF273jws.d.cts} +5 -2
  69. package/dist/shared/{stack.Clil75lW.d.ts → stack.lPKwd_v0.d.ts} +37 -37
  70. package/package.json +1 -1
  71. package/src/plugins/ai-chat/__tests__/authorization.test.ts +56 -30
  72. package/src/plugins/ai-chat/__tests__/classnames-overrides.test.tsx +25 -0
  73. package/src/plugins/ai-chat/__tests__/client-sweep.test.tsx +101 -0
  74. package/src/plugins/ai-chat/__tests__/persisted-compaction.test.ts +819 -0
  75. package/src/plugins/ai-chat/api/compaction.ts +107 -40
  76. package/src/plugins/ai-chat/api/context-checkpoint.ts +66 -0
  77. package/src/plugins/ai-chat/api/getters.ts +2 -0
  78. package/src/plugins/ai-chat/api/operations.ts +361 -99
  79. package/src/plugins/ai-chat/api/plugin.ts +5 -8
  80. package/src/plugins/ai-chat/api/serializers.ts +1 -0
  81. package/src/plugins/ai-chat/client/components/chat-interface.tsx +43 -22
  82. package/src/plugins/ai-chat/client/components/chat-message.tsx +32 -2
  83. package/src/plugins/ai-chat/client/localization/index.ts +1 -0
  84. package/src/plugins/ai-chat/db.ts +8 -0
  85. package/src/plugins/ai-chat/history.ts +68 -0
  86. package/src/plugins/ai-chat/types.ts +2 -0
  87. package/dist/shared/{stack.CGmh-Lpe.d.ts → stack.BuxUQqL8.d.ts} +12 -12
  88. package/dist/shared/{stack.Csi-4Mgz.d.cts → stack.CKc_Bais.d.cts} +7 -7
  89. package/dist/shared/{stack.Csi-4Mgz.d.mts → stack.CKc_Bais.d.mts} +7 -7
  90. package/dist/shared/{stack.Csi-4Mgz.d.ts → stack.CKc_Bais.d.ts} +7 -7
  91. package/dist/shared/{stack.zch70Loj.d.cts → stack.CX83YNuS.d.cts} +12 -12
  92. package/dist/shared/{stack.Doy2_wgl.d.mts → stack.Egr3RWhA.d.mts} +12 -12
@@ -2,7 +2,7 @@ import * as _tanstack_react_query from '@tanstack/react-query';
2
2
  import { z } from 'zod';
3
3
  import { ResourceFormConfig, ResourceFormResult } from '@btst/stack/plugins/client/hooks';
4
4
  import { S as SerializedPost, a as SerializedTag } from './stack.Dsatjh0B.js';
5
- import { c as createPostSchema, u as updatePostSchema } from './stack.Csi-4Mgz.js';
5
+ import { c as createPostSchema, u as updatePostSchema } from './stack.CKc_Bais.js';
6
6
 
7
7
  /**
8
8
  * Options for the usePosts hook
@@ -136,6 +136,10 @@ declare function useSuspenseTags(): {
136
136
  declare function usePostForm<TValues>(config: ResourceFormConfig<TValues, SerializedPost | null, SerializedPost | null>): ResourceFormResult<TValues, SerializedPost | null, SerializedPost | null>;
137
137
  /** Create a new post */
138
138
  declare function useCreatePost(): _tanstack_react_query.UseMutationResult<SerializedPost | null, Error, {
139
+ title: string;
140
+ content: string;
141
+ excerpt: string;
142
+ published: boolean;
139
143
  tags: ({
140
144
  name: string;
141
145
  } | {
@@ -143,10 +147,6 @@ declare function useCreatePost(): _tanstack_react_query.UseMutationResult<Serial
143
147
  name: string;
144
148
  slug: string;
145
149
  })[];
146
- title: string;
147
- content: string;
148
- excerpt: string;
149
- published: boolean;
150
150
  slug?: string | undefined;
151
151
  publishedAt?: Date | undefined;
152
152
  createdAt?: Date | undefined;
@@ -31,6 +31,8 @@ type Message = {
31
31
  conversationId: string;
32
32
  role: "system" | "user" | "assistant" | "data";
33
33
  content: string;
34
+ /** True when generation stopped before a successful completion. */
35
+ interrupted?: boolean | null;
34
36
  createdAt: Date;
35
37
  };
36
38
  type SerializedConversation = Omit<Conversation, "createdAt" | "updatedAt"> & {
@@ -55,7 +57,7 @@ interface AiChatPageContentConfig {
55
57
  description?: string;
56
58
  }
57
59
 
58
- /** Opt-in context compaction for public, text-only conversations. */
60
+ /** Opt-in context compaction for text-only conversations. */
59
61
  interface AiChatCompactionConfig {
60
62
  /** Actual context window. Configure the model's output limit at or below 20% of this. */
61
63
  contextWindowTokens: number;
@@ -228,7 +230,7 @@ interface AiChatBackendConfig {
228
230
  */
229
231
  access?: AiChatAccess;
230
232
  systemPrompt?: string;
231
- /** Automatically summarize older context in public text chats; off by default. */
233
+ /** Automatically summarize older context in text chats; off by default. */
232
234
  compaction?: AiChatCompactionConfig;
233
235
  tools?: Record<string, Tool>;
234
236
  /** Enable the standard readPage server tool for full page content. Reserves the readPage tool name. */
@@ -319,6 +321,7 @@ declare const aiChatBackendPlugin: <TTools extends Record<string, Tool> = Record
319
321
  readonly conversationId: string;
320
322
  readonly role: "system" | "user" | "assistant" | "data";
321
323
  readonly content: string;
324
+ readonly interrupted?: boolean | null | undefined;
322
325
  readonly createdAt: string;
323
326
  }[];
324
327
  }>;
@@ -44,6 +44,7 @@ interface AiChatLocalization {
44
44
  CHAT_EMPTY_STATE: string;
45
45
  CHAT_LOADING: string;
46
46
  CHAT_COMPACTING?: string;
47
+ CHAT_INTERRUPTED?: string;
47
48
  CHAT_ERROR: string;
48
49
  CHAT_RETRY: string;
49
50
  CHAT_GENERIC_ERROR_TITLE: string;
@@ -31,6 +31,8 @@ type Message = {
31
31
  conversationId: string;
32
32
  role: "system" | "user" | "assistant" | "data";
33
33
  content: string;
34
+ /** True when generation stopped before a successful completion. */
35
+ interrupted?: boolean | null;
34
36
  createdAt: Date;
35
37
  };
36
38
  type SerializedConversation = Omit<Conversation, "createdAt" | "updatedAt"> & {
@@ -55,7 +57,7 @@ interface AiChatPageContentConfig {
55
57
  description?: string;
56
58
  }
57
59
 
58
- /** Opt-in context compaction for public, text-only conversations. */
60
+ /** Opt-in context compaction for text-only conversations. */
59
61
  interface AiChatCompactionConfig {
60
62
  /** Actual context window. Configure the model's output limit at or below 20% of this. */
61
63
  contextWindowTokens: number;
@@ -228,7 +230,7 @@ interface AiChatBackendConfig {
228
230
  */
229
231
  access?: AiChatAccess;
230
232
  systemPrompt?: string;
231
- /** Automatically summarize older context in public text chats; off by default. */
233
+ /** Automatically summarize older context in text chats; off by default. */
232
234
  compaction?: AiChatCompactionConfig;
233
235
  tools?: Record<string, Tool>;
234
236
  /** Enable the standard readPage server tool for full page content. Reserves the readPage tool name. */
@@ -319,6 +321,7 @@ declare const aiChatBackendPlugin: <TTools extends Record<string, Tool> = Record
319
321
  readonly conversationId: string;
320
322
  readonly role: "system" | "user" | "assistant" | "data";
321
323
  readonly content: string;
324
+ readonly interrupted?: boolean | null | undefined;
322
325
  readonly createdAt: string;
323
326
  }[];
324
327
  }>;
@@ -2,7 +2,7 @@ import * as _btst_stack_plugins_client from '@btst/stack/plugins/client';
2
2
  import { createApiClient } from '@btst/stack/plugins/client';
3
3
  import * as zod from 'zod';
4
4
  import { z } from 'zod';
5
- import { c as createPostSchema, u as updatePostSchema } from './stack.Csi-4Mgz.js';
5
+ import { c as createPostSchema, u as updatePostSchema } from './stack.CKc_Bais.js';
6
6
  import { P as Post, T as Tag, S as SerializedPost, a as SerializedTag } from './stack.Dsatjh0B.js';
7
7
  import * as _btst_stack_plugins_api from '@btst/stack/plugins/api';
8
8
  import { DeepReadonly, OperationContext, OperationErrorContext } from '@btst/stack/plugins/api';
@@ -89,6 +89,12 @@ declare const NextPreviousPostsQuerySchema: z.ZodObject<{
89
89
  date: z.ZodPipe<z.ZodCoercedDate<unknown>, z.ZodTransform<string, Date>>;
90
90
  }, z.core.$strip>;
91
91
  declare const CreatePostOperationInputSchema: z.ZodObject<{
92
+ slug: z.ZodOptional<z.ZodString>;
93
+ title: z.ZodString;
94
+ content: z.ZodString;
95
+ excerpt: z.ZodString;
96
+ image: z.ZodOptional<z.ZodString>;
97
+ published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
92
98
  tags: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
93
99
  name: z.ZodString;
94
100
  }, z.core.$strip>, z.ZodObject<{
@@ -96,12 +102,6 @@ declare const CreatePostOperationInputSchema: z.ZodObject<{
96
102
  name: z.ZodString;
97
103
  slug: z.ZodString;
98
104
  }, z.core.$strip>]>>>>;
99
- slug: z.ZodOptional<z.ZodString>;
100
- title: z.ZodString;
101
- content: z.ZodString;
102
- excerpt: z.ZodString;
103
- image: z.ZodOptional<z.ZodString>;
104
- published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
105
105
  publishedAt: z.ZodOptional<z.ZodPipe<z.ZodCoercedDate<unknown>, z.ZodTransform<string, Date>>>;
106
106
  createdAt: z.ZodOptional<z.ZodPipe<z.ZodCoercedDate<unknown>, z.ZodTransform<string, Date>>>;
107
107
  updatedAt: z.ZodOptional<z.ZodPipe<z.ZodCoercedDate<unknown>, z.ZodTransform<string, Date>>>;
@@ -109,6 +109,12 @@ declare const CreatePostOperationInputSchema: z.ZodObject<{
109
109
  declare const UpdatePostOperationInputSchema: z.ZodObject<{
110
110
  id: z.ZodString;
111
111
  data: z.ZodObject<{
112
+ slug: z.ZodString;
113
+ title: z.ZodString;
114
+ content: z.ZodString;
115
+ excerpt: z.ZodString;
116
+ image: z.ZodOptional<z.ZodString>;
117
+ published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
112
118
  tags: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
113
119
  name: z.ZodString;
114
120
  }, z.core.$strip>, z.ZodObject<{
@@ -116,12 +122,6 @@ declare const UpdatePostOperationInputSchema: z.ZodObject<{
116
122
  name: z.ZodString;
117
123
  slug: z.ZodString;
118
124
  }, z.core.$strip>]>>>>;
119
- slug: z.ZodString;
120
- title: z.ZodString;
121
- content: z.ZodString;
122
- excerpt: z.ZodString;
123
- image: z.ZodOptional<z.ZodString>;
124
- published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
125
125
  publishedAt: z.ZodOptional<z.ZodPipe<z.ZodCoercedDate<unknown>, z.ZodTransform<string, Date>>>;
126
126
  createdAt: z.ZodOptional<z.ZodPipe<z.ZodCoercedDate<unknown>, z.ZodTransform<string, Date>>>;
127
127
  updatedAt: z.ZodOptional<z.ZodPipe<z.ZodCoercedDate<unknown>, z.ZodTransform<string, Date>>>;
@@ -386,6 +386,9 @@ declare const blogBackendPlugin: (options?: BlogBackendOptions) => _btst_stack_p
386
386
  title: string;
387
387
  content: string;
388
388
  excerpt: string;
389
+ slug?: string | undefined;
390
+ image?: string | undefined;
391
+ published?: boolean | undefined;
389
392
  tags?: ({
390
393
  name: string;
391
394
  } | {
@@ -393,9 +396,6 @@ declare const blogBackendPlugin: (options?: BlogBackendOptions) => _btst_stack_p
393
396
  name: string;
394
397
  slug: string;
395
398
  })[] | undefined;
396
- slug?: string | undefined;
397
- image?: string | undefined;
398
- published?: boolean | undefined;
399
399
  publishedAt?: unknown;
400
400
  createdAt?: unknown;
401
401
  updatedAt?: unknown;
@@ -403,6 +403,9 @@ declare const blogBackendPlugin: (options?: BlogBackendOptions) => _btst_stack_p
403
403
  title: string;
404
404
  content: string;
405
405
  excerpt: string;
406
+ slug?: string | undefined;
407
+ image?: string | undefined;
408
+ published?: boolean | undefined;
406
409
  tags?: ({
407
410
  name: string;
408
411
  } | {
@@ -410,9 +413,6 @@ declare const blogBackendPlugin: (options?: BlogBackendOptions) => _btst_stack_p
410
413
  name: string;
411
414
  slug: string;
412
415
  })[] | undefined;
413
- slug?: string | undefined;
414
- image?: string | undefined;
415
- published?: boolean | undefined;
416
416
  publishedAt?: unknown;
417
417
  createdAt?: unknown;
418
418
  updatedAt?: unknown;
@@ -448,6 +448,8 @@ declare const blogBackendPlugin: (options?: BlogBackendOptions) => _btst_stack_p
448
448
  title: string;
449
449
  content: string;
450
450
  excerpt: string;
451
+ image?: string | undefined;
452
+ published?: boolean | undefined;
451
453
  tags?: ({
452
454
  name: string;
453
455
  } | {
@@ -455,8 +457,6 @@ declare const blogBackendPlugin: (options?: BlogBackendOptions) => _btst_stack_p
455
457
  name: string;
456
458
  slug: string;
457
459
  })[] | undefined;
458
- image?: string | undefined;
459
- published?: boolean | undefined;
460
460
  publishedAt?: unknown;
461
461
  createdAt?: unknown;
462
462
  updatedAt?: unknown;
@@ -465,6 +465,8 @@ declare const blogBackendPlugin: (options?: BlogBackendOptions) => _btst_stack_p
465
465
  title: string;
466
466
  content: string;
467
467
  excerpt: string;
468
+ image?: string | undefined;
469
+ published?: boolean | undefined;
468
470
  tags?: ({
469
471
  name: string;
470
472
  } | {
@@ -472,8 +474,6 @@ declare const blogBackendPlugin: (options?: BlogBackendOptions) => _btst_stack_p
472
474
  name: string;
473
475
  slug: string;
474
476
  })[] | undefined;
475
- image?: string | undefined;
476
- published?: boolean | undefined;
477
477
  publishedAt?: unknown;
478
478
  createdAt?: unknown;
479
479
  updatedAt?: unknown;
@@ -627,6 +627,12 @@ declare const blogBackendPlugin: (options?: BlogBackendOptions) => _btst_stack_p
627
627
  total: number;
628
628
  }, false>;
629
629
  readonly createPost: _btst_stack_plugins_api.Operation<zod.ZodObject<{
630
+ slug: zod.ZodOptional<zod.ZodString>;
631
+ title: zod.ZodString;
632
+ content: zod.ZodString;
633
+ excerpt: zod.ZodString;
634
+ image: zod.ZodOptional<zod.ZodString>;
635
+ published: zod.ZodDefault<zod.ZodOptional<zod.ZodBoolean>>;
630
636
  tags: zod.ZodDefault<zod.ZodOptional<zod.ZodArray<zod.ZodUnion<readonly [zod.ZodObject<{
631
637
  name: zod.ZodString;
632
638
  }, zod_v4_core.$strip>, zod.ZodObject<{
@@ -634,12 +640,6 @@ declare const blogBackendPlugin: (options?: BlogBackendOptions) => _btst_stack_p
634
640
  name: zod.ZodString;
635
641
  slug: zod.ZodString;
636
642
  }, zod_v4_core.$strip>]>>>>;
637
- slug: zod.ZodOptional<zod.ZodString>;
638
- title: zod.ZodString;
639
- content: zod.ZodString;
640
- excerpt: zod.ZodString;
641
- image: zod.ZodOptional<zod.ZodString>;
642
- published: zod.ZodDefault<zod.ZodOptional<zod.ZodBoolean>>;
643
643
  publishedAt: zod.ZodOptional<zod.ZodPipe<zod.ZodCoercedDate<unknown>, zod.ZodTransform<string, Date>>>;
644
644
  createdAt: zod.ZodOptional<zod.ZodPipe<zod.ZodCoercedDate<unknown>, zod.ZodTransform<string, Date>>>;
645
645
  updatedAt: zod.ZodOptional<zod.ZodPipe<zod.ZodCoercedDate<unknown>, zod.ZodTransform<string, Date>>>;
@@ -673,6 +673,12 @@ declare const blogBackendPlugin: (options?: BlogBackendOptions) => _btst_stack_p
673
673
  readonly updatePost: _btst_stack_plugins_api.Operation<zod.ZodObject<{
674
674
  id: zod.ZodString;
675
675
  data: zod.ZodObject<{
676
+ slug: zod.ZodString;
677
+ title: zod.ZodString;
678
+ content: zod.ZodString;
679
+ excerpt: zod.ZodString;
680
+ image: zod.ZodOptional<zod.ZodString>;
681
+ published: zod.ZodDefault<zod.ZodOptional<zod.ZodBoolean>>;
676
682
  tags: zod.ZodDefault<zod.ZodOptional<zod.ZodArray<zod.ZodUnion<readonly [zod.ZodObject<{
677
683
  name: zod.ZodString;
678
684
  }, zod_v4_core.$strip>, zod.ZodObject<{
@@ -680,12 +686,6 @@ declare const blogBackendPlugin: (options?: BlogBackendOptions) => _btst_stack_p
680
686
  name: zod.ZodString;
681
687
  slug: zod.ZodString;
682
688
  }, zod_v4_core.$strip>]>>>>;
683
- slug: zod.ZodString;
684
- title: zod.ZodString;
685
- content: zod.ZodString;
686
- excerpt: zod.ZodString;
687
- image: zod.ZodOptional<zod.ZodString>;
688
- published: zod.ZodDefault<zod.ZodOptional<zod.ZodBoolean>>;
689
689
  publishedAt: zod.ZodOptional<zod.ZodPipe<zod.ZodCoercedDate<unknown>, zod.ZodTransform<string, Date>>>;
690
690
  createdAt: zod.ZodOptional<zod.ZodPipe<zod.ZodCoercedDate<unknown>, zod.ZodTransform<string, Date>>>;
691
691
  updatedAt: zod.ZodOptional<zod.ZodPipe<zod.ZodCoercedDate<unknown>, zod.ZodTransform<string, Date>>>;
@@ -900,8 +900,8 @@ declare const blogResources: {
900
900
  method: "POST";
901
901
  input: (vars: PostCreateInput) => {
902
902
  body: {
903
- title: string;
904
903
  published: boolean;
904
+ title: string;
905
905
  content: string;
906
906
  excerpt: string;
907
907
  tags: ({
@@ -1072,8 +1072,8 @@ declare function createBlogQueryKeys(client: ReturnType<typeof createApiClient<B
1072
1072
  method: "POST";
1073
1073
  input: (vars: PostCreateInput) => {
1074
1074
  body: {
1075
- title: string;
1076
1075
  published: boolean;
1076
+ title: string;
1077
1077
  content: string;
1078
1078
  excerpt: string;
1079
1079
  tags: ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@btst/stack",
3
- "version": "4.4.0",
3
+ "version": "4.5.0",
4
4
  "description": "A composable, plugin-based library for building full-stack applications.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,18 +14,53 @@ import {
14
14
  import { aiChatPermissions } from "../permissions";
15
15
  import type { Conversation, Message } from "../types";
16
16
 
17
- const { streamText } = vi.hoisted(() => ({
17
+ const { streamText, completions } = vi.hoisted(() => ({
18
+ completions: [] as Array<
19
+ NonNullable<
20
+ Parameters<typeof import("ai").createUIMessageStream>[0]["onFinish"]
21
+ >
22
+ >,
18
23
  streamText: vi.fn(() => ({
24
+ toUIMessageStream: () =>
25
+ new ReadableStream({
26
+ start(controller) {
27
+ controller.close();
28
+ },
29
+ }),
19
30
  toUIMessageStreamResponse: () => new Response("stream", { status: 200 }),
20
31
  })),
21
32
  }));
22
33
 
23
- vi.mock("ai", async (importOriginal) => ({
24
- ...(await importOriginal<typeof import("ai")>()),
25
- convertToModelMessages: (messages: unknown) => messages,
26
- stepCountIs: () => () => false,
27
- streamText,
28
- }));
34
+ vi.mock("ai", async (importOriginal) => {
35
+ const original = await importOriginal<typeof import("ai")>();
36
+ return {
37
+ ...original,
38
+ createUIMessageStream: (
39
+ options: Parameters<typeof original.createUIMessageStream>[0],
40
+ ) => {
41
+ if (options.onFinish) completions.push(options.onFinish);
42
+ return original.createUIMessageStream(options);
43
+ },
44
+ convertToModelMessages: (messages: unknown) => messages,
45
+ stepCountIs: () => () => false,
46
+ streamText,
47
+ };
48
+ });
49
+
50
+ function completion(text: string) {
51
+ const responseMessage = {
52
+ id: "server-response",
53
+ role: "assistant" as const,
54
+ parts: [{ type: "text" as const, text }],
55
+ };
56
+ return {
57
+ responseMessage,
58
+ messages: [responseMessage],
59
+ isAborted: false,
60
+ isContinuation: false,
61
+ finishReason: "stop" as const,
62
+ };
63
+ }
29
64
 
30
65
  type Identity = { id: string; role: "user" | "admin" };
31
66
 
@@ -173,7 +208,10 @@ async function seedMessage(
173
208
  }
174
209
 
175
210
  describe("AI Chat operation authorization", () => {
176
- beforeEach(() => streamText.mockClear());
211
+ beforeEach(() => {
212
+ streamText.mockClear();
213
+ completions.length = 0;
214
+ });
177
215
 
178
216
  it("publishes one executable inventory for every maintained transport", () => {
179
217
  const plugin = aiChatBackendPlugin({ model });
@@ -815,15 +853,7 @@ describe("AI Chat operation authorization", () => {
815
853
  await app
816
854
  .forRequest(request("/chat", { identity: owner }))
817
855
  .operations.aiChat.startStream(messageBody);
818
- const finish = (
819
- streamText.mock.calls as unknown as Array<
820
- [
821
- {
822
- onFinish: (completion: { text: string }) => Promise<void>;
823
- },
824
- ]
825
- >
826
- )[0]?.[0].onFinish;
856
+ const finish = completions[0];
827
857
  expect(finish).toBeDefined();
828
858
  if (!finish) throw new Error("Missing stream completion callback");
829
859
  const adapterConfig = app.adapter.options?.adapterConfig;
@@ -831,7 +861,7 @@ describe("AI Chat operation authorization", () => {
831
861
  if (!adapterConfig) throw new Error("Missing adapter config");
832
862
  adapterConfig.transaction = false;
833
863
 
834
- await finish({ text: "unsafe answer" });
864
+ await finish(completion("unsafe answer"));
835
865
 
836
866
  expect(
837
867
  await app.adapter.count({
@@ -876,6 +906,7 @@ describe("AI Chat operation authorization", () => {
876
906
  identity: z.object({ id: z.string(), role: z.enum(["user", "admin"]) }),
877
907
  permissions: [aiChatPermissions] as const,
878
908
  rules: ({ aiChat }) => [
909
+ aiChat.conversation.read.allow(),
879
910
  aiChat.stream.start.allow(),
880
911
  aiChat.message.retry.when(({ facts }) => {
881
912
  observed.push({ intent: "retry", messageId: facts.messageId });
@@ -959,6 +990,7 @@ describe("AI Chat operation authorization", () => {
959
990
  identity: z.object({ id: z.string(), role: z.enum(["user", "admin"]) }),
960
991
  permissions: [aiChatPermissions] as const,
961
992
  rules: ({ aiChat }) => [
993
+ aiChat.conversation.read.allow(),
962
994
  aiChat.stream.start.allow(),
963
995
  aiChat.message.retry.allow(),
964
996
  aiChat.message.edit.when(() => false),
@@ -1030,6 +1062,7 @@ describe("AI Chat operation authorization", () => {
1030
1062
  identity: z.object({ id: z.string(), role: z.enum(["user", "admin"]) }),
1031
1063
  permissions: [aiChatPermissions] as const,
1032
1064
  rules: ({ aiChat }) => [
1065
+ aiChat.conversation.read.allow(),
1033
1066
  aiChat.stream.start.allow(),
1034
1067
  aiChat.message.send.allow(),
1035
1068
  aiChat.message.retry.when(({ facts }) => observedRetry(facts)),
@@ -1490,7 +1523,7 @@ describe("AI Chat operation authorization", () => {
1490
1523
  expect(before).not.toHaveBeenCalled();
1491
1524
  expect(streamText).not.toHaveBeenCalled();
1492
1525
  expect(await app.adapter.count({ model: "message" })).toBe(0);
1493
- expect(onError).toHaveBeenCalledOnce();
1526
+ expect(onError).not.toHaveBeenCalled();
1494
1527
  });
1495
1528
 
1496
1529
  it("rechecks a missing requested conversation before entering hooks", async () => {
@@ -1962,20 +1995,13 @@ describe("AI Chat operation authorization", () => {
1962
1995
  await app
1963
1996
  .forRequest(request("/first", { identity: owner }))
1964
1997
  .operations.aiChat.startStream(input);
1965
- const streamCalls = streamText.mock.calls as unknown as Array<
1966
- [
1967
- {
1968
- onFinish: (completion: { text: string }) => Promise<void>;
1969
- },
1970
- ]
1971
- >;
1972
- const firstFinish = streamCalls[0]?.[0].onFinish;
1998
+ const firstFinish = completions[0];
1973
1999
  await app
1974
2000
  .forRequest(request("/second", { identity: owner }))
1975
2001
  .operations.aiChat.startStream(input);
1976
- const secondFinish = streamCalls[1]?.[0].onFinish;
2002
+ const secondFinish = completions[1];
1977
2003
 
1978
- await firstFinish?.({ text: "stale answer" });
2004
+ await firstFinish?.(completion("stale answer"));
1979
2005
  expect(
1980
2006
  await app.adapter.count({
1981
2007
  model: "message",
@@ -1984,7 +2010,7 @@ describe("AI Chat operation authorization", () => {
1984
2010
  ).toBe(0);
1985
2011
  expect(onErrorChat).toHaveBeenCalledOnce();
1986
2012
 
1987
- await secondFinish?.({ text: "current answer" });
2013
+ await secondFinish?.(completion("current answer"));
1988
2014
  const assistants = await app.adapter.findMany<Message>({
1989
2015
  model: "message",
1990
2016
  where: [{ field: "role", value: "assistant", operator: "eq" }],
@@ -161,6 +161,31 @@ function query(selector: string): HTMLElement {
161
161
  }
162
162
 
163
163
  describe("ai-chat classNames overrides (issue #36)", () => {
164
+ it("labels interrupted responses and stops unfinished tool indicators", async () => {
165
+ await render(
166
+ <ChatMessage
167
+ message={{
168
+ ...assistantMessage,
169
+ metadata: { interrupted: true },
170
+ parts: [
171
+ ...assistantMessage.parts,
172
+ {
173
+ type: "tool-inspect",
174
+ toolCallId: "pending",
175
+ state: "input-available",
176
+ input: {},
177
+ },
178
+ ],
179
+ }}
180
+ />,
181
+ );
182
+ expect(query('[data-testid="chat-interrupted"]').textContent).toBe(
183
+ "Response interrupted",
184
+ );
185
+ expect(container.textContent).not.toContain("Executing...");
186
+ expect(container.querySelector(".animate-spin")).toBeNull();
187
+ });
188
+
164
189
  it("applies message, userMessage and assistantMessage to a user message", async () => {
165
190
  await render(<ChatMessage message={userMessage} />);
166
191
 
@@ -301,6 +301,107 @@ function menuItem(text: string) {
301
301
  }
302
302
 
303
303
  describe("AI Chat permissions", () => {
304
+ it("shows authorized compaction status and ignores browser checkpoints", async () => {
305
+ let chatOptions: any;
306
+ const messages = [
307
+ { id: "u1", role: "user", parts: [{ type: "text", text: "Hello" }] },
308
+ ];
309
+ mocks.useAiChatIdentityPartition.mockReturnValue({
310
+ id: "owner-1",
311
+ role: "user",
312
+ });
313
+ mocks.useChat.mockImplementation((options) => {
314
+ chatOptions = options;
315
+ return {
316
+ messages,
317
+ status: "streaming",
318
+ error: null,
319
+ sendMessage: vi.fn(),
320
+ setMessages: vi.fn(),
321
+ regenerate: vi.fn(),
322
+ addToolOutput: vi.fn(),
323
+ stop: vi.fn(),
324
+ };
325
+ });
326
+ await render(<ChatInterface id={conversation.id} />);
327
+ await act(async () => {
328
+ chatOptions.transport.prepareSendMessagesRequest({ messages });
329
+ chatOptions.onData({
330
+ type: "data-context-status",
331
+ data: { compacting: true },
332
+ });
333
+ chatOptions.onData({
334
+ type: "data-context-checkpoint",
335
+ data: { summary: "Do not trust this", throughMessageId: "u1" },
336
+ });
337
+ });
338
+ expect(container.textContent).toContain("Summarizing");
339
+ let next: any;
340
+ await act(async () => {
341
+ next = chatOptions.transport.prepareSendMessagesRequest({ messages });
342
+ });
343
+ expect(next.body.messages).toEqual(messages);
344
+ expect(next.body).not.toHaveProperty("contextSummary");
345
+ });
346
+
347
+ it.each([
348
+ { interrupted: true, state: "output-available" },
349
+ { interrupted: false, state: "input-available" },
350
+ { interrupted: false, state: "input-streaming" },
351
+ ])(
352
+ "restores interrupted or abandoned $state tools without a live callback",
353
+ async ({ interrupted, state }) => {
354
+ const setMessages = vi.fn();
355
+ mocks.useChat.mockReturnValue({
356
+ messages: [],
357
+ status: "ready",
358
+ error: null,
359
+ sendMessage: vi.fn(),
360
+ setMessages,
361
+ regenerate: vi.fn(),
362
+ addToolOutput: vi.fn(),
363
+ stop: vi.fn(),
364
+ });
365
+ const parts = [
366
+ { type: "text", text: "Partial response" },
367
+ {
368
+ type: "tool-inspect",
369
+ toolCallId: "call",
370
+ state,
371
+ input: {},
372
+ ...(state === "output-available" ? { output: "saved result" } : {}),
373
+ },
374
+ ];
375
+ mocks.useConversation.mockReturnValue({
376
+ conversation: {
377
+ ...conversation,
378
+ messages: [
379
+ {
380
+ id: "answer",
381
+ conversationId: conversation.id,
382
+ role: "assistant",
383
+ content: JSON.stringify(parts),
384
+ interrupted,
385
+ createdAt: conversation.createdAt,
386
+ },
387
+ ],
388
+ },
389
+ isLoading: false,
390
+ error: null,
391
+ refetch: vi.fn(),
392
+ });
393
+ await render(<ChatInterface id={conversation.id} />);
394
+ expect(setMessages).toHaveBeenCalledWith([
395
+ {
396
+ id: "answer",
397
+ role: "assistant",
398
+ parts,
399
+ metadata: { interrupted: true },
400
+ },
401
+ ]);
402
+ },
403
+ );
404
+
304
405
  it("uses the resolved AI Chat endpoint for the browser stream transport", async () => {
305
406
  const observeRuntime = vi.fn();
306
407
  const stack = createClientStack({