@chat-js/cli 0.4.0 → 0.6.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 (124) hide show
  1. package/dist/index.js +1548 -969
  2. package/package.json +4 -3
  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/route.ts +13 -5
  8. package/templates/chat-app/app/(chat)/layout.tsx +4 -1
  9. package/templates/chat-app/app/api/trpc/[trpc]/route.ts +1 -0
  10. package/templates/chat-app/app/globals.css +9 -9
  11. package/templates/chat-app/app/layout.tsx +4 -2
  12. package/templates/chat-app/biome.jsonc +3 -3
  13. package/templates/chat-app/chat.config.ts +144 -141
  14. package/templates/chat-app/components/ai-elements/prompt-input.tsx +1 -1
  15. package/templates/chat-app/components/anonymous-session-init.tsx +10 -6
  16. package/templates/chat-app/components/artifact-actions.tsx +81 -18
  17. package/templates/chat-app/components/artifact-panel.tsx +142 -41
  18. package/templates/chat-app/components/attachment-list.tsx +1 -1
  19. package/templates/chat-app/components/{social-auth-providers.tsx → auth-providers.tsx} +49 -4
  20. package/templates/chat-app/components/chat/chat-welcome.tsx +3 -3
  21. package/templates/chat-app/components/chat-menu-items.tsx +1 -1
  22. package/templates/chat-app/components/chat-sync.tsx +3 -8
  23. package/templates/chat-app/components/console.tsx +9 -9
  24. package/templates/chat-app/components/context-usage.tsx +2 -2
  25. package/templates/chat-app/components/create-artifact.tsx +15 -5
  26. package/templates/chat-app/components/data-stream-handler.tsx +57 -16
  27. package/templates/chat-app/components/device-login-page.tsx +191 -0
  28. package/templates/chat-app/components/diffview.tsx +8 -2
  29. package/templates/chat-app/components/electron-auth-handler.tsx +184 -0
  30. package/templates/chat-app/components/electron-auth-ui.tsx +121 -0
  31. package/templates/chat-app/components/favicon-group.tsx +1 -1
  32. package/templates/chat-app/components/feedback-actions.tsx +1 -1
  33. package/templates/chat-app/components/greeting.tsx +1 -1
  34. package/templates/chat-app/components/interactive-chart-impl.tsx +3 -4
  35. package/templates/chat-app/components/interactive-charts.tsx +1 -1
  36. package/templates/chat-app/components/login-form.tsx +52 -10
  37. package/templates/chat-app/components/message-editor.tsx +4 -5
  38. package/templates/chat-app/components/model-selector.tsx +661 -655
  39. package/templates/chat-app/components/multimodal-input.tsx +13 -10
  40. package/templates/chat-app/components/parallel-response-cards.tsx +53 -35
  41. package/templates/chat-app/components/part/code-execution.tsx +8 -2
  42. package/templates/chat-app/components/part/document-common.tsx +1 -1
  43. package/templates/chat-app/components/part/document-preview.tsx +5 -5
  44. package/templates/chat-app/components/part/retrieve-url.tsx +12 -12
  45. package/templates/chat-app/components/part/text-message-part.tsx +13 -9
  46. package/templates/chat-app/components/project-chat-item.tsx +1 -1
  47. package/templates/chat-app/components/project-menu-items.tsx +1 -1
  48. package/templates/chat-app/components/research-task.tsx +1 -1
  49. package/templates/chat-app/components/research-tasks.tsx +1 -1
  50. package/templates/chat-app/components/retry-button.tsx +1 -1
  51. package/templates/chat-app/components/sandbox.tsx +1 -1
  52. package/templates/chat-app/components/sheet-editor.tsx +7 -7
  53. package/templates/chat-app/components/sidebar-chats-list.tsx +1 -1
  54. package/templates/chat-app/components/sidebar-toggle.tsx +15 -2
  55. package/templates/chat-app/components/sidebar-top-row.tsx +27 -12
  56. package/templates/chat-app/components/sidebar-user-nav.tsx +10 -1
  57. package/templates/chat-app/components/signup-form.tsx +49 -10
  58. package/templates/chat-app/components/sources.tsx +4 -4
  59. package/templates/chat-app/components/text-editor.tsx +5 -2
  60. package/templates/chat-app/components/toolbar.tsx +3 -3
  61. package/templates/chat-app/components/ui/sidebar.tsx +0 -1
  62. package/templates/chat-app/components/upgrade-cta/limit-display.tsx +1 -1
  63. package/templates/chat-app/components/user-message.tsx +135 -134
  64. package/templates/chat-app/electron.d.ts +41 -0
  65. package/templates/chat-app/evals/my-eval.eval.ts +3 -1
  66. package/templates/chat-app/hooks/use-artifact.tsx +13 -13
  67. package/templates/chat-app/lib/ai/gateways/provider-types.ts +19 -10
  68. package/templates/chat-app/lib/ai/stream-errors.test.ts +72 -0
  69. package/templates/chat-app/lib/ai/stream-errors.ts +94 -0
  70. package/templates/chat-app/lib/ai/tools/code-execution.javascript.ts +171 -0
  71. package/templates/chat-app/lib/ai/tools/code-execution.python.ts +336 -0
  72. package/templates/chat-app/lib/ai/tools/code-execution.shared.test.ts +71 -0
  73. package/templates/chat-app/lib/ai/tools/code-execution.shared.ts +59 -0
  74. package/templates/chat-app/lib/ai/tools/code-execution.ts +62 -391
  75. package/templates/chat-app/lib/ai/tools/code-execution.types.ts +24 -0
  76. package/templates/chat-app/lib/ai/tools/steps/multi-query-web-search.ts +3 -2
  77. package/templates/chat-app/lib/anonymous-session-client.ts +0 -3
  78. package/templates/chat-app/lib/artifacts/code/client.tsx +35 -5
  79. package/templates/chat-app/lib/artifacts/sheet/client.tsx +11 -3
  80. package/templates/chat-app/lib/auth-client.ts +23 -1
  81. package/templates/chat-app/lib/auth.ts +18 -1
  82. package/templates/chat-app/lib/blob.ts +1 -1
  83. package/templates/chat-app/lib/clone-messages.ts +1 -1
  84. package/templates/chat-app/lib/config-schema.ts +13 -1
  85. package/templates/chat-app/lib/constants.ts +3 -4
  86. package/templates/chat-app/lib/db/migrations/meta/0044_snapshot.json +42 -129
  87. package/templates/chat-app/lib/db/migrations/meta/_journal.json +1 -1
  88. package/templates/chat-app/lib/editor/config.ts +4 -4
  89. package/templates/chat-app/lib/electron-auth.ts +96 -0
  90. package/templates/chat-app/lib/env-schema.ts +33 -4
  91. package/templates/chat-app/lib/message-conversion.ts +1 -1
  92. package/templates/chat-app/lib/playwright-test-environment.ts +18 -0
  93. package/templates/chat-app/lib/social-auth.ts +5 -0
  94. package/templates/chat-app/lib/stores/hooks-threads.ts +2 -1
  95. package/templates/chat-app/lib/stores/with-threads.test.ts +1 -1
  96. package/templates/chat-app/lib/stores/with-threads.ts +5 -6
  97. package/templates/chat-app/lib/stores/with-tracing.ts +1 -1
  98. package/templates/chat-app/lib/thread-utils.ts +19 -21
  99. package/templates/chat-app/lib/utils/download-assets.ts +6 -7
  100. package/templates/chat-app/lib/utils/rate-limit.ts +9 -3
  101. package/templates/chat-app/package.json +22 -19
  102. package/templates/chat-app/playwright.config.ts +0 -19
  103. package/templates/chat-app/providers/chat-input-provider.tsx +1 -1
  104. package/templates/chat-app/proxy.ts +28 -3
  105. package/templates/chat-app/scripts/check-env.ts +10 -0
  106. package/templates/chat-app/trpc/server.tsx +7 -2
  107. package/templates/chat-app/tsconfig.json +2 -1
  108. package/templates/chat-app/vercel.json +0 -10
  109. package/templates/electron/CHANGELOG.md +7 -0
  110. package/templates/electron/README.md +54 -0
  111. package/templates/electron/entitlements.mac.plist +10 -0
  112. package/templates/electron/forge.config.ts +152 -0
  113. package/templates/electron/icon.png +0 -0
  114. package/templates/electron/package.json +53 -0
  115. package/templates/electron/scripts/generate-icons.test.js +37 -0
  116. package/templates/electron/scripts/generate-icons.ts +29 -0
  117. package/templates/electron/scripts/run-forge.cjs +28 -0
  118. package/templates/electron/scripts/write-branding.ts +18 -0
  119. package/templates/electron/src/config.ts +16 -0
  120. package/templates/electron/src/lib/auth-client.ts +64 -0
  121. package/templates/electron/src/main.ts +670 -0
  122. package/templates/electron/src/preload.d.ts +27 -0
  123. package/templates/electron/src/preload.ts +25 -0
  124. package/templates/electron/tsconfig.json +18 -0
