@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
@@ -29,9 +29,9 @@ import { useArtifact } from "@/hooks/use-artifact";
29
29
  import { useIsMobile } from "@/hooks/use-mobile";
30
30
  import type { AppModelId } from "@/lib/ai/app-model-id";
31
31
  import {
32
- expandSelectedModelValue,
33
32
  type Attachment,
34
33
  type ChatMessage,
34
+ expandSelectedModelValue,
35
35
  type SelectedModelValue,
36
36
  type UiToolName,
37
37
  } from "@/lib/ai/types";
@@ -325,7 +325,9 @@ function PureMultimodalInput({
325
325
  );
326
326
 
327
327
  const getCurrentProjectId = useCallback(() => {
328
- const projectMatch = window.location.pathname.match(PROJECT_CHAT_ROUTE_REGEX);
328
+ const projectMatch = window.location.pathname.match(
329
+ PROJECT_CHAT_ROUTE_REGEX
330
+ );
329
331
  return projectMatch?.[1];
330
332
  }, []);
331
333
 
@@ -499,7 +501,8 @@ function PureMultimodalInput({
499
501
  trimMessagesInEditMode(parentMessageId);
500
502
  }
501
503
 
502
- const isParallelRequest = parallelResponsesEnabled && requestedModelIds.length > 1;
504
+ const isParallelRequest =
505
+ parallelResponsesEnabled && requestedModelIds.length > 1;
503
506
  const parallelGroupId = isParallelRequest ? generateUUID() : null;
504
507
  const requestSpecs = isParallelRequest
505
508
  ? requestedModelIds.map(
@@ -576,13 +579,13 @@ function PureMultimodalInput({
576
579
  });
577
580
  }
578
581
 
579
- void runParallelSecondaryRequests({
582
+ runParallelSecondaryRequests({
580
583
  message,
581
584
  secondaryRequestSpecs: requestSpecs.slice(1),
582
585
  }).catch((error) => {
583
586
  console.error("Failed to complete parallel requests", error);
584
587
  toast.error("Failed to complete all parallel responses");
585
- void invalidatePersistedMessages();
588
+ invalidatePersistedMessages();
586
589
  });
587
590
  } else {
588
591
  sendMessage(message);
@@ -833,10 +836,10 @@ function PureMultimodalInput({
833
836
  <PromptInput
834
837
  className={cn(
835
838
  "@container relative transition-colors",
836
- isDragActive && "border-blue-500 bg-blue-50 dark:bg-blue-950/20",
839
+ isDragActive && "border-primary bg-accent",
837
840
  className
838
841
  )}
839
- inputGroupClassName="dark:bg-muted bg-muted"
842
+ inputGroupClassName="bg-muted dark:bg-muted"
840
843
  {...getRootProps({ onError: undefined, onSubmit: undefined })}
841
844
  onSubmit={(_message, event) => {
842
845
  event.preventDefault();
@@ -852,8 +855,8 @@ function PureMultimodalInput({
852
855
  <input {...getInputProps()} />
853
856
 
854
857
  {isDragActive && attachmentsEnabled && (
855
- <div className="absolute inset-0 z-10 flex items-center justify-center rounded-xl border-2 border-blue-500 border-dashed bg-blue-50/80 dark:bg-blue-950/40">
856
- <div className="font-medium text-blue-600 dark:text-blue-400">
858
+ <div className="absolute inset-0 z-10 flex items-center justify-center rounded-xl border-2 border-primary border-dashed bg-accent/80">
859
+ <div className="font-medium text-primary">
857
860
  Drop images or PDFs here to attach
858
861
  </div>
859
862
  </div>
@@ -1107,9 +1110,9 @@ function PureChatInputBottomControls({
1107
1110
  )}
1108
1111
  <ModelSelector
1109
1112
  className="@[500px]:h-10 h-8 w-fit max-w-none shrink justify-start truncate @[500px]:px-3 px-2 @[500px]:text-sm text-xs"
1113
+ onModelSelectionChangeAction={onModelSelectionChange}
1110
1114
  selectedModelId={selectedModelId}
1111
1115
  selectedModelSelection={selectedModelSelection}
1112
- onModelSelectionChangeAction={onModelSelectionChange}
1113
1116
  />
1114
1117
  <ConnectorsDropdown />
1115
1118
  <ResponsiveTools
@@ -5,6 +5,7 @@ import { LoaderCircle } from "lucide-react";
5
5
  import { memo, useMemo } from "react";
6
6
  import { Button } from "@/components/ui/button";
7
7
  import { useNavigateToMessage } from "@/hooks/use-navigate-to-message";
8
+ import type { AppModelId } from "@/lib/ai/app-models";
8
9
  import {
9
10
  type ChatMessage,
10
11
  expandSelectedModelValue,
@@ -15,11 +16,39 @@ import { cn } from "@/lib/utils";
15
16
  import { useChatInput } from "@/providers/chat-input-provider";
16
17
  import { useChatModels } from "@/providers/chat-models-provider";
17
18
 
18
- function PureParallelResponseCards({
19
- messageId,
20
- }: {
21
- messageId: string;
22
- }) {
19
+ function getEffectiveModelId(
20
+ message: {
21
+ metadata: { selectedModel: ChatMessage["metadata"]["selectedModel"] };
22
+ } | null,
23
+ fallbackModelId: AppModelId
24
+ ): AppModelId | undefined {
25
+ return message?.metadata.selectedModel
26
+ ? (getPrimarySelectedModelId(message.metadata.selectedModel) ?? undefined)
27
+ : fallbackModelId;
28
+ }
29
+
30
+ function getModelOrderIndex(
31
+ modelId: AppModelId | undefined,
32
+ models: Array<{ id: string }>
33
+ ): number {
34
+ if (!modelId) {
35
+ return Number.POSITIVE_INFINITY;
36
+ }
37
+ const index = models.findIndex((m) => m.id === modelId);
38
+ return index === -1 ? Number.POSITIVE_INFINITY : index;
39
+ }
40
+
41
+ function getStatusLabel(isSelected: boolean, isStreaming: boolean): string {
42
+ if (isSelected) {
43
+ return "Selected";
44
+ }
45
+ if (isStreaming) {
46
+ return "Generating...";
47
+ }
48
+ return "Task completed";
49
+ }
50
+
51
+ function PureParallelResponseCards({ messageId }: { messageId: string }) {
23
52
  const message = useMessageById<ChatMessage>(messageId);
24
53
  const parallelGroupInfo = useParallelGroupInfo(messageId);
25
54
  const navigateToMessage = useNavigateToMessage();
@@ -36,7 +65,9 @@ function PureParallelResponseCards({
36
65
  return [];
37
66
  }
38
67
 
39
- const requestedModelIds = expandSelectedModelValue(message.metadata.selectedModel);
68
+ const requestedModelIds = expandSelectedModelValue(
69
+ message.metadata.selectedModel
70
+ );
40
71
 
41
72
  return requestedModelIds.map((modelId, parallelIndex) => {
42
73
  const actualMessage = parallelGroupInfo?.messages.find(
@@ -53,24 +84,14 @@ function PureParallelResponseCards({
53
84
 
54
85
  const sortedCardSlots = useMemo(() => {
55
86
  return [...cardSlots].sort((left, right) => {
56
- const leftModelId =
57
- left.message?.metadata.selectedModel
58
- ? getPrimarySelectedModelId(left.message.metadata.selectedModel)
59
- : left.modelId;
60
- const rightModelId =
61
- right.message?.metadata.selectedModel
62
- ? getPrimarySelectedModelId(right.message.metadata.selectedModel)
63
- : right.modelId;
64
-
65
- const leftIndex = leftModelId
66
- ? models.findIndex((m) => m.id === leftModelId)
67
- : -1;
68
- const rightIndex = rightModelId
69
- ? models.findIndex((m) => m.id === rightModelId)
70
- : -1;
71
-
72
- const leftOrder = leftIndex === -1 ? Infinity : leftIndex;
73
- const rightOrder = rightIndex === -1 ? Infinity : rightIndex;
87
+ const leftOrder = getModelOrderIndex(
88
+ getEffectiveModelId(left.message, left.modelId),
89
+ models
90
+ );
91
+ const rightOrder = getModelOrderIndex(
92
+ getEffectiveModelId(right.message, right.modelId),
93
+ models
94
+ );
74
95
 
75
96
  if (leftOrder !== rightOrder) {
76
97
  return leftOrder - rightOrder;
@@ -105,20 +126,15 @@ function PureParallelResponseCards({
105
126
  return (
106
127
  <div className="mt-3 flex flex-wrap justify-end gap-2">
107
128
  {sortedCardSlots.map((slot) => {
108
- const modelId =
109
- slot.message?.metadata.selectedModel
110
- ? getPrimarySelectedModelId(slot.message.metadata.selectedModel)
111
- : slot.modelId;
112
- const modelName = modelId ? getModelById(modelId)?.name ?? modelId : "Model";
129
+ const modelId = getEffectiveModelId(slot.message, slot.modelId);
130
+ const modelName = modelId
131
+ ? (getModelById(modelId)?.name ?? modelId)
132
+ : "Model";
113
133
  const isSelected = selectedParallelIndex === slot.parallelIndex;
114
134
  const isStreaming = slot.message
115
135
  ? slot.message.metadata.activeStreamId !== null
116
136
  : true;
117
- const statusLabel = isSelected
118
- ? "Selected"
119
- : isStreaming
120
- ? "Generating..."
121
- : "Task completed";
137
+ const statusLabel = getStatusLabel(isSelected, isStreaming);
122
138
 
123
139
  return (
124
140
  <Button
@@ -141,7 +157,9 @@ function PureParallelResponseCards({
141
157
  >
142
158
  <span className="font-medium text-sm">{modelName}</span>
143
159
  <span className="flex items-center gap-1 text-muted-foreground text-xs">
144
- {isStreaming ? <LoaderCircle className="size-3 animate-spin" /> : null}
160
+ {isStreaming ? (
161
+ <LoaderCircle className="size-3 animate-spin" />
162
+ ) : null}
145
163
  {statusLabel}
146
164
  </span>
147
165
  </Button>
@@ -33,7 +33,12 @@ function isPngChart(input: unknown): input is PngChart {
33
33
  }
34
34
 
35
35
  export function CodeExecution({ tool }: { tool: CodeExecutionTool }) {
36
- const args = tool.input ?? { code: "", title: "", icon: "default" };
36
+ const args = tool.input ?? {
37
+ code: "",
38
+ title: "",
39
+ language: "python",
40
+ icon: "default",
41
+ };
37
42
  const result = tool.state === "output-available" ? tool.output : null;
38
43
  const chart: BaseChart | null =
39
44
  result && isBaseChart(result.chart) ? result.chart : null;
@@ -41,11 +46,12 @@ export function CodeExecution({ tool }: { tool: CodeExecutionTool }) {
41
46
  result && isPngChart(result.chart) ? result.chart : null;
42
47
  const code = typeof args.code === "string" ? args.code : "";
43
48
  const title = typeof args.title === "string" ? args.title : "";
49
+ const language = args.language === "javascript" ? "javascript" : "python";
44
50
  return (
45
51
  <div className="space-y-6">
46
52
  <SandboxComposed
47
53
  code={code}
48
- language="python"
54
+ language={language}
49
55
  output={result?.message}
50
56
  state={tool.state}
51
57
  title={title}
@@ -128,7 +128,7 @@ function PureDocumentToolCall({
128
128
  type="button"
129
129
  >
130
130
  <div className="flex flex-row items-start gap-3">
131
- <div className="mt-1 text-zinc-500">
131
+ <div className="mt-1 text-muted-foreground">
132
132
  {(() => {
133
133
  if (type === "create") {
134
134
  return <File size={16} />;
@@ -155,7 +155,7 @@ const LoadingSkeleton = ({
155
155
  artifactKind: ArtifactKind;
156
156
  }) => (
157
157
  <div className="w-full">
158
- <div className="flex h-[57px] flex-row items-center justify-between gap-2 rounded-t-2xl border border-b-0 p-4 dark:border-zinc-700 dark:bg-muted">
158
+ <div className="flex h-[57px] flex-row items-center justify-between gap-2 rounded-t-2xl border border-b-0 bg-muted p-4">
159
159
  <div className="flex flex-row items-center gap-3">
160
160
  <div className="text-muted-foreground">
161
161
  <div className="size-4 animate-pulse rounded-md bg-muted-foreground/20" />
@@ -167,7 +167,7 @@ const LoadingSkeleton = ({
167
167
  </div>
168
168
  </div>
169
169
 
170
- <div className="overflow-y-scroll rounded-b-2xl border border-t-0 bg-muted p-8 pt-4 dark:border-zinc-700">
170
+ <div className="overflow-y-scroll rounded-b-2xl border border-t-0 bg-muted p-8 pt-4">
171
171
  <InlineDocumentSkeleton />
172
172
  </div>
173
173
  </div>
@@ -217,7 +217,7 @@ const PureHitboxLayer = ({
217
217
  role="presentation"
218
218
  >
219
219
  <div className="flex w-full items-center justify-end p-4">
220
- <div className="absolute top-[13px] right-[9px] rounded-md p-2 hover:bg-zinc-100 dark:hover:bg-zinc-700">
220
+ <div className="absolute top-[13px] right-[9px] rounded-md p-2 hover:bg-accent">
221
221
  <Maximize size={16} />
222
222
  </div>
223
223
  </div>
@@ -257,7 +257,7 @@ const PureDocumentHeader = ({
257
257
  isStreaming: boolean;
258
258
  type: "create" | "update";
259
259
  }) => (
260
- <div className="flex flex-row items-start justify-between gap-2 rounded-t-2xl border border-b-0 p-4 sm:items-center dark:border-zinc-700 dark:bg-muted">
260
+ <div className="flex flex-row items-start justify-between gap-2 rounded-t-2xl border border-b-0 bg-muted p-4 sm:items-center">
261
261
  <div className="flex flex-row items-start gap-3 sm:items-center">
262
262
  <div className="text-muted-foreground">
263
263
  {(() => {
@@ -299,7 +299,7 @@ const DocumentContent = ({ document }: { document: Document }) => {
299
299
  const { artifact } = useArtifact();
300
300
 
301
301
  const containerClassName = cn(
302
- "h-[257px] overflow-y-scroll rounded-b-2xl border border-t-0 dark:border-zinc-700 dark:bg-muted",
302
+ "h-[257px] overflow-y-scroll rounded-b-2xl border border-t-0 bg-muted",
303
303
  {
304
304
  "p-4 sm:px-14 sm:py-16": document.kind === "text",
305
305
  "p-0": document.kind === "code",
@@ -12,17 +12,17 @@ export type RetrieveUrlTool = Extract<
12
12
 
13
13
  function LoadingState() {
14
14
  return (
15
- <div className="my-4 rounded-xl border border-neutral-200 bg-linear-to-b from-white to-neutral-50 p-4 dark:border-neutral-800 dark:from-neutral-900 dark:to-neutral-900/90">
15
+ <div className="my-4 rounded-xl border border-border bg-card p-4">
16
16
  <div className="flex items-center gap-4">
17
17
  <div className="relative h-10 w-10">
18
18
  <div className="absolute inset-0 animate-pulse rounded-full bg-primary/10" />
19
19
  <Globe className="absolute inset-0 m-auto h-5 w-5 text-primary/70" />
20
20
  </div>
21
21
  <div className="flex-1 space-y-2">
22
- <div className="h-4 w-36 animate-pulse rounded-md bg-neutral-200 dark:bg-neutral-800" />
22
+ <div className="h-4 w-36 animate-pulse rounded-md bg-muted-foreground/20" />
23
23
  <div className="space-y-1.5">
24
- <div className="h-3 w-full animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-800/50" />
25
- <div className="h-3 w-2/3 animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-800/50" />
24
+ <div className="h-3 w-full animate-pulse rounded-md bg-muted-foreground/15" />
25
+ <div className="h-3 w-2/3 animate-pulse rounded-md bg-muted-foreground/15" />
26
26
  </div>
27
27
  </div>
28
28
  </div>
@@ -86,10 +86,10 @@ function RetrievedContentHeader({ firstItem }: { firstItem: unknown }) {
86
86
  />
87
87
  </div>
88
88
  <div className="min-w-0 flex-1 space-y-2">
89
- <h2 className="truncate font-semibold text-lg text-neutral-900 tracking-tight dark:text-neutral-100">
89
+ <h2 className="truncate font-semibold text-foreground text-lg tracking-tight">
90
90
  {title}
91
91
  </h2>
92
- <p className="line-clamp-2 text-neutral-600 text-sm dark:text-neutral-400">
92
+ <p className="line-clamp-2 text-muted-foreground text-sm">
93
93
  {description}
94
94
  </p>
95
95
  <div className="flex items-center gap-3">
@@ -97,7 +97,7 @@ function RetrievedContentHeader({ firstItem }: { firstItem: unknown }) {
97
97
  {language}
98
98
  </span>
99
99
  <a
100
- className="inline-flex items-center gap-1.5 text-neutral-500 text-xs transition-colors hover:text-primary"
100
+ className="inline-flex items-center gap-1.5 text-muted-foreground text-xs transition-colors hover:text-primary"
101
101
  href={url || "#"}
102
102
  rel="noopener noreferrer"
103
103
  target="_blank"
@@ -116,16 +116,16 @@ function RetrievedContentDetails({ firstItem }: { firstItem: unknown }) {
116
116
  const content = getItemProperty(firstItem, "content", "No content available");
117
117
 
118
118
  return (
119
- <div className="border-neutral-200 border-t dark:border-neutral-800">
119
+ <div className="border-border border-t">
120
120
  <details className="group">
121
- <summary className="flex w-full cursor-pointer items-center justify-between px-4 py-2 text-neutral-700 text-sm transition-colors hover:bg-neutral-50 dark:text-neutral-300 dark:hover:bg-neutral-800/50">
121
+ <summary className="flex w-full cursor-pointer items-center justify-between px-4 py-2 text-muted-foreground text-sm transition-colors hover:bg-muted">
122
122
  <div className="flex items-center gap-2">
123
- <TextIcon className="h-4 w-4 text-neutral-400" />
123
+ <TextIcon className="h-4 w-4 text-muted-foreground" />
124
124
  <span>View content</span>
125
125
  </div>
126
126
  <ChevronDown className="h-4 w-4 transition-transform duration-200 group-open:rotate-180" />
127
127
  </summary>
128
- <div className="max-h-[50vh] overflow-y-auto bg-neutral-50/50 p-4 dark:bg-neutral-800/30">
128
+ <div className="max-h-[50vh] overflow-y-auto bg-muted/50 p-4">
129
129
  <div className="prose prose-neutral dark:prose-invert prose-sm max-w-none">
130
130
  <ReactMarkdown>{content}</ReactMarkdown>
131
131
  </div>
@@ -173,7 +173,7 @@ export function RetrieveUrl({ tool }: { tool: RetrieveUrlTool }) {
173
173
  }
174
174
 
175
175
  return (
176
- <div className="my-4 overflow-hidden rounded-xl border border-neutral-200 bg-linear-to-b from-white to-neutral-50 dark:border-neutral-800 dark:from-neutral-900 dark:to-neutral-900/90">
176
+ <div className="my-4 overflow-hidden rounded-xl border border-border bg-card">
177
177
  <RetrievedContentHeader firstItem={firstItem} />
178
178
  <RetrievedContentDetails firstItem={firstItem} />
179
179
  </div>
@@ -4,13 +4,17 @@ import { memo } from "react";
4
4
  import { Response } from "../ai-elements/response";
5
5
 
6
6
  export const TextMessagePart = memo(
7
- ({ text, isLoading }: { text: string; isLoading: boolean }) => (
8
- <Response
9
- animated
10
- isAnimating={isLoading}
11
- mode={isLoading ? "streaming" : "static"}
12
- >
13
- {text}
14
- </Response>
15
- ),
7
+ ({ text, isLoading }: { text: string; isLoading: boolean }) => (
8
+ <Response
9
+ animated={{
10
+ duration: 200, // milliseconds (default: 150)
11
+ stagger: 0,
12
+ }}
13
+ isAnimating={isLoading}
14
+ linkSafety={{ enabled: false }}
15
+ mode={isLoading ? "streaming" : "static"}
16
+ >
17
+ {text}
18
+ </Response>
19
+ )
16
20
  );
@@ -75,7 +75,7 @@ export function ProjectChatItem({
75
75
  <ShareMenuItem onShare={() => setShareDialogOpen(true)} />
76
76
 
77
77
  <DropdownMenuItem
78
- className="cursor-pointer text-destructive focus:bg-destructive/15 focus:text-destructive dark:text-red-500"
78
+ className="cursor-pointer text-destructive focus:bg-destructive/15 focus:text-destructive"
79
79
  onSelect={() => onDelete(chat.id)}
80
80
  >
81
81
  <Trash2 size={16} />
@@ -19,7 +19,7 @@ export function ProjectMenuItems({
19
19
  <span>Rename</span>
20
20
  </DropdownMenuItem>
21
21
  <DropdownMenuItem
22
- className="cursor-pointer text-destructive focus:bg-destructive/15 focus:text-destructive dark:text-red-500"
22
+ className="cursor-pointer text-destructive focus:bg-destructive/15 focus:text-destructive"
23
23
  onSelect={onDelete}
24
24
  >
25
25
  <Trash2 size={16} />
@@ -71,7 +71,7 @@ export const ResearchTask = ({
71
71
  {update.type === "web" && update.status === "running" && (
72
72
  <div className="py-2">
73
73
  <div className="flex items-center gap-3">
74
- <Loader2 className="size-4 animate-spin text-neutral-500" />
74
+ <Loader2 className="size-4 animate-spin text-muted-foreground" />
75
75
  <p className="text-xsize-neutral-500">Searching the web...</p>
76
76
  </div>
77
77
  </div>
@@ -11,7 +11,7 @@ export const ResearchTasks = ({ updates }: { updates: ResearchUpdate[] }) => (
11
11
  {updates.map((update, index) => (
12
12
  <StepWrapper
13
13
  isLast={index === updates.length - 1}
14
- key={`${update.type}-${index}`}
14
+ key={update.toolCallId}
15
15
  update={update}
16
16
  >
17
17
  <ResearchTask
@@ -7,7 +7,7 @@ import { RefreshCcw } from "lucide-react";
7
7
  import { useCallback } from "react";
8
8
  import { toast } from "sonner";
9
9
  import { Action } from "@/components/ai-elements/actions";
10
- import { getPrimarySelectedModelId, type ChatMessage } from "@/lib/ai/types";
10
+ import { type ChatMessage, getPrimarySelectedModelId } from "@/lib/ai/types";
11
11
 
12
12
  export function RetryButton({
13
13
  messageId,
@@ -38,7 +38,7 @@ export function SandboxComposed({
38
38
  <SandboxHeader state={state} title={title} />
39
39
  <SandboxContent>
40
40
  <SandboxTabs onValueChange={setActiveTab} value={activeTab}>
41
- <div className="flex items-center border-neutral-200 border-b dark:border-neutral-800">
41
+ <div className="flex items-center border-border border-b">
42
42
  <SandboxTabsList>
43
43
  <SandboxTabsTrigger value="code">Code</SandboxTabsTrigger>
44
44
  <SandboxTabsTrigger value="output">Output</SandboxTabsTrigger>
@@ -57,8 +57,8 @@ const PureSpreadsheetEditor = ({
57
57
  frozen: true,
58
58
  width: 50,
59
59
  renderCell: ({ rowIdx }: { rowIdx: number }) => rowIdx + 1,
60
- cellClass: "border-t border-r dark:bg-zinc-950 dark:text-zinc-50",
61
- headerCellClass: "border-t border-r dark:bg-zinc-900 dark:text-zinc-50",
60
+ cellClass: "border-t border-r bg-background text-foreground",
61
+ headerCellClass: "border-t border-r bg-muted text-foreground",
62
62
  };
63
63
 
64
64
  const dataColumns = Array.from({ length: MIN_COLS }, (_, i) => ({
@@ -66,10 +66,10 @@ const PureSpreadsheetEditor = ({
66
66
  name: String.fromCharCode(65 + i),
67
67
  renderEditCell: isReadonly ? undefined : textEditor,
68
68
  width: 120,
69
- cellClass: cn("border-t dark:bg-zinc-950 dark:text-zinc-50", {
69
+ cellClass: cn("border-t bg-background text-foreground", {
70
70
  "border-l": i !== 0,
71
71
  }),
72
- headerCellClass: cn("border-t dark:bg-zinc-900 dark:text-zinc-50", {
72
+ headerCellClass: cn("border-t bg-muted text-foreground", {
73
73
  "border-l": i !== 0,
74
74
  }),
75
75
  }));
@@ -80,7 +80,7 @@ const PureSpreadsheetEditor = ({
80
80
  const initialRows = useMemo(
81
81
  () =>
82
82
  parseData.map((row, rowIndex) => {
83
- const rowData: any = {
83
+ const rowData: Record<string, string | number> = {
84
84
  id: rowIndex,
85
85
  rowNumber: rowIndex + 1,
86
86
  };
@@ -100,9 +100,9 @@ const PureSpreadsheetEditor = ({
100
100
  setLocalRows(initialRows);
101
101
  }, [initialRows]);
102
102
 
103
- const generateCsv = (data: any[][]) => unparse(data);
103
+ const generateCsv = (data: Array<Array<string | number>>) => unparse(data);
104
104
 
105
- const handleRowsChange = (newRows: any[]) => {
105
+ const handleRowsChange = (newRows: Record<string, string | number>[]) => {
106
106
  if (isReadonly) {
107
107
  return;
108
108
  }
@@ -106,7 +106,7 @@ export function SidebarChatsList() {
106
106
 
107
107
  if (chats.length === 0) {
108
108
  return (
109
- <div className="flex w-full flex-row items-center justify-center gap-2 px-2 py-4 text-sm text-zinc-500">
109
+ <div className="flex w-full flex-row items-center justify-center gap-2 px-2 py-4 text-muted-foreground text-sm">
110
110
  Start chatting to see your conversation history!
111
111
  </div>
112
112
  );
@@ -11,14 +11,27 @@ import {
11
11
  import { Button } from "./ui/button";
12
12
 
13
13
  export function SidebarToggle({
14
- className: _className,
14
+ className,
15
+ onClick,
16
+ ...props
15
17
  }: ComponentProps<typeof SidebarTrigger>) {
16
18
  const { toggleSidebar } = useSidebar();
17
19
 
18
20
  return (
19
21
  <Tooltip>
20
22
  <TooltipTrigger asChild>
21
- <Button onClick={toggleSidebar} size="icon" variant="ghost">
23
+ <Button
24
+ {...props}
25
+ className={className}
26
+ onClick={(event) => {
27
+ onClick?.(event);
28
+ if (!event.defaultPrevented) {
29
+ toggleSidebar();
30
+ }
31
+ }}
32
+ size="icon"
33
+ variant="ghost"
34
+ >
22
35
  <PanelLeft size={16} />
23
36
  </Button>
24
37
  </TooltipTrigger>
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { PanelLeft } from "lucide-react";
3
4
  import Image from "next/image";
4
5
  import Link from "next/link";
5
6
  import { SidebarToggle } from "@/components/sidebar-toggle";
@@ -8,16 +9,13 @@ import { config } from "@/lib/config";
8
9
  import { useChatId } from "@/providers/chat-id-provider";
9
10
 
10
11
  export function SidebarTopRow() {
11
- const { setOpenMobile, open, openMobile } = useSidebar();
12
+ const { isMobile, openMobile, setOpenMobile, state, toggleSidebar } =
13
+ useSidebar();
12
14
  const { refreshChatID } = useChatId();
13
- const isExpanded = open || openMobile;
15
+ const isExpanded = isMobile ? openMobile : state === "expanded";
14
16
 
15
17
  return (
16
- <div
17
- className={`flex w-full items-center ${
18
- isExpanded ? "justify-between gap-2" : "justify-start"
19
- }`}
20
- >
18
+ <div className="flex w-full items-center justify-between gap-2">
21
19
  {isExpanded ? (
22
20
  <Link
23
21
  className="flex flex-row items-center gap-2"
@@ -30,16 +28,33 @@ export function SidebarTopRow() {
30
28
  <span className="flex cursor-pointer items-center gap-2 rounded-md p-1 font-semibold text-lg hover:bg-muted">
31
29
  <Image
32
30
  alt={config.appName}
33
- className="h-6 w-6"
34
- height={24}
31
+ className="h-5 w-5"
32
+ height={20}
35
33
  src="/icon.svg"
36
- width={24}
34
+ width={20}
37
35
  />
38
36
  {config.appName}
39
37
  </span>
40
38
  </Link>
41
- ) : null}
42
- <SidebarToggle className="md:h-fit md:px-2" />
39
+ ) : (
40
+ <button
41
+ aria-label="Expand sidebar"
42
+ className="group/logo relative flex size-8 items-center justify-center rounded-md hover:bg-muted"
43
+ onClick={toggleSidebar}
44
+ type="button"
45
+ >
46
+ <Image
47
+ alt={config.appName}
48
+ className="h-5 w-5 transition-opacity duration-150 group-hover/logo:opacity-0"
49
+ height={20}
50
+ src="/icon.svg"
51
+ width={20}
52
+ />
53
+ <PanelLeft className="absolute size-4 opacity-0 transition-opacity duration-150 group-hover/logo:opacity-100" />
54
+ </button>
55
+ )}
56
+
57
+ {isExpanded && <SidebarToggle className="md:h-fit md:px-2" />}
43
58
  </div>
44
59
  );
45
60
  }
@@ -30,6 +30,7 @@ import {
30
30
  } from "@/components/ui/sidebar";
31
31
  import { useGetCredits } from "@/hooks/chat-sync-hooks";
32
32
  import authClient from "@/lib/auth-client";
33
+ import { isElectronRenderer } from "@/lib/electron-auth";
33
34
  import { cn } from "@/lib/utils";
34
35
  import { useSession } from "@/providers/session-provider";
35
36
 
@@ -149,7 +150,15 @@ export function SidebarUserNav() {
149
150
  <DropdownMenuSeparator />
150
151
  <DropdownMenuItem
151
152
  onClick={async () => {
152
- await authClient.signOut();
153
+ if (
154
+ isElectronRenderer() &&
155
+ typeof window.signOut === "function"
156
+ ) {
157
+ await window.signOut();
158
+ await window.electronAPI?.syncAuthSession?.();
159
+ } else {
160
+ await authClient.signOut();
161
+ }
153
162
  window.location.href = "/";
154
163
  }}
155
164
  >