@lobehub/lobehub 2.0.0-next.327 → 2.0.0-next.329

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 (83) hide show
  1. package/.env.example +0 -3
  2. package/.env.example.development +0 -3
  3. package/CHANGELOG.md +58 -0
  4. package/Dockerfile +1 -2
  5. package/changelog/v1.json +18 -0
  6. package/docs/self-hosting/advanced/auth.mdx +5 -6
  7. package/docs/self-hosting/advanced/auth.zh-CN.mdx +5 -6
  8. package/docs/self-hosting/environment-variables/auth.mdx +0 -7
  9. package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +0 -7
  10. package/locales/en-US/chat.json +6 -1
  11. package/locales/en-US/discover.json +1 -0
  12. package/locales/zh-CN/chat.json +5 -0
  13. package/locales/zh-CN/discover.json +1 -0
  14. package/package.json +1 -1
  15. package/packages/agent-runtime/src/agents/GeneralChatAgent.ts +24 -0
  16. package/packages/agent-runtime/src/agents/__tests__/GeneralChatAgent.test.ts +210 -0
  17. package/packages/agent-runtime/src/types/instruction.ts +46 -2
  18. package/packages/builtin-tool-gtd/src/const.ts +1 -0
  19. package/packages/builtin-tool-gtd/src/executor/index.ts +38 -21
  20. package/packages/builtin-tool-gtd/src/manifest.ts +15 -0
  21. package/packages/builtin-tool-gtd/src/systemRole.ts +33 -1
  22. package/packages/builtin-tool-gtd/src/types.ts +55 -33
  23. package/packages/builtin-tool-local-system/src/client/Inspector/ReadLocalFile/index.tsx +1 -0
  24. package/packages/builtin-tool-local-system/src/client/Inspector/RunCommand/index.tsx +1 -1
  25. package/packages/builtin-tool-local-system/src/client/Render/WriteFile/index.tsx +1 -1
  26. package/packages/builtin-tool-local-system/src/client/Streaming/WriteFile/index.tsx +5 -1
  27. package/packages/builtin-tool-notebook/src/systemRole.ts +27 -7
  28. package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +13 -1
  29. package/packages/conversation-flow/src/transformation/__tests__/FlatListBuilder.test.ts +40 -0
  30. package/packages/database/src/models/__tests__/messages/message.thread-query.test.ts +134 -1
  31. package/packages/database/src/models/message.ts +8 -1
  32. package/packages/database/src/models/thread.ts +1 -1
  33. package/packages/types/src/message/ui/chat.ts +2 -0
  34. package/packages/types/src/topic/thread.ts +20 -0
  35. package/scripts/prebuild.mts +2 -2
  36. package/src/app/[variants]/(main)/community/(list)/agent/features/List/Item.tsx +1 -0
  37. package/src/components/StreamingMarkdown/index.tsx +10 -43
  38. package/src/envs/__tests__/app.test.ts +81 -0
  39. package/src/envs/app.ts +14 -2
  40. package/src/envs/auth.test.ts +0 -13
  41. package/src/envs/auth.ts +0 -41
  42. package/src/features/Conversation/Messages/AssistantGroup/components/MessageContent.tsx +0 -2
  43. package/src/features/Conversation/Messages/Task/ClientTaskDetail/CompletedState.tsx +108 -0
  44. package/src/features/Conversation/Messages/Task/ClientTaskDetail/InitializingState.tsx +66 -0
  45. package/src/features/Conversation/Messages/Task/ClientTaskDetail/InstructionAccordion.tsx +63 -0
  46. package/src/features/Conversation/Messages/Task/ClientTaskDetail/ProcessingState.tsx +123 -0
  47. package/src/features/Conversation/Messages/Task/ClientTaskDetail/index.tsx +106 -0
  48. package/src/features/Conversation/Messages/Task/TaskDetailPanel/index.tsx +1 -0
  49. package/src/features/Conversation/Messages/Task/index.tsx +11 -6
  50. package/src/features/Conversation/Messages/Tasks/TaskItem/TaskTitle.tsx +3 -2
  51. package/src/features/Conversation/Messages/Tasks/shared/InitializingState.tsx +0 -4
  52. package/src/features/Conversation/Messages/Tasks/shared/utils.ts +22 -1
  53. package/src/features/Conversation/Messages/components/ContentLoading.tsx +1 -1
  54. package/src/features/Conversation/components/Thinking/index.tsx +9 -30
  55. package/src/features/Conversation/store/slices/data/action.ts +2 -3
  56. package/src/features/NavPanel/components/BackButton.tsx +10 -13
  57. package/src/features/NavPanel/components/NavPanelDraggable.tsx +4 -0
  58. package/src/hooks/useAutoScroll.ts +117 -0
  59. package/src/libs/better-auth/auth-client.ts +0 -9
  60. package/src/libs/better-auth/define-config.ts +13 -12
  61. package/src/libs/better-auth/sso/index.ts +2 -1
  62. package/src/libs/better-auth/utils/config.ts +2 -2
  63. package/src/libs/next/proxy/define-config.ts +4 -6
  64. package/src/locales/default/chat.ts +6 -1
  65. package/src/locales/default/discover.ts +2 -0
  66. package/src/server/routers/lambda/__tests__/integration/topic.integration.test.ts +74 -0
  67. package/src/server/routers/lambda/aiAgent.ts +239 -1
  68. package/src/server/routers/lambda/thread.ts +2 -0
  69. package/src/server/routers/lambda/topic.ts +6 -0
  70. package/src/server/services/agentRuntime/AgentRuntimeService.test.ts +4 -1
  71. package/src/server/services/agentRuntime/AgentRuntimeService.ts +2 -1
  72. package/src/server/services/message/__tests__/index.test.ts +37 -0
  73. package/src/server/services/message/index.ts +6 -1
  74. package/src/services/aiAgent.ts +51 -0
  75. package/src/services/topic/index.ts +4 -0
  76. package/src/store/chat/agents/createAgentExecutors.ts +714 -12
  77. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +6 -1
  78. package/src/store/chat/slices/message/actions/query.ts +33 -1
  79. package/src/store/chat/slices/message/selectors/displayMessage.test.ts +10 -0
  80. package/src/store/chat/slices/message/selectors/displayMessage.ts +1 -0
  81. package/src/store/chat/slices/operation/types.ts +4 -0
  82. package/src/store/chat/slices/topic/action.test.ts +2 -1
  83. package/src/store/chat/slices/topic/action.ts +1 -1
