@btst/stack 4.3.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 (120) hide show
  1. package/README.md +2 -2
  2. package/dist/packages/stack/src/plugins/ai-chat/api/compaction.cjs +289 -0
  3. package/dist/packages/stack/src/plugins/ai-chat/api/compaction.mjs +281 -0
  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 +285 -89
  9. package/dist/packages/stack/src/plugins/ai-chat/api/operations.mjs +285 -89
  10. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.cjs +6 -3
  11. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +6 -3
  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 +62 -17
  15. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.mjs +62 -17
  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/client/context-checkpoint.cjs +30 -0
  19. package/dist/packages/stack/src/plugins/ai-chat/client/context-checkpoint.mjs +27 -0
  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/packages/stack/src/plugins/ai-chat/schemas.cjs +2 -0
  25. package/dist/packages/stack/src/plugins/ai-chat/schemas.mjs +2 -0
  26. package/dist/packages/ui/src/components/markdown-content.cjs +9 -9
  27. package/dist/packages/ui/src/components/markdown-content.mjs +9 -9
  28. package/dist/plugins/ai-chat/api/index.d.cts +2 -2
  29. package/dist/plugins/ai-chat/api/index.d.mts +2 -2
  30. package/dist/plugins/ai-chat/api/index.d.ts +2 -2
  31. package/dist/plugins/ai-chat/client/components/index.d.cts +1 -1
  32. package/dist/plugins/ai-chat/client/components/index.d.mts +1 -1
  33. package/dist/plugins/ai-chat/client/components/index.d.ts +1 -1
  34. package/dist/plugins/ai-chat/client/hooks/index.d.cts +2 -2
  35. package/dist/plugins/ai-chat/client/hooks/index.d.mts +2 -2
  36. package/dist/plugins/ai-chat/client/hooks/index.d.ts +2 -2
  37. package/dist/plugins/ai-chat/client/index.d.cts +3 -3
  38. package/dist/plugins/ai-chat/client/index.d.mts +3 -3
  39. package/dist/plugins/ai-chat/client/index.d.ts +3 -3
  40. package/dist/plugins/ai-chat/query-keys.d.cts +1 -1
  41. package/dist/plugins/ai-chat/query-keys.d.mts +1 -1
  42. package/dist/plugins/ai-chat/query-keys.d.ts +1 -1
  43. package/dist/plugins/blog/api/index.d.cts +2 -2
  44. package/dist/plugins/blog/api/index.d.mts +2 -2
  45. package/dist/plugins/blog/api/index.d.ts +2 -2
  46. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  47. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  48. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  49. package/dist/plugins/blog/client/index.d.cts +2 -2
  50. package/dist/plugins/blog/client/index.d.mts +2 -2
  51. package/dist/plugins/blog/client/index.d.ts +2 -2
  52. package/dist/plugins/blog/query-keys.d.cts +2 -2
  53. package/dist/plugins/blog/query-keys.d.mts +2 -2
  54. package/dist/plugins/blog/query-keys.d.ts +2 -2
  55. package/dist/plugins/cms/api/index.d.cts +1 -1
  56. package/dist/plugins/cms/api/index.d.mts +1 -1
  57. package/dist/plugins/cms/api/index.d.ts +1 -1
  58. package/dist/plugins/cms/query-keys.d.cts +1 -1
  59. package/dist/plugins/cms/query-keys.d.mts +1 -1
  60. package/dist/plugins/cms/query-keys.d.ts +1 -1
  61. package/dist/plugins/form-builder/api/index.d.cts +1 -1
  62. package/dist/plugins/form-builder/api/index.d.mts +1 -1
  63. package/dist/plugins/form-builder/api/index.d.ts +1 -1
  64. package/dist/plugins/form-builder/client/hooks/index.d.cts +1 -1
  65. package/dist/plugins/form-builder/client/hooks/index.d.mts +1 -1
  66. package/dist/plugins/form-builder/client/hooks/index.d.ts +1 -1
  67. package/dist/plugins/form-builder/query-keys.d.cts +1 -1
  68. package/dist/plugins/form-builder/query-keys.d.mts +1 -1
  69. package/dist/plugins/form-builder/query-keys.d.ts +1 -1
  70. package/dist/plugins/ui-builder/client/hooks/index.d.cts +1 -1
  71. package/dist/plugins/ui-builder/client/hooks/index.d.mts +1 -1
  72. package/dist/plugins/ui-builder/client/hooks/index.d.ts +1 -1
  73. package/dist/plugins/ui-builder/client/index.d.cts +1 -1
  74. package/dist/plugins/ui-builder/client/index.d.mts +1 -1
  75. package/dist/plugins/ui-builder/client/index.d.ts +1 -1
  76. package/dist/plugins/ui-builder/query-keys.d.cts +1 -1
  77. package/dist/plugins/ui-builder/query-keys.d.mts +1 -1
  78. package/dist/plugins/ui-builder/query-keys.d.ts +1 -1
  79. package/dist/shared/{stack.D2y4gl03.d.mts → stack.94kN3pEK.d.mts} +5 -5
  80. package/dist/shared/{stack.DBH1Bjko.d.ts → stack.B2EYZyh_.d.ts} +16 -2
  81. package/dist/shared/{stack.rmWRtSjs.d.cts → stack.BTStHVTy.d.cts} +5 -5
  82. package/dist/shared/{stack.CZ1h_T4k.d.mts → stack.BggTHKlo.d.mts} +90 -90
  83. package/dist/shared/{stack.DzJgoNqJ.d.cts → stack.Cei2T2gc.d.cts} +2 -0
  84. package/dist/shared/{stack.DiYg7Jw1.d.cts → stack.CtrnoQgH.d.cts} +90 -90
  85. package/dist/shared/{stack.CHrtGKLz.d.mts → stack.DZRZAzcx.d.mts} +2 -0
  86. package/dist/shared/{stack.CnDMBj4z.d.ts → stack.DfrRYOIl.d.ts} +5 -5
  87. package/dist/shared/{stack.B7Mj74BQ.d.mts → stack.DivuAnNI.d.mts} +16 -2
  88. package/dist/shared/{stack.qMp2VzNw.d.ts → stack.byjorQi5.d.ts} +2 -0
  89. package/dist/shared/{stack.Dgt3MCtW.d.cts → stack.fF273jws.d.cts} +16 -2
  90. package/dist/shared/{stack.DQpyA3Cq.d.ts → stack.lPKwd_v0.d.ts} +90 -90
  91. package/package.json +1 -1
  92. package/src/__tests__/markdown-link-navigation.test.tsx +29 -0
  93. package/src/plugins/ai-chat/__tests__/authorization.test.ts +56 -30
  94. package/src/plugins/ai-chat/__tests__/classnames-overrides.test.tsx +25 -0
  95. package/src/plugins/ai-chat/__tests__/client-sweep.test.tsx +101 -0
  96. package/src/plugins/ai-chat/__tests__/compaction.test.ts +272 -0
  97. package/src/plugins/ai-chat/__tests__/persisted-compaction.test.ts +819 -0
  98. package/src/plugins/ai-chat/api/compaction.ts +377 -0
  99. package/src/plugins/ai-chat/api/context-checkpoint.ts +66 -0
  100. package/src/plugins/ai-chat/api/getters.ts +2 -0
  101. package/src/plugins/ai-chat/api/operations.ts +380 -93
  102. package/src/plugins/ai-chat/api/plugin.ts +15 -4
  103. package/src/plugins/ai-chat/api/serializers.ts +1 -0
  104. package/src/plugins/ai-chat/client/components/chat-interface.tsx +97 -21
  105. package/src/plugins/ai-chat/client/components/chat-message.tsx +32 -2
  106. package/src/plugins/ai-chat/client/context-checkpoint.ts +47 -0
  107. package/src/plugins/ai-chat/client/localization/index.ts +2 -0
  108. package/src/plugins/ai-chat/db.ts +8 -0
  109. package/src/plugins/ai-chat/history.ts +68 -0
  110. package/src/plugins/ai-chat/schemas.ts +2 -0
  111. package/src/plugins/ai-chat/types.ts +2 -0
  112. package/dist/shared/{stack.lsbdKRj7.d.mts → stack.B8PG21zX.d.mts} +16 -16
  113. package/dist/shared/{stack.CGmh-Lpe.d.ts → stack.BuxUQqL8.d.ts} +12 -12
  114. package/dist/shared/{stack.DtZgEX84.d.cts → stack.CKc_Bais.d.cts} +6 -6
  115. package/dist/shared/{stack.DtZgEX84.d.mts → stack.CKc_Bais.d.mts} +6 -6
  116. package/dist/shared/{stack.DtZgEX84.d.ts → stack.CKc_Bais.d.ts} +6 -6
  117. package/dist/shared/{stack.zch70Loj.d.cts → stack.CX83YNuS.d.cts} +12 -12
  118. package/dist/shared/{stack.BMlTZnZp.d.ts → stack.CyalJ0j6.d.ts} +16 -16
  119. package/dist/shared/{stack.BV0uNV4f.d.cts → stack.D6Pg-ufX.d.cts} +16 -16
  120. package/dist/shared/{stack.Doy2_wgl.d.mts → stack.Egr3RWhA.d.mts} +12 -12
