@lobehub/lobehub 2.0.0-next.267 → 2.0.0-next.269

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 (135) hide show
  1. package/.cursor/rules/microcopy-cn.mdc +75 -63
  2. package/.cursor/rules/microcopy-en.mdc +4 -8
  3. package/CHANGELOG.md +50 -0
  4. package/README.md +8 -8
  5. package/README.zh-CN.md +8 -8
  6. package/apps/desktop/src/main/core/browser/Browser.ts +6 -0
  7. package/apps/desktop/src/main/locales/default/common.ts +2 -2
  8. package/changelog/v1.json +10 -0
  9. package/docs/development/database-schema.dbml +4 -0
  10. package/e2e/CLAUDE.md +9 -8
  11. package/e2e/cucumber.config.js +1 -0
  12. package/e2e/src/features/page/README.md +118 -0
  13. package/e2e/src/features/page/crud.feature +62 -0
  14. package/e2e/src/features/page/editor-content.feature +93 -0
  15. package/e2e/src/features/page/editor-meta.feature +60 -0
  16. package/e2e/src/steps/agent/conversation.steps.ts +4 -4
  17. package/e2e/src/steps/home/sidebarAgent.steps.ts +91 -94
  18. package/e2e/src/steps/home/sidebarGroup.steps.ts +4 -4
  19. package/e2e/src/steps/hooks.ts +2 -0
  20. package/e2e/src/steps/page/editor-content.steps.ts +344 -0
  21. package/e2e/src/steps/page/editor-meta.steps.ts +410 -0
  22. package/e2e/src/steps/page/page-crud.steps.ts +363 -0
  23. package/e2e/src/support/world.ts +12 -0
  24. package/locales/ar/file.json +2 -0
  25. package/locales/bg-BG/file.json +2 -0
  26. package/locales/de-DE/file.json +2 -0
  27. package/locales/en-US/auth.json +1 -1
  28. package/locales/en-US/file.json +2 -0
  29. package/locales/en-US/metadata.json +2 -2
  30. package/locales/es-ES/file.json +2 -0
  31. package/locales/fa-IR/file.json +2 -0
  32. package/locales/fr-FR/file.json +2 -0
  33. package/locales/it-IT/file.json +2 -0
  34. package/locales/ja-JP/file.json +2 -0
  35. package/locales/ko-KR/file.json +2 -0
  36. package/locales/nl-NL/file.json +2 -0
  37. package/locales/pl-PL/file.json +2 -0
  38. package/locales/pt-BR/file.json +2 -0
  39. package/locales/ru-RU/file.json +2 -0
  40. package/locales/tr-TR/file.json +2 -0
  41. package/locales/vi-VN/file.json +2 -0
  42. package/locales/zh-CN/file.json +2 -0
  43. package/locales/zh-TW/file.json +2 -0
  44. package/package.json +1 -1
  45. package/packages/builtin-agents/src/agents/agent-builder/index.ts +1 -1
  46. package/packages/builtin-agents/src/agents/group-agent-builder/index.ts +1 -1
  47. package/packages/builtin-agents/src/agents/page-agent/index.ts +1 -1
  48. package/packages/const/src/settings/group.ts +0 -10
  49. package/packages/database/migrations/0068_update_group_data.sql +4 -0
  50. package/packages/database/migrations/meta/0068_snapshot.json +9588 -0
  51. package/packages/database/migrations/meta/_journal.json +7 -0
  52. package/packages/database/src/models/__tests__/chatGroup.test.ts +5 -7
  53. package/packages/database/src/models/__tests__/knowledgeBase.test.ts +185 -0
  54. package/packages/database/src/models/knowledgeBase.ts +67 -3
  55. package/packages/database/src/repositories/agentGroup/index.test.ts +23 -29
  56. package/packages/database/src/repositories/agentGroup/index.ts +4 -9
  57. package/packages/database/src/repositories/knowledge/index.ts +3 -3
  58. package/packages/database/src/schemas/chatGroup.ts +4 -3
  59. package/packages/database/src/types/chatGroup.ts +0 -7
  60. package/packages/types/src/agentGroup/index.ts +30 -9
  61. package/packages/utils/src/index.ts +1 -0
  62. package/packages/utils/src/platform.ts +35 -3
  63. package/src/app/[variants]/(desktop)/desktop-onboarding/_layout/index.tsx +30 -22
  64. package/src/app/[variants]/(desktop)/desktop-onboarding/_layout/style.ts +8 -5
  65. package/src/app/[variants]/(main)/_layout/DesktopLayoutContainer.tsx +2 -3
  66. package/src/app/[variants]/(main)/home/_layout/Body/Agent/ModalProvider.tsx +9 -32
  67. package/src/app/[variants]/(main)/home/_layout/hooks/useCreateMenuItems.tsx +3 -37
  68. package/src/app/[variants]/(main)/home/_layout/hooks/useSessionGroupMenuItems.tsx +7 -53
  69. package/src/app/[variants]/(main)/home/features/RecentPage/List.tsx +2 -1
  70. package/src/app/[variants]/(main)/resource/features/DndContextWrapper.tsx +1 -1
  71. package/src/app/[variants]/(main)/resource/library/_layout/Sidebar.tsx +2 -2
  72. package/src/app/[variants]/(main)/resource/library/features/LibraryMenu.tsx +2 -2
  73. package/src/app/[variants]/(mobile)/chat/settings/features/SettingButton.tsx +2 -12
  74. package/src/components/ChatGroupWizard/ChatGroupWizard.tsx +5 -27
  75. package/src/components/DragUpload/index.tsx +24 -27
  76. package/src/components/MemberSelectionModal/MemberSelectionModal.tsx +2 -11
  77. package/src/features/CommandMenu/useCommandMenu.ts +4 -14
  78. package/src/features/ElectronTitlebar/SimpleTitleBar.tsx +31 -0
  79. package/src/features/ElectronTitlebar/index.tsx +1 -0
  80. package/src/features/ResourceManager/components/Editor/index.tsx +2 -3
  81. package/src/features/ResourceManager/components/Explorer/Header/index.tsx +13 -17
  82. package/src/features/ResourceManager/components/Explorer/ItemDropdown/useFileItemDropdown.tsx +1 -1
  83. package/src/features/ResourceManager/components/Explorer/ListView/ListItem/TruncatedFileName.tsx +130 -0
  84. package/src/features/ResourceManager/components/Explorer/ListView/ListItem/index.tsx +36 -4
  85. package/src/features/ResourceManager/components/Explorer/ListView/Skeleton.tsx +4 -3
  86. package/src/features/ResourceManager/components/Explorer/ListView/index.tsx +58 -2
  87. package/src/features/ResourceManager/components/Explorer/MasonryView/index.tsx +58 -6
  88. package/src/features/ResourceManager/components/Explorer/MoveToFolderModal.tsx +2 -5
  89. package/src/features/ResourceManager/components/Explorer/ToolBar/BatchActionsDropdown.tsx +9 -5
  90. package/src/features/ResourceManager/components/Explorer/index.tsx +11 -56
  91. package/src/features/ResourceManager/components/Header/AddButton.tsx +5 -6
  92. package/src/features/ResourceManager/components/LibraryHierarchy/HierarchyNode.tsx +382 -0
  93. package/src/features/ResourceManager/components/LibraryHierarchy/index.tsx +396 -0
  94. package/src/features/ResourceManager/components/LibraryHierarchy/styles.ts +19 -0
  95. package/src/features/ResourceManager/components/LibraryHierarchy/treeState.ts +178 -0
  96. package/src/features/ResourceManager/components/LibraryHierarchy/types.ts +10 -0
  97. package/src/features/ResourceManager/index.tsx +3 -0
  98. package/src/layout/GlobalProvider/GroupWizardProvider.tsx +6 -29
  99. package/src/locales/default/auth.ts +1 -1
  100. package/src/locales/default/file.ts +2 -0
  101. package/src/locales/default/metadata.ts +2 -2
  102. package/src/server/modules/AgentRuntime/AgentRuntimeCoordinator.ts +30 -30
  103. package/src/server/modules/AgentRuntime/AgentStateManager.ts +23 -23
  104. package/src/server/modules/AgentRuntime/InMemoryAgentStateManager.ts +16 -16
  105. package/src/server/modules/AgentRuntime/InMemoryStreamEventManager.ts +13 -13
  106. package/src/server/modules/AgentRuntime/RuntimeExecutors.ts +2 -2
  107. package/src/server/modules/AgentRuntime/StreamEventManager.ts +18 -18
  108. package/src/server/modules/AgentRuntime/types.ts +21 -21
  109. package/src/server/routers/lambda/__tests__/agentGroup.test.ts +8 -8
  110. package/src/server/routers/lambda/agentGroup.ts +10 -12
  111. package/src/server/services/document/index.ts +1 -0
  112. package/src/store/agentGroup/slices/curd.test.ts +4 -4
  113. package/src/store/file/slices/fileManager/action.ts +12 -4
  114. package/src/store/home/slices/homeInput/action.ts +0 -3
  115. package/src/store/session/slices/session/action.ts +5 -9
  116. package/src/utils/platform.ts +2 -0
  117. package/src/app/[variants]/(mobile)/chat/settings/features/AgentTeamSettings/index.tsx +0 -95
  118. package/src/features/GroupChatSettings/AgentCard.tsx +0 -154
  119. package/src/features/GroupChatSettings/AgentTeamChatSettings.tsx +0 -179
  120. package/src/features/GroupChatSettings/AgentTeamMembersSettings.tsx +0 -244
  121. package/src/features/GroupChatSettings/AgentTeamMetaSettings.tsx +0 -94
  122. package/src/features/GroupChatSettings/AgentTeamSettings.tsx +0 -54
  123. package/src/features/GroupChatSettings/GroupCategory/index.tsx +0 -30
  124. package/src/features/GroupChatSettings/GroupCategory/useGroupCategory.tsx +0 -42
  125. package/src/features/GroupChatSettings/GroupChatSettingsProvider.tsx +0 -19
  126. package/src/features/GroupChatSettings/HostMemberCard.tsx +0 -113
  127. package/src/features/GroupChatSettings/StoreUpdater.tsx +0 -34
  128. package/src/features/GroupChatSettings/hooks/useGroupChatSettings.ts +0 -25
  129. package/src/features/GroupChatSettings/index.ts +0 -16
  130. package/src/features/GroupChatSettings/store/action.ts +0 -105
  131. package/src/features/GroupChatSettings/store/index.ts +0 -18
  132. package/src/features/GroupChatSettings/store/initialState.ts +0 -23
  133. package/src/features/GroupChatSettings/store/selectors.ts +0 -13
  134. package/src/features/ResourceManager/components/Tree/index.tsx +0 -883
  135. /package/src/features/ResourceManager/components/{Tree → LibraryHierarchy}/TreeSkeleton.tsx +0 -0
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable no-undef */
2
2
  import { Center, Flexbox, Icon } from '@lobehub/ui';