@@ -0,0 +1,123 @@
1
+ 'use client';
2
+
3
+ import { type AssistantContentBlock } from '@lobechat/types';
4
+ import { Block, Flexbox, ScrollShadow, Text } from '@lobehub/ui';
5
+ import { createStaticStyles } from 'antd-style';
6
+ import { type RefObject, memo, useEffect, useMemo, useState } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+
9
+ import NeuralNetworkLoading from '@/components/NeuralNetworkLoading';
10
+ import AnimatedNumber from '@/features/Conversation/Messages/components/Extras/Usage/UsageDetail/AnimatedNumber';
11
+ import { useAutoScroll } from '@/hooks/useAutoScroll';
12
+
13
+ import ContentBlock from '../../AssistantGroup/components/ContentBlock';
14
+ import { accumulateUsage, formatElapsedTime } from '../../Tasks/shared/utils';
15
+ import Usage from '../../components/Extras/Usage';
16
+
17
+ const styles = createStaticStyles(({ css }) => ({
18
+ contentScroll: css`
19
+ max-height: min(50vh, 300px);
20
+ `,
21
+ }));
22
+
23
+ interface ProcessingStateProps {
24
+ assistantId: string;
25
+ blocks: AssistantContentBlock[];
26
+ model?: string;
27
+ provider?: string;
28
+ startTime?: number;
29
+ }
30
+
31
+ const ProcessingState = memo<ProcessingStateProps>(
32
+ ({ blocks, assistantId, startTime, model, provider }) => {
33
+ const { t } = useTranslation('chat');
34
+ const [elapsedTime, setElapsedTime] = useState(0);
35
+ const { ref, handleScroll } = useAutoScroll<HTMLDivElement>({
36
+ deps: [blocks],
37
+ enabled: true,
38
+ });
39
+
40
+ const totalToolCalls = useMemo(
41
+ () => blocks.reduce((sum, block) => sum + (block.tools?.length || 0), 0),
42
+ [blocks],
43
+ );
44
+
45
+ // Accumulate usage from all blocks
46
+ const accumulatedUsage = useMemo(() => accumulateUsage(blocks), [blocks]);
47
+
48
+ // Calculate initial elapsed time
49
+ useEffect(() => {
50
+ if (startTime) {
51
+ setElapsedTime(Math.max(0, Date.now() - startTime));
52
+ }
53
+ }, [startTime]);
54
+
55
+ // Timer for updating elapsed time every second
56
+ useEffect(() => {
57
+ if (!startTime) return;
58
+
59
+ const timer = setInterval(() => {
60
+ setElapsedTime(Math.max(0, Date.now() - startTime));
61
+ }, 1000);
62
+
63
+ return () => clearInterval(timer);
64
+ }, [startTime]);
65
+
66
+ return (
67
+ <Flexbox gap={8}>
68
+ <Flexbox align="center" gap={8} horizontal paddingInline={4}>
69
+ <Block
70
+ align="center"
71
+ flex="none"
72
+ gap={4}
73
+ height={24}
74
+ horizontal
75
+ justify="center"
76
+ style={{ fontSize: 12 }}
77
+ variant="outlined"
78
+ width={24}
79
+ >
80
+ <NeuralNetworkLoading size={16} />
81
+ </Block>
82
+ <Flexbox align="center" gap={4} horizontal>
83
+ <Text as="span" type="secondary" weight={500}>
84
+ <AnimatedNumber
85
+ duration={500}
86
+ formatter={(v) => Math.round(v).toString()}
87
+ value={totalToolCalls}
88
+ />
89
+ </Text>
90
+ <Text as="span" type="secondary">
91
+ {t('task.metrics.toolCallsShort')}
92
+ </Text>
93
+ {startTime && (
94
+ <Text as="span" type="secondary">
95
+ ({formatElapsedTime(elapsedTime)})
96
+ </Text>
97
+ )}
98
+ </Flexbox>
99
+ </Flexbox>
100
+ <ScrollShadow
101
+ className={styles.contentScroll}
102
+ offset={12}
103
+ onScroll={handleScroll}
104
+ ref={ref as RefObject<HTMLDivElement>}
105
+ size={8}
106
+ >
107
+ <Flexbox gap={8}>
108
+ {blocks.map((block) => (
109
+ <ContentBlock {...block} assistantId={assistantId} disableEditing key={block.id} />
110
+ ))}
111
+ </Flexbox>
112
+ </ScrollShadow>
113
+
114
+ {/* Usage display */}
115
+ {model && provider && <Usage model={model} provider={provider} usage={accumulatedUsage} />}
116
+ </Flexbox>
117
+ );
118
+ },
119
+ );
120
+
121
+ ProcessingState.displayName = 'ClientProcessingState';
122
+
123
+ export default ProcessingState;
@@ -0,0 +1,106 @@
1
+ 'use client';
2
+
3
+ import { type TaskDetail, ThreadStatus } from '@lobechat/types';
4
+ import { Flexbox } from '@lobehub/ui';
5
+ import { memo, useMemo } from 'react';
6
+
7
+ import BubblesLoading from '@/components/BubblesLoading';
8
+ import { useChatStore } from '@/store/chat';
9
+ import { displayMessageSelectors } from '@/store/chat/selectors';
10
+ import { messageMapKey } from '@/store/chat/utils/messageMapKey';
11
+
12
+ import CompletedState from './CompletedState';
13
+ import InitializingState from './InitializingState';
14
+ import InstructionAccordion from './InstructionAccordion';
15
+ import ProcessingState from './ProcessingState';
16
+
17
+ interface ClientTaskDetailProps {
18
+ content?: string;
19
+ messageId: string;
20
+ taskDetail?: TaskDetail;
21
+ }
22
+
23
+ const ClientTaskDetail = memo<ClientTaskDetailProps>(({ taskDetail }) => {
24
+ const threadId = taskDetail?.threadId;
25
+ const isExecuting = taskDetail?.status === ThreadStatus.Processing;
26
+
27
+ const [activeAgentId, activeTopicId, useFetchMessages] = useChatStore((s) => [
28
+ s.activeAgentId,
29
+ s.activeTopicId,
30
+ s.useFetchMessages,
31
+ ]);
32
+
33
+ const threadContext = useMemo(
34
+ () => ({
35
+ agentId: activeAgentId,
36
+ scope: 'thread' as const,
37
+ threadId,
38
+ topicId: activeTopicId,
39
+ }),
40
+ [activeAgentId, activeTopicId, threadId],
41
+ );
42
+
43
+ const threadMessageKey = useMemo(
44
+ () => (threadId ? messageMapKey(threadContext) : null),
45
+ [threadId],
46
+ );
47
+
48
+ // Fetch thread messages (skip when executing - messages come from real-time updates)
49
+ useFetchMessages(threadContext, isExecuting);
50
+
51
+ // Get thread messages from store using selector
52
+ const threadMessages = useChatStore((s) =>
53
+ threadMessageKey
54
+ ? displayMessageSelectors.getDisplayMessagesByKey(threadMessageKey)(s)
55
+ : undefined,
56
+ );
57
+
58
+ if (!threadMessages) return <BubblesLoading />;
59
+
60
+ // Find the assistantGroup message which contains the children blocks
61
+ const assistantGroupMessage = threadMessages.find((item) => item.role === 'assistantGroup');
62
+ const instruction = threadMessages.find((item) => item.role === 'user')?.content;
63
+ const childrenCount = assistantGroupMessage?.children?.length ?? 0;
64
+
65
+ // Get model/provider from assistantGroup message
66
+ const model = assistantGroupMessage?.model;
67
+ const provider = assistantGroupMessage?.provider;
68
+
69
+ // Initializing state: no status yet (task just created, waiting for client execution)
70
+ if (threadMessages.length === 0 || !assistantGroupMessage?.children || childrenCount === 0) {
71
+ return <InitializingState />;
72
+ }
73
+
74
+ return (
75
+ <Flexbox gap={4}>
76
+ {instruction && (
77
+ <InstructionAccordion childrenCount={childrenCount} instruction={instruction} />
78
+ )}
79
+
80
+ {isExecuting ? (
81
+ <ProcessingState
82
+ assistantId={assistantGroupMessage.id}
83
+ blocks={assistantGroupMessage.children}
84
+ model={model ?? undefined}
85
+ provider={provider ?? undefined}
86
+ startTime={assistantGroupMessage.createdAt}
87
+ />
88
+ ) : (
89
+ <CompletedState
90
+ assistantId={assistantGroupMessage.id}
91
+ blocks={assistantGroupMessage.children}
92
+ duration={taskDetail?.duration}
93
+ model={model ?? undefined}
94
+ provider={provider ?? undefined}
95
+ totalCost={taskDetail?.totalCost}
96
+ totalTokens={taskDetail?.totalTokens}
97
+ totalToolCalls={taskDetail?.totalToolCalls}
98
+ />
99
+ )}
100
+ </Flexbox>
101
+ );
102
+ });
103
+
104
+ ClientTaskDetail.displayName = 'ClientClientTaskDetail';
105
+
106
+ export default ClientTaskDetail;
@@ -17,6 +17,7 @@ interface TaskDetailPanelProps {
17
17
  }
