@chat-js/cli 0.3.0 → 0.6.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/index.js +1173 -964
  2. package/package.json +1 -1
  3. package/templates/chat-app/app/(auth)/device-login/page.tsx +37 -0
  4. package/templates/chat-app/app/(auth)/login/page.tsx +26 -2
  5. package/templates/chat-app/app/(auth)/register/page.tsx +0 -12
  6. package/templates/chat-app/app/(chat)/api/chat/filter-reasoning-parts.ts +1 -1
  7. package/templates/chat-app/app/(chat)/api/chat/prepare/route.ts +94 -0
  8. package/templates/chat-app/app/(chat)/api/chat/route.ts +107 -16
  9. package/templates/chat-app/app/(chat)/layout.tsx +4 -1
  10. package/templates/chat-app/app/api/trpc/[trpc]/route.ts +1 -0
  11. package/templates/chat-app/app/globals.css +9 -9
  12. package/templates/chat-app/app/layout.tsx +4 -2
  13. package/templates/chat-app/biome.jsonc +3 -3
  14. package/templates/chat-app/chat.config.ts +32 -12
  15. package/templates/chat-app/components/ai-elements/prompt-input.tsx +1 -1
  16. package/templates/chat-app/components/anonymous-session-init.tsx +10 -6
  17. package/templates/chat-app/components/artifact-actions.tsx +81 -18
  18. package/templates/chat-app/components/artifact-panel.tsx +142 -41
  19. package/templates/chat-app/components/attachment-list.tsx +1 -1
  20. package/templates/chat-app/components/{social-auth-providers.tsx → auth-providers.tsx} +49 -4
  21. package/templates/chat-app/components/chat/chat-welcome.tsx +3 -3
  22. package/templates/chat-app/components/chat-menu-items.tsx +1 -1
  23. package/templates/chat-app/components/chat-sync.tsx +9 -11
  24. package/templates/chat-app/components/console.tsx +9 -9
  25. package/templates/chat-app/components/context-usage.tsx +2 -2
  26. package/templates/chat-app/components/create-artifact.tsx +15 -5
  27. package/templates/chat-app/components/data-stream-handler.tsx +57 -16
  28. package/templates/chat-app/components/device-login-page.tsx +191 -0
  29. package/templates/chat-app/components/diffview.tsx +8 -2
  30. package/templates/chat-app/components/electron-auth-handler.tsx +184 -0
  31. package/templates/chat-app/components/electron-auth-ui.tsx +121 -0
  32. package/templates/chat-app/components/favicon-group.tsx +1 -1
  33. package/templates/chat-app/components/feedback-actions.tsx +7 -3
  34. package/templates/chat-app/components/greeting.tsx +1 -1
  35. package/templates/chat-app/components/interactive-chart-impl.tsx +3 -4
  36. package/templates/chat-app/components/interactive-charts.tsx +1 -1
  37. package/templates/chat-app/components/login-form.tsx +52 -10
  38. package/templates/chat-app/components/message-editor.tsx +7 -3
  39. package/templates/chat-app/components/message-siblings.tsx +14 -1
  40. package/templates/chat-app/components/model-selector.tsx +295 -27
  41. package/templates/chat-app/components/multimodal-input.tsx +259 -22
  42. package/templates/chat-app/components/parallel-response-cards.tsx +175 -0
  43. package/templates/chat-app/components/part/code-execution.tsx +8 -2
  44. package/templates/chat-app/components/part/document-common.tsx +1 -1
  45. package/templates/chat-app/components/part/document-preview.tsx +5 -5
  46. package/templates/chat-app/components/part/retrieve-url.tsx +12 -12
  47. package/templates/chat-app/components/part/text-message-part.tsx +9 -1
  48. package/templates/chat-app/components/project-chat-item.tsx +1 -1
  49. package/templates/chat-app/components/project-menu-items.tsx +1 -1
  50. package/templates/chat-app/components/research-task.tsx +1 -1
  51. package/templates/chat-app/components/research-tasks.tsx +1 -1
  52. package/templates/chat-app/components/retry-button.tsx +25 -8
  53. package/templates/chat-app/components/sandbox.tsx +1 -1
  54. package/templates/chat-app/components/sheet-editor.tsx +7 -7
  55. package/templates/chat-app/components/sidebar-chats-list.tsx +1 -1
  56. package/templates/chat-app/components/sidebar-toggle.tsx +15 -2
  57. package/templates/chat-app/components/sidebar-top-row.tsx +27 -12
  58. package/templates/chat-app/components/sidebar-user-nav.tsx +10 -1
  59. package/templates/chat-app/components/signup-form.tsx +49 -10
  60. package/templates/chat-app/components/sources.tsx +4 -4
  61. package/templates/chat-app/components/text-editor.tsx +5 -2
  62. package/templates/chat-app/components/toolbar.tsx +3 -3
  63. package/templates/chat-app/components/ui/sidebar.tsx +0 -1
  64. package/templates/chat-app/components/upgrade-cta/limit-display.tsx +1 -1
  65. package/templates/chat-app/components/user-message.tsx +14 -2
  66. package/templates/chat-app/electron.d.ts +41 -0
  67. package/templates/chat-app/evals/my-eval.eval.ts +3 -1
  68. package/templates/chat-app/hooks/chat-sync-hooks.ts +11 -0
  69. package/templates/chat-app/hooks/use-artifact.tsx +13 -13
  70. package/templates/chat-app/hooks/use-navigate-to-message.ts +39 -0
  71. package/templates/chat-app/lib/ai/gateways/provider-types.ts +19 -10
  72. package/templates/chat-app/lib/ai/stream-errors.test.ts +72 -0
  73. package/templates/chat-app/lib/ai/stream-errors.ts +94 -0
  74. package/templates/chat-app/lib/ai/tools/code-execution.javascript.ts +171 -0
  75. package/templates/chat-app/lib/ai/tools/code-execution.python.ts +336 -0
  76. package/templates/chat-app/lib/ai/tools/code-execution.shared.test.ts +71 -0
  77. package/templates/chat-app/lib/ai/tools/code-execution.shared.ts +59 -0
  78. package/templates/chat-app/lib/ai/tools/code-execution.ts +62 -391
  79. package/templates/chat-app/lib/ai/tools/code-execution.types.ts +24 -0
  80. package/templates/chat-app/lib/ai/tools/steps/multi-query-web-search.ts +3 -2
  81. package/templates/chat-app/lib/ai/types.ts +74 -3
  82. package/templates/chat-app/lib/anonymous-session-client.ts +0 -3
  83. package/templates/chat-app/lib/artifacts/code/client.tsx +35 -5
  84. package/templates/chat-app/lib/artifacts/sheet/client.tsx +11 -3
  85. package/templates/chat-app/lib/auth-client.ts +23 -1
  86. package/templates/chat-app/lib/auth.ts +18 -1
  87. package/templates/chat-app/lib/blob.ts +1 -1
  88. package/templates/chat-app/lib/clone-messages.ts +1 -1
  89. package/templates/chat-app/lib/config-schema.ts +18 -1
  90. package/templates/chat-app/lib/constants.ts +3 -4
  91. package/templates/chat-app/lib/db/migrations/0044_gray_red_shift.sql +5 -0
  92. package/templates/chat-app/lib/db/migrations/meta/0044_snapshot.json +1480 -0
  93. package/templates/chat-app/lib/db/migrations/meta/_journal.json +7 -0
  94. package/templates/chat-app/lib/db/queries.ts +84 -4
  95. package/templates/chat-app/lib/db/schema.ts +4 -1
  96. package/templates/chat-app/lib/editor/config.ts +4 -4
  97. package/templates/chat-app/lib/electron-auth.ts +96 -0
  98. package/templates/chat-app/lib/env-schema.ts +33 -4
  99. package/templates/chat-app/lib/message-conversion.ts +14 -2
  100. package/templates/chat-app/lib/playwright-test-environment.ts +18 -0
  101. package/templates/chat-app/lib/social-auth.ts +5 -0
  102. package/templates/chat-app/lib/stores/hooks-threads.ts +38 -1
  103. package/templates/chat-app/lib/stores/with-threads.test.ts +137 -0
  104. package/templates/chat-app/lib/stores/with-threads.ts +159 -7
  105. package/templates/chat-app/lib/stores/with-tracing.ts +1 -1
  106. package/templates/chat-app/lib/thread-utils.ts +22 -3
  107. package/templates/chat-app/lib/utils/download-assets.ts +6 -7
  108. package/templates/chat-app/lib/utils/rate-limit.ts +9 -3
  109. package/templates/chat-app/package.json +20 -18
  110. package/templates/chat-app/playwright.config.ts +0 -19
  111. package/templates/chat-app/providers/chat-input-provider.tsx +40 -2
  112. package/templates/chat-app/proxy.ts +28 -3
  113. package/templates/chat-app/scripts/check-env.ts +10 -0
  114. package/templates/chat-app/scripts/db-branch-delete.sh +7 -1
  115. package/templates/chat-app/scripts/db-branch-use.sh +7 -1
  116. package/templates/chat-app/scripts/with-db.sh +7 -1
  117. package/templates/chat-app/trpc/server.tsx +7 -2
  118. package/templates/chat-app/tsconfig.json +2 -1
  119. package/templates/chat-app/vercel.json +0 -10
  120. package/templates/chat-app/vitest.config.ts +2 -0
  121. package/templates/electron/CHANGELOG.md +7 -0
  122. package/templates/electron/README.md +54 -0
  123. package/templates/electron/entitlements.mac.plist +10 -0
  124. package/templates/electron/forge.config.ts +157 -0
  125. package/templates/electron/icon.png +0 -0
  126. package/templates/electron/package.json +53 -0
  127. package/templates/electron/scripts/generate-icons.test.js +37 -0
  128. package/templates/electron/scripts/generate-icons.ts +29 -0
  129. package/templates/electron/scripts/run-forge.cjs +28 -0
  130. package/templates/electron/scripts/write-branding.ts +18 -0
  131. package/templates/electron/src/config.ts +16 -0
  132. package/templates/electron/src/lib/auth-client.ts +64 -0
  133. package/templates/electron/src/main.ts +670 -0
  134. package/templates/electron/src/preload.d.ts +27 -0
  135. package/templates/electron/src/preload.ts +25 -0
  136. package/templates/electron/tsconfig.json +18 -0