@@ -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
@@ -2,8 +2,8 @@ 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
4
  import {
5
- isSelectedModelValue,
6
5
  type ChatMessage,
6
+ isSelectedModelValue,
7
7
  type UiToolName,
8
8
  } from "./ai/types";
9
9
 
@@ -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
+ };
@@ -117,7 +117,8 @@ export function useParallelGroupInfo(
117
117
  a.messages.every(
118
118
  (msg, i) =>
119
119
  msg.id === b.messages[i]?.id &&
120
- msg.metadata?.activeStreamId === b.messages[i]?.metadata?.activeStreamId
120
+ msg.metadata?.activeStreamId ===
121
+ b.messages[i]?.metadata?.activeStreamId
121
122
  )
122
123
  );
123
124
  }
@@ -3,7 +3,7 @@ import type { StoreState as BaseChatStoreState } from "@ai-sdk-tools/store";
3
3
  import { describe, it } from "vitest";
4
4
  import { createStore } from "zustand/vanilla";
5
5
  import type { ChatMessage } from "../ai/types";
6
- import { withThreads, type ThreadAugmentedState } from "./with-threads";
6
+ import { type ThreadAugmentedState, withThreads } from "./with-threads";
7
7
 
8
8
  function createMessage({
9
9
  id,
@@ -177,11 +177,11 @@ export const withThreads =
177
177
  set((state) => {
178
178
  const idx = state.allMessages.findIndex((m) => m.id === message.id);
179
179
  let next: UI_MESSAGE[];
180
- if (idx !== -1) {
180
+ if (idx === -1) {
181
+ next = [...state.allMessages, message];
182
+ } else {
181
183
  next = [...state.allMessages];
182
184
  next[idx] = message;
183
- } else {
184
- next = [...state.allMessages, message];
185
185
  }
186
186
  return { ...state, allMessages: next, childrenMap: rebuildMap(next) };
187
187
  });
@@ -254,9 +254,8 @@ export const withThreads =
254
254
 
255
255
  const visibleMessageIds = new Set(state.messages.map((m) => m.id));
256
256
  const selectedMessageId =
257
- groupMessages.find((candidate) =>
258
- visibleMessageIds.has(candidate.id)
259
- )?.id ?? null;
257
+ groupMessages.find((candidate) => visibleMessageIds.has(candidate.id))
258
+ ?.id ?? null;
260
259
 
261
260
  return {
262
261
  messages: groupMessages,
@@ -2,7 +2,7 @@
2
2
 
3
3
  import type { StateCreator } from "zustand";
4
4
 
5
- type AnyFn = (...args: any[]) => any;
5
+ type AnyFn = (...args: unknown[]) => unknown;
6
6
 
7
7
  function safeStringifyArgs(args: unknown[]) {
8
8
  try {
@@ -104,28 +104,26 @@ export function buildChildrenMap<T extends MessageNode>(
104
104
  map.get(parentId)?.push(message);
105
105
  }
106
106
  for (const siblings of map.values()) {
107
- siblings.sort(
108
- (a, b) => {
109
- const aParallelIndex = a.metadata?.parallelIndex;
110
- const bParallelIndex = b.metadata?.parallelIndex;
111
- const sameParallelGroup =
112
- a.metadata?.parallelGroupId &&
113
- a.metadata?.parallelGroupId === b.metadata?.parallelGroupId;
114
-
115
- if (
116
- sameParallelGroup &&
117
- typeof aParallelIndex === "number" &&
118
- typeof bParallelIndex === "number" &&
119
- aParallelIndex !== bParallelIndex
120
- ) {
121
- return aParallelIndex - bParallelIndex;
122
- }
123
-
124
- return (
125
- toTimestamp(a.metadata?.createdAt) - toTimestamp(b.metadata?.createdAt)
126
- );
107
+ siblings.sort((a, b) => {
108
+ const aParallelIndex = a.metadata?.parallelIndex;
109
+ const bParallelIndex = b.metadata?.parallelIndex;
110
+ const sameParallelGroup =
111
+ a.metadata?.parallelGroupId &&
112
+ a.metadata?.parallelGroupId === b.metadata?.parallelGroupId;
113
+
114
+ if (
115
+ sameParallelGroup &&
116
+ typeof aParallelIndex === "number" &&
117
+ typeof bParallelIndex === "number" &&
118
+ aParallelIndex !== bParallelIndex
119
+ ) {
120
+ return aParallelIndex - bParallelIndex;
127
121
  }
128
- );
122
+
123
+ return (
124
+ toTimestamp(a.metadata?.createdAt) - toTimestamp(b.metadata?.createdAt)
125
+ );
126
+ });
129
127
  }
130
128
  return map;
131
129
  }
@@ -135,8 +135,8 @@ export async function replaceFilePartUrlByBinaryDataInMessages(
135
135
  );
136
136
 
137
137
  const mapPart = (
138
- part: TextPart | ImagePart | FilePart | any
139
- ): TextPart | ImagePart | FilePart | any => {
138
+ part: TextPart | ImagePart | FilePart
139
+ ): TextPart | ImagePart | FilePart => {
140
140
  if (part.type === "file") {
141
141
  return mapFilePart(part as FilePart, downloaded);
142
142
  }
@@ -148,14 +148,13 @@ export async function replaceFilePartUrlByBinaryDataInMessages(
148
148
  };
149
149
 
150
150
  return messages.map((message) => {
151
- if (typeof message.content === "string") {
151
+ if (message.role !== "user" || typeof message.content === "string") {
152
152
  return message;
153
153
  }
154
+
154
155
  return {
155
156
  ...message,
156
- content: (
157
- message.content as Array<TextPart | ImagePart | FilePart | any>
158
- ).map(mapPart),
159
- } as ModelMessage;
157
+ content: message.content.map(mapPart),
158
+ };
160
159
  });
161
160
  }
@@ -2,6 +2,12 @@ import "server-only";
2
2
  import { config } from "@/lib/config";
3
3
  import { ANONYMOUS_LIMITS } from "@/lib/types/anonymous";
4
4
 
5
+ interface RedisClient {
6
+ expire(key: string, seconds: number): Promise<unknown>;
7
+ get(key: string): Promise<string | null>;
8
+ incr(key: string): Promise<number>;
9
+ }
10
+
5
11
  interface RateLimitResult {
6
12
  error?: string;
7
13
  remaining: number;
@@ -13,7 +19,7 @@ interface RateLimitOptions {
13
19
  identifier: string;
14
20
  keyPrefix: string;
15
21
  limit: number;
16
- redisClient: any;
22
+ redisClient: RedisClient | null;
17
23
  windowSize: number;
18
24
  }
19
25
 
@@ -82,7 +88,7 @@ const WINDOW_SIZE_MONTH = 30 * 24 * 60 * 60;
82
88
 
83
89
  export async function checkAnonymousRateLimit(
84
90
  ip: string,
85
- redisClient: any
91
+ redisClient: RedisClient | null
86
92
  ): Promise<{
87
93
  success: boolean;
88
94
  error?: string;
@@ -126,7 +132,7 @@ export async function checkAnonymousRateLimit(
126
132
  );
127
133
  return {
128
134
  success: false,
129
- error: `Monthly message limit exceeded. You can make ${RATE_LIMIT.REQUESTS_PER_MONTH} requests per month. You've made ${RATE_LIMIT.REQUESTS_PER_MONTH - monthResult.remaining} requests this month. Try again in ${daysUntilReset} day${daysUntilReset !== 1 ? "s" : ""}.`,
135
+ error: `Monthly message limit exceeded. You can make ${RATE_LIMIT.REQUESTS_PER_MONTH} requests per month. You've made ${RATE_LIMIT.REQUESTS_PER_MONTH - monthResult.remaining} requests this month. Try again in ${daysUntilReset} day${daysUntilReset === 1 ? "" : "s"}.`,
130
136
  headers: {
131
137
  "X-RateLimit-Limit": RATE_LIMIT.REQUESTS_PER_MONTH.toString(),
132
138
  "X-RateLimit-Remaining": monthResult.remaining.toString(),
@@ -11,8 +11,8 @@
11
11
  "analyze": "next experimental-analyze",
12
12
  "start": "next start",
13
13
  "prod": "bun run build && bun run start",
14
- "lint": "bunx ultracite@7.2.3 check",
15
- "format": "bunx ultracite@7.2.3 fix",
14
+ "lint": "bunx ultracite@7.4.3 check",
15
+ "format": "bunx ultracite@7.4.3 fix",
16
16
  "check-env": "bun scripts/check-env.ts",
17
17
  "db:generate": "drizzle-kit generate",
18
18
  "db:migrate": "export VERCEL_ENV=production && bash scripts/with-db.sh bunx tsx lib/db/migrate.ts",
@@ -40,15 +40,17 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@ai-sdk-tools/store": "1.2.0",
43
- "@ai-sdk/anthropic": "^3.0.46",
43
+ "@ai-sdk/anthropic": "^3.0.67",
44
44
  "@ai-sdk/devtools": "^0.0.15",
45
- "@ai-sdk/gateway": "3.0.53",
46
- "@ai-sdk/google": "^3.0.30",
47
- "@ai-sdk/mcp": "^1.0.21",
48
- "@ai-sdk/openai": "^3.0.30",
49
- "@ai-sdk/openai-compatible": "^2.0.30",
50
- "@ai-sdk/provider": "3.0.8",
51
- "@ai-sdk/react": "^3.0.99",
45
+ "@ai-sdk/gateway": "^3.0.92",
46
+ "@ai-sdk/google": "^3.0.59",
47
+ "@ai-sdk/mcp": "^1.0.35",
48
+ "@ai-sdk/openai": "^3.0.51",
49
+ "@ai-sdk/openai-compatible": "^2.0.40",
50
+ "@ai-sdk/provider": "^3.0.8",
51
+ "@ai-sdk/react": "^3.0.152",
52
+ "@better-auth/core": "1.5.6",
53
+ "@better-auth/electron": "^1.5.6",
52
54
  "@codemirror/lang-javascript": "^6.2.2",
53
55
  "@codemirror/lang-python": "^6.1.6",
54
56
  "@codemirror/state": "^6.5.0",
@@ -88,7 +90,7 @@
88
90
  "@radix-ui/react-toggle": "^1.1.10",
89
91
  "@radix-ui/react-tooltip": "^1.2.8",
90
92
  "@radix-ui/react-use-controllable-state": "^1.2.2",
91
- "@streamdown/code": "^1.0.3",
93
+ "@streamdown/code": "^1.1.1",
92
94
  "@streamdown/math": "^1.0.2",
93
95
  "@streamdown/mermaid": "^1.0.2",
94
96
  "@t3-oss/env-nextjs": "^0.13.8",
@@ -104,8 +106,8 @@
104
106
  "@vercel/otel": "^2.1.0",
105
107
  "@vercel/sandbox": "^1.0.2",
106
108
  "@vercel/speed-insights": "^1.3.1",
107
- "ai": "6.0.97",
108
- "better-auth": "^1.4.3",
109
+ "ai": "^6.0.150",
110
+ "better-auth": "^1.5.6",
109
111
  "browser-image-compression": "^2.0.2",
110
112
  "class-variance-authority": "^0.7.1",
111
113
  "clsx": "^2.1.1",
@@ -124,7 +126,7 @@
124
126
  "lucide-react": "0.553.0",
125
127
  "motion": "^12.23.24",
126
128
  "nanoid": "^5.0.8",
127
- "next": "16.1.6",
129
+ "next": "16.2.0",
128
130
  "next-themes": "^0.4.6",
129
131
  "nuqs": "^2.8.5",
130
132
  "papaparse": "^5.5.2",
@@ -142,7 +144,7 @@
142
144
  "server-only": "^0.0.1",
143
145
  "shiki": "^3.19.0",
144
146
  "sonner": "^2.0.7",
145
- "streamdown": "^2.2.0",
147
+ "streamdown": "^2.5.0",
146
148
  "superjson": "^2.2.2",
147
149
  "tailwind-merge": "^3.3.1",
148
150
  "throttleit": "^2.1.0",
@@ -162,7 +164,7 @@
162
164
  }
163
165
  },
164
166
  "devDependencies": {
165
- "@biomejs/biome": "^2.2.4",
167
+ "@biomejs/biome": "2.4.10",
166
168
  "@playwright/test": "^1.50.1",
167
169
  "@tailwindcss/postcss": "^4.1.12",
168
170
  "@tailwindcss/typography": "^0.5.19",
@@ -178,9 +180,10 @@
178
180
  "tailwindcss": "^4.1.12",
179
181
  "tsx": "^4.19.1",
180
182
  "tw-animate-css": "^1.3.7",
181
- "typescript": "5.8.3",
182
- "ultracite": "7.2.3",
183
+ "typescript": "6.0.2",
184
+ "ultracite": "7.4.3",
183
185
  "vite-tsconfig-paths": "^6.1.1",
184
186
  "vitest": "^4.0.0"
185
- }
187
+ },
188
+ "packageManager": "bun@1.3.1"
186
189
  }
@@ -51,44 +51,25 @@ export default defineConfig({
51
51
 
52
52
  /* Configure projects */
53
53
  projects: [
54
- {
55
- name: "setup:auth",
56
- testMatch: /auth.setup.e2e.ts/,
57
- },
58
- {
59
- name: "setup:reasoning",
60
- testMatch: /reasoning.setup.e2e.ts/,
61
- dependencies: ["setup:auth"],
62
- use: {
63
- ...devices["Desktop Chrome"],
64
- storageState: "playwright/.auth/session.json",
65
- },
66
- },
67
54
  {
68
55
  name: "chat",
69
56
  testMatch: /chat.e2e.ts/,
70
- dependencies: ["setup:auth"],
71
57
  use: {
72
58
  ...devices["Desktop Chrome"],
73
- storageState: "playwright/.auth/session.json",
74
59
  },
75
60
  },
76
61
  {
77
62
  name: "reasoning",
78
63
  testMatch: /reasoning.e2e.ts/,
79
- dependencies: ["setup:reasoning"],
80
64
  use: {
81
65
  ...devices["Desktop Chrome"],
82
- storageState: "playwright/.reasoning/session.json",
83
66
  },
84
67
  },
85
68
  {
86
69
  name: "artifacts",
87
70
  testMatch: /artifacts.e2e.ts/,
88
- dependencies: ["setup:auth"],
89
71
  use: {
90
72
  ...devices["Desktop Chrome"],
91
- storageState: "playwright/.auth/session.json",
92
73
  },
93
74
  },
94
75
 
@@ -14,8 +14,8 @@ import React, {
14
14
  import type { LexicalChatInputRef } from "@/components/lexical-chat-input";
15
15
  import type { AppModelId } from "@/lib/ai/app-models";
16
16
  import {
17
- getPrimarySelectedModelId,
18
17
  type Attachment,
18
+ getPrimarySelectedModelId,
19
19
  type SelectedModelValue,
20
20
  type UiToolName,
21
21
  } from "@/lib/ai/types";
@@ -1,6 +1,8 @@
1
1
  import type { NextRequest } from "next/server";
2
2
  import { NextResponse } from "next/server";
3
3
  import { auth } from "@/lib/auth";
4
+ import { config as appConfig } from "@/lib/config";
5
+ import { isPlaywrightTestEnvironment } from "@/lib/constants";
4
6
 
5
7
  function isPublicApiRoute(pathname: string): boolean {
6
8
  return (
@@ -33,7 +35,23 @@ function isPublicPage(pathname: string): boolean {
33
35
  }
34
36
 
35
37
  function isAuthPage(pathname: string): boolean {
36
- return pathname.startsWith("/login") || pathname.startsWith("/register");
38
+ return (
39
+ pathname.startsWith("/login") ||
40
+ pathname.startsWith("/register") ||
41
+ isDeviceLoginPage(pathname)
42
+ );
43
+ }
44
+
45
+ function isDeviceLoginPage(pathname: string): boolean {
46
+ return appConfig.desktopApp.enabled && pathname.startsWith("/device-login");
47
+ }
48
+
49
+ function getSafeReturnTo(url: URL): string | null {
50
+ const returnTo = url.searchParams.get("returnTo");
51
+ if (!returnTo?.startsWith("/") || returnTo.startsWith("//")) {
52
+ return null;
53
+ }
54
+ return returnTo;
37
55
  }
38
56
 
39
57
  export async function proxy(req: NextRequest) {
@@ -44,11 +62,18 @@ export async function proxy(req: NextRequest) {
44
62
  return;
45
63
  }
46
64
 
65
+ if (isPlaywrightTestEnvironment) {
66
+ // Playwright CI runs the app anonymously and should never reach session I/O.
67
+ return;
68
+ }
69
+
47
70
  const session = await auth.api.getSession({ headers: req.headers });
48
71
  const isLoggedIn = !!session?.user;
72
+ const isDeviceLoginRoute = isDeviceLoginPage(pathname);
73
+ const returnTo = getSafeReturnTo(url);
49
74
 
50
- if (isLoggedIn && isAuthPage(pathname)) {
51
- return NextResponse.redirect(new URL("/", url));
75
+ if (isLoggedIn && isAuthPage(pathname) && !isDeviceLoginRoute) {
76
+ return NextResponse.redirect(new URL(returnTo ?? "/", url));
52
77
  }
53
78
 
54
79
  if (isAuthPage(pathname) || isPublicPage(pathname)) {
@@ -16,6 +16,7 @@ import {
16
16
  getMissingRequirement,
17
17
  isRequirementSatisfied,
18
18
  } from "../lib/config-requirements";
19
+ import { isPlaywrightTestEnvironment } from "../lib/playwright-test-environment";
19
20
 
20
21
  interface ValidationError {
21
22
  feature: string;
@@ -158,6 +159,15 @@ function checkGatewaySnapshot(): string | null {
158
159
 
159
160
  function checkEnv(): void {
160
161
  const env = process.env;
162
+ if (isPlaywrightTestEnvironment(env)) {
163
+ console.log(
164
+ "✅ Skipping optional environment validation in Playwright test mode"
165
+ );
166
+ // Playwright CI only exercises anonymous flows, so optional feature checks
167
+ // and the gateway snapshot warning stay enforced in non-Playwright builds.
168
+ return;
169
+ }
170
+
161
171
  const baseUrlError = validateBaseUrl(env);
162
172
  const errors = [
163
173
  ...(baseUrlError ? [baseUrlError] : []),
@@ -2,6 +2,7 @@ import "server-only"; // <-- ensure this file cannot be imported from the client
2
2
  import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
3
3
  import {
4
4
  createTRPCOptionsProxy,
5
+ type ResolverDef,
5
6
  type TRPCQueryOptions,
6
7
  } from "@trpc/tanstack-react-query";
7
8
  import { cache } from "react";
@@ -28,12 +29,16 @@ export function HydrateClient(props: { children: React.ReactNode }) {
28
29
  );
29
30
  }
30
31
 
31
- export function prefetch<T extends ReturnType<TRPCQueryOptions<any>>>(
32
+ export function prefetch<T extends ReturnType<TRPCQueryOptions<ResolverDef>>>(
32
33
  queryOptions: T
33
34
  ) {
34
35
  const queryClient = getQueryClient();
35
36
  if (queryOptions.queryKey[1]?.type === "infinite") {
36
- queryClient.prefetchInfiniteQuery(queryOptions as any);
37
+ queryClient.prefetchInfiniteQuery(
38
+ queryOptions as unknown as Parameters<
39
+ typeof queryClient.prefetchInfiniteQuery
40
+ >[0]
41
+ );
37
42
  } else {
38
43
  queryClient.prefetchQuery(queryOptions);
39
44
  }
@@ -20,7 +20,8 @@
20
20
  "paths": {
21
21
  "@/*": ["./*"]
22
22
  },
23
- "strictNullChecks": true
23
+ "strictNullChecks": true,
24
+ "types": ["node"]
24
25
  },
25
26
  "include": [
26
27
  "next-env.d.ts",
@@ -7,15 +7,5 @@
7
7
  "path": "/api/cron/cleanup",
8
8
  "schedule": "0 2 * * *"
9
9
  }
10
- ],
11
- "rewrites": [
12
- {
13
- "source": "/docs",
14
- "destination": "https://chatjs.mintlify.dev/docs"
15
- },
16
- {
17
- "source": "/docs/:match*",
18
- "destination": "https://chatjs.mintlify.dev/docs/:match*"
19
- }
20
10
  ]
21
11
  }
@@ -0,0 +1,7 @@
1
+ # @chatjs/electron
2
+
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#136](https://github.com/FranciscoMoretti/chat-js/pull/136) [`6b1458c`](https://github.com/FranciscoMoretti/chat-js/commit/6b1458cb071fe4e67bca99fa86ff55bfb4852c64) Thanks [@FranciscoMoretti](https://github.com/FranciscoMoretti)! - Add a PR-driven desktop release flow for the Electron app. Desktop releases now follow the same Changesets version-bump pattern as the CLI: merge the generated version PR, then publish macOS and Windows installers to GitHub Releases using stable download asset names for the site.