@lobehub/lobehub 2.1.5 → 2.1.6

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 (30) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/apps/desktop/package.json +4 -4
  3. package/apps/desktop/src/main/controllers/LocalFileCtr.ts +45 -11
  4. package/apps/desktop/src/main/controllers/__tests__/LocalFileCtr.test.ts +509 -1
  5. package/changelog/v2.json +9 -0
  6. package/locales/en-US/plugin.json +1 -0
  7. package/locales/zh-CN/plugin.json +1 -0
  8. package/package.json +1 -1
  9. package/packages/builtin-tool-cloud-sandbox/src/ExecutionRuntime/index.ts +4 -4
  10. package/packages/builtin-tool-local-system/package.json +1 -2
  11. package/packages/builtin-tool-local-system/src/client/Inspector/GlobLocalFiles/index.tsx +21 -22
  12. package/packages/builtin-tool-local-system/src/executor/index.ts +12 -3
  13. package/packages/builtin-tool-local-system/src/manifest.ts +18 -1
  14. package/packages/builtin-tool-local-system/src/systemRole.ts +8 -2
  15. package/packages/builtin-tool-local-system/src/types.ts +1 -0
  16. package/packages/const/src/file.ts +11 -1
  17. package/packages/const/src/index.ts +1 -0
  18. package/packages/const/src/version.ts +1 -1
  19. package/packages/electron-client-ipc/src/types/localSystem.ts +32 -0
  20. package/packages/file-loaders/src/blackList.ts +8 -1
  21. package/packages/prompts/src/prompts/fileSystem/formatFileList.test.ts +297 -5
  22. package/packages/prompts/src/prompts/fileSystem/formatFileList.ts +86 -3
  23. package/src/envs/file.ts +1 -1
  24. package/src/features/Conversation/Messages/AssistantGroup/Tool/Inspector/ToolTitle.tsx +100 -32
  25. package/src/features/Conversation/Messages/AssistantGroup/Tool/Inspector/index.tsx +5 -0
  26. package/src/features/Conversation/Messages/Supervisor/components/ContentBlock.tsx +1 -1
  27. package/src/locales/default/plugin.ts +1 -0
  28. package/src/services/electron/localFileService.ts +2 -1
  29. package/packages/builtin-tool-local-system/src/ExecutionRuntime/index.ts +0 -466
  30. package/src/features/Conversation/Messages/Supervisor/components/MessageContent.tsx +0 -29