3
- import { createStaticStyles, cssVar } from 'antd-style';
3
+ import { createStyles } from 'antd-style';
4
4
  import { FileImage, FileText, FileUpIcon } from 'lucide-react';
5
5
  import { memo } from 'react';
6
6
  import { createPortal } from 'react-dom';
@@ -11,22 +11,22 @@ import { getContainer, useDragUpload } from './useDragUpload';
11
11
  const BLOCK_SIZE = 64;
12
12
  const ICON_SIZE = { size: 36, strokeWidth: 1.5 };
13
13
 
14
- const styles = createStaticStyles(({ css }) => {
14
+ const useStyles = createStyles(({ css, token }) => {
15
15
  return {
16
16
  container: css`
17
17
  width: 320px;
18
18
  height: 200px;
19
- padding: calc(${cssVar.borderRadiusLG} + 4px);
19
+ padding: calc(${token.borderRadiusLG}px + 4px);
20
20
  border-radius: 16px;
21
21
 
22
- background: ${cssVar.geekblue};
22
+ background: ${token.geekblue};
23
23
  `,
24
24
  content: css`
25
25
  width: 100%;
26
26
  height: 100%;
27
27
  padding: 16px;
28
28
  border: 1.5px dashed #fff;
29
- border-radius: ${cssVar.borderRadiusLG};
29
+ border-radius: ${token.borderRadiusLG}px;
30
30
  `,
31
31
  desc: css`
32
32
  font-size: 14px;
@@ -34,13 +34,25 @@ const styles = createStaticStyles(({ css }) => {
34
34
  color: #fff;
35
35
  `,
36
36
  icon: css`
37
- border-radius: ${cssVar.borderRadiusLG};
38
- color: color-mix(in srgb, ${cssVar.geekblue} 95%, black);
39
- background: color-mix(in srgb, ${cssVar.geekblue} 38%, white);
37
+ border-radius: ${token.borderRadiusLG}px;
38
+ color: color-mix(in srgb, ${token.geekblue} 95%, black);
39
+ background: color-mix(in srgb, ${token.geekblue} 38%, white);
40
40
  `,
41
41
  iconGroup: css`
42
42
  margin-block-start: -44px;
43
43
  `,
44
+ iconLeft: css`
45
+ transform: rotateZ(-20deg) translateX(10px);
46
+ border-radius: ${token.borderRadiusLG}px;
47
+ color: color-mix(in srgb, ${token.geekblue} 95%, black);
48
+ background: color-mix(in srgb, ${token.geekblue} 68%, white);
49
+ `,
50
+ iconRight: css`
51
+ transform: rotateZ(20deg) translateX(-10px);
52
+ border-radius: ${token.borderRadiusLG}px;
53
+ color: color-mix(in srgb, ${token.geekblue} 95%, black);
54
+ background: color-mix(in srgb, ${token.geekblue} 68%, white);
55
+ `,
44
56
  title: css`
45
57
  font-size: 20px;
46
58
  font-weight: bold;
@@ -54,7 +66,7 @@ const styles = createStaticStyles(({ css }) => {
54
66
  width: 100%;
55
67
  height: 100%;
56
68
 
57
- background: ${cssVar.colorBgMask};
69
+ background: ${token.colorBgMask};
58
70
 
59
71
  transition: all 0.3s ease-in-out;
60
72
  `,
@@ -68,6 +80,7 @@ interface DragUploadProps {
68
80
 
69
81
  const DragUpload = memo<DragUploadProps>(({ enabledFiles = true, onUploadFiles }) => {
70
82
  const { t } = useTranslation('components');
83
+ const { styles } = useStyles();
71
84
 
72
85
  const isDragging = useDragUpload(onUploadFiles);
73
86
 
@@ -78,15 +91,7 @@ const DragUpload = memo<DragUploadProps>(({ enabledFiles = true, onUploadFiles }
78
91
  <div className={styles.container}>
79
92
  <Center className={styles.content} gap={12}>
80
93
  <Flexbox className={styles.iconGroup} horizontal>
81
- <Center
82
- className={styles.icon}
83
- height={BLOCK_SIZE * 1.25}
84
- style={{
85
- background: `color-mix(in srgb, ${cssVar.geekblue} 68%, white)`,
86
- transform: 'rotateZ(-20deg) translateX(10px)',
87
- }}
88
- width={BLOCK_SIZE}
89
- >
94
+ <Center className={styles.iconLeft} height={BLOCK_SIZE * 1.25} width={BLOCK_SIZE}>
90
95
  <Icon icon={FileImage} size={ICON_SIZE} />
91
96
  </Center>
92
97
  <Center
@@ -100,15 +105,7 @@ const DragUpload = memo<DragUploadProps>(({ enabledFiles = true, onUploadFiles }
100
105
  >
101
106
  <Icon icon={FileUpIcon} size={ICON_SIZE} />
102
107
  </Center>
103
- <Center
104
- className={styles.icon}
105
- height={BLOCK_SIZE * 1.25}
106
- style={{
107
- background: `color-mix(in srgb, ${cssVar.geekblue} 68%, white)`,
108
- transform: 'rotateZ(20deg) translateX(-10px)',
109
- }}
110
- width={BLOCK_SIZE}
111
- >
108
+ <Center className={styles.iconRight} height={BLOCK_SIZE * 1.25} width={BLOCK_SIZE}>
112
109
  <Icon icon={FileText} size={ICON_SIZE} />
113
110
  </Center>
114
111
  </Flexbox>
@@ -162,11 +162,7 @@ export interface MemberSelectionModalProps {
162
162
  */
163
163
  mode: MemberSelectionMode;
164
164
  onCancel: () => void;
165
- onConfirm: (
166
- selectedAgents: string[],
167
- hostConfig?: { model?: string; provider?: string },
168
- enableSupervisor?: boolean,
169
- ) => void | Promise<void>;
165
+ onConfirm: (selectedAgents: string[]) => void | Promise<void>;
170
166
  open: boolean;
171
167
  /**
172
168
  * Pre-selected agent IDs (useful for editing existing groups)
@@ -338,12 +334,7 @@ const MemberSelectionModal = memo<MemberSelectionModalProps>(
338
334
  const handleConfirm = async () => {
339
335
  try {
340
336
  setIsAdding(true);
341
- // Only pass host config if the host card is visible (being managed in this modal)
342
- const shouldManageHost = !isHostCurrentlyEnabled;
343
- const hostConfig =
344
- shouldManageHost && !isHostRemoved ? normalizedHostModelConfig : undefined;
345
- const enableSupervisor = shouldManageHost ? !isHostRemoved : undefined;
346
- await onConfirm(selectedAgents, hostConfig, enableSupervisor);
337
+ await onConfirm(selectedAgents);
347
338
  handleReset();
348
339
  } catch (error) {
349
340
  console.error('Failed to confirm action:', error);
@@ -169,21 +169,11 @@ export const useCommandMenu = () => {
169
169
  const handleCreateAgentTeam = async () => {
170
170
  closeCommandMenu();
171
171
  openGroupWizard({
172
- onCreateCustom: async (selectedAgents, hostConfig, enableSupervisor) => {
173
- await createGroupWithMembers(selectedAgents, undefined, hostConfig, enableSupervisor);
172
+ onCreateCustom: async (selectedAgents) => {
173
+ await createGroupWithMembers(selectedAgents);
174
174
  },
175
- onCreateFromTemplate: async (
176
- templateId,
177
- hostConfig,
178
- enableSupervisor,
179
- selectedMemberTitles,
180
- ) => {
181
- await createGroupFromTemplate(
182
- templateId,
183
- hostConfig,
184
- enableSupervisor,
185
- selectedMemberTitles,
186
- );
175
+ onCreateFromTemplate: async (templateId, selectedMemberTitles) => {
176
+ await createGroupFromTemplate(templateId, selectedMemberTitles);
187
177
  },
188
178
  });
189
179
  };
@@ -0,0 +1,31 @@
1
+ 'use client';
2
+
3
+ import { Flexbox } from '@lobehub/ui';
4
+ import { type FC } from 'react';
5
+
6
+ import { ProductLogo } from '@/components/Branding/ProductLogo';
7
+ import { electronStylish } from '@/styles/electron';
8
+
9
+ import { TITLE_BAR_HEIGHT } from './const';
10
+
11
+ /**
12
+ * A simple, minimal TitleBar for Electron windows.
13
+ * Provides draggable area without business logic (navigation, updates, etc.)
14
+ * Use this for secondary windows like onboarding, settings, etc.
15
+ */
16
+ const SimpleTitleBar: FC = () => {
17
+ return (
18
+ <Flexbox
19
+ align={'center'}
20
+ className={electronStylish.draggable}
21
+ height={TITLE_BAR_HEIGHT}
22
+ horizontal
23
+ justify={'center'}
24
+ width={'100%'}
25
+ >
26
+ <ProductLogo size={16} type={'text'} />
27
+ </Flexbox>
28
+ );
29
+ };
30
+
31
+ export default SimpleTitleBar;
@@ -67,3 +67,4 @@ const TitleBar = memo(() => {
67
67
  export default TitleBar;
68
68
 
69
69
  export { TITLE_BAR_HEIGHT } from './const';
70
+ export { default as SimpleTitleBar } from './SimpleTitleBar';
@@ -4,7 +4,7 @@ import { BUILTIN_AGENT_SLUGS } from '@lobechat/builtin-agents';
4
4
  import { ActionIcon, Flexbox } from '@lobehub/ui';
5
5
  import { Modal } from 'antd';
6
6
  import { cssVar, useTheme } from 'antd-style';
7
- import { ArrowLeftIcon, BotMessageSquareIcon, DownloadIcon, InfoIcon } from 'lucide-react';
7
+ import { ArrowLeftIcon, DownloadIcon, InfoIcon } from 'lucide-react';
8
8
  import { memo, useState } from 'react';
9
9
  import { useTranslation } from 'react-i18next';
10
10
 
@@ -13,7 +13,6 @@ import { useResourceManagerStore } from '@/app/[variants]/(main)/resource/featur
13
13
  import Loading from '@/components/Loading/BrandTextLoading';
14
14
  import NavHeader from '@/features/NavHeader';
15
15
  import PageAgentProvider from '@/features/PageEditor/PageAgentProvider';
16
- import ToggleRightPanelButton from '@/features/RightPanel/ToggleRightPanelButton';
17
16
  import { useAgentStore } from '@/store/agent';
18
17
  import { builtinAgentSelectors } from '@/store/agent/selectors';
19
18
  import { fileManagerSelectors, useFileStore } from '@/store/file';
@@ -56,7 +55,7 @@ const FileEditorCanvas = memo<FileEditorProps>(({ onBack }) => {
56
55
  }
57
56
  right={
58
57
  <Flexbox gap={8} horizontal>
59
- <ToggleRightPanelButton icon={BotMessageSquareIcon} showActive={true} size={20} />
58
+ {/* <ToggleRightPanelButton icon={BotMessageSquareIcon} showActive={true} size={20} /> */}
60
59
  {fileDetail?.url && (
61
60
  <ActionIcon
62
61
  icon={DownloadIcon}
@@ -29,18 +29,18 @@ const Header = memo(() => {
29
29
  ]);
30
30
  const toggleCommandMenu = useGlobalStore((s) => s.toggleCommandMenu);
31
31
 
32
- // Disable batch actions dropdown when no items selected and not in any library
33
- const isBatchActionsDisabled = selectFileIds.length === 0 && !libraryId;
34
-
35
- // If no libraryId, show just the category name
36
- const leftContent =
37
- !libraryId && category && category !== FilesTabs.All ? (
38
- <Flexbox style={{ marginLeft: 8 }}>{t(`tab.${category as FilesTabs}` as any)}</Flexbox>
39
- ) : (
40
- <Flexbox style={{ marginLeft: 8 }}>
41
- <Breadcrumb category={category} knowledgeBaseId={libraryId} />
42
- </Flexbox>
43
- );
32
+ // If no libraryId, show category name or "Resource" for All
33
+ const leftContent = !libraryId ? (
34
+ <Flexbox style={{ marginLeft: 8 }}>
35
+ {category === FilesTabs.All
36
+ ? t('resource', { defaultValue: 'Resource' })
37
+ : t(`tab.${category as FilesTabs}` as any)}
38
+ </Flexbox>
39
+ ) : (
40
+ <Flexbox style={{ marginLeft: 8 }}>
41
+ <Breadcrumb category={category} knowledgeBaseId={libraryId} />
42
+ </Flexbox>
43
+ );
44
44
 
45
45
  return (
46
46
  <NavHeader
@@ -49,11 +49,7 @@ const Header = memo(() => {
49
49
  <>
50
50
  <ActionIcon icon={SearchIcon} onClick={() => toggleCommandMenu(true)} />
51
51
  <SortDropdown />
52
- <BatchActionsDropdown
53
- disabled={isBatchActionsDisabled}
54
- onActionClick={onActionClick}
55
- selectCount={selectFileIds.length}
56
- />
52
+ <BatchActionsDropdown onActionClick={onActionClick} selectCount={selectFileIds.length} />
57
53
  <ViewSwitcher />
58
54
  <Flexbox style={{ marginLeft: 8 }}>
59
55
  <AddButton />
@@ -15,7 +15,7 @@ import { useTranslation } from 'react-i18next';
15
15
  import { shallow } from 'zustand/shallow';
16
16
 
17
17
  import RepoIcon from '@/components/LibIcon';
18
- import { clearTreeFolderCache } from '@/features/ResourceManager/components/Tree';
18
+ import { clearTreeFolderCache } from '@/features/ResourceManager/components/LibraryHierarchy';
19
19
  import { PAGE_FILE_TYPE } from '@/features/ResourceManager/constants';
20
20
  import { documentService } from '@/services/document';
21
21
  import { useFileStore } from '@/store/file';
@@ -0,0 +1,130 @@
1
+ import { memo, useEffect, useRef, useState } from 'react';
2
+
3
+ interface TruncatedFileNameProps {
4
+ className?: string;
5
+ name: string;
6
+ }
7
+
8
+ /**
9
+ * Truncates file name from the center, preserving the extension at the end
10
+ * Similar to macOS Finder behavior
11
+ */
12
+ const TruncatedFileName = memo<TruncatedFileNameProps>(({ name, className }) => {
13
+ const containerRef = useRef<HTMLSpanElement>(null);
14
+ const [displayName, setDisplayName] = useState(name);
15
+
16
+ useEffect(() => {
17
+ const container = containerRef.current;
18
+ if (!container) return;
19
+
20
+ const updateTruncation = () => {
21
+ const containerWidth = container.offsetWidth;
22
+ if (containerWidth === 0) return;
23
+
24
+ // Create a temporary span to measure text width
25
+ const measureSpan = document.createElement('span');
26
+ measureSpan.style.visibility = 'hidden';
27
+ measureSpan.style.position = 'absolute';
28
+ measureSpan.style.whiteSpace = 'nowrap';
29
+ measureSpan.style.font = window.getComputedStyle(container).font;
30
+ document.body.append(measureSpan);
31
+
32
+ // Measure full name
33
+ measureSpan.textContent = name;
34
+ const fullWidth = measureSpan.offsetWidth;
35
+
36
+ // If it fits, show the full name
37
+ if (fullWidth <= containerWidth) {
38
+ setDisplayName(name);
39
+ measureSpan.remove();
40
+ return;
41
+ }
42
+
43
+ // Split filename and extension
44
+ const lastDotIndex = name.lastIndexOf('.');
45
+ let baseName = name;
46
+ let extension = '';
47
+
48
+ // Only treat as extension if dot is not at the start and there's content after it
49
+ if (lastDotIndex > 0 && lastDotIndex < name.length - 1) {
50
+ baseName = name.slice(0, lastDotIndex);
51
+ extension = name.slice(lastDotIndex); // includes the dot
52
+ }
53
+
54
+ // Measure ellipsis width
55
+ measureSpan.textContent = '...';
56
+ const ellipsisWidth = measureSpan.offsetWidth;
57
+
58
+ // Measure extension width
59
+ measureSpan.textContent = extension;
60
+ const extensionWidth = measureSpan.offsetWidth;
61
+
62
+ // Calculate available width for base name
63
+ const availableWidth = containerWidth - ellipsisWidth - extensionWidth;
64
+
65
+ if (availableWidth <= 0) {
66
+ // Not enough space, just show ellipsis + extension
67
+ setDisplayName(`...${extension}`);
68
+ measureSpan.remove();
69
+ return;
70
+ }
71
+
72
+ // Binary search to find the optimal split point
73
+ let left = 0;
74
+ let right = baseName.length;
75
+ let bestFit = '';
76
+
77
+ while (left <= right) {
78
+ const mid = Math.floor((left + right) / 2);
79
+ const startChars = Math.ceil(mid / 2);
80
+ const endChars = Math.floor(mid / 2);
81
+
82
+ const truncated =
83
+ baseName.slice(0, startChars) + (mid > 0 ? baseName.slice(-endChars) : '');
84
+
85
+ measureSpan.textContent = truncated;
86
+ const truncatedWidth = measureSpan.offsetWidth;
87
+
88
+ if (truncatedWidth <= availableWidth) {
89
+ bestFit = truncated;
90
+ left = mid + 1;
91
+ } else {
92
+ right = mid - 1;
93
+ }
94
+ }
95
+
96
+ measureSpan.remove();
97
+
98
+ // Construct final truncated name
99
+ if (bestFit.length === 0) {
100
+ setDisplayName(`...${extension}`);
101
+ } else {
102
+ const startChars = Math.ceil(bestFit.length / 2);
103
+ const endChars = Math.floor(bestFit.length / 2);
104
+ setDisplayName(
105
+ `${baseName.slice(0, startChars)}...${baseName.slice(-endChars)}${extension}`,
106
+ );
107
+ }
108
+ };
109
+
110
+ updateTruncation();
111
+
112
+ // Use ResizeObserver to handle container size changes
113
+ const resizeObserver = new ResizeObserver(updateTruncation);
114
+ resizeObserver.observe(container);
115
+
116
+ return () => {
117
+ resizeObserver.disconnect();
118
+ };
119
+ }, [name]);
120
+
121
+ return (
122
+ <span className={className} ref={containerRef} title={name}>
123
+ {displayName}
124
+ </span>
125
+ );
126
+ });
127
+
128
+ TruncatedFileName.displayName = 'TruncatedFileName';
129
+
130
+ export default TruncatedFileName;
@@ -17,7 +17,7 @@ import {
17
17
  } from '@/app/[variants]/(main)/resource/features/DndContextWrapper';
18
18
  import { useResourceManagerStore } from '@/app/[variants]/(main)/resource/features/store';
19
19
  import FileIcon from '@/components/FileIcon';
20
- import { clearTreeFolderCache } from '@/features/ResourceManager/components/Tree';
20
+ import { clearTreeFolderCache } from '@/features/ResourceManager/components/LibraryHierarchy';
21
21
  import { PAGE_FILE_TYPE } from '@/features/ResourceManager/constants';
22
22
  import { fileManagerSelectors, useFileStore } from '@/store/file';
23
23
  import { type FileListItem as FileListItemType } from '@/types/files';
@@ -27,6 +27,7 @@ import { isChunkingUnsupported } from '@/utils/isChunkingUnsupported';
27
27
  import DropdownMenu from '../../ItemDropdown/DropdownMenu';
28
28
  import { useFileItemDropdown } from '../../ItemDropdown/useFileItemDropdown';
29
29
  import ChunksBadge from './ChunkTag';
30
+ import TruncatedFileName from './TruncatedFileName';
30
31
 
31
32
  // Initialize dayjs plugin once at module level
32
33
  dayjs.extend(relativeTime);
@@ -40,6 +41,7 @@ const styles = createStaticStyles(({ css }) => {
40
41
  cursor: pointer;
41
42
  min-width: 800px;
42
43
 
44
+ /* Hover effect for individual rows */
43
45
  &:hover {
44
46
  background: ${cssVar.colorFillTertiary};
45
47
  }
@@ -59,6 +61,25 @@ const styles = createStaticStyles(({ css }) => {
59
61
  opacity: 0.5;
60
62
  `,
61
63
 
64
+ evenRow: css`
65
+ background: ${cssVar.colorFillQuaternary};
66
+
67
+ /* Hover effect overrides zebra striping on the hovered row only */
68
+ &:hover {
69
+ background: ${cssVar.colorFillTertiary};
70
+ }
71
+
72
+ /* Hide zebra striping when any row is hovered */
73
+ .any-row-hovered & {
74
+ background: transparent;
75
+ }
76
+
77
+ /* But keep hover effect on the actual hovered row */
78
+ .any-row-hovered &:hover {
79
+ background: ${cssVar.colorFillTertiary};
80
+ }
81
+ `,
82
+
62
83
  hover: css`
63
84
  opacity: 0;
64
85
 
@@ -80,7 +101,6 @@ const styles = createStaticStyles(({ css }) => {
80
101
  margin-inline-start: 12px;
81
102
 
82
103
  color: ${cssVar.colorText};
83
- text-overflow: ellipsis;
84
104
  white-space: nowrap;
85
105
  `,
86
106
  nameContainer: css`
@@ -105,6 +125,8 @@ interface FileListItemProps extends FileListItemType {
105
125
  size: number;
106
126
  };
107
127
  index: number;
128
+ isAnyRowHovered: boolean;
129
+ onHoverChange: (isHovered: boolean) => void;
108
130
  onSelectedChange: (id: string, selected: boolean, shiftKey: boolean, index: number) => void;
109
131
  pendingRenameItemId?: string | null;
110
132
  selected?: boolean;
@@ -133,6 +155,7 @@ const FileListItem = memo<FileListItemProps>(
133
155
  sourceType,
134
156
  slug,
135
157
  pendingRenameItemId,
158
+ onHoverChange,
136
159
  }) => {
137
160
  const { t } = useTranslation(['components', 'file']);
138
161
  const { message } = App.useApp();
@@ -376,12 +399,14 @@ const FileListItem = memo<FileListItemProps>(
376
399
  className={cx(
377
400
  styles.container,
378
401
  'file-list-item-group',
402
+ index % 2 === 0 && styles.evenRow,
379
403
  selected && styles.selected,
380
404
  isDragging && styles.dragging,
381
405
  isOver && styles.dragOver,
382
406
  )}
383
407
  data-drop-target-id={id}
384
408
  data-is-folder={String(isFolder)}
409
+ data-row-index={index}
385
410
  draggable={!!resourceManagerState.libraryId}
386
411
  height={48}
387
412
  horizontal
@@ -390,6 +415,8 @@ const FileListItem = memo<FileListItemProps>(
390
415
  onDragOver={handleDragOver}
391
416
  onDragStart={handleDragStart}
392
417
  onDrop={handleDrop}
418
+ onMouseEnter={() => onHoverChange(true)}
419
+ onMouseLeave={() => onHoverChange(false)}
393
420
  paddingInline={8}
394
421
  style={{
395
422
  borderBlockEnd: `1px solid ${cssVar.colorBorderSecondary}`,
@@ -469,7 +496,10 @@ const FileListItem = memo<FileListItemProps>(
469
496
  value={renamingValue}
470
497
  />
471
498
  ) : (
472
- <span className={styles.name}>{name || t('file:pageList.untitled')}</span>
499
+ <TruncatedFileName
500
+ className={styles.name}
501
+ name={name || t('file:pageList.untitled')}
502
+ />
473
503
  )}
474
504
  </Flexbox>
475
505
  <Flexbox
@@ -482,6 +512,7 @@ const FileListItem = memo<FileListItemProps>(
482
512
  onPointerDown={(e) => e.stopPropagation()}
483
513
  >
484
514
  {!isFolder &&
515
+ !isPage &&
485
516
  (fileStoreState.isCreatingFileParseTask ||
486
517
  isNull(chunkingStatus) ||
487
518
  !chunkingStatus ? (
@@ -562,7 +593,8 @@ const FileListItem = memo<FileListItemProps>(
562
593
  prevProps.url === nextProps.url &&
563
594
  prevProps.columnWidths.name === nextProps.columnWidths.name &&
564
595
  prevProps.columnWidths.date === nextProps.columnWidths.date &&
565
- prevProps.columnWidths.size === nextProps.columnWidths.size
596
+ prevProps.columnWidths.size === nextProps.columnWidths.size &&
597
+ prevProps.isAnyRowHovered === nextProps.isAnyRowHovered
566
598
  );
567
599
  },
568
600
  );
@@ -29,6 +29,7 @@ const ListViewSkeleton = ({
29
29
  key={index}
30
30
  paddingInline={8}
31
31
  style={{
32
+ background: index % 2 === 0 ? cssVar.colorFillQuaternary : 'transparent',
32
33
  borderBlockEnd: `1px solid ${cssVar.colorBorderSecondary}`,
33
34
  opacity: getOpacity(index),
34
35
  }}
@@ -39,21 +40,21 @@ const ListViewSkeleton = ({
39
40
  <Flexbox
40
41
  align={'center'}
41
42
  horizontal
42
- paddingInline={8}
43
43
  style={{
44
44
  flexShrink: 0,
45
45
  maxWidth: columnWidths.name,
46
46
  minWidth: columnWidths.name,
47
+ paddingInline: 8,
47
48
  width: columnWidths.name,
48
49
  }}
49
50
  >
50
51
  <Skeleton.Avatar active shape={'square'} size={24} style={{ marginInline: 8 }} />
51
52
  <Skeleton.Button active style={{ height: 16, width: '60%' }} />
52
53
  </Flexbox>
53
- <Flexbox paddingInline={24} style={{ flexShrink: 0 }} width={columnWidths.date}>
54
+ <Flexbox style={{ flexShrink: 0, paddingInline: '0 24px' }} width={columnWidths.date}>
54
55
  <Skeleton.Button active style={{ height: 16, width: '80%' }} />
55
56
  </Flexbox>
56
- <Flexbox paddingInline={24} style={{ flexShrink: 0 }} width={columnWidths.size}>
57
+ <Flexbox style={{ flexShrink: 0, paddingInline: '0 24px' }} width={columnWidths.size}>
57
58
  <Skeleton.Button active style={{ height: 16, width: '60%' }} />
58
59
  </Flexbox>
59
60
  </Flexbox>