@@ -2,6 +2,7 @@
2
2
  'use strict';
3
3
 
4
4
  const jsxRuntime = require('react/jsx-runtime');
5
+ const history = require('../../history.cjs');
5
6
  const react = require('@ai-sdk/react');
6
7
  const React = require('react');
7
8
  const reactQuery = require('@tanstack/react-query');
@@ -21,6 +22,7 @@ const chatHooks = require('../hooks/chat-hooks.cjs');
21
22
  const plugins_aiChat_client_context_pageAiContext = require('../../../../../../../plugins/ai-chat/client/context/page-ai-context.cjs');
22
23
  const chatAnalytics = require('../context/chat-analytics.cjs');
23
24
  const navigation = require('../navigation.cjs');
25
+ const contextCheckpoint = require('../context-checkpoint.cjs');
24
26
 
25
27
  function reconcilePersistedMessageIds(messages, persistedMessages) {
26
28
  const userMessages = messages.filter((message) => message.role === "user");
@@ -42,23 +44,18 @@ function reconcilePersistedMessageIds(messages, persistedMessages) {
42
44
  }
43
45
  return messages.map((message) => ({
44
46
  ...message,
45
- id: persistedIds.get(message) ?? message.id
47
+ id: persistedIds.get(message) ?? message.id,
48
+ ...persistedMessages.some(
49
+ (saved) => saved.id === message.id && saved.interrupted
50
+ ) ? { metadata: { ...message.metadata, interrupted: true } } : {}
46
51
  }));
47
52
  }