@@ -1,4 +1,5 @@
1
1
  {
2
+ "arguments.moreParams": "等 {{count}} 个参数",
2
3
  "arguments.title": "参数列表",
3
4
  "builtins.lobe-agent-builder.apiName.getAvailableModels": "获取可用模型",
4
5
  "builtins.lobe-agent-builder.apiName.getAvailableTools": "获取可用技能",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.1.5",
3
+ "version": "2.1.6",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -76,13 +76,13 @@ export class CloudSandboxExecutionRuntime {
76
76
  const files = result.result?.files || [];
77
77
  const state: ListLocalFilesState = { files };
78
78
 
79
- const content = formatFileList(
80
- files.map((f: { isDirectory: boolean; name: string }) => ({
79
+ const content = formatFileList({
80
+ directory: args.directoryPath,
81
+ files: files.map((f: { isDirectory: boolean; name: string }) => ({
81
82
  isDirectory: f.isDirectory,
82
83
  name: f.name,
83
84
  })),
84
- args.directoryPath,
85
- );
85
+ });
86
86
 
87
87
  return {
88
88
  content,
@@ -5,8 +5,7 @@
5
5
  "exports": {
6
6
  ".": "./src/index.ts",
7
7
  "./client": "./src/client/index.ts",
8
- "./executor": "./src/executor/index.ts",
9
- "./executionRuntime": "./src/ExecutionRuntime/index.ts"
8
+ "./executor": "./src/executor/index.ts"
10
9
  },
11
10
  "main": "./src/index.ts",
12
11
  "dependencies": {
@@ -2,8 +2,8 @@
2
2
 
3
3
  import { type GlobFilesParams } from '@lobechat/electron-client-ipc';
4
4
  import { type BuiltinInspectorProps } from '@lobechat/types';
5
- import { createStaticStyles, cssVar, cx } from 'antd-style';
6
- import { Check, X } from 'lucide-react';
5
+ import { Text } from '@lobehub/ui';
6
+ import { cssVar, cx } from 'antd-style';
7
7
  import { memo } from 'react';
8
8
  import { useTranslation } from 'react-i18next';
9
9
 
@@ -11,13 +11,6 @@ import { highlightTextStyles, inspectorTextStyles, shinyTextStyles } from '@/sty
11
11
 
12
12
  import { type GlobFilesState } from '../../..';
13
13
 
14
- const styles = createStaticStyles(({ css }) => ({
15
- statusIcon: css`
16
- margin-block-end: -2px;
17
- margin-inline-start: 4px;
18
- `,
19
- }));
20
-
21
14
  export const GlobLocalFilesInspector = memo<BuiltinInspectorProps<GlobFilesParams, GlobFilesState>>(
22
15
  ({ args, partialArgs, isArgumentsStreaming, pluginState, isLoading }) => {
23
16
  const { t } = useTranslation('plugin');
@@ -41,22 +34,28 @@ export const GlobLocalFilesInspector = memo<BuiltinInspectorProps<GlobFilesParam
41
34
  );
42
35
  }
43
36
 
44
- // Check if glob was successful
45
- const isSuccess = pluginState?.result?.success;
37
+ // Check result count
38
+ const resultCount = pluginState?.result?.total_files ?? 0;
39
+ const hasResults = resultCount > 0;
46
40
 
47
41
  return (
48
42
  <div className={cx(inspectorTextStyles.root, isLoading && shinyTextStyles.shinyText)}>
49
- <span style={{ marginInlineStart: 2 }}>
50
- <span>{t('builtins.lobe-local-system.apiName.globLocalFiles')}: </span>
51
- {pattern && <span className={highlightTextStyles.primary}>{pattern}</span>}
52
- {isLoading ? null : pluginState?.result ? (
53
- isSuccess ? (
54
- <Check className={styles.statusIcon} color={cssVar.colorSuccess} size={14} />
55
- ) : (
56
- <X className={styles.statusIcon} color={cssVar.colorError} size={14} />
57
- )
58
- ) : null}
59
- </span>
43
+ <span>{t('builtins.lobe-local-system.apiName.globLocalFiles')}: </span>
44
+ {pattern && <span className={highlightTextStyles.primary}>{pattern}</span>}
45
+ {!isLoading &&
46
+ pluginState?.result &&
47
+ (hasResults ? (
48
+ <span style={{ marginInlineStart: 4 }}>({resultCount})</span>
49
+ ) : (
50
+ <Text
51
+ as={'span'}
52
+ color={cssVar.colorTextDescription}
53
+ fontSize={12}
54
+ style={{ marginInlineStart: 4 }}
55
+ >
56
+ ({t('builtins.lobe-local-system.inspector.noResults')})
57
+ </Text>
58
+ ))}
60
59
  </div>
61
60
  );
62
61
  },
@@ -87,11 +87,20 @@ class LocalSystemExecutor extends BaseExecutor<typeof LocalSystemApiEnum> {
87
87
 
88
88
  listLocalFiles = async (params: ListLocalFileParams): Promise<BuiltinToolResult> => {
89
89
  try {
90
- const result: LocalFileItem[] = await localFileService.listLocalFiles(params);
90
+ const result = await localFileService.listLocalFiles(params);
91
91
 
92
- const state: LocalFileListState = { listResults: result };
92
+ const state: LocalFileListState = {
93
+ listResults: result.files,
94
+ totalCount: result.totalCount,
95
+ };
93
96
 
94
- const content = formatFileList(result, params.path);
97
+ const content = formatFileList({
98
+ directory: params.path,
99
+ files: result.files,
100
+ sortBy: params.sortBy,
101
+ sortOrder: params.sortOrder,
102
+ totalCount: result.totalCount,
103
+ });
95
104
 
96
105
  return {
97
106
  content,
@@ -7,14 +7,31 @@ export const LocalSystemManifest: BuiltinToolManifest = {
7
7
  api: [
8
8
  {
9
9
  description:
10
- 'List files and folders in a specified directory. Input should be a path. Output is a JSON array of file/folder names.',
10
+ 'List files and folders in a specified directory. Returns file/folder names with metadata (size, modified time). Results are sorted by modified time (newest first) by default and limited to 100 items.',
11
11
  name: LocalSystemApiName.listLocalFiles,
12
12
  parameters: {
13
13
  properties: {
14
+ limit: {
15
+ default: 100,
16
+ description: 'Maximum number of items to return (default: 100)',
17
+ type: 'number',
18
+ },
14
19
  path: {
15
20
  description: 'The directory path to list',
16
21
  type: 'string',
17
22
  },
23
+ sortBy: {
24
+ default: 'modifiedTime',
25
+ description: 'Field to sort by (default: modifiedTime)',
26
+ enum: ['name', 'modifiedTime', 'createdTime', 'size'],
27
+ type: 'string',
28
+ },
29
+ sortOrder: {
30
+ default: 'desc',
31
+ description: 'Sort order (default: desc)',
32
+ enum: ['asc', 'desc'],
33
+ type: 'string',
34
+ },
18
35
  },
19
36
  required: ['path'],
20
37
  type: 'object',
@@ -21,7 +21,7 @@ Use these paths when the user refers to these common locations by name (e.g., "m
21
21
  You have access to a set of tools to interact with the user's local file system:
22
22
 
23
23
  **File Operations:**
24
- 1. **listLocalFiles**: Lists files and directories in a specified path.
24
+ 1. **listLocalFiles**: Lists files and directories in a specified path. Returns metadata including file size and modification time. Results are sorted by modification time (newest first) by default and limited to 100 items.
25
25
  2. **readLocalFile**: Reads the content of a specified file, optionally within a line range. You can read file types such as Word, Excel, PowerPoint, PDF, and plain text files.
26
26
  3. **writeLocalFile**: Write content to a specific file, only support plain text file like \`.text\` or \`.md\`
27
27
  4. **editLocalFile**: Performs exact string replacements in files. Must read the file first before editing.
@@ -50,7 +50,13 @@ You have access to a set of tools to interact with the user's local file system:
50
50
  </workflow>
51
51
 
52
52
  <tool_usage_guidelines>
53
- - For listing directory contents: Use 'listFiles' with the target directory path.
53
+ - For listing directory contents: Use 'listLocalFiles'. Provide the following parameters:
54
+ - 'path': The directory path to list.
55
+ - 'sortBy' (Optional): Field to sort results by. Options: 'name', 'modifiedTime', 'createdTime', 'size'. Defaults to 'modifiedTime'.
56
+ - 'sortOrder' (Optional): Sort order. Options: 'asc', 'desc'. Defaults to 'desc' (newest/largest first).
57
+ - 'limit' (Optional): Maximum number of items to return. Defaults to 100.
58
+ - The response includes file/folder names with metadata (size in bytes, modification time) for each item.
59
+ - System files (e.g., '.DS_Store', 'Thumbs.db', '$RECYCLE.BIN') are automatically filtered out.
54
60
  - For reading a file: Use 'readFile'. Provide the following parameters:
55
61
  - 'path': The exact file path.
56
62
  - 'loc' (Optional): A two-element array [startLine, endLine] to specify a line range to read (e.g., '[301, 400]' reads lines 301 to 400).
@@ -47,6 +47,7 @@ export interface LocalFileSearchState {
47
47
 
48
48
  export interface LocalFileListState {
49
49
  listResults: LocalFileItem[];
50
+ totalCount: number;
50
51
  }
51
52
 
52
53
  export interface LocalReadFileState {
@@ -1,10 +1,20 @@
1
- export const FILE_UPLOAD_BLACKLIST = [
1
+ /**
2
+ * System files to be filtered out when listing directory contents
3
+ */
4
+ export const SYSTEM_FILES_BLACKLIST = [
2
5
  '.DS_Store',
3
6
  'Thumbs.db',
4
7
  'desktop.ini',
5
8
  '.localized',
6
9
  'ehthumbs.db',
7
10
  'ehthumbs_vista.db',
11
+ '$RECYCLE.BIN',
12
+ 'System Volume Information',
13
+ '.Spotlight-V100',
14
+ '.fseventsd',
15
+ '.Trashes',
8
16
  ];
9
17
 
18
+ export const FILE_UPLOAD_BLACKLIST = SYSTEM_FILES_BLACKLIST;
19
+
10
20
  export const MAX_UPLOAD_FILE_COUNT = 10;
@@ -2,6 +2,7 @@ export * from './currency';
2
2
  export * from './desktop';
3
3
  export * from './discover';
4
4
  export * from './editor';
5
+ export * from './file';
5
6
  export * from './klavis';
6
7
  export * from './layoutTokens';
7
8
  export * from './lobehubSkill';
@@ -1,6 +1,6 @@
1
1
  import { BRANDING_NAME, ORG_NAME } from '@lobechat/business-const';
2
2
 
3
- import pkg from '@/../package.json';
3
+ import pkg from '../../../package.json';
4
4
 
5
5
  export const CURRENT_VERSION = pkg.version;
6
6
 
@@ -16,8 +16,40 @@ export interface LocalFileItem {
16
16
  type: string;
17
17
  }
18
18
 
19
+ export type ListLocalFileSortBy = 'name' | 'modifiedTime' | 'createdTime' | 'size';
20
+ export type ListLocalFileSortOrder = 'asc' | 'desc';
21
+
19
22
  export interface ListLocalFileParams {
23
+ /**
24
+ * Maximum number of files to return
25
+ * @default 100
26
+ */
27
+ limit?: number;
28
+ /**
29
+ * Directory path to list
30
+ */
20
31
  path: string;
32
+ /**
33
+ * Field to sort by
34
+ * @default 'modifiedTime'
35
+ */
36
+ sortBy?: ListLocalFileSortBy;
37
+ /**
38
+ * Sort order
39
+ * @default 'desc'
40
+ */
41
+ sortOrder?: ListLocalFileSortOrder;
42
+ }
43
+
44
+ export interface ListLocalFilesResult {
45
+ /**
46
+ * List of files (truncated to limit)
47
+ */
48
+ files: LocalFileItem[];
49
+ /**
50
+ * Total count of files before truncation
51
+ */
52
+ totalCount: number;
21
53
  }
22
54
 
23
55
  export interface MoveLocalFileParams {
@@ -1,4 +1,6 @@
1
- // List of system files/directories to ignore
1
+ /**
2
+ * System files to be filtered out when listing directory contents
3
+ */
2
4
  export const SYSTEM_FILES_TO_IGNORE = [
3
5
  '.DS_Store',
4
6
  'Thumbs.db',
@@ -6,4 +8,9 @@ export const SYSTEM_FILES_TO_IGNORE = [
6
8
  '.localized',
7
9
  'ehthumbs.db',
8
10
  'ehthumbs_vista.db',
11
+ '$RECYCLE.BIN',
12
+ 'System Volume Information',
13
+ '.Spotlight-V100',
14
+ '.fseventsd',
15
+ '.Trashes',
9
16
  ];
@@ -3,8 +3,158 @@ import { describe, expect, it } from 'vitest';
3
3
  import { formatFileList } from './formatFileList';
4
4
 
5
5
  describe('formatFileList', () => {
6
+ describe('snapshot tests', () => {
7
+ it('should match snapshot for complete extended output', () => {
8
+ const files = [
9
+ {
10
+ isDirectory: true,
11
+ modifiedTime: new Date('2024-01-20T14:30:00'),
12
+ name: 'Documents',
13
+ size: 4096,
14
+ },
15
+ {
16
+ isDirectory: true,
17
+ modifiedTime: new Date('2024-01-18T09:15:00'),
18
+ name: 'Downloads',
19
+ size: 4096,
20
+ },
21
+ {
22
+ isDirectory: false,
23
+ modifiedTime: new Date('2024-01-15T11:20:00'),
24
+ name: 'report.pdf',
25
+ size: 2457600,
26
+ },
27
+ {
28
+ isDirectory: false,
29
+ modifiedTime: new Date('2024-01-10T16:45:00'),
30
+ name: 'screenshot.png',
31
+ size: 1153434,
32
+ },
33
+ {
34
+ isDirectory: false,
35
+ modifiedTime: new Date('2024-01-05T08:30:00'),
36
+ name: 'notes.txt',
37
+ size: 2048,
38
+ },
39
+ ];
40
+
41
+ const result = formatFileList({
42
+ directory: '/Users/test/Desktop',
43
+ files,
44
+ sortBy: 'modifiedTime',
45
+ sortOrder: 'desc',
46
+ totalCount: 150,
47
+ });
48
+
49
+ expect(result).toMatchInlineSnapshot(`
50
+ "Found 150 item(s) in /Users/test/Desktop (showing first 5, sorted by modifiedTime desc):
51
+ [D] Documents 2024-01-20 14:30 --
52
+ [D] Downloads 2024-01-18 09:15 --
53
+ [F] report.pdf 2024-01-15 11:20 2.3 MB
54
+ [F] screenshot.png 2024-01-10 16:45 1.1 MB
55
+ [F] notes.txt 2024-01-05 08:30 2 KB"
56
+ `);
57
+ });
58
+
59
+ it('should match snapshot for simple output without extended info', () => {
60
+ const files = [
61
+ { isDirectory: true, name: 'src' },
62
+ { isDirectory: true, name: 'dist' },
63
+ { isDirectory: false, name: 'package.json' },
64
+ { isDirectory: false, name: 'README.md' },
65
+ { isDirectory: false, name: 'tsconfig.json' },
66
+ ];
67
+
68
+ const result = formatFileList({ directory: '/project', files });
69
+
70
+ expect(result).toMatchInlineSnapshot(`
71
+ "Found 5 item(s) in /project:
72
+ [D] src
73
+ [D] dist
74
+ [F] package.json
75
+ [F] README.md
76
+ [F] tsconfig.json"
77
+ `);
78
+ });
79
+
80
+ it('should match snapshot for output with sorting info only', () => {
81
+ const files = [
82
+ { isDirectory: false, name: 'alpha.txt' },
83
+ { isDirectory: false, name: 'beta.txt' },
84
+ { isDirectory: false, name: 'gamma.txt' },
85
+ ];
86
+
87
+ const result = formatFileList({
88
+ directory: '/test',
89
+ files,
90
+ sortBy: 'name',
91
+ sortOrder: 'asc',
92
+ });
93
+
94
+ expect(result).toMatchInlineSnapshot(`
95
+ "Found 3 item(s) in /test (sorted by name asc):
96
+ [F] alpha.txt
97
+ [F] beta.txt
98
+ [F] gamma.txt"
99
+ `);
100
+ });
101
+
102
+ it('should match snapshot for various file sizes', () => {
103
+ const files = [
104
+ {
105
+ isDirectory: false,
106
+ modifiedTime: new Date('2024-01-01T00:00:00'),
107
+ name: 'empty.txt',
108
+ size: 0,
109
+ },
110
+ {
111
+ isDirectory: false,
112
+ modifiedTime: new Date('2024-01-01T00:00:00'),
113
+ name: 'tiny.txt',
114
+ size: 512,
115
+ },
116
+ {
117
+ isDirectory: false,
118
+ modifiedTime: new Date('2024-01-01T00:00:00'),
119
+ name: 'small.txt',
120
+ size: 1024,
121
+ },
122
+ {
123
+ isDirectory: false,
124
+ modifiedTime: new Date('2024-01-01T00:00:00'),
125
+ name: 'medium.txt',
126
+ size: 1048576,
127
+ },
128
+ {
129
+ isDirectory: false,
130
+ modifiedTime: new Date('2024-01-01T00:00:00'),
131
+ name: 'large.txt',
132
+ size: 1073741824,
133
+ },
134
+ {
135
+ isDirectory: false,
136
+ modifiedTime: new Date('2024-01-01T00:00:00'),
137
+ name: 'huge.txt',
138
+ size: 1099511627776,
139
+ },
140
+ ];
141
+
142
+ const result = formatFileList({ directory: '/test', files });
143
+
144
+ expect(result).toMatchInlineSnapshot(`
145
+ "Found 6 item(s) in /test:
146
+ [F] empty.txt 2024-01-01 00:00 0 B
147
+ [F] tiny.txt 2024-01-01 00:00 512 B
148
+ [F] small.txt 2024-01-01 00:00 1 KB
149
+ [F] medium.txt 2024-01-01 00:00 1 MB
150
+ [F] large.txt 2024-01-01 00:00 1 GB
151
+ [F] huge.txt 2024-01-01 00:00 1 TB"
152
+ `);
153
+ });
154
+ });
155
+
6
156
  it('should format empty directory', () => {
7
- const result = formatFileList([], '/home/user');
157
+ const result = formatFileList({ directory: '/home/user', files: [] });
8
158
  expect(result).toMatchInlineSnapshot(`"Directory /home/user is empty"`);
9
159
  });
10
160
 
@@ -13,7 +163,7 @@ describe('formatFileList', () => {
13
163
  { isDirectory: false, name: 'file1.txt' },
14
164
  { isDirectory: false, name: 'file2.js' },
15
165
  ];
16
- const result = formatFileList(files, '/home/user');
166
+ const result = formatFileList({ directory: '/home/user', files });
17
167
  expect(result).toMatchInlineSnapshot(`
18
168
  "Found 2 item(s) in /home/user:
19
169
  [F] file1.txt
@@ -26,7 +176,7 @@ describe('formatFileList', () => {
26
176
  { isDirectory: true, name: 'src' },
27
177
  { isDirectory: true, name: 'dist' },
28
178
  ];
29
- const result = formatFileList(files, '/project');
179
+ const result = formatFileList({ directory: '/project', files });
30
180
  expect(result).toMatchInlineSnapshot(`
31
181
  "Found 2 item(s) in /project:
32
182
  [D] src
@@ -41,7 +191,7 @@ describe('formatFileList', () => {
41
191
  { isDirectory: true, name: 'node_modules' },
42
192
  { isDirectory: false, name: 'README.md' },
43
193
  ];
44
- const result = formatFileList(files, '/project');
194
+ const result = formatFileList({ directory: '/project', files });
45
195
  expect(result).toMatchInlineSnapshot(`
46
196
  "Found 4 item(s) in /project:
47
197
  [D] src
@@ -53,10 +203,152 @@ describe('formatFileList', () => {
53
203
 
54
204
  it('should format single item', () => {
55
205
  const files = [{ isDirectory: false, name: 'index.ts' }];
56
- const result = formatFileList(files, '/src');
206
+ const result = formatFileList({ directory: '/src', files });
57
207
  expect(result).toMatchInlineSnapshot(`
58
208
  "Found 1 item(s) in /src:
59
209
  [F] index.ts"
60
210
  `);
61
211
  });
212
+
213
+ describe('extended info', () => {
214
+ it('should format files with size and modified time', () => {
215
+ const files = [
216
+ {
217
+ isDirectory: true,
218
+ modifiedTime: new Date('2024-01-20T14:30:00'),
219
+ name: 'src',
220
+ size: 4096,
221
+ },
222
+ {
223
+ isDirectory: false,
224
+ modifiedTime: new Date('2024-01-18T09:15:00'),
225
+ name: 'report.pdf',
226
+ size: 2457600, // 2.4 MB
227
+ },
228
+ {
229
+ isDirectory: false,
230
+ modifiedTime: new Date('2024-01-15T11:20:00'),
231
+ name: 'screenshot.png',
232
+ size: 1153434, // ~1.1 MB
233
+ },
234
+ ];
235
+ const result = formatFileList({ directory: '/Users/test/Downloads', files });
236
+ expect(result).toContain('Found 3 item(s) in /Users/test/Downloads');
237
+ expect(result).toContain('[D] src');
238
+ expect(result).toContain('[F] report.pdf');
239
+ expect(result).toContain('2024-01-20 14:30');
240
+ expect(result).toContain('2.3 MB');
241
+ });
242
+
243
+ it('should show sorting and limit info in header', () => {
244
+ const files = [
245
+ { isDirectory: false, name: 'file1.txt' },
246
+ { isDirectory: false, name: 'file2.txt' },
247
+ ];
248
+ const result = formatFileList({
249
+ directory: '/test',
250
+ files,
251
+ sortBy: 'modifiedTime',
252
+ sortOrder: 'desc',
253
+ totalCount: 150,
254
+ });
255
+ expect(result).toContain('Found 150 item(s)');
256
+ expect(result).toContain('showing first 2');
257
+ expect(result).toContain('sorted by modifiedTime desc');
258
+ });
259
+
260
+ it('should not show limit info when not truncated', () => {
261
+ const files = [
262
+ { isDirectory: false, name: 'file1.txt' },
263
+ { isDirectory: false, name: 'file2.txt' },
264
+ ];
265
+ const result = formatFileList({
266
+ directory: '/test',
267
+ files,
268
+ sortBy: 'name',
269
+ sortOrder: 'asc',
270
+ });
271
+ expect(result).not.toContain('showing');
272
+ expect(result).toContain('sorted by name asc');
273
+ });
274
+
275
+ it('should show -- for directory size', () => {
276
+ const files = [
277
+ {
278
+ isDirectory: true,
279
+ modifiedTime: new Date('2024-01-20T14:30:00'),
280
+ name: 'my_folder',
281
+ size: 4096,
282
+ },
283
+ ];
284
+ const result = formatFileList({ directory: '/test', files });
285
+ expect(result).toContain('--');
286
+ });
287
+
288
+ it('should format various file sizes correctly', () => {
289
+ const files = [
290
+ { isDirectory: false, modifiedTime: new Date('2024-01-01'), name: 'zero.txt', size: 0 },
291
+ { isDirectory: false, modifiedTime: new Date('2024-01-01'), name: 'bytes.txt', size: 500 },
292
+ { isDirectory: false, modifiedTime: new Date('2024-01-01'), name: 'kb.txt', size: 2048 },
293
+ { isDirectory: false, modifiedTime: new Date('2024-01-01'), name: 'mb.txt', size: 5242880 },
294
+ {
295
+ isDirectory: false,
296
+ modifiedTime: new Date('2024-01-01'),
297
+ name: 'gb.txt',
298
+ size: 1073741824,
299
+ },
300
+ ];
301
+ const result = formatFileList({ directory: '/test', files });
302
+ expect(result).toContain('0 B');
303
+ expect(result).toContain('500 B');
304
+ expect(result).toContain('2 KB');
305
+ expect(result).toContain('5 MB');
306
+ expect(result).toContain('1 GB');
307
+ });
308
+
309
+ it('should handle files with only size (no modifiedTime)', () => {
310
+ const files = [{ isDirectory: false, name: 'file.txt', size: 1024 }];
311
+ const result = formatFileList({ directory: '/test', files });
312
+ expect(result).toContain('[F] file.txt');
313
+ expect(result).toContain('1 KB');
314
+ });
315
+
316
+ it('should handle files with only modifiedTime (no size)', () => {
317
+ const files = [
318
+ { isDirectory: false, modifiedTime: new Date('2024-06-15T10:30:00'), name: 'file.txt' },
319
+ ];
320
+ const result = formatFileList({ directory: '/test', files });
321
+ expect(result).toContain('[F] file.txt');
322
+ expect(result).toContain('2024-06-15 10:30');
323
+ });
324
+
325
+ it('should handle long file names', () => {
326
+ const files = [
327
+ {
328
+ isDirectory: false,
329
+ modifiedTime: new Date('2024-01-01'),
330
+ name: 'this_is_a_very_long_filename_that_exceeds_normal_length.txt',
331
+ size: 1024,
332
+ },
333
+ ];
334
+ const result = formatFileList({ directory: '/test', files });
335
+ expect(result).toContain('this_is_a_very_long_filename_that_exceeds_normal_length.txt');
336
+ });
337
+
338
+ it('should handle options with only totalCount', () => {
339
+ const files = [{ isDirectory: false, name: 'file.txt' }];
340
+ const result = formatFileList({ directory: '/test', files, totalCount: 100 });
341
+ expect(result).toContain('Found 100 item(s)');
342
+ expect(result).toContain('showing first 1');
343
+ });
344
+
345
+ it('should not show extra info when totalCount equals file count', () => {
346
+ const files = [
347
+ { isDirectory: false, name: 'file1.txt' },
348
+ { isDirectory: false, name: 'file2.txt' },
349
+ ];
350
+ const result = formatFileList({ directory: '/test', files, totalCount: 2 });
351
+ expect(result).not.toContain('showing');
352
+ });
353
+ });
62
354
  });