@btst/stack 4.4.0 → 4.5.1

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 (103) hide show
  1. package/README.md +2 -2
  2. package/dist/packages/stack/src/plugins/ai-chat/api/compaction.cjs +166 -39
  3. package/dist/packages/stack/src/plugins/ai-chat/api/compaction.mjs +167 -40
  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 +275 -95
  9. package/dist/packages/stack/src/plugins/ai-chat/api/operations.mjs +276 -96
  10. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.cjs +8 -5
  11. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +8 -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-input.cjs +3 -2
  15. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-input.mjs +3 -2
  16. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.cjs +26 -17
  17. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.mjs +26 -17
  18. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-message.cjs +22 -2
  19. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-message.mjs +22 -2
  20. package/dist/packages/stack/src/plugins/ai-chat/db.cjs +8 -0
  21. package/dist/packages/stack/src/plugins/ai-chat/db.mjs +8 -0
  22. package/dist/packages/stack/src/plugins/ai-chat/history.cjs +47 -0
  23. package/dist/packages/stack/src/plugins/ai-chat/history.mjs +45 -0
  24. package/dist/plugins/ai-chat/api/index.d.cts +2 -2
  25. package/dist/plugins/ai-chat/api/index.d.mts +2 -2
  26. package/dist/plugins/ai-chat/api/index.d.ts +2 -2
  27. package/dist/plugins/ai-chat/client/components/index.d.cts +1 -1
  28. package/dist/plugins/ai-chat/client/components/index.d.mts +1 -1
  29. package/dist/plugins/ai-chat/client/components/index.d.ts +1 -1
  30. package/dist/plugins/ai-chat/client/hooks/index.d.cts +2 -2
  31. package/dist/plugins/ai-chat/client/hooks/index.d.mts +2 -2
  32. package/dist/plugins/ai-chat/client/hooks/index.d.ts +2 -2
  33. package/dist/plugins/ai-chat/client/index.d.cts +3 -3
  34. package/dist/plugins/ai-chat/client/index.d.mts +3 -3
  35. package/dist/plugins/ai-chat/client/index.d.ts +3 -3
  36. package/dist/plugins/ai-chat/query-keys.d.cts +1 -1
  37. package/dist/plugins/ai-chat/query-keys.d.mts +1 -1
  38. package/dist/plugins/ai-chat/query-keys.d.ts +1 -1
  39. package/dist/plugins/blog/api/index.d.cts +1 -1
  40. package/dist/plugins/blog/api/index.d.mts +1 -1
  41. package/dist/plugins/blog/api/index.d.ts +1 -1
  42. package/dist/plugins/blog/query-keys.d.cts +1 -1
  43. package/dist/plugins/blog/query-keys.d.mts +1 -1
  44. package/dist/plugins/blog/query-keys.d.ts +1 -1
  45. package/dist/plugins/cms/api/index.d.cts +1 -1
  46. package/dist/plugins/cms/api/index.d.mts +1 -1
  47. package/dist/plugins/cms/api/index.d.ts +1 -1
  48. package/dist/plugins/cms/query-keys.d.cts +1 -1
  49. package/dist/plugins/cms/query-keys.d.mts +1 -1
  50. package/dist/plugins/cms/query-keys.d.ts +1 -1
  51. package/dist/plugins/form-builder/api/index.d.cts +1 -1
  52. package/dist/plugins/form-builder/api/index.d.mts +1 -1
  53. package/dist/plugins/form-builder/api/index.d.ts +1 -1
  54. package/dist/plugins/form-builder/client/hooks/index.d.cts +1 -1
  55. package/dist/plugins/form-builder/client/hooks/index.d.mts +1 -1
  56. package/dist/plugins/form-builder/client/hooks/index.d.ts +1 -1
  57. package/dist/plugins/form-builder/query-keys.d.cts +1 -1
  58. package/dist/plugins/form-builder/query-keys.d.mts +1 -1
  59. package/dist/plugins/form-builder/query-keys.d.ts +1 -1
  60. package/dist/plugins/ui-builder/client/hooks/index.d.cts +1 -1
  61. package/dist/plugins/ui-builder/client/hooks/index.d.mts +1 -1
  62. package/dist/plugins/ui-builder/client/hooks/index.d.ts +1 -1
  63. package/dist/plugins/ui-builder/client/index.d.cts +1 -1
  64. package/dist/plugins/ui-builder/client/index.d.mts +1 -1
  65. package/dist/plugins/ui-builder/client/index.d.ts +1 -1
  66. package/dist/plugins/ui-builder/query-keys.d.cts +1 -1
  67. package/dist/plugins/ui-builder/query-keys.d.mts +1 -1
  68. package/dist/plugins/ui-builder/query-keys.d.ts +1 -1
  69. package/dist/shared/{stack.DDQlE2Pm.d.cts → stack.Cei2T2gc.d.cts} +1 -0
  70. package/dist/shared/{stack.C4zspX0T.d.ts → stack.DDQTbADz.d.ts} +7 -2
  71. package/dist/shared/{stack.tAUeRAtd.d.mts → stack.DZRZAzcx.d.mts} +1 -0
  72. package/dist/shared/{stack.ymWsP4HZ.d.cts → stack.IY-ngxuw.d.cts} +7 -2
  73. package/dist/shared/{stack.DlehgNZZ.d.ts → stack.byjorQi5.d.ts} +1 -0
  74. package/dist/shared/{stack.Ps3MDhTC.d.mts → stack.mU9iMF2j.d.mts} +7 -2
  75. package/package.json +1 -1
  76. package/src/plugins/ai-chat/__tests__/authorization.test.ts +56 -30
  77. package/src/plugins/ai-chat/__tests__/chat-input-authorization.test.tsx +42 -0
  78. package/src/plugins/ai-chat/__tests__/classnames-overrides.test.tsx +25 -0
  79. package/src/plugins/ai-chat/__tests__/client-sweep.test.tsx +101 -0
  80. package/src/plugins/ai-chat/__tests__/compaction.test.ts +79 -19
  81. package/src/plugins/ai-chat/__tests__/persisted-compaction.test.ts +948 -0
  82. package/src/plugins/ai-chat/api/compaction.ts +243 -52
  83. package/src/plugins/ai-chat/api/context-checkpoint.ts +66 -0
  84. package/src/plugins/ai-chat/api/getters.ts +2 -0
  85. package/src/plugins/ai-chat/api/operations.ts +377 -101
  86. package/src/plugins/ai-chat/api/plugin.ts +14 -8
  87. package/src/plugins/ai-chat/api/serializers.ts +1 -0
  88. package/src/plugins/ai-chat/client/components/chat-input.tsx +3 -3
  89. package/src/plugins/ai-chat/client/components/chat-interface.tsx +43 -23
  90. package/src/plugins/ai-chat/client/components/chat-message.tsx +32 -2
  91. package/src/plugins/ai-chat/client/localization/index.ts +1 -0
  92. package/src/plugins/ai-chat/db.ts +8 -0
  93. package/src/plugins/ai-chat/history.ts +68 -0
  94. package/src/plugins/ai-chat/types.ts +2 -0
  95. package/dist/shared/{stack.B8PG21zX.d.mts → stack.BLYaUcQa.d.mts} +88 -88
  96. package/dist/shared/{stack.DNw6Hv5o.d.cts → stack.BOZRaVpu.d.cts} +2 -2
  97. package/dist/shared/{stack.D6Pg-ufX.d.cts → stack.Bf6BGZNk.d.cts} +88 -88
  98. package/dist/shared/{stack.CGmh-Lpe.d.ts → stack.BuxUQqL8.d.ts} +12 -12
  99. package/dist/shared/{stack.zch70Loj.d.cts → stack.CX83YNuS.d.cts} +12 -12
  100. package/dist/shared/{stack.CyalJ0j6.d.ts → stack.CyltNNZZ.d.ts} +88 -88
  101. package/dist/shared/{stack.Clil75lW.d.ts → stack.DlBoNqYu.d.ts} +2 -2
  102. package/dist/shared/{stack.DhZtP2JZ.d.mts → stack.Dvy7FmaA.d.mts} +2 -2
  103. package/dist/shared/{stack.Doy2_wgl.d.mts → stack.Egr3RWhA.d.mts} +12 -12
