@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
@@ -1,23 +1,44 @@
1
1
  "use client";
2
+ import type { DataUIPart } from "ai";
2
3
  import { type Dispatch, type SetStateAction, useEffect, useRef } from "react";
4
+ import type { ArtifactMetadata } from "@/components/create-artifact";
3
5
  import { useArtifact } from "@/hooks/use-artifact";
4
- import type { UiToolName } from "@/lib/ai/types";
6
+ import type { CustomUIDataTypes, UiToolName } from "@/lib/ai/types";
7
+ import {
8
+ codeArtifact,
9
+ getCodeArtifactMetadata,
10
+ } from "@/lib/artifacts/code/client";
11
+ import {
12
+ getSheetArtifactMetadata,
13
+ sheetArtifact,
14
+ } from "@/lib/artifacts/sheet/client";
15
+ import { textArtifact } from "@/lib/artifacts/text/client";
5
16
  import { useChatId } from "@/providers/chat-id-provider";
6
17
  import { useChatInput } from "@/providers/chat-input-provider";
7
18
  import { useSession } from "@/providers/session-provider";
8
- import { artifactDefinitions } from "./artifact-panel";
9
19
  import { useDataStream } from "./data-stream-provider";
10
20
 
21
+ function createTypedMetadataSetter<M extends ArtifactMetadata>(
22
+ setMetadata: Dispatch<SetStateAction<ArtifactMetadata>>,
23
+ coerce: (metadata: ArtifactMetadata) => M
24
+ ): Dispatch<SetStateAction<M>> {
25
+ return (value) => {
26
+ setMetadata((current) => {
27
+ const typedCurrent = coerce(current);
28
+ return typeof value === "function" ? value(typedCurrent) : value;
29
+ });
30
+ };
31
+ }
32
+
11
33
  function handleResearchUpdate({
12
34
  delta,
13
35
  setSelectedTool,
14
36
  }: {
15
- delta: any;
37
+ delta: DataUIPart<CustomUIDataTypes>;
16
38
  setSelectedTool: Dispatch<SetStateAction<UiToolName | null>>;
17
39
  }): void {
18
40
  if (delta.type === "data-researchUpdate") {
19
- // TODO: fix this type
20
- const update: any = (delta as any).data;
41
+ const update = delta.data;
21
42
  if (update?.type === "completed") {
22
43
  setSelectedTool((current) =>
23
44
  current === "deepResearch" ? null : current
@@ -36,21 +57,41 @@ function processArtifactStreamPart({
36
57
  setArtifact,
37
58
  setMetadata,
38
59
  }: {
39
- delta: any;
60
+ delta: DataUIPart<CustomUIDataTypes>;
40
61
  artifact: ReturnType<typeof useArtifact>["artifact"];
41
62
  setArtifact: ReturnType<typeof useArtifact>["setArtifact"];
42
63
  setMetadata: ReturnType<typeof useArtifact>["setMetadata"];
43
64
  }): void {
44
- const artifactDefinition = artifactDefinitions.find(
45
- (definition) => definition.kind === artifact.kind
46
- );
47
-
48
- if (artifactDefinition?.onStreamPart) {
49
- artifactDefinition.onStreamPart({
50
- streamPart: delta,
51
- setArtifact,
52
- setMetadata,
53
- });
65
+ switch (artifact.kind) {
66
+ case "code":
67
+ codeArtifact.onStreamPart?.({
68
+ streamPart: delta,
69
+ setArtifact,
70
+ setMetadata: createTypedMetadataSetter(
71
+ setMetadata,
72
+ getCodeArtifactMetadata
73
+ ),
74
+ });
75
+ break;
76
+ case "sheet":
77
+ sheetArtifact.onStreamPart?.({
78
+ streamPart: delta,
79
+ setArtifact,
80
+ setMetadata: createTypedMetadataSetter(
81
+ setMetadata,
82
+ getSheetArtifactMetadata
83
+ ),
84
+ });
85
+ break;
86
+ case "text":
87
+ textArtifact.onStreamPart?.({
88
+ streamPart: delta,
89
+ setArtifact,
90
+ setMetadata,
91
+ });
92
+ break;
93
+ default:
94
+ break;
54
95
  }
55
96
  }
56
97
 
@@ -0,0 +1,191 @@
1
+ "use client";
2
+
3
+ import { CheckCircle2, LoaderCircle } from "lucide-react";
4
+ import { usePathname, useSearchParams } from "next/navigation";
5
+ import { useEffect, useMemo, useRef, useState } from "react";
6
+ import { Button } from "@/components/ui/button";
7
+ import {
8
+ Card,
9
+ CardContent,
10
+ CardDescription,
11
+ CardHeader,
12
+ CardTitle,
13
+ } from "@/components/ui/card";
14
+ import authClient from "@/lib/auth-client";
15
+ import { config } from "@/lib/config";
16
+ import { isElectronTransferQuery } from "@/lib/electron-auth";
17
+
18
+ type DeviceLoginState = "checking-session" | "transferring" | "waiting-for-app";
19
+
20
+ const DEVICE_LOGIN_COMPLETED_PARAM = "done";
21
+
22
+ export function DeviceLoginPage() {
23
+ const pathname = usePathname();
24
+ const searchParams = useSearchParams();
25
+ const [state, setState] = useState<DeviceLoginState>("checking-session");
26
+ const transferStartedRef = useRef(false);
27
+
28
+ const query = useMemo(
29
+ () => Object.fromEntries(searchParams.entries()),
30
+ [searchParams]
31
+ );
32
+ const isCompletedView =
33
+ searchParams.get(DEVICE_LOGIN_COMPLETED_PARAM) === "1";
34
+
35
+ useEffect(() => {
36
+ if (isCompletedView) {
37
+ setState("waiting-for-app");
38
+ return;
39
+ }
40
+
41
+ if (!isElectronTransferQuery(query)) {
42
+ setState("waiting-for-app");
43
+ return;
44
+ }
45
+
46
+ let cancelled = false;
47
+
48
+ const checkSession = async () => {
49
+ const { data: session } = await authClient.getSession();
50
+
51
+ if (cancelled || transferStartedRef.current) {
52
+ return;
53
+ }
54
+
55
+ if (!session?.user) {
56
+ setState("waiting-for-app");
57
+ return;
58
+ }
59
+
60
+ transferStartedRef.current = true;
61
+ setState("transferring");
62
+
63
+ await authClient.electron.transferUser({
64
+ fetchOptions: {
65
+ query,
66
+ onSuccess: () => {
67
+ window.history.replaceState(
68
+ {},
69
+ "",
70
+ `${pathname}?${DEVICE_LOGIN_COMPLETED_PARAM}=1`
71
+ );
72
+ setState("waiting-for-app");
73
+ },
74
+ onError: () => {
75
+ transferStartedRef.current = false;
76
+ setState("waiting-for-app");
77
+ },
78
+ },
79
+ });
80
+ };
81
+
82
+ checkSession().catch(() => {
83
+ if (cancelled) {
84
+ return;
85
+ }
86
+
87
+ transferStartedRef.current = false;
88
+ setState("waiting-for-app");
89
+ });
90
+
91
+ return () => {
92
+ cancelled = true;
93
+ };
94
+ }, [isCompletedView, pathname, query]);
95
+
96
+ return (
97
+ <DeviceAuthScreen
98
+ onRetry={() => {
99
+ transferStartedRef.current = false;
100
+ setState("transferring");
101
+ authClient.electron
102
+ .transferUser({
103
+ fetchOptions: {
104
+ query,
105
+ onSuccess: () => {
106
+ window.history.replaceState(
107
+ {},
108
+ "",
109
+ `${pathname}?${DEVICE_LOGIN_COMPLETED_PARAM}=1`
110
+ );
111
+ setState("waiting-for-app");
112
+ },
113
+ onError: () => {
114
+ transferStartedRef.current = false;
115
+ setState("waiting-for-app");
116
+ },
117
+ },
118
+ })
119
+ .catch(() => {
120
+ transferStartedRef.current = false;
121
+ setState("waiting-for-app");
122
+ });
123
+ }}
124
+ state={state}
125
+ />
126
+ );
127
+ }
128
+
129
+ function DeviceAuthScreen({
130
+ state,
131
+ onRetry,
132
+ }: {
133
+ state: "checking-session" | "transferring" | "waiting-for-app";
134
+ onRetry: () => void;
135
+ }) {
136
+ const isLoading = state === "checking-session" || state === "transferring";
137
+ let title = "You're signed in";
138
+
139
+ if (isLoading) {
140
+ title =
141
+ state === "checking-session"
142
+ ? "Checking your session..."
143
+ : "Opening the desktop app...";
144
+ }
145
+
146
+ return (
147
+ <div className="flex min-h-dvh w-screen items-center justify-center bg-background">
148
+ <div className="w-full max-w-sm px-6">
149
+ <Card>
150
+ <CardHeader className="text-center">
151
+ <div className="mb-2 flex justify-center">
152
+ {isLoading ? (
153
+ <LoaderCircle className="size-8 animate-spin text-muted-foreground" />
154
+ ) : (
155
+ <div className="inline-flex h-14 w-14 items-center justify-center rounded-2xl bg-foreground text-background">
156
+ <CheckCircle2 className="size-7" />
157
+ </div>
158
+ )}
159
+ </div>
160
+ <CardTitle className="text-xl">{title}</CardTitle>
161
+ {!isLoading && (
162
+ <CardDescription>
163
+ You can close this tab and return to {config.appName}.
164
+ </CardDescription>
165
+ )}
166
+ </CardHeader>
167
+ {!isLoading && (
168
+ <CardContent className="text-center">
169
+ <div className="mb-4">
170
+ <Button asChild className="w-full" variant="outline">
171
+ <a href="/">Continue on web</a>
172
+ </Button>
173
+ </div>
174
+ <p className="text-muted-foreground/60 text-xs">
175
+ Didn&apos;t open?{" "}
176
+ <Button
177
+ className="h-auto p-0 text-muted-foreground/60 text-xs underline underline-offset-2 hover:text-muted-foreground hover:no-underline"
178
+ onClick={onRetry}
179
+ type="button"
180
+ variant="link"
181
+ >
182
+ Try again
183
+ </Button>
184
+ </p>
185
+ </CardContent>
186
+ )}
187
+ </Card>
188
+ </div>
189
+ </div>
190
+ );
191
+ }
@@ -9,6 +9,7 @@ import {
9
9
  $getRoot,
10
10
  type EditorConfig,
11
11
  type LexicalEditor,
12
+ type SerializedTextNode,
12
13
  TextNode,
13
14
  } from "lexical";
14
15
  import { useEffect } from "react";
@@ -23,6 +24,11 @@ const DiffType = {
23
24
 
24
25
  // Define diff types
25
26
  type DiffTypeValue = (typeof DiffType)[keyof typeof DiffType];
27
+ type SerializedDiffTextNode = SerializedTextNode & {
28
+ diffType?: DiffTypeValue;
29
+ type: "diff-text";
30
+ version: 1;
31
+ };
26
32
 
27
33
  // Custom diff text node that supports styling
28
34
  class DiffTextNode extends TextNode {
@@ -38,7 +44,7 @@ class DiffTextNode extends TextNode {
38
44
  return newNode;
39
45
  }
40
46
 
41
- static importJSON(serializedNode: any): DiffTextNode {
47
+ static importJSON(serializedNode: SerializedDiffTextNode): DiffTextNode {
42
48
  const { text, diffType } = serializedNode;
43
49
  const node = new DiffTextNode(text);
44
50
  if (diffType !== undefined) {
@@ -47,7 +53,7 @@ class DiffTextNode extends TextNode {
47
53
  return node;
48
54
  }
49
55
 
50
- exportJSON(): any {
56
+ exportJSON(): SerializedDiffTextNode {
51
57
  return {
52
58
  ...super.exportJSON(),
53
59
  diffType: this.__diffType,
@@ -0,0 +1,184 @@
1
+ "use client";
2
+
3
+ import { AlertCircle, LoaderCircle } from "lucide-react";
4
+ import { useRouter } from "next/navigation";
5
+ import { useEffect, useState } from "react";
6
+ import { toast } from "sonner";
7
+ import { Button } from "@/components/ui/button";
8
+ import authClient from "@/lib/auth-client";
9
+ import { config } from "@/lib/config";
10
+
11
+ /**
12
+ * Handles the electron auth redirect after OAuth completes in the browser.
13
+ * When the user finishes OAuth, `ensureElectronRedirect` detects the
14
+ * electron redirect cookie and sends the user back to the Electron app
15
+ * via deep link.
16
+ *
17
+ * Mount this in the root layout so it runs on every page.
18
+ */
19
+ export function ElectronAuthHandler() {
20
+ const isDesktopAppEnabled = config.desktopApp.enabled;
21
+ const router = useRouter();
22
+ const [authState, setAuthState] = useState<ElectronRendererAuthState>({
23
+ status: "idle",
24
+ message: null,
25
+ });
26
+
27
+ useEffect(() => {
28
+ if (!isDesktopAppEnabled) {
29
+ return;
30
+ }
31
+
32
+ const id = authClient.ensureElectronRedirect();
33
+ return () => clearInterval(id);
34
+ }, [isDesktopAppEnabled]);
35
+
36
+ useEffect(() => {
37
+ if (!isDesktopAppEnabled) {
38
+ return;
39
+ }
40
+
41
+ if (typeof window.requestAuth !== "function") {
42
+ return;
43
+ }
44
+
45
+ if (
46
+ typeof window.onAuthenticated !== "function" ||
47
+ typeof window.onUserUpdated !== "function" ||
48
+ typeof window.onAuthError !== "function" ||
49
+ typeof window.electronAPI?.onAuthStateChanged !== "function"
50
+ ) {
51
+ return;
52
+ }
53
+
54
+ const authStatePromise = window.electronAPI?.getAuthState?.();
55
+ authStatePromise
56
+ ?.then((state) => {
57
+ if (state) {
58
+ setAuthState(state);
59
+ }
60
+ })
61
+ ?.catch((error) => {
62
+ console.error("Failed to read Electron auth state", error);
63
+ });
64
+
65
+ const syncAndRefresh = async () => {
66
+ await window.electronAPI?.syncAuthSession?.();
67
+ router.refresh();
68
+ };
69
+
70
+ const unsubscribeAuthenticated = window.onAuthenticated(() => {
71
+ syncAndRefresh().catch((error) => {
72
+ console.error(
73
+ "Failed to sync auth session after authentication",
74
+ error
75
+ );
76
+ });
77
+ });
78
+ const unsubscribeUserUpdated = window.onUserUpdated(() => {
79
+ syncAndRefresh().catch((error) => {
80
+ console.error("Failed to sync auth session after user update", error);
81
+ });
82
+ });
83
+ const unsubscribeAuthError = window.onAuthError(
84
+ (ctx: ElectronAuthErrorContext) => {
85
+ toast.error(ctx.message || "Authentication failed");
86
+ }
87
+ );
88
+ const unsubscribeAuthState = window.electronAPI.onAuthStateChanged(
89
+ (state) => {
90
+ setAuthState(state);
91
+ }
92
+ );
93
+
94
+ return () => {
95
+ unsubscribeAuthenticated();
96
+ unsubscribeUserUpdated();
97
+ unsubscribeAuthError();
98
+ unsubscribeAuthState();
99
+ };
100
+ }, [isDesktopAppEnabled, router]);
101
+
102
+ if (!isDesktopAppEnabled) {
103
+ return null;
104
+ }
105
+
106
+ const overlayKey = `${authState.status}:${authState.message ?? ""}:${
107
+ authState.status === "idle" ? "" : (authState.detail ?? "")
108
+ }`;
109
+
110
+ return <ElectronAuthOverlay key={overlayKey} state={authState} />;
111
+ }
112
+
113
+ function ElectronAuthOverlay({ state }: { state: ElectronRendererAuthState }) {
114
+ const [isDismissed, setIsDismissed] = useState(false);
115
+
116
+ if (state.status === "idle" || !state.message) {
117
+ return null;
118
+ }
119
+
120
+ const isLoading =
121
+ state.status === "awaiting-browser" || state.status === "finishing";
122
+ const canCancel = state.status === "awaiting-browser";
123
+ let detailMessage: string;
124
+
125
+ if (state.status === "awaiting-browser") {
126
+ detailMessage = "Complete sign-in in your browser, then come back here.";
127
+ } else if (state.status === "finishing") {
128
+ detailMessage =
129
+ "Your browser has returned to ChatJS. We're finalizing the session now.";
130
+ } else {
131
+ detailMessage =
132
+ state.detail || "If nothing changes, try the browser flow again.";
133
+ }
134
+
135
+ if (!isLoading && isDismissed) {
136
+ return null;
137
+ }
138
+
139
+ return (
140
+ <div className="pointer-events-auto fixed inset-0 z-[999999] flex items-center justify-center bg-background/90 px-4 backdrop-blur-sm">
141
+ <div className="w-full max-w-sm rounded-2xl border bg-background p-6 shadow-2xl">
142
+ <div className="flex items-start gap-3">
143
+ <div className="mt-0.5 text-muted-foreground">
144
+ {isLoading ? (
145
+ <LoaderCircle className="size-5 animate-spin" />
146
+ ) : (
147
+ <AlertCircle className="size-5 text-amber-600" />
148
+ )}
149
+ </div>
150
+ <div className="space-y-2">
151
+ <p className="font-medium">{state.message}</p>
152
+ <p className="text-muted-foreground text-sm">{detailMessage}</p>
153
+ {canCancel ? (
154
+ <Button
155
+ className="mt-2"
156
+ onClick={() => {
157
+ window.electronAPI?.cancelAuthFlow?.().catch((error) => {
158
+ console.error("Failed to cancel Electron auth flow", error);
159
+ });
160
+ }}
161
+ size="sm"
162
+ type="button"
163
+ variant="outline"
164
+ >
165
+ Go back
166
+ </Button>
167
+ ) : null}
168
+ {isLoading ? null : (
169
+ <Button
170
+ className="mt-2"
171
+ onClick={() => setIsDismissed(true)}
172
+ size="sm"
173
+ type="button"
174
+ variant="outline"
175
+ >
176
+ Dismiss
177
+ </Button>
178
+ )}
179
+ </div>
180
+ </div>
181
+ </div>
182
+ </div>
183
+ );
184
+ }
@@ -0,0 +1,121 @@
1
+ "use client";
2
+
3
+ import { ExternalLink, LoaderCircle } from "lucide-react";
4
+ import { useRouter } from "next/navigation";
5
+ import { useEffect, useMemo, useRef, useState, useTransition } from "react";
6
+ import config from "@/chat.config";
7
+ import type { Session } from "@/lib/auth";
8
+ import authClient from "@/lib/auth-client";
9
+ import { Button } from "./ui/button";
10
+
11
+ export function ElectronBrowserSignIn({
12
+ buttonLabel = "Continue with browser",
13
+ }: {
14
+ buttonLabel?: string;
15
+ }) {
16
+ const [opened, setOpened] = useState(false);
17
+
18
+ return (
19
+ <div className="space-y-3">
20
+ <p className="text-center text-muted-foreground text-sm">
21
+ Sign-in opens in your browser. On macOS, {config.appName} may ask to use
22
+ Keychain so it can store your session securely.
23
+ </p>
24
+ <Button
25
+ className="w-full"
26
+ onClick={() => {
27
+ const requestAuth = window.requestAuth;
28
+ if (typeof requestAuth !== "function") {
29
+ return;
30
+ }
31
+ Promise.resolve()
32
+ .then(() => requestAuth())
33
+ .catch((error) => {
34
+ console.error("Failed to launch browser sign-in", error);
35
+ });
36
+ window.setTimeout(() => setOpened(true), 300);
37
+ }}
38
+ type="button"
39
+ variant="outline"
40
+ >
41
+ <ExternalLink className="mr-2 size-4" />
42
+ {buttonLabel}
43
+ </Button>
44
+
45
+ {opened ? (
46
+ <p className="text-center text-muted-foreground text-sm">
47
+ Finish signing in through your browser. If macOS asks about Keychain
48
+ access, allow it to keep your session saved securely.
49
+ </p>
50
+ ) : null}
51
+ </div>
52
+ );
53
+ }
54
+
55
+ export function ElectronTransferUser({
56
+ query,
57
+ session,
58
+ }: {
59
+ query: Record<string, string>;
60
+ session: Session;
61
+ }) {
62
+ const [isPending, startTransition] = useTransition();
63
+ const router = useRouter();
64
+ const hasStartedTransferRef = useRef(false);
65
+ const useAnotherAccountHref = useMemo(() => {
66
+ const params = new URLSearchParams(query);
67
+ params.delete("client_id");
68
+ params.delete("state");
69
+ params.delete("code_challenge");
70
+ params.delete("code_challenge_method");
71
+
72
+ const nextQuery = params.toString();
73
+ return nextQuery ? `/login?${nextQuery}` : "/login";
74
+ }, [query]);
75
+
76
+ useEffect(() => {
77
+ if (hasStartedTransferRef.current) {
78
+ return;
79
+ }
80
+ hasStartedTransferRef.current = true;
81
+
82
+ startTransition(async () => {
83
+ await authClient.electron.transferUser({ fetchOptions: { query } });
84
+ router.refresh();
85
+ });
86
+ }, [query, router]);
87
+
88
+ return (
89
+ <div className="space-y-4">
90
+ <div className="rounded-lg border px-4 py-3 text-sm">
91
+ <p className="font-medium">{session.user.name || session.user.email}</p>
92
+ <p className="text-muted-foreground">{session.user.email}</p>
93
+ </div>
94
+
95
+ <Button
96
+ className="w-full"
97
+ disabled={isPending}
98
+ onClick={() => {
99
+ startTransition(async () => {
100
+ await authClient.electron.transferUser({ fetchOptions: { query } });
101
+ router.refresh();
102
+ });
103
+ }}
104
+ type="button"
105
+ >
106
+ {isPending ? (
107
+ <>
108
+ <LoaderCircle className="mr-2 size-4 animate-spin" />
109
+ Connecting…
110
+ </>
111
+ ) : (
112
+ "Continue to desktop app"
113
+ )}
114
+ </Button>
115
+
116
+ <Button asChild className="w-full" variant="ghost">
117
+ <a href={useAnotherAccountHref}>Use another account</a>
118
+ </Button>
119
+ </div>
120
+ );
121
+ }
@@ -27,7 +27,7 @@ export const FaviconGroup: React.FC<FaviconGroupProps> = ({
27
27
  <Favicon
28
28
  alt={`Favicon for ${source.title || new URL(source.url).hostname}`}
29
29
  className={cn(
30
- "h-5 w-5 rounded-full border-2 border-white dark:border-neutral-800", // Slightly thicker border for contrast
30
+ "h-5 w-5 rounded-full border-2 border-background",
31
31
  index > 0 ? "-ml-2" : ""
32
32
  )}
33
33
  key={source.url || index}
@@ -2,7 +2,7 @@ import { useMessageById } from "@ai-sdk-tools/store";
2
2
  import { useMutation, useQueryClient } from "@tanstack/react-query";
3
3
  import { ThumbsDown, ThumbsUp } from "lucide-react";
4
4
  import { toast } from "sonner";
5
- import { getPrimarySelectedModelId, type ChatMessage } from "@/lib/ai/types";
5
+ import { type ChatMessage, getPrimarySelectedModelId } from "@/lib/ai/types";
6
6
  import type { Vote } from "@/lib/db/schema";
7
7
  import { useSession } from "@/providers/session-provider";
8
8
  import { useTRPC } from "@/trpc/react";
@@ -16,7 +16,7 @@ export const Greeting = () => (
16
16
  </motion.div>
17
17
  <motion.div
18
18
  animate={{ opacity: 1, y: 0 }}
19
- className="text-2xl text-zinc-500"
19
+ className="text-2xl text-muted-foreground"
20
20
  exit={{ opacity: 0, y: 10 }}
21
21
  initial={{ opacity: 0, y: 10 }}
22
22
  transition={{ delay: 0.6 }}
@@ -82,8 +82,7 @@ function InteractiveChart({ chart }: { chart: BaseChart }) {
82
82
  backgroundColor: tooltipBg,
83
83
  borderWidth: 0,
84
84
  padding: [6, 10],
85
- className:
86
- "echarts-tooltip rounded-lg! border! border-neutral-200! dark:border-neutral-800!",
85
+ className: "echarts-tooltip rounded-lg! border! border-border!",
87
86
  textStyle: {
88
87
  color: textColor,
89
88
  fontSize: 13,
@@ -247,10 +246,10 @@ function InteractiveChart({ chart }: { chart: BaseChart }) {
247
246
  initial={{ opacity: 0, y: 20 }}
248
247
  transition={{ duration: 0.5 }}
249
248
  >
250
- <Card className="overflow-hidden border-neutral-200 bg-white dark:border-neutral-800 dark:bg-neutral-900">
249
+ <Card className="overflow-hidden border-border bg-card">
251
250
  <div className="p-6">
252
251
  {chart.title && (
253
- <h3 className="mb-4 font-medium text-lg text-neutral-900 dark:text-neutral-100">
252
+ <h3 className="mb-4 font-medium text-foreground text-lg">
254
253
  {chart.title}
255
254
  </h3>
256
255
  )}