@@ -309,6 +309,13 @@
309
309
  "when": 1769940346934,
310
310
  "tag": "0043_lyrical_pride",
311
311
  "breakpoints": true
312
+ },
313
+ {
314
+ "idx": 44,
315
+ "version": "7",
316
+ "when": 1773499312081,
317
+ "tag": "0044_gray_red_shift",
318
+ "breakpoints": true
312
319
  }
313
320
  ]
314
321
  }
@@ -17,6 +17,7 @@ import type {
17
17
  ToolName,
18
18
  ToolOutput,
19
19
  } from "@/lib/ai/types";
20
+ import { isSelectedModelValue } from "@/lib/ai/types";
20
21
  import { createModuleLogger } from "@/lib/logger";
21
22
  import { chatMessageToDbMessage } from "@/lib/message-conversion";
22
23
 
@@ -79,6 +80,35 @@ export async function saveChat({
79
80
  }
80
81
  }
81
82
 
83
+ export async function saveChatIfNotExists({
84
+ id,
85
+ userId,
86
+ title,
87
+ projectId,
88
+ }: {
89
+ id: string;
90
+ userId: string;
91
+ title: string;
92
+ projectId?: string;
93
+ }) {
94
+ try {
95
+ return await db
96
+ .insert(chat)
97
+ .values({
98
+ id,
99
+ createdAt: new Date(),
100
+ updatedAt: new Date(),
101
+ userId,
102
+ title,
103
+ projectId: projectId ?? null,
104
+ })
105
+ .onConflictDoNothing();
106
+ } catch (error) {
107
+ console.error("Failed to save chat in database");
108
+ throw error;
109
+ }
110
+ }
111
+
82
112
  export async function deleteChatById({ id }: { id: string }) {
83
113
  try {
84
114
  // Get all messages for this chat to clean up their attachments
@@ -341,6 +371,43 @@ export async function saveMessage({
341
371
  }
342
372
  }
343
373
 
374
+ export async function saveMessageIfNotExists({
375
+ id,
376
+ chatId,
377
+ message: chatMessage,
378
+ }: {
379
+ id: string;
380
+ chatId: string;
381
+ message: ChatMessage;
382
+ }) {
383
+ try {
384
+ return await db.transaction(async (tx) => {
385
+ const dbMessage = chatMessageToDbMessage(chatMessage, chatId);
386
+ dbMessage.id = id;
387
+
388
+ const insertedMessages = await tx
389
+ .insert(message)
390
+ .values(dbMessage)
391
+ .onConflictDoNothing()
392
+ .returning({ id: message.id });
393
+
394
+ if (insertedMessages.length === 0) {
395
+ return;
396
+ }
397
+
398
+ const mappedDBParts = mapUIMessagePartsToDBParts(chatMessage.parts, id);
399
+ if (mappedDBParts.length > 0) {
400
+ await tx.insert(part).values(mappedDBParts);
401
+ }
402
+
403
+ await updateChatUpdatedAt({ chatId });
404
+ });
405
+ } catch (error) {
406
+ logger.error({ error, chatId, id }, "saveMessageIfNotExists failed");
407
+ throw error;
408
+ }
409
+ }
410
+
344
411
  export async function saveChatMessages({
345
412
  messages,
346
413
  }: {
@@ -413,6 +480,11 @@ export async function updateMessage({
413
480
  attachments: dbMessage.attachments,
414
481
  createdAt: dbMessage.createdAt,
415
482
  parentMessageId: dbMessage.parentMessageId,
483
+ selectedModel: dbMessage.selectedModel,
484
+ selectedTool: dbMessage.selectedTool,
485
+ parallelGroupId: dbMessage.parallelGroupId,
486
+ parallelIndex: dbMessage.parallelIndex,
487
+ isPrimaryParallel: dbMessage.isPrimaryParallel,
416
488
  lastContext: dbMessage.lastContext,
417
489
  activeStreamId: dbMessage.activeStreamId,
418
490
  })
@@ -492,8 +564,12 @@ export async function getAllMessagesByChatId({
492
564
  createdAt: msg.createdAt,
493
565
  activeStreamId: msg.activeStreamId,
494
566
  parentMessageId: msg.parentMessageId,
495
- selectedModel: (msg.selectedModel ||
496
- "") as ChatMessage["metadata"]["selectedModel"],
567
+ parallelGroupId: msg.parallelGroupId,
568
+ parallelIndex: msg.parallelIndex,
569
+ isPrimaryParallel: msg.isPrimaryParallel,
570
+ selectedModel: isSelectedModelValue(msg.selectedModel)
571
+ ? msg.selectedModel
572
+ : ("" as ChatMessage["metadata"]["selectedModel"]),
497
573
  selectedTool: (msg.selectedTool ||
498
574
  undefined) as ChatMessage["metadata"]["selectedTool"],
499
575
  usage: msg.lastContext as ChatMessage["metadata"]["usage"],
@@ -797,8 +873,12 @@ export async function getChatMessageWithPartsById({
797
873
  createdAt: dbMessage.createdAt,
798
874
  activeStreamId: dbMessage.activeStreamId,
799
875
  parentMessageId: dbMessage.parentMessageId,
800
- selectedModel: (dbMessage.selectedModel ||
801
- "") as ChatMessage["metadata"]["selectedModel"],
876
+ parallelGroupId: dbMessage.parallelGroupId,
877
+ parallelIndex: dbMessage.parallelIndex,
878
+ isPrimaryParallel: dbMessage.isPrimaryParallel,
879
+ selectedModel: isSelectedModelValue(dbMessage.selectedModel)
880
+ ? dbMessage.selectedModel
881
+ : ("" as ChatMessage["metadata"]["selectedModel"]),
802
882
  selectedTool: (dbMessage.selectedTool ||
803
883
  undefined) as ChatMessage["metadata"]["selectedTool"],
804
884
  usage: dbMessage.lastContext as ChatMessage["metadata"]["usage"],
@@ -113,8 +113,11 @@ export const message = pgTable("Message", {
113
113
  attachments: json("attachments").notNull(),
114
114
  createdAt: timestamp("createdAt").notNull(),
115
115
  annotations: json("annotations"),
116
- selectedModel: varchar("selectedModel", { length: 256 }).default(""),
116
+ selectedModel: json("selectedModel"),
117
117
  selectedTool: varchar("selectedTool", { length: 256 }).default(""),
118
+ parallelGroupId: uuid("parallelGroupId"),
119
+ parallelIndex: integer("parallelIndex"),
120
+ isPrimaryParallel: boolean("isPrimaryParallel"),
118
121
  lastContext: json("lastContext"),
119
122
  activeStreamId: varchar("activeStreamId", { length: 64 }),
120
123
  /** Timestamp when this message's stream was canceled by the user. Null means not canceled. */
@@ -8,8 +8,8 @@ import {
8
8
  type HeadingTagType,
9
9
  QuoteNode,
10
10
  } from "@lexical/rich-text";
11
- import type { LexicalEditor } from "lexical";
12
- import { $getSelection, $insertNodes } from "lexical";
11
+ import type { EditorState, LexicalEditor } from "lexical";
12
+ import { $getSelection, $insertNodes, type TextNode } from "lexical";
13
13
 
14
14
  // Create initial editor configuration
15
15
  export function createEditorConfig() {
@@ -37,7 +37,7 @@ function _createHeadingTransform(level: number) {
37
37
  export: null,
38
38
  importDOM: null,
39
39
  regExp: new RegExp(`^(#{1,${level}})\\s$`),
40
- replace: (_textNode: any) => {
40
+ replace: (_textNode: TextNode) => {
41
41
  const selection = $getSelection();
42
42
  if (selection) {
43
43
  const headingTag = `h${level}` as HeadingTagType;
@@ -56,7 +56,7 @@ export const handleEditorChange = ({
56
56
  editor,
57
57
  onSaveContent,
58
58
  }: {
59
- editorState: any;
59
+ editorState: EditorState;
60
60
  editor: LexicalEditor;
61
61
  onSaveContent: (updatedContent: string, debounce: boolean) => void;
62
62
  }) => {
@@ -0,0 +1,96 @@
1
+ import { config } from "@/lib/config";
2
+ import type { SocialAuthSignInOptions } from "@/lib/social-auth";
3
+
4
+ export const ELECTRON_AUTH_CLIENT_ID = "electron";
5
+ export const ELECTRON_AUTH_COOKIE_PREFIX = "better-auth";
6
+ export const ELECTRON_AUTH_CALLBACK_PATH = "/auth/callback";
7
+ export const ELECTRON_APP_SCHEME = config.appPrefix;
8
+ // @better-auth/electron uses `${scheme}:/...` for its synthetic Origin header
9
+ // and deep-link callback URLs. Keep the legacy `scheme://` form alongside it so
10
+ // existing packaged registrations continue to validate too.
11
+ export const ELECTRON_TRUSTED_ORIGINS = [
12
+ `${ELECTRON_APP_SCHEME}:/`,
13
+ `${ELECTRON_APP_SCHEME}://`,
14
+ ] as const;
15
+
16
+ export function isDesktopAppEnabled(): boolean {
17
+ return config.desktopApp.enabled;
18
+ }
19
+
20
+ export function isElectronRenderer(): boolean {
21
+ return (
22
+ isDesktopAppEnabled() &&
23
+ typeof window !== "undefined" &&
24
+ typeof window.requestAuth === "function"
25
+ );
26
+ }
27
+
28
+ type SearchParamValue = string | string[] | undefined;
29
+
30
+ export function toSearchParamRecord(
31
+ searchParams: Record<string, SearchParamValue>
32
+ ): Record<string, string> {
33
+ const query: Record<string, string> = {};
34
+
35
+ for (const [key, value] of Object.entries(searchParams)) {
36
+ if (typeof value === "string") {
37
+ query[key] = value;
38
+ continue;
39
+ }
40
+
41
+ if (Array.isArray(value) && value[0]) {
42
+ query[key] = value[0];
43
+ }
44
+ }
45
+
46
+ return query;
47
+ }
48
+
49
+ export function buildAuthPageHref(
50
+ pathname: string,
51
+ searchParams: Record<string, SearchParamValue>
52
+ ): string {
53
+ const query = new URLSearchParams(
54
+ toSearchParamRecord(searchParams)
55
+ ).toString();
56
+ return query ? `${pathname}?${query}` : pathname;
57
+ }
58
+
59
+ export function isElectronTransferQuery(
60
+ query: Record<string, string>
61
+ ): boolean {
62
+ return query.client_id === ELECTRON_AUTH_CLIENT_ID;
63
+ }
64
+
65
+ export function buildSocialAuthRequest(
66
+ query: Record<string, string>,
67
+ origin?: string
68
+ ): {
69
+ callbackURL?: string;
70
+ onRedirectToUrl?: (url: string) => void;
71
+ signInOptions?: SocialAuthSignInOptions;
72
+ } {
73
+ const isElectronTransfer =
74
+ isDesktopAppEnabled() && isElectronTransferQuery(query);
75
+ const deviceLoginCallbackURL = origin
76
+ ? new URL("/device-login", origin).toString()
77
+ : "/device-login";
78
+
79
+ if (isElectronTransfer) {
80
+ return {
81
+ callbackURL: deviceLoginCallbackURL,
82
+ onRedirectToUrl: (url: string) => {
83
+ globalThis.location?.assign(url);
84
+ },
85
+ signInOptions: {
86
+ disableRedirect: true,
87
+ errorCallbackURL: deviceLoginCallbackURL,
88
+ newUserCallbackURL: deviceLoginCallbackURL,
89
+ },
90
+ };
91
+ }
92
+
93
+ return {
94
+ callbackURL: query.returnTo,
95
+ };
96
+ }
@@ -1,4 +1,9 @@
1
1
  import { z } from "zod";
2
+ import { isPlaywrightTestEnvironment } from "@/lib/playwright-test-environment";
3
+
4
+ const isPlaywrightTestEnvironmentEnabled = isPlaywrightTestEnvironment(
5
+ process.env
6
+ );
2
7
 
3
8
  /**
4
9
  * Server environment variable schemas with descriptions.
@@ -12,10 +17,23 @@ import { z } from "zod";
12
17
  */
13
18
  export const serverEnvSchema = {
14
19
  // Required core
15
- DATABASE_URL: z.string().min(1).describe("Postgres connection string"),
20
+ DATABASE_URL: z
21
+ .preprocess(
22
+ (value) =>
23
+ isPlaywrightTestEnvironmentEnabled && (value == null || value === "")
24
+ ? "postgres://postgres:postgres@127.0.0.1:5432/playwright"
25
+ : value,
26
+ z.string().min(1)
27
+ )
28
+ .describe("Postgres connection string"),
16
29
  AUTH_SECRET: z
17
- .string()
18
- .min(1)
30
+ .preprocess(
31
+ (value) =>
32
+ isPlaywrightTestEnvironmentEnabled && (value == null || value === "")
33
+ ? "playwright-test-auth-secret"
34
+ : value,
35
+ z.string().min(1)
36
+ )
19
37
  .describe("NextAuth.js secret for signing session tokens"),
20
38
 
21
39
  // Optional blob storage (enable in chat.config.ts)
@@ -102,8 +120,19 @@ export const serverEnvSchema = {
102
120
  .describe("Vercel API token for sandbox (non-Vercel deployments)"),
103
121
  VERCEL_SANDBOX_RUNTIME: z
104
122
  .string()
123
+ .min(1)
124
+ .optional()
125
+ .describe("Legacy default Vercel sandbox runtime identifier for Python"),
126
+ VERCEL_SANDBOX_RUNTIME_PYTHON: z
127
+ .string()
128
+ .min(1)
129
+ .optional()
130
+ .describe("Vercel sandbox runtime identifier for Python execution"),
131
+ VERCEL_SANDBOX_RUNTIME_JAVASCRIPT: z
132
+ .string()
133
+ .min(1)
105
134
  .optional()
106
- .describe("Vercel sandbox runtime identifier"),
135
+ .describe("Vercel sandbox runtime identifier for JavaScript execution"),
107
136
 
108
137
  // App URL (for non-Vercel deployments) - full URL including https://
109
138
  APP_URL: z
@@ -1,7 +1,11 @@
1
1
  import type { ModelId } from "@/lib/ai/app-models";
2
2
  import type { Chat, DBMessage } from "@/lib/db/schema";
3
3
  import type { UIChat } from "@/lib/types/ui-chat";
4
- import type { ChatMessage, UiToolName } from "./ai/types";
4
+ import {
5
+ type ChatMessage,
6
+ isSelectedModelValue,
7
+ type UiToolName,
8
+ } from "./ai/types";
5
9
 
6
10
  // Helper functions for type conversion
7
11
  export function dbChatToUIChat(chat: Chat): UIChat {
@@ -29,7 +33,12 @@ function _dbMessageToChatMessage(message: DBMessage): ChatMessage {
29
33
  createdAt: message.createdAt,
30
34
  activeStreamId: message.activeStreamId,
31
35
  parentMessageId: message.parentMessageId,
32
- selectedModel: (message.selectedModel as ModelId) || ("" as ModelId),
36
+ parallelGroupId: message.parallelGroupId,
37
+ parallelIndex: message.parallelIndex,
38
+ isPrimaryParallel: message.isPrimaryParallel,
39
+ selectedModel: isSelectedModelValue(message.selectedModel)
40
+ ? message.selectedModel
41
+ : ("" as ModelId),
33
42
  selectedTool: (message.selectedTool as UiToolName | null) || undefined,
34
43
  usage: message.lastContext as ChatMessage["metadata"]["usage"],
35
44
  },
@@ -66,6 +75,9 @@ export function chatMessageToDbMessage(
66
75
  parentMessageId,
67
76
  selectedModel,
68
77
  selectedTool: message.metadata?.selectedTool || null,
78
+ parallelGroupId: message.metadata?.parallelGroupId || null,
79
+ parallelIndex: message.metadata?.parallelIndex ?? null,
80
+ isPrimaryParallel: message.metadata?.isPrimaryParallel ?? null,
69
81
  activeStreamId: message.metadata?.activeStreamId || null,
70
82
  canceledAt: null,
71
83
  };
@@ -0,0 +1,18 @@
1
+ function isEnabledFlag(value: string | undefined): boolean {
2
+ if (!value) {
3
+ return false;
4
+ }
5
+
6
+ const normalizedValue = value.trim().toLowerCase();
7
+ return !["0", "false", "no", "off"].includes(normalizedValue);
8
+ }
9
+
10
+ export function isPlaywrightTestEnvironment(
11
+ env: NodeJS.ProcessEnv = process.env
12
+ ): boolean {
13
+ return Boolean(
14
+ env.PLAYWRIGHT_TEST_BASE_URL ||
15
+ isEnabledFlag(env.PLAYWRIGHT) ||
16
+ isEnabledFlag(env.CI_PLAYWRIGHT)
17
+ );
18
+ }
@@ -0,0 +1,5 @@
1
+ export type SocialAuthSignInOptions = {
2
+ disableRedirect?: boolean;
3
+ errorCallbackURL?: string;
4
+ newUserCallbackURL?: string;
5
+ };
@@ -7,7 +7,7 @@ import {
7
7
  type CustomChatStoreState,
8
8
  useCustomChatStoreApi,
9
9
  } from "./custom-store-provider";
10
- import type { MessageSiblingInfo } from "./with-threads";
10
+ import type { MessageSiblingInfo, ParallelGroupInfo } from "./with-threads";
11
11
 
12
12
  function useThreadStore<T>(
13
13
  selector: (store: CustomChatStoreState<ChatMessage>) => T,
@@ -95,3 +95,40 @@ export const useSwitchToSibling = () => {
95
95
  [store]
96
96
  );
97
97
  };
98
+
99
+ export function useParallelGroupInfo(
100
+ messageId: string
101
+ ): ParallelGroupInfo<ChatMessage> | null {
102
+ return useThreadStore(
103
+ (state) => state.getParallelGroupInfo(messageId),
104
+ (a, b) => {
105
+ if (a === null && b === null) {
106
+ return true;
107
+ }
108
+
109
+ if (a === null || b === null) {
110
+ return false;
111
+ }
112
+
113
+ return (
114
+ a.parallelGroupId === b.parallelGroupId &&
115
+ a.selectedMessageId === b.selectedMessageId &&
116
+ a.messages.length === b.messages.length &&
117
+ a.messages.every(
118
+ (msg, i) =>
119
+ msg.id === b.messages[i]?.id &&
120
+ msg.metadata?.activeStreamId ===
121
+ b.messages[i]?.metadata?.activeStreamId
122
+ )
123
+ );
124
+ }
125
+ );
126
+ }
127
+
128
+ export const useSwitchToMessage = () => {
129
+ const store = useCustomChatStoreApi<ChatMessage>();
130
+ return useCallback(
131
+ (messageId: string) => store.getState().switchToMessage(messageId),
132
+ [store]
133
+ );
134
+ };
@@ -0,0 +1,137 @@
1
+ import assert from "node:assert/strict";
2
+ import type { StoreState as BaseChatStoreState } from "@ai-sdk-tools/store";
3
+ import { describe, it } from "vitest";
4
+ import { createStore } from "zustand/vanilla";
5
+ import type { ChatMessage } from "../ai/types";
6
+ import { type ThreadAugmentedState, withThreads } from "./with-threads";
7
+
8
+ function createMessage({
9
+ id,
10
+ role,
11
+ createdAt,
12
+ parentMessageId = null,
13
+ parallelGroupId = null,
14
+ parallelIndex = null,
15
+ activeStreamId = null,
16
+ }: {
17
+ id: string;
18
+ role: ChatMessage["role"];
19
+ createdAt: string;
20
+ parentMessageId?: string | null;
21
+ parallelGroupId?: string | null;
22
+ parallelIndex?: number | null;
23
+ activeStreamId?: string | null;
24
+ }): ChatMessage {
25
+ return {
26
+ id,
27
+ role,
28
+ parts: [],
29
+ metadata: {
30
+ createdAt: new Date(createdAt),
31
+ parentMessageId,
32
+ parallelGroupId,
33
+ parallelIndex,
34
+ isPrimaryParallel: parallelIndex === null ? null : parallelIndex === 0,
35
+ selectedModel: "openai/gpt-4o-mini",
36
+ activeStreamId,
37
+ selectedTool: undefined,
38
+ },
39
+ };
40
+ }
41
+
42
+ function createThreadStore(initialMessages: ChatMessage[]) {
43
+ return createStore<ThreadAugmentedState<ChatMessage>>()(
44
+ withThreads<ChatMessage, BaseChatStoreState<ChatMessage>>(
45
+ (set) =>
46
+ ({
47
+ messages: initialMessages,
48
+ setMessages: (messages: ChatMessage[]) => set({ messages }),
49
+ }) as BaseChatStoreState<ChatMessage>
50
+ )
51
+ );
52
+ }
53
+
54
+ describe("withThreads", () => {
55
+ it("preserves local-only optimistic branch nodes across server syncs", () => {
56
+ const rootUser = createMessage({
57
+ id: "user-root",
58
+ role: "user",
59
+ createdAt: "2024-01-01T00:00:00.000Z",
60
+ });
61
+ const branchA = createMessage({
62
+ id: "assistant-a",
63
+ role: "assistant",
64
+ createdAt: "2024-01-01T00:00:01.000Z",
65
+ parentMessageId: rootUser.id,
66
+ parallelGroupId: "group-root",
67
+ parallelIndex: 0,
68
+ });
69
+ const branchB = createMessage({
70
+ id: "assistant-b",
71
+ role: "assistant",
72
+ createdAt: "2024-01-01T00:00:02.000Z",
73
+ parentMessageId: rootUser.id,
74
+ parallelGroupId: "group-root",
75
+ parallelIndex: 1,
76
+ });
77
+ const nestedUser = createMessage({
78
+ id: "user-nested",
79
+ role: "user",
80
+ createdAt: "2024-01-01T00:00:03.000Z",
81
+ parentMessageId: branchA.id,
82
+ });
83
+ const nestedBranchA = createMessage({
84
+ id: "assistant-nested-a",
85
+ role: "assistant",
86
+ createdAt: "2024-01-01T00:00:04.000Z",
87
+ parentMessageId: nestedUser.id,
88
+ parallelGroupId: "group-nested",
89
+ parallelIndex: 0,
90
+ activeStreamId: "pending:assistant-nested-a",
91
+ });
92
+ const nestedBranchB = createMessage({
93
+ id: "assistant-nested-b",
94
+ role: "assistant",
95
+ createdAt: "2024-01-01T00:00:05.000Z",
96
+ parentMessageId: nestedUser.id,
97
+ parallelGroupId: "group-nested",
98
+ parallelIndex: 1,
99
+ activeStreamId: "pending:assistant-nested-b",
100
+ });
101
+
102
+ const store = createThreadStore([
103
+ rootUser,
104
+ branchA,
105
+ nestedUser,
106
+ nestedBranchA,
107
+ ]);
108
+
109
+ store.getState().addMessageToTree(branchB);
110
+ store.getState().addMessageToTree(nestedBranchB);
111
+
112
+ store.getState().setMessagesWithEpoch([rootUser, branchB]);
113
+ store.getState().setAllMessages([rootUser, branchA, branchB]);
114
+
115
+ const allMessageIds = store
116
+ .getState()
117
+ .allMessages.map((message: ChatMessage) => message.id);
118
+ assert.deepEqual(allMessageIds, [
119
+ "user-root",
120
+ "assistant-a",
121
+ "assistant-b",
122
+ "user-nested",
123
+ "assistant-nested-a",
124
+ "assistant-nested-b",
125
+ ]);
126
+
127
+ const restoredThread = store.getState().switchToMessage(branchA.id);
128
+ assert.deepEqual(
129
+ restoredThread?.map((message: ChatMessage) => message.id),
130
+ ["user-root", "assistant-a", "user-nested", "assistant-nested-b"]
131
+ );
132
+ assert.equal(
133
+ restoredThread?.at(-1)?.metadata.activeStreamId,
134
+ "pending:assistant-nested-b"
135
+ );
136
+ });
137
+ });