@@ -1,5 +1,7 @@
1
1
  "use client";
2
2
 
3
+ import { persistedHistory } from "../../history";
4
+
3
5
  import { useChat } from "@ai-sdk/react";
4
6
  import {
5
7
  useEffect,
@@ -94,30 +96,28 @@ function reconcilePersistedMessageIds(
94
96
  return messages.map((message) => ({
95
97
  ...message,
96
98
  id: persistedIds.get(message) ?? message.id,
99
+ ...(persistedMessages.some(
100
+ (saved) => saved.id === message.id && saved.interrupted,
101
+ )
102
+ ? { metadata: { ...(message.metadata as object), interrupted: true } }
103
+ : {}),
97
104
  }));
98
105
  }
99
106
 
100
107
  function persistedMessagesToUiMessages(
101
108
  messages: readonly SerializedMessage[],
102
109
  ): UIMessage[] {
103
- return messages
104
- .filter((message) => message.role !== "data")
105
- .map((message) => {
106
- let parts: UIMessage["parts"];
107
- try {
108
- const parsed = JSON.parse(message.content);
109
- parts = Array.isArray(parsed)
110
- ? parsed
111
- : [{ type: "text" as const, text: message.content }];
112
- } catch {
113
- parts = [{ type: "text" as const, text: message.content }];
114
- }
115
- return {
116
- id: message.id,
117
- role: message.role as "user" | "assistant" | "system",
118
- parts,
119
- };
120
- });
110
+ return persistedHistory(messages).map((message) => {
111
+ // Browser tool callbacks do not resume when saved history is restored.
112
+ const abandoned = message.parts.some(
113
+ (part) =>
114
+ "toolCallId" in part &&
115
+ (part.state === "input-streaming" || part.state === "input-available"),
116
+ );
117
+ return abandoned
118
+ ? { ...message, metadata: { interrupted: true } }
119
+ : message;
120
+ });
121
121
  }
122
122
 
123
123
  function ChatActionCheck({
@@ -566,17 +566,19 @@ export function ChatInterface({
566
566
  id: `${chatInstanceId}:${isPublicMode ? "public" : identitySessionVersion}`,
567
567
  transport,
568
568
  onData: ({ type, data }) => {
569
+ if (!mounted.current || !data || typeof data !== "object") return;
569
570
  if (
570
- !isPublicMode ||
571
- !mounted.current ||
572
- !data ||
573
- typeof data !== "object"
571
+ !isPublicMode &&
572
+ (identitySessionVersion !== identitySessionGeneration.current ||
573
+ activeStreamPartitionKey.current !==
574
+ latestIdentityPartitionKey.current)
574
575
  )
575
576
  return;
576
577
  if (type === "data-context-status" && "compacting" in data) {
577
578
  setIsCompacting(data.compacting === true);
578
579
  }
579
580
  if (
581
+ isPublicMode &&
580
582
  type === "data-context-checkpoint" &&
581
583
  "summary" in data &&
582
584
  "throughMessageId" in data &&
@@ -726,6 +728,25 @@ export function ChatInterface({
726
728
  ).length,
727
729
  });
728
730
  }
731
+ if (
732
+ completion?.isAbort ||
733
+ completion?.isError ||
734
+ completion?.isDisconnect
735
+ ) {
736
+ setMessages((current) =>
737
+ current.map((message) =>
738
+ message.id === completion.message.id
739
+ ? {
740
+ ...message,
741
+ metadata: {
742
+ ...(message.metadata as object),
743
+ interrupted: true,
744
+ },
745
+ }
746
+ : message,
747
+ ),
748
+ );
749
+ }
729
750
  // In public mode, skip all persistence-related operations
730
751
  if (isPublicMode) {
731
752
  activeStreamPartitionKey.current = undefined;
@@ -1449,7 +1470,6 @@ export function ChatInterface({
1449
1470
  variant={isWidget ? "compact" : "default"}
1450
1471
  onFilesAttached={setAttachedFiles}
1451
1472
  attachedFiles={attachedFiles}
1452
- allowAttachments={!isPublicMode}
1453
1473
  attachmentPermissionFacts={{
1454
1474
  ...(currentConversationId
1455
1475
  ? { conversationId: currentConversationId }
@@ -132,6 +132,12 @@ export function ChatMessage({
132
132
  isRetrying = false,
133
133
  }: ChatMessageProps) {
134
134
  const isUser = message.role === "user";
135
+ const interrupted = Boolean(
136
+ message.metadata &&
137
+ typeof message.metadata === "object" &&
138
+ "interrupted" in message.metadata &&
139
+ message.metadata.interrupted,
140
+ );
135
141
 
136
142
  const {
137
143
  localization: customLocalization,
@@ -479,10 +485,22 @@ export function ChatMessage({
479
485
  {toolParts.map((part: any) => {
480
486
  const toolName = getToolName(part);
481
487
  const toolCallId = part.toolCallId;
482
- const state = part.state as ToolCallState;
488
+ const unfinished =
489
+ interrupted &&
490
+ (part.state === "input-streaming" ||
491
+ part.state === "input-available");
492
+ const state = (
493
+ unfinished ? "output-error" : part.state
494
+ ) as ToolCallState;
483
495
  const input = part.input;
484
496
  const output = part.output;
485
- const errorText = part.errorText;
497
+ const errorText = unfinished
498
+ ? tr(
499
+ "CHAT_INTERRUPTED",
500
+ "aiChat.interrupted",
501
+ "Response interrupted",
502
+ )
503
+ : part.errorText;
486
504
  const isLoading =
487
505
  state === "input-streaming" ||
488
506
  state === "input-available";
@@ -513,6 +531,18 @@ export function ChatMessage({
513
531
  </div>
514
532
  )}
515
533
  {/* Text content */}
534
+ {interrupted && !isStreaming && (
535
+ <p
536
+ className="text-xs text-muted-foreground"
537
+ data-testid="chat-interrupted"
538
+ >
539
+ {tr(
540
+ "CHAT_INTERRUPTED",
541
+ "aiChat.interrupted",
542
+ "Response interrupted",
543
+ )}
544
+ </p>
545
+ )}
516
546
  {displayContent ? (
517
547
  <MarkdownContent
518
548
  markdown={displayContent}
@@ -13,6 +13,7 @@ export interface AiChatLocalization {
13
13
  CHAT_EMPTY_STATE: string;
14
14
  CHAT_LOADING: string;
15
15
  CHAT_COMPACTING?: string;
16
+ CHAT_INTERRUPTED?: string;
16
17
  CHAT_ERROR: string;
17
18
  CHAT_RETRY: string;
18
19
 
@@ -21,6 +21,10 @@ export const aiChatSchema = createDbPlugin("aiChat", {
21
21
  type: "string",
22
22
  required: true,
23
23
  },
24
+ contextCheckpoint: {
25
+ type: "string",
26
+ required: false,
27
+ },
24
28
  createdAt: {
25
29
  type: "date",
26
30
  defaultValue: () => new Date(),
@@ -56,6 +60,10 @@ export const aiChatSchema = createDbPlugin("aiChat", {
56
60
  type: "string",
57
61
  required: true,
58
62
  },
63
+ interrupted: {
64
+ type: "boolean",
65
+ required: false,
66
+ },
59
67
  createdAt: {
60
68
  type: "date",
61
69
  defaultValue: () => new Date(),
@@ -0,0 +1,68 @@
1
+ import type { UIMessage } from "ai";
2
+ import type { Message } from "./types";
3
+
4
+ function isMessagePart(value: unknown): boolean {
5
+ if (!value || typeof value !== "object" || !("type" in value)) return false;
6
+ const part = value as Record<string, unknown>;
7
+ if (typeof part.type !== "string") return false;
8
+ switch (part.type) {
9
+ case "text":
10
+ case "reasoning":
11
+ return typeof part.text === "string";
12
+ case "file":
13
+ return typeof part.url === "string" && typeof part.mediaType === "string";
14
+ case "source-url":
15
+ return typeof part.sourceId === "string" && typeof part.url === "string";
16
+ case "source-document":
17
+ return (
18
+ typeof part.sourceId === "string" &&
19
+ typeof part.mediaType === "string" &&
20
+ typeof part.title === "string"
21
+ );
22
+ case "step-start":
23
+ return true;
24
+ default:
25
+ if (part.type.startsWith("data-")) return "data" in part;
26
+ return (
27
+ (part.type.startsWith("tool-") ||
28
+ (part.type === "dynamic-tool" &&
29
+ typeof part.toolName === "string")) &&
30
+ typeof part.toolCallId === "string" &&
31
+ [
32
+ "input-streaming",
33
+ "input-available",
34
+ "output-available",
35
+ "output-error",
36
+ ].includes(String(part.state))
37
+ );
38
+ }
39
+ }
40
+
41
+ export function persistedHistory(
42
+ messages: readonly Pick<Message, "id" | "role" | "content" | "interrupted">[],
43
+ ): UIMessage[] {
44
+ return messages
45
+ .filter((message) => message.role !== "data")
46
+ .map((message) => {
47
+ let parts: UIMessage["parts"];
48
+ try {
49
+ const parsed = JSON.parse(message.content);
50
+ // The old completion writer saved [] for empty assistant replies.
51
+ // Empty arrays in legacy user text must remain literal text.
52
+ parts =
53
+ Array.isArray(parsed) &&
54
+ (parsed.length > 0 || message.role === "assistant") &&
55
+ parsed.every(isMessagePart)
56
+ ? parsed
57
+ : [{ type: "text", text: message.content }];
58
+ } catch {
59
+ parts = [{ type: "text", text: message.content }];
60
+ }
61
+ return {
62
+ id: message.id,
63
+ role: message.role as UIMessage["role"],
64
+ parts,
65
+ ...(message.interrupted ? { metadata: { interrupted: true } } : {}),
66
+ };
67
+ });
68
+ }
@@ -15,6 +15,8 @@ export type Message = {
15
15
  conversationId: string;
16
16
  role: "system" | "user" | "assistant" | "data";
17
17
  content: string;
18
+ /** True when generation stopped before a successful completion. */
19
+ interrupted?: boolean | null;
18
20
  createdAt: Date;
19
21
  };
20
22