18
18
 
19
19
  const TaskDetailPanel = memo<TaskDetailPanelProps>(({ taskDetail, content, messageId }) => {
20
+ // Default: server-side task execution
20
21
  return <StatusContent content={content} messageId={messageId} taskDetail={taskDetail} />;
21
22
  });
22
23
 
@@ -19,6 +19,7 @@ import { useAgentMeta, useDoubleClickEdit } from '../../hooks';
19
19
  import { dataSelectors, messageStateSelectors, useConversationStore } from '../../store';
20
20
  import { normalizeThinkTags, processWithArtifact } from '../../utils/markdown';
21
21
  import { AssistantActionsBar } from './Actions';
22
+ import ClientTaskDetail from './ClientTaskDetail';
22
23
  import TaskDetailPanel from './TaskDetailPanel';
23
24
 
24
25
  interface TaskMessageProps {
@@ -91,12 +92,16 @@ const TaskMessage = memo<TaskMessageProps>(({ id, index, disableEditing, isLates
91
92
  time={createdAt}
92
93
  titleAddon={<Tag>{t('task.subtask')}</Tag>}
93
94
  >
94
- <TaskDetailPanel
95
- content={content}
96
- instruction={metadata?.instruction}
97
- messageId={id}
98
- taskDetail={taskDetail}
99
- />
95
+ {taskDetail?.clientMode ? (
96
+ <ClientTaskDetail messageId={id} taskDetail={taskDetail} />
97
+ ) : (
98
+ <TaskDetailPanel
99
+ content={content}
100
+ instruction={metadata?.instruction}
101
+ messageId={id}
102
+ taskDetail={taskDetail}
103
+ />
104
+ )}
100
105
  </ChatItem>
101
106
  );
102
107
  }, isEqual);
@@ -2,9 +2,10 @@
2
2
 
3
3
  import { Block, Flexbox, Icon, Text } from '@lobehub/ui';
4
4
  import { cssVar } from 'antd-style';
5
- import { ListChecksIcon, Loader2, XIcon } from 'lucide-react';
5
+ import { ListChecksIcon, XIcon } from 'lucide-react';
6
6
  import { memo } from 'react';
7
7
 
8
+ import NeuralNetworkLoading from '@/components/NeuralNetworkLoading';
8
9
  import { ThreadStatus } from '@/types/index';
9
10
 
10
11
  import { isProcessingStatus } from '../shared';
@@ -27,7 +28,7 @@ const TaskStatusIndicator = memo<{ status?: ThreadStatus }>(({ status }) => {
27
28
  } else if (isError) {
28
29
  icon = <Icon color={cssVar.colorError} icon={XIcon} />;
29
30
  } else if (isProcessing || isInitializing) {
30
- icon = <Icon color={cssVar.colorTextDescription} icon={Loader2} spin />;
31
+ icon = <NeuralNetworkLoading size={16} />;
31
32
  } else {
32
33
  return null;
33
34
  }
@@ -57,10 +57,6 @@ const InitializingState = memo(() => {
57
57
  {t('task.status.initializing')}
58
58
  </Text>
59
59
  </Flexbox>
60
-
61
- <div className={styles.progress}>
62
- <div className={styles.progressShimmer} />
63
- </div>
64
60
  </Flexbox>
65
61
  );
66
62
  });
@@ -1,4 +1,9 @@
1
- import { type TaskCurrentActivity, ThreadStatus } from '@/types/index';
1
+ import {
2
+ type AssistantContentBlock,
3
+ type ModelUsage,
4
+ type TaskCurrentActivity,
5
+ ThreadStatus,
6
+ } from '@lobechat/types';
2
7
 
3
8
  /**
4
9
  * Format duration in milliseconds to human-readable string
@@ -68,3 +73,19 @@ export const isProcessingStatus = (status?: ThreadStatus): boolean => {
68
73
  status === ThreadStatus.Todo
69
74
  );
70
75
  };
76
+
77
+ /**
78
+ * Accumulate usage from all blocks
79
+ */
80
+ export const accumulateUsage = (blocks: AssistantContentBlock[]): ModelUsage => {
81
+ return blocks.reduce((acc, block) => {
82
+ const usage = block.usage;
83
+ if (!usage) return acc;
84
+ return {
85
+ cost: (acc.cost || 0) + (usage.cost || 0),
86
+ totalInputTokens: (acc.totalInputTokens || 0) + (usage.totalInputTokens || 0),
87
+ totalOutputTokens: (acc.totalOutputTokens || 0) + (usage.totalOutputTokens || 0),
88
+ totalTokens: (acc.totalTokens || 0) + (usage.totalTokens || 0),
89
+ };
90
+ }, {} as ModelUsage);
91
+ };
@@ -18,7 +18,7 @@ interface ContentLoadingProps {
18
18
  const ContentLoading = memo<ContentLoadingProps>(({ id }) => {
19
19
  const { t } = useTranslation('chat');
20
20
  const runningOp = useChatStore(operationSelectors.getDeepestRunningOperationByMessage(id));
21
- console.log('runningOp', runningOp);
21
+
22
22
  const [elapsedSeconds, setElapsedSeconds] = useState(0);
23
23
  const [startTime, setStartTime] = useState(runningOp?.metadata?.startTime);
24
24
 
@@ -6,11 +6,11 @@ import {
6
6
  type RefObject,
7
7
  memo,
8
8
  useEffect,
9
- useRef,
10
9
  useState,
11
10
  } from 'react';
12
11
 
13
12
  import MarkdownMessage from '@/features/Conversation/Markdown';
13
+ import { useAutoScroll } from '@/hooks/useAutoScroll';
14
14
  import { type ChatCitationItem } from '@/types/index';
15
15
 
16
16
  import Title from './Title';
@@ -40,39 +40,17 @@ interface ThinkingProps {
40
40
  const Thinking = memo<ThinkingProps>((props) => {
41
41
  const { content, duration, thinking, citations, thinkingAnimated } = props;
42
42
  const [showDetail, setShowDetail] = useState(false);
43
- const contentRef = useRef<HTMLDivElement | null>(null);
43
+
44
+ const { ref, handleScroll } = useAutoScroll<HTMLDivElement>({
45
+ deps: [content, showDetail],
46
+ enabled: thinking && showDetail,
47
+ threshold: 120,
48
+ });
44
49
 
45
50
  useEffect(() => {
46
51
  setShowDetail(!!thinking);
47
52
  }, [thinking]);
48
53
 
49
- // 当内容变更且正在思考时,如果用户接近底部则自动滚动到底部
50
- useEffect(() => {
51
- if (!thinking || !showDetail) return;
52
- const container = contentRef.current;
53
- if (!container) return;
54
-
55
- // 仅当用户接近底部时才自动滚动,避免打断用户查看上方内容
56
- const distanceToBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
57
- const isNearBottom = distanceToBottom < 120;
58
-
59
- if (isNearBottom) {
60
- requestAnimationFrame(() => {
61
- container.scrollTop = container.scrollHeight;
62
- });
63
- }
64
- }, [content, thinking, showDetail]);
65
-
66
- // 展开时滚动到底部,便于查看最新内容
67
- useEffect(() => {
68
- if (!showDetail) return;
69
- const container = contentRef.current;
70
- if (!container) return;
71
- requestAnimationFrame(() => {
72
- container.scrollTop = container.scrollHeight;
73
- });
74
- }, [showDetail]);
75
-
76
54
  return (
77
55
  <Accordion
78
56
  expandedKeys={showDetail ? ['thinking'] : []}
@@ -88,7 +66,8 @@ const Thinking = memo<ThinkingProps>((props) => {
88
66
  <ScrollShadow
89
67
  className={styles.contentScroll}
90
68
  offset={12}
91
- ref={contentRef as unknown as RefObject<HTMLDivElement>}
69
+ onScroll={handleScroll}
70
+ ref={ref as RefObject<HTMLDivElement>}
92
71
  size={12}
93
72
  >
94
73
  {typeof content === 'string' ? (
@@ -106,12 +106,11 @@ export const dataSlice: StateCreator<
106
106
  // Also skip fetch when topicId is null (new conversation state) - there's no server data,
107
107
  // only local optimistic updates. Fetching would return empty array and overwrite local data.
108
108
  const shouldFetch = !skipFetch && !!context.agentId && !!context.topicId;
109
+
109
110
  return useClientDataSWRWithSync<UIChatMessage[]>(
110
111
  shouldFetch ? ['CONVERSATION_FETCH_MESSAGES', context] : null,
111
112
 
112
- async () => {
113
- return messageService.getMessages(context);
114
- },
113
+ () => messageService.getMessages(context),
115
114
  {
116
115
  onData: (data) => {
117
116
  if (!data) return;
@@ -1,26 +1,23 @@
1
1
  import { ActionIcon, type ActionIconProps } from '@lobehub/ui';
2
2
  import { ChevronLeftIcon } from 'lucide-react';
3
3
  import { memo } from 'react';
4
- import { useNavigate } from 'react-router-dom';
4
+ import { Link } from 'react-router-dom';
5
5
 
6
6
  import { DESKTOP_HEADER_ICON_SIZE } from '@/const/layoutTokens';
7
7
 
8
8
  export const BACK_BUTTON_ID = 'lobe-back-button';
9
9
 
10
10
  const BackButton = memo<ActionIconProps & { to?: string }>(({ to = '/', onClick, ...rest }) => {
11
- const navigate = useNavigate();
12
-
13
11
  return (
14
- <ActionIcon
15
- icon={ChevronLeftIcon}
16
- id={BACK_BUTTON_ID}
17
- onClick={(e) => {
18
- navigate(to);
19
- onClick?.(e);
20
- }}
21
- size={DESKTOP_HEADER_ICON_SIZE}
22
- {...rest}
23
- />
12
+ // @ts-expect-error
13
+ <Link onClick={onClick} to={to}>
14
+ <ActionIcon
15
+ icon={ChevronLeftIcon}
16
+ id={BACK_BUTTON_ID}
17
+ size={DESKTOP_HEADER_ICON_SIZE}
18
+ {...rest}
19
+ />
20
+ </Link>
24
21
  );
25
22
  });
26
23
 
@@ -13,6 +13,7 @@ import { systemStatusSelectors } from '@/store/global/selectors';
13
13
  import { isMacOS } from '@/utils/platform';
14
14
 
15
15
  import { useNavPanelSizeChangeHandler } from '../hooks/useNavPanel';
16
+ import { BACK_BUTTON_ID } from './BackButton';
16
17
 
17
18
  const motionVariants = {
18
19
  animate: { opacity: 1, x: 0 },
@@ -76,6 +77,9 @@ const draggableStyles = createStaticStyles(({ css, cssVar }) => ({
76
77
  opacity,
77
78
  width 0.2s ${cssVar.motionEaseOut};
78
79
  }
80
+ #${BACK_BUTTON_ID} {
81
+ width: 24px !important;
82
+ }
79
83
 
80
84
  &:hover {
81
85
  #${TOGGLE_BUTTON_ID} {
@@ -0,0 +1,117 @@
1
+ import { type RefObject, useCallback, useEffect, useRef, useState } from 'react';
2
+
3
+ interface UseAutoScrollOptions {
4
+ /**
5
+ * Dependencies that trigger auto-scroll when changed
6
+ */
7
+ deps?: unknown[];
8
+ /**
9
+ * Whether auto-scroll is enabled (e.g., only when streaming/executing)
10
+ * @default true
11
+ */
12
+ enabled?: boolean;
13
+ /**
14
+ * Distance threshold from bottom to consider "near bottom" (in pixels)
15
+ * @default 20
16
+ */
17
+ threshold?: number;
18
+ }
19
+
20
+ interface UseAutoScrollReturn<T extends HTMLElement> {
21
+ /**
22
+ * Callback to handle scroll events, attach to onScroll
23
+ */
24
+ handleScroll: () => void;
25
+ /**
26
+ * Ref to attach to the scrollable container
27
+ */
28
+ ref: RefObject<T | null>;
29
+ /**
30
+ * Reset the scroll lock state (e.g., when new content starts)
31
+ */
32
+ resetScrollLock: () => void;
33
+ /**
34
+ * Whether user has scrolled away from bottom (scroll lock active)
35
+ */
36
+ userHasScrolled: boolean;
37
+ }
38
+
39
+ /**
40
+ * Hook for auto-scrolling content with user scroll detection
41
+ *
42
+ * Features:
43
+ * - Auto-scrolls to bottom when dependencies change
44
+ * - Detects when user scrolls away from bottom and stops auto-scrolling
45
+ * - Provides reset function for when new content starts
46
+ * - Ignores scroll events triggered by auto-scroll itself
47
+ *
48
+ * @example
49
+ * ```tsx
50
+ * const { ref, handleScroll } = useAutoScroll<HTMLDivElement>({
51
+ * deps: [content],
52
+ * enabled: isStreaming,
53
+ * });
54
+ *
55
+ * return (
56
+ * <ScrollShadow ref={ref} onScroll={handleScroll}>
57
+ * {content}
58
+ * </ScrollShadow>
59
+ * );
60
+ * ```
61
+ */
62
+ export function useAutoScroll<T extends HTMLElement = HTMLDivElement>(
63
+ options: UseAutoScrollOptions = {},
64
+ ): UseAutoScrollReturn<T> {
65
+ const { deps = [], enabled = true, threshold = 20 } = options;
66
+
67
+ const ref = useRef<T | null>(null);
68
+ const [userHasScrolled, setUserHasScrolled] = useState(false);
69
+ const isAutoScrollingRef = useRef(false);
70
+
71
+ // Handle user scroll detection
72
+ const handleScroll = useCallback(() => {
73
+ // Ignore scroll events triggered by auto-scroll
74
+ if (isAutoScrollingRef.current) return;
75
+
76
+ const container = ref.current;
77
+ if (!container) return;
78
+
79
+ // Check if user scrolled away from bottom
80
+ const distanceToBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
81
+ const isAtBottom = distanceToBottom < threshold;
82
+
83
+ // If user scrolled up, stop auto-scrolling
84
+ if (!isAtBottom) {
85
+ setUserHasScrolled(true);
86
+ }
87
+ }, [threshold]);
88
+
89
+ // Reset scroll lock state
90
+ const resetScrollLock = useCallback(() => {
91
+ setUserHasScrolled(false);
92
+ }, []);
93
+
94
+ // Auto scroll to bottom when deps change (unless user has scrolled or disabled)
95
+ useEffect(() => {
96
+ if (!enabled || userHasScrolled) return;
97
+
98
+ const container = ref.current;
99
+ if (!container) return;
100
+
101
+ isAutoScrollingRef.current = true;
102
+ requestAnimationFrame(() => {
103
+ container.scrollTop = container.scrollHeight;
104
+ // Reset the flag after scroll completes
105
+ requestAnimationFrame(() => {
106
+ isAutoScrollingRef.current = false;
107
+ });
108
+ });
109
+ }, [enabled, userHasScrolled, ...deps]);
110
+
111
+ return {
112
+ handleScroll,
113
+ ref,
114
+ resetScrollLock,
115
+ userHasScrolled,
116
+ };
117
+ }
@@ -7,9 +7,6 @@ import {
7
7
  import { createAuthClient } from 'better-auth/react';
8
8
 
9
9
  import type { auth } from '@/auth';
10
- import { getAuthConfig } from '@/envs/auth';
11
-
12
- const { NEXT_PUBLIC_AUTH_URL } = getAuthConfig();
13
10
 
14
11
  export const {
15
12
  linkSocial,
@@ -24,12 +21,6 @@ export const {
24
21
  unlinkAccount,
25
22
  useSession,
26
23
  } = createAuthClient({
27
- /** The base URL of the server (optional if you're using the same domain) */
28
- ...(NEXT_PUBLIC_AUTH_URL
29
- ? {
30
- baseURL: NEXT_PUBLIC_AUTH_URL,
31
- }
32
- : {}),
33
24
  plugins: [
34
25
  adminClient(),
35
26
  inferAdditionalFields<typeof auth>(),
@@ -14,6 +14,7 @@ import { admin, emailOTP, genericOAuth, magicLink } from 'better-auth/plugins';
14
14
  import { type BetterAuthPlugin } from 'better-auth/types';
15
15
 
16
16
  import { businessEmailValidator } from '@/business/server/better-auth';
17
+ import { appEnv } from '@/envs/app';
17
18
  import { authEnv } from '@/envs/auth';
18
19
  import {
19
20
  getMagicLinkEmailTemplate,
@@ -32,13 +33,13 @@ import { UserService } from '@/server/services/user';
32
33
  const VERIFICATION_LINK_EXPIRES_IN = 3600;
33
34
 
34
35
  /**
35
- * Safely extract hostname from AUTH_URL for passkey rpID.
36
- * Returns undefined if AUTH_URL is not set (e.g., in e2e tests).
36
+ * Safely extract hostname from APP_URL for passkey rpID.
37
+ * Returns undefined if APP_URL is not set (e.g., in e2e tests).
37
38
  */
38
39
  const getPasskeyRpID = (): string | undefined => {
39
- if (!authEnv.NEXT_PUBLIC_AUTH_URL) return undefined;
40
+ if (!appEnv.APP_URL) return undefined;
40
41
  try {
41
- return new URL(authEnv.NEXT_PUBLIC_AUTH_URL).hostname;
42
+ return new URL(appEnv.APP_URL).hostname;
42
43
  } catch {
43
44
  return undefined;
44
45
  }
@@ -46,14 +47,15 @@ const getPasskeyRpID = (): string | undefined => {
46
47
 
47
48
  /**
48
49
  * Get passkey origins array.
49
- * Returns undefined if AUTH_URL is not set (e.g., in e2e tests).
50
+ * Returns undefined if APP_URL is not set (e.g., in e2e tests).
50
51
  */
51
52
  const getPasskeyOrigins = (): string[] | undefined => {
52
- if (!authEnv.NEXT_PUBLIC_AUTH_URL) return undefined;
53
- return [
54
- // Web origin
55
- authEnv.NEXT_PUBLIC_AUTH_URL,
56
- ];
53
+ if (!appEnv.APP_URL) return undefined;
54
+ try {
55
+ return [new URL(appEnv.APP_URL).origin];
56
+ } catch {
57
+ return undefined;
58
+ }
57
59
  };
58
60
  const MAGIC_LINK_EXPIRES_IN = 900;
59
61
  // OTP expiration time (in seconds) - 5 minutes for mobile OTP verification
@@ -81,8 +83,7 @@ export function defineConfig(customOptions: CustomBetterAuthOptions) {
81
83
  },
82
84
  },
83
85
 
84
- // Use renamed env vars (fallback to next-auth vars is handled in src/envs/auth.ts)
85
- baseURL: authEnv.NEXT_PUBLIC_AUTH_URL,
86
+ baseURL: appEnv.APP_URL,
86
87
  secret: authEnv.AUTH_SECRET,
87
88
  trustedOrigins: getTrustedOrigins(enabledSSOProviders),
88
89