48
53
  function persistedMessagesToUiMessages(messages) {
49
- return messages.filter((message) => message.role !== "data").map((message) => {
50
- let parts;
51
- try {
52
- const parsed = JSON.parse(message.content);
53
- parts = Array.isArray(parsed) ? parsed : [{ type: "text", text: message.content }];
54
- } catch {
55
- parts = [{ type: "text", text: message.content }];
56
- }
57
- return {
58
- id: message.id,
59
- role: message.role,
60
- parts
61
- };
54
+ return history.persistedHistory(messages).map((message) => {
55
+ const abandoned = message.parts.some(
56
+ (part) => "toolCallId" in part && (part.state === "input-streaming" || part.state === "input-available")
57
+ );
58
+ return abandoned ? { ...message, metadata: { interrupted: true } } : message;
62
59
  });
63
60
  }
64
61
  function ChatActionCheck({
@@ -208,6 +205,9 @@ function ChatInterface({
208
205
  const latestStreamRequestGeneration = React.useRef(0);
209
206
  const pendingStreamRequests = React.useRef([]);
210
207
  const latestMessages = React.useRef([]);
208
+ const contextCheckpoint$1 = React.useRef(void 0);
209
+ const submittedMessages = React.useRef([]);
210
+ const [isCompacting, setIsCompacting] = React.useState(false);
211
211
  const conversationsListQueryKey = React.useMemo(() => {
212
212
  if (isPublicMode) return ["ai-chat", "disabled"];
213
213
  const client$1 = client.createApiClient({
@@ -307,6 +307,7 @@ function ChatInterface({
307
307
  pendingStreamRequests.current.push(request);
308
308
  activeStreamPartitionKey.current = request.identityPartitionKey;
309
309
  }
310
+ setIsCompacting(false);
310
311
  const currentPageContext = pageAIContextRef.current;
311
312
  const pageContextBody = currentPageContext?.pageDescription ? {
312
313
  pageContext: currentPageContext.pageDescription,
@@ -319,18 +320,20 @@ function ChatInterface({
319
320
  if (newUserMessage) {
320
321
  messagesToSend.push(newUserMessage);
321
322
  }
323
+ submittedMessages.current = messagesToSend;
322
324
  editMessagesRef.current = null;
323
325
  return {
324
326
  body: {
325
- messages: messagesToSend,
327
+ ...isPublicMode ? contextCheckpoint.contextRequest(messagesToSend, contextCheckpoint$1.current) : { messages: messagesToSend },
326
328
  conversationId: conversationIdRef.current,
327
329
  ...pageContextBody
328
330
  }
329
331
  };
330
332
  }
333
+ submittedMessages.current = hookMessages;
331
334
  return {
332
335
  body: {
333
- messages: hookMessages,
336
+ ...isPublicMode ? contextCheckpoint.contextRequest(hookMessages, contextCheckpoint$1.current) : { messages: hookMessages },
334
337
  conversationId: conversationIdRef.current,
335
338
  ...pageContextBody
336
339
  }
@@ -374,6 +377,21 @@ function ChatInterface({
374
377
  } = react.useChat({
375
378
  id: `${chatInstanceId}:${isPublicMode ? "public" : identitySessionVersion}`,
376
379
  transport,
380
+ onData: ({ type, data }) => {
381
+ if (!mounted.current || !data || typeof data !== "object") return;
382
+ if (!isPublicMode && (identitySessionVersion !== identitySessionGeneration.current || activeStreamPartitionKey.current !== latestIdentityPartitionKey.current))
383
+ return;
384
+ if (type === "data-context-status" && "compacting" in data) {
385
+ setIsCompacting(data.compacting === true);
386
+ }
387
+ if (isPublicMode && type === "data-context-checkpoint" && "summary" in data && "throughMessageId" in data && typeof data.summary === "string" && typeof data.throughMessageId === "string") {
388
+ contextCheckpoint$1.current = contextCheckpoint.captureContextCheckpoint(
389
+ submittedMessages.current,
390
+ data.summary,
391
+ data.throughMessageId
392
+ );
393
+ }
394
+ },
377
395
  // Automatically resubmit after all client-side tool results are provided
378
396
  sendAutomaticallyWhen: (options) => mounted.current && (isPublicMode || identitySessionVersion === identitySessionGeneration.current) && ai.lastAssistantMessageIsCompleteWithToolCalls(options),
379
397
  onToolCall: async ({ toolCall }) => {
@@ -424,6 +442,7 @@ function ChatInterface({
424
442
  }
425
443
  },
426
444
  onError: (err) => {
445
+ setIsCompacting(false);
427
446
  if (!mounted.current) return;
428
447
  console.error("useChat onError:", err);
429
448
  if (!isPublicMode && identitySessionVersion !== identitySessionGeneration.current) {
@@ -437,6 +456,7 @@ function ChatInterface({
437
456
  trackFailure();
438
457
  },
439
458
  onFinish: async (completion) => {
459
+ setIsCompacting(false);
440
460
  if (!mounted.current || !isPublicMode && identitySessionVersion !== identitySessionGeneration.current)
441
461
  return;
442
462
  if (completion?.isError || completion?.isDisconnect) trackFailure();
@@ -458,6 +478,19 @@ function ChatInterface({
458
478
  ).length
459
479
  });
460
480
  }
481
+ if (completion?.isAbort || completion?.isError || completion?.isDisconnect) {
482
+ setMessages(
483
+ (current) => current.map(
484
+ (message) => message.id === completion.message.id ? {
485
+ ...message,
486
+ metadata: {
487
+ ...message.metadata,
488
+ interrupted: true
489
+ }
490
+ } : message
491
+ )
492
+ );
493
+ }
461
494
  if (isPublicMode) {
462
495
  activeStreamPartitionKey.current = void 0;
463
496
  return;
@@ -932,7 +965,19 @@ function ChatInterface({
932
965
  },
933
966
  m.id || `msg-${index}`
934
967
  )),
935
- isLoading && messages[messages.length - 1]?.role !== "assistant" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2 text-muted-foreground text-sm py-4", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "animate-pulse", children: tr("CHAT_LOADING", "aiChat.chat.loading", "Thinking...") }) }),
968
+ isLoading && isCompacting && /* @__PURE__ */ jsxRuntime.jsx(
969
+ "div",
970
+ {
971
+ role: "status",
972
+ className: "text-muted-foreground text-sm py-4",
973
+ children: tr(
974
+ "CHAT_COMPACTING",
975
+ "aiChat.chat.compacting",
976
+ "Summarizing earlier conversation\u2026"
977
+ )
978
+ }
979
+ ),
980
+ isLoading && !isCompacting && messages[messages.length - 1]?.role !== "assistant" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2 text-muted-foreground text-sm py-4", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "animate-pulse", children: tr("CHAT_LOADING", "aiChat.chat.loading", "Thinking...") }) }),
936
981
  (error || historySyncError) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-destructive text-sm py-4 px-3 bg-destructive/10 rounded-md", children: [
937
982
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1", children: tr(
938
983
  "CHAT_ERROR",
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
3
+ import { persistedHistory } from '../../history.mjs';
3
4
  import { useChat } from '@ai-sdk/react';
4
5
  import { useId, useRef, useState, useMemo, useEffect, useCallback, useLayoutEffect } from 'react';
5
6
  import { useQueryClient, hashKey } from '@tanstack/react-query';
@@ -19,6 +20,7 @@ import { useAiChatIdentityPartition, useConversation, useConversations } from '.
19
20
  import { usePageAIContext } from '../../../../../../../plugins/ai-chat/client/context/page-ai-context.mjs';
20
21
  import { useChatAnalytics } from '../context/chat-analytics.mjs';
21
22
  import { navigateAiChatCrossOrigin } from '../navigation.mjs';
23
+ import { contextRequest, captureContextCheckpoint } from '../context-checkpoint.mjs';
22
24
 
23
25
  function reconcilePersistedMessageIds(messages, persistedMessages) {
24
26
  const userMessages = messages.filter((message) => message.role === "user");
@@ -40,23 +42,18 @@ function reconcilePersistedMessageIds(messages, persistedMessages) {
40
42
  }
41
43
  return messages.map((message) => ({
42
44
  ...message,
43
- id: persistedIds.get(message) ?? message.id
45
+ id: persistedIds.get(message) ?? message.id,
46
+ ...persistedMessages.some(
47
+ (saved) => saved.id === message.id && saved.interrupted
48
+ ) ? { metadata: { ...message.metadata, interrupted: true } } : {}
44
49
  }));
45
50
  }
46
51
  function persistedMessagesToUiMessages(messages) {
47
- return messages.filter((message) => message.role !== "data").map((message) => {
48
- let parts;
49
- try {
50
- const parsed = JSON.parse(message.content);
51
- parts = Array.isArray(parsed) ? parsed : [{ type: "text", text: message.content }];
52
- } catch {
53
- parts = [{ type: "text", text: message.content }];
54
- }
55
- return {
56
- id: message.id,
57
- role: message.role,
58
- parts
59
- };
52
+ return persistedHistory(messages).map((message) => {
53
+ const abandoned = message.parts.some(
54
+ (part) => "toolCallId" in part && (part.state === "input-streaming" || part.state === "input-available")
55
+ );
56
+ return abandoned ? { ...message, metadata: { interrupted: true } } : message;
60
57
  });
61
58
  }
62
59
  function ChatActionCheck({
@@ -206,6 +203,9 @@ function ChatInterface({
206
203
  const latestStreamRequestGeneration = useRef(0);
207
204
  const pendingStreamRequests = useRef([]);
208
205
  const latestMessages = useRef([]);
206
+ const contextCheckpoint = useRef(void 0);
207
+ const submittedMessages = useRef([]);
208
+ const [isCompacting, setIsCompacting] = useState(false);
209
209
  const conversationsListQueryKey = useMemo(() => {
210
210
  if (isPublicMode) return ["ai-chat", "disabled"];
211
211
  const client = createApiClient({
@@ -305,6 +305,7 @@ function ChatInterface({
305
305
  pendingStreamRequests.current.push(request);
306
306
  activeStreamPartitionKey.current = request.identityPartitionKey;
307
307
  }
308
+ setIsCompacting(false);
308
309
  const currentPageContext = pageAIContextRef.current;
309
310
  const pageContextBody = currentPageContext?.pageDescription ? {
310
311
  pageContext: currentPageContext.pageDescription,
@@ -317,18 +318,20 @@ function ChatInterface({
317
318
  if (newUserMessage) {
318
319
  messagesToSend.push(newUserMessage);
319
320
  }
321
+ submittedMessages.current = messagesToSend;
320
322
  editMessagesRef.current = null;
321
323
  return {
322
324
  body: {
323
- messages: messagesToSend,
325
+ ...isPublicMode ? contextRequest(messagesToSend, contextCheckpoint.current) : { messages: messagesToSend },
324
326
  conversationId: conversationIdRef.current,
325
327
  ...pageContextBody
326
328
  }
327
329
  };
328
330
  }
331
+ submittedMessages.current = hookMessages;
329
332
  return {
330
333
  body: {
331
- messages: hookMessages,
334
+ ...isPublicMode ? contextRequest(hookMessages, contextCheckpoint.current) : { messages: hookMessages },
332
335
  conversationId: conversationIdRef.current,
333
336
  ...pageContextBody
334
337
  }
@@ -372,6 +375,21 @@ function ChatInterface({
372
375
  } = useChat({
373
376
  id: `${chatInstanceId}:${isPublicMode ? "public" : identitySessionVersion}`,
374
377
  transport,
378
+ onData: ({ type, data }) => {
379
+ if (!mounted.current || !data || typeof data !== "object") return;
380
+ if (!isPublicMode && (identitySessionVersion !== identitySessionGeneration.current || activeStreamPartitionKey.current !== latestIdentityPartitionKey.current))
381
+ return;
382
+ if (type === "data-context-status" && "compacting" in data) {
383
+ setIsCompacting(data.compacting === true);
384
+ }
385
+ if (isPublicMode && type === "data-context-checkpoint" && "summary" in data && "throughMessageId" in data && typeof data.summary === "string" && typeof data.throughMessageId === "string") {
386
+ contextCheckpoint.current = captureContextCheckpoint(
387
+ submittedMessages.current,
388
+ data.summary,
389
+ data.throughMessageId
390
+ );
391
+ }
392
+ },
375
393
  // Automatically resubmit after all client-side tool results are provided
376
394
  sendAutomaticallyWhen: (options) => mounted.current && (isPublicMode || identitySessionVersion === identitySessionGeneration.current) && lastAssistantMessageIsCompleteWithToolCalls(options),
377
395
  onToolCall: async ({ toolCall }) => {
@@ -422,6 +440,7 @@ function ChatInterface({
422
440
  }
423
441
  },
424
442
  onError: (err) => {
443
+ setIsCompacting(false);
425
444
  if (!mounted.current) return;
426
445
  console.error("useChat onError:", err);
427
446
  if (!isPublicMode && identitySessionVersion !== identitySessionGeneration.current) {
@@ -435,6 +454,7 @@ function ChatInterface({
435
454
  trackFailure();
436
455
  },
437
456
  onFinish: async (completion) => {
457
+ setIsCompacting(false);
438
458
  if (!mounted.current || !isPublicMode && identitySessionVersion !== identitySessionGeneration.current)
439
459
  return;
440
460
  if (completion?.isError || completion?.isDisconnect) trackFailure();
@@ -456,6 +476,19 @@ function ChatInterface({
456
476
  ).length
457
477
  });
458
478
  }
479
+ if (completion?.isAbort || completion?.isError || completion?.isDisconnect) {
480
+ setMessages(
481
+ (current) => current.map(
482
+ (message) => message.id === completion.message.id ? {
483
+ ...message,
484
+ metadata: {
485
+ ...message.metadata,
486
+ interrupted: true
487
+ }
488
+ } : message
489
+ )
490
+ );
491
+ }
459
492
  if (isPublicMode) {
460
493
  activeStreamPartitionKey.current = void 0;
461
494
  return;
@@ -930,7 +963,19 @@ function ChatInterface({
930
963
  },
931
964
  m.id || `msg-${index}`
932
965
  )),
933
- isLoading && messages[messages.length - 1]?.role !== "assistant" && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2 text-muted-foreground text-sm py-4", children: /* @__PURE__ */ jsx("div", { className: "animate-pulse", children: tr("CHAT_LOADING", "aiChat.chat.loading", "Thinking...") }) }),
966
+ isLoading && isCompacting && /* @__PURE__ */ jsx(
967
+ "div",
968
+ {
969
+ role: "status",
970
+ className: "text-muted-foreground text-sm py-4",
971
+ children: tr(
972
+ "CHAT_COMPACTING",
973
+ "aiChat.chat.compacting",
974
+ "Summarizing earlier conversation\u2026"
975
+ )
976
+ }
977
+ ),
978
+ isLoading && !isCompacting && messages[messages.length - 1]?.role !== "assistant" && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2 text-muted-foreground text-sm py-4", children: /* @__PURE__ */ jsx("div", { className: "animate-pulse", children: tr("CHAT_LOADING", "aiChat.chat.loading", "Thinking...") }) }),
934
979
  (error || historySyncError) && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-destructive text-sm py-4 px-3 bg-destructive/10 rounded-md", children: [
935
980
  /* @__PURE__ */ jsx("span", { className: "flex-1", children: tr(
936
981
  "CHAT_ERROR",
@@ -82,6 +82,9 @@ function ChatMessage({
82
82
  isRetrying = false
83
83
  }) {
84
84
  const isUser = message.role === "user";
85
+ const interrupted = Boolean(
86
+ message.metadata && typeof message.metadata === "object" && "interrupted" in message.metadata && message.metadata.interrupted
87
+ );
85
88
  const {
86
89
  localization: customLocalization,
87
90
  toolRenderers,
@@ -359,10 +362,15 @@ function ChatMessage({
359
362
  toolParts.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2", children: toolParts.map((part) => {
360
363
  const toolName = ai.getToolName(part);
361
364
  const toolCallId = part.toolCallId;
362
- const state = part.state;
365
+ const unfinished = interrupted && (part.state === "input-streaming" || part.state === "input-available");
366
+ const state = unfinished ? "output-error" : part.state;
363
367
  const input = part.input;
364
368
  const output = part.output;
365
- const errorText = part.errorText;
369
+ const errorText = unfinished ? tr(
370
+ "CHAT_INTERRUPTED",
371
+ "aiChat.interrupted",
372
+ "Response interrupted"
373
+ ) : part.errorText;
366
374
  const isLoading = state === "input-streaming" || state === "input-available";
367
375
  const toolCallProps = {
368
376
  toolCallId,
@@ -379,6 +387,18 @@ function ChatMessage({
379
387
  }
380
388
  return /* @__PURE__ */ jsxRuntime.jsx(toolCallDisplay.ToolCallDisplay, { ...toolCallProps }, toolCallId);
381
389
  }) }),
390
+ interrupted && !isStreaming && /* @__PURE__ */ jsxRuntime.jsx(
391
+ "p",
392
+ {
393
+ className: "text-xs text-muted-foreground",
394
+ "data-testid": "chat-interrupted",
395
+ children: tr(
396
+ "CHAT_INTERRUPTED",
397
+ "aiChat.interrupted",
398
+ "Response interrupted"
399
+ )
400
+ }
401
+ ),
382
402
  displayContent ? /* @__PURE__ */ jsxRuntime.jsx(
383
403
  markdownContent.MarkdownContent,
384
404
  {
@@ -76,6 +76,9 @@ function ChatMessage({
76
76
  isRetrying = false
77
77
  }) {
78
78
  const isUser = message.role === "user";
79
+ const interrupted = Boolean(
80
+ message.metadata && typeof message.metadata === "object" && "interrupted" in message.metadata && message.metadata.interrupted
81
+ );
79
82
  const {
80
83
  localization: customLocalization,
81
84
  toolRenderers,
@@ -353,10 +356,15 @@ function ChatMessage({
353
356
  toolParts.length > 0 && /* @__PURE__ */ jsx("div", { className: "space-y-2", children: toolParts.map((part) => {
354
357
  const toolName = getToolName(part);
355
358
  const toolCallId = part.toolCallId;
356
- const state = part.state;
359
+ const unfinished = interrupted && (part.state === "input-streaming" || part.state === "input-available");
360
+ const state = unfinished ? "output-error" : part.state;
357
361
  const input = part.input;
358
362
  const output = part.output;
359
- const errorText = part.errorText;
363
+ const errorText = unfinished ? tr(
364
+ "CHAT_INTERRUPTED",
365
+ "aiChat.interrupted",
366
+ "Response interrupted"
367
+ ) : part.errorText;
360
368
  const isLoading = state === "input-streaming" || state === "input-available";
361
369
  const toolCallProps = {
362
370
  toolCallId,
@@ -373,6 +381,18 @@ function ChatMessage({
373
381
  }
374
382
  return /* @__PURE__ */ jsx(ToolCallDisplay, { ...toolCallProps }, toolCallId);
375
383
  }) }),
384
+ interrupted && !isStreaming && /* @__PURE__ */ jsx(
385
+ "p",
386
+ {
387
+ className: "text-xs text-muted-foreground",
388
+ "data-testid": "chat-interrupted",
389
+ children: tr(
390
+ "CHAT_INTERRUPTED",
391
+ "aiChat.interrupted",
392
+ "Response interrupted"
393
+ )
394
+ }
395
+ ),
376
396
  displayContent ? /* @__PURE__ */ jsx(
377
397
  MarkdownContent,
378
398
  {
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+
3
+ function captureContextCheckpoint(messages, summary, throughMessageId) {
4
+ const index = messages.findIndex(
5
+ (message) => message.id === throughMessageId
6
+ );
7
+ if (index < 0) return void 0;
8
+ return {
9
+ summary,
10
+ throughMessageId,
11
+ prefix: JSON.stringify(messages.slice(0, index + 1))
12
+ };
13
+ }
14
+ function contextRequest(messages, checkpoint) {
15
+ if (checkpoint) {
16
+ const index = messages.findIndex(
17
+ (message) => message.id === checkpoint.throughMessageId
18
+ );
19
+ if (index >= 0 && JSON.stringify(messages.slice(0, index + 1)) === checkpoint.prefix) {
20
+ return {
21
+ messages: messages.slice(index + 1),
22
+ contextSummary: checkpoint.summary
23
+ };
24
+ }
25
+ }
26
+ return { messages };
27
+ }
28
+
29
+ exports.captureContextCheckpoint = captureContextCheckpoint;
30
+ exports.contextRequest = contextRequest;
@@ -0,0 +1,27 @@
1
+ function captureContextCheckpoint(messages, summary, throughMessageId) {
2
+ const index = messages.findIndex(
3
+ (message) => message.id === throughMessageId
4
+ );
5
+ if (index < 0) return void 0;
6
+ return {
7
+ summary,
8
+ throughMessageId,
9
+ prefix: JSON.stringify(messages.slice(0, index + 1))
10
+ };
11
+ }
12
+ function contextRequest(messages, checkpoint) {
13
+ if (checkpoint) {
14
+ const index = messages.findIndex(
15
+ (message) => message.id === checkpoint.throughMessageId
16
+ );
17
+ if (index >= 0 && JSON.stringify(messages.slice(0, index + 1)) === checkpoint.prefix) {
18
+ return {
19
+ messages: messages.slice(index + 1),
20
+ contextSummary: checkpoint.summary
21
+ };
22
+ }
23
+ }
24
+ return { messages };
25
+ }
26
+
27
+ export { captureContextCheckpoint, contextRequest };
@@ -19,6 +19,10 @@ const aiChatSchema = db.createDbPlugin("aiChat", {
19
19
  type: "string",
20
20
  required: true
21
21
  },
22
+ contextCheckpoint: {
23
+ type: "string",
24
+ required: false
25
+ },
22
26
  createdAt: {
23
27
  type: "date",
24
28
  defaultValue: () => /* @__PURE__ */ new Date()
@@ -54,6 +58,10 @@ const aiChatSchema = db.createDbPlugin("aiChat", {
54
58
  type: "string",
55
59
  required: true
56
60
  },
61
+ interrupted: {
62
+ type: "boolean",
63
+ required: false
64
+ },
57
65
  createdAt: {
58
66
  type: "date",
59
67
  defaultValue: () => /* @__PURE__ */ new Date()
@@ -17,6 +17,10 @@ const aiChatSchema = createDbPlugin("aiChat", {
17
17
  type: "string",
18
18
  required: true
19
19
  },
20
+ contextCheckpoint: {
21
+ type: "string",
22
+ required: false
23
+ },
20
24
  createdAt: {
21
25
  type: "date",
22
26
  defaultValue: () => /* @__PURE__ */ new Date()
@@ -52,6 +56,10 @@ const aiChatSchema = createDbPlugin("aiChat", {
52
56
  type: "string",
53
57
  required: true
54
58
  },
59
+ interrupted: {
60
+ type: "boolean",
61
+ required: false
62
+ },
55
63
  createdAt: {
56
64
  type: "date",
57
65
  defaultValue: () => /* @__PURE__ */ new Date()
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ function isMessagePart(value) {
4
+ if (!value || typeof value !== "object" || !("type" in value)) return false;
5
+ const part = value;
6
+ if (typeof part.type !== "string") return false;
7
+ switch (part.type) {
8
+ case "text":
9
+ case "reasoning":
10
+ return typeof part.text === "string";
11
+ case "file":
12
+ return typeof part.url === "string" && typeof part.mediaType === "string";
13
+ case "source-url":
14
+ return typeof part.sourceId === "string" && typeof part.url === "string";
15
+ case "source-document":
16
+ return typeof part.sourceId === "string" && typeof part.mediaType === "string" && typeof part.title === "string";
17
+ case "step-start":
18
+ return true;
19
+ default:
20
+ if (part.type.startsWith("data-")) return "data" in part;
21
+ return (part.type.startsWith("tool-") || part.type === "dynamic-tool" && typeof part.toolName === "string") && typeof part.toolCallId === "string" && [
22
+ "input-streaming",
23
+ "input-available",
24
+ "output-available",
25
+ "output-error"
26
+ ].includes(String(part.state));
27
+ }
28
+ }
29
+ function persistedHistory(messages) {
30
+ return messages.filter((message) => message.role !== "data").map((message) => {
31
+ let parts;
32
+ try {
33
+ const parsed = JSON.parse(message.content);
34
+ parts = Array.isArray(parsed) && (parsed.length > 0 || message.role === "assistant") && parsed.every(isMessagePart) ? parsed : [{ type: "text", text: message.content }];
35
+ } catch {
36
+ parts = [{ type: "text", text: message.content }];
37
+ }
38
+ return {
39
+ id: message.id,
40
+ role: message.role,
41
+ parts,
42
+ ...message.interrupted ? { metadata: { interrupted: true } } : {}
43
+ };
44
+ });
45
+ }
46
+
47
+ exports.persistedHistory = persistedHistory;
@@ -0,0 +1,45 @@
1
+ function isMessagePart(value) {
2
+ if (!value || typeof value !== "object" || !("type" in value)) return false;
3
+ const part = value;
4
+ if (typeof part.type !== "string") return false;
5
+ switch (part.type) {
6
+ case "text":
7
+ case "reasoning":
8
+ return typeof part.text === "string";
9
+ case "file":
10
+ return typeof part.url === "string" && typeof part.mediaType === "string";
11
+ case "source-url":
12
+ return typeof part.sourceId === "string" && typeof part.url === "string";
13
+ case "source-document":
14
+ return typeof part.sourceId === "string" && typeof part.mediaType === "string" && typeof part.title === "string";
15
+ case "step-start":
16
+ return true;
17
+ default:
18
+ if (part.type.startsWith("data-")) return "data" in part;
19
+ return (part.type.startsWith("tool-") || part.type === "dynamic-tool" && typeof part.toolName === "string") && typeof part.toolCallId === "string" && [
20
+ "input-streaming",
21
+ "input-available",
22
+ "output-available",
23
+ "output-error"
24
+ ].includes(String(part.state));
25
+ }
26
+ }
27
+ function persistedHistory(messages) {
28
+ return messages.filter((message) => message.role !== "data").map((message) => {
29
+ let parts;
30
+ try {
31
+ const parsed = JSON.parse(message.content);
32
+ parts = Array.isArray(parsed) && (parsed.length > 0 || message.role === "assistant") && parsed.every(isMessagePart) ? parsed : [{ type: "text", text: message.content }];
33
+ } catch {
34
+ parts = [{ type: "text", text: message.content }];
35
+ }
36
+ return {
37
+ id: message.id,
38
+ role: message.role,
39
+ parts,
40
+ ...message.interrupted ? { metadata: { interrupted: true } } : {}
41
+ };
42
+ });
43
+ }
44
+
45
+ export { persistedHistory };
@@ -34,6 +34,8 @@ const chatRequestSchema = z.z.object({
34
34
  ])
35
35
  ),
36
36
  conversationId: z.z.string().optional(),
37
+ /** Untrusted continuation summary for opt-in public context compaction. */
38
+ contextSummary: z.z.string().max(32e3).optional(),
37
39
  model: z.z.string().optional(),
38
40
  /**
39
41
  * Description of the current page context, injected into the AI system prompt.
@@ -32,6 +32,8 @@ const chatRequestSchema = z.object({
32
32
  ])
33
33
  ),
34
34
  conversationId: z.string().optional(),
35
+ /** Untrusted continuation summary for opt-in public context compaction. */
36
+ contextSummary: z.string().max(32e3).optional(),
35
37
  model: z.string().optional(),
36
38
  /**
37
39
  * Description of the current page context, injected into the AI system prompt.