@nextclaw/ui 0.15.5 → 0.15.7

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 (169) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/assets/api-V8m62-13.js +13 -0
  3. package/dist/assets/app-presenter-provider-Bzj9h_ZS.js +106 -0
  4. package/dist/assets/{appearance-settings-page-80fLa4-9.js → appearance-settings-page-DwBwu1AU.js} +1 -1
  5. package/dist/assets/{book-open-CN7Shpij.js → book-open-DtVSr5O9.js} +1 -1
  6. package/dist/assets/{channels-list-page-R7M6pzp6.js → channels-list-page-CYIqMliJ.js} +2 -2
  7. package/dist/assets/{chat-page-152YuwfU.js → chat-page-BC0icTtI.js} +1 -1
  8. package/dist/assets/{config-split-page-BlfvfmTm.js → config-split-page-B88prd8s.js} +1 -1
  9. package/dist/assets/{confirm-dialog-Dt8Kdbgp.js → confirm-dialog-BB09IC2u.js} +1 -1
  10. package/dist/assets/desktop-update-config-B-q0OhxO.js +1 -0
  11. package/dist/assets/{dist-D5ycbwCo.js → dist-Bytser-a.js} +1 -1
  12. package/dist/assets/{doc-browser-C9u7N9vH.js → doc-browser-B1FXvZm6.js} +1 -1
  13. package/dist/assets/doc-browser-Bodhf0fj2.js +1 -0
  14. package/dist/assets/{doc-browser-context-CCdW9TJf.js → doc-browser-context-D5VLNoj3.js} +1 -1
  15. package/dist/assets/{download-CyPgglKr.js → download-CdO6NoHj.js} +1 -1
  16. package/dist/assets/{external-link-BahW1sl3.js → external-link-C5NFoh5I.js} +1 -1
  17. package/dist/assets/{eye-D8gGXMkD.js → eye-B51oNRRc.js} +1 -1
  18. package/dist/assets/{folder-DGWqFDvE.js → folder-DxH7Nef-.js} +1 -1
  19. package/dist/assets/{form-actions-D2imZLnU.js → form-actions-C9pUwtYV.js} +1 -1
  20. package/dist/assets/index-DtWf7kk9.css +1 -0
  21. package/dist/assets/index-UnY4HtVa.js +2 -0
  22. package/dist/assets/loader-circle-BRSfaztA.js +1 -0
  23. package/dist/assets/{logo-badge-CuPetu-v.js → logo-badge-D2vORX3a.js} +1 -1
  24. package/dist/assets/mcp-marketplace-page-CGVu7mfL.js +9 -0
  25. package/dist/assets/mcp-marketplace-page-DnV0e1JS.js +1 -0
  26. package/dist/assets/{model-config-page-DiJcvBML.js → model-config-page-DYpchGVE.js} +1 -1
  27. package/dist/assets/plus-BgMBV9HJ.js +1 -0
  28. package/dist/assets/{popover-Btp7KVc3.js → popover-CONujDHf.js} +1 -1
  29. package/dist/assets/{provider-scoped-model-input-DZMtoAJG.js → provider-scoped-model-input-DA3Ua9v8.js} +1 -1
  30. package/dist/assets/providers-config-page-DXsoyMrh.js +1 -0
  31. package/dist/assets/{react-BY-U3h3t.js → react-Io6Pfq8D.js} +1 -1
  32. package/dist/assets/{refresh-cw-Cme5qFej.js → refresh-cw-C6j2tjul.js} +1 -1
  33. package/dist/assets/remote-C1y3ARHu.js +1 -0
  34. package/dist/assets/rotate-cw-Bv2B6fAV.js +1 -0
  35. package/dist/assets/runtime-config-page-eYrrKBrA.js +1 -0
  36. package/dist/assets/{save-DNg2yJTS.js → save-CBBZVvZ-.js} +1 -1
  37. package/dist/assets/{search-3ykfrAFt.js → search-DFF842z2.js} +1 -1
  38. package/dist/assets/{search-config-page-DrVKa_Ls.js → search-config-page-DUyuwD7d.js} +1 -1
  39. package/dist/assets/{secrets-config-page-B39uQ5kF.js → secrets-config-page-DwzKQJ76.js} +2 -2
  40. package/dist/assets/security-config-D8me3pXl.js +1 -0
  41. package/dist/assets/{select-B5EecXJt.js → select-BiOpo2kV.js} +1 -1
  42. package/dist/assets/{setting-row-Qv1Ps79v.js → setting-row-C3WQXLtS.js} +1 -1
  43. package/dist/assets/{settings-2-D-3um9Az.js → settings-2-DyOvA_6K.js} +1 -1
  44. package/dist/assets/{skeleton-BgkzQd4U.js → skeleton-CQIAEbA4.js} +1 -1
  45. package/dist/assets/{tabs-BVD8gp3y.js → tabs-BTSENlI2.js} +1 -1
  46. package/dist/assets/{tag-chip-0qn-f3Bo.js → tag-chip-BG5JIGwY.js} +1 -1
  47. package/dist/assets/{trash-2-cnsMppRC.js → trash-2-CTIfwpt1.js} +1 -1
  48. package/dist/assets/use-config-CY4uF2Ou.js +1 -0
  49. package/dist/assets/{use-confirm-dialog-C_o0eQeH.js → use-confirm-dialog-Bj4u78F9.js} +1 -1
  50. package/dist/assets/{use-viewport-layout-BWav4KCL.js → use-viewport-layout-ChJUcSQ6.js} +1 -1
  51. package/dist/assets/x-C6cM8wkT.js +1 -0
  52. package/dist/index.html +30 -29
  53. package/index.html +1 -0
  54. package/package.json +8 -8
  55. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +97 -3
  56. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +13 -2
  57. package/src/features/chat/components/layout/__tests__/chat-sidebar-test.utils.ts +22 -0
  58. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +13 -24
  59. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +16 -4
  60. package/src/features/chat/components/layout/chat-sidebar.tsx +38 -2
  61. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +1 -0
  62. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +70 -2
  63. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +22 -0
  64. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +8 -9
  65. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-input-state.test.tsx +55 -0
  66. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +12 -11
  67. package/src/features/chat/features/conversation/hooks/use-session-conversation-preference-actions.ts +94 -0
  68. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +82 -0
  69. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +187 -63
  70. package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +225 -0
  71. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +23 -0
  72. package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +6 -1
  73. package/src/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils.ts +357 -0
  74. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +4 -1
  75. package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +26 -0
  76. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +67 -5
  77. package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +4 -10
  78. package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +29 -0
  79. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +83 -8
  80. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +1 -0
  81. package/src/features/chat/features/message/components/__tests__/chat-inline-file-preview.test.tsx +159 -0
  82. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +92 -26
  83. package/src/features/chat/features/message/components/chat-inline-file-preview.tsx +90 -21
  84. package/src/features/chat/features/message/components/chat-message-list.container.tsx +24 -3
  85. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +1 -0
  86. package/src/features/chat/features/ncp/hooks/__tests__/use-hydrated-ncp-agent.test.tsx +96 -3
  87. package/src/features/chat/features/project/components/__tests__/chat-project-create-dialog.test.tsx +67 -0
  88. package/src/features/chat/features/project/components/chat-project-create-dialog.tsx +203 -0
  89. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +7 -7
  90. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-dialog.test.tsx +47 -0
  91. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +6 -3
  92. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -0
  93. package/src/features/chat/features/session/components/session-header/chat-session-project-dialog.tsx +3 -0
  94. package/src/features/chat/features/session/components/session-header/chat-session-switcher-groups.tsx +245 -0
  95. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +82 -128
  96. package/src/features/chat/features/session/utils/__tests__/chat-sidebar-session-groups.utils.test.ts +22 -0
  97. package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +6 -6
  98. package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +12 -1
  99. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +3 -0
  100. package/src/features/chat/features/welcome/components/chat-welcome.tsx +1 -0
  101. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +20 -0
  102. package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +15 -2
  103. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +5 -6
  104. package/src/features/chat/features/workspace/components/__tests__/workspace-file-content-preview.test.tsx +67 -0
  105. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +9 -7
  106. package/src/features/chat/features/workspace/components/workspace-file-content-preview.tsx +95 -4
  107. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +23 -0
  108. package/src/features/marketplace/components/__tests__/marketplace-page-detail.test.tsx +1 -0
  109. package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +62 -0
  110. package/src/features/marketplace/components/curated-shelves/__tests__/marketplace-curated-scene-route.test.tsx +1 -0
  111. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +2 -15
  112. package/src/features/marketplace/components/marketplace-page.tsx +10 -2
  113. package/src/features/marketplace/hooks/__tests__/use-marketplace.test.tsx +26 -1
  114. package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +11 -0
  115. package/src/features/marketplace/hooks/use-marketplace-list-model.ts +1 -1
  116. package/src/features/marketplace/hooks/use-marketplace.ts +28 -1
  117. package/src/features/settings/components/config/provider-form.tsx +0 -4
  118. package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +7 -9
  119. package/src/features/settings/utils/provider-form-model.utils.ts +0 -4
  120. package/src/features/system-status/hooks/use-current-version-release-notes-link.ts +33 -0
  121. package/src/features/system-status/index.ts +9 -0
  122. package/src/features/system-status/utils/update-release-notes.utils.ts +44 -3
  123. package/src/index.css +10 -16
  124. package/src/shared/components/common/__tests__/brand-header.test.tsx +110 -3
  125. package/src/shared/components/common/brand-header.tsx +121 -35
  126. package/src/shared/components/path-picker/__tests__/server-path-picker-dialog.test.tsx +288 -38
  127. package/src/shared/components/path-picker/server-path-picker-dialog.tsx +290 -199
  128. package/src/shared/components/path-picker/server-path-picker-directory-list.tsx +103 -0
  129. package/src/shared/components/path-picker/server-path-picker-footer.tsx +58 -0
  130. package/src/shared/components/path-picker/server-path-picker-locations.tsx +148 -0
  131. package/src/shared/components/path-picker/server-path-picker-navigation-bar.tsx +198 -0
  132. package/src/shared/components/path-picker/server-path-picker-new-folder.tsx +59 -0
  133. package/src/shared/components/path-picker/server-path-picker-toolbar.tsx +25 -0
  134. package/src/shared/hooks/use-projects.ts +28 -0
  135. package/src/shared/hooks/use-server-path-create-directory.ts +6 -0
  136. package/src/shared/hooks/use-server-path-search.ts +40 -0
  137. package/src/shared/lib/api/index.ts +1 -0
  138. package/src/shared/lib/api/projects/project.utils.ts +14 -0
  139. package/src/shared/lib/api/types.ts +27 -0
  140. package/src/shared/lib/api/utils/server-path.utils.ts +21 -1
  141. package/src/shared/lib/i18n/locales/en-US/chat.json +40 -2
  142. package/src/shared/lib/i18n/locales/en-US/core.json +2 -3
  143. package/src/shared/lib/i18n/locales/en-US/desktop-update.json +5 -1
  144. package/src/shared/lib/i18n/locales/en-US/marketplace.json +1 -0
  145. package/src/shared/lib/i18n/locales/en-US/path-picker.json +25 -0
  146. package/src/shared/lib/i18n/locales/zh-CN/chat.json +40 -2
  147. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -3
  148. package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +5 -1
  149. package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +1 -0
  150. package/src/shared/lib/i18n/locales/zh-CN/path-picker.json +25 -0
  151. package/dist/assets/api-IlX_CbwU.js +0 -13
  152. package/dist/assets/app-presenter-provider-Bbl_MVbg.js +0 -106
  153. package/dist/assets/desktop-update-config-DreMJu9z.js +0 -1
  154. package/dist/assets/doc-browser-DJQ5XPsi2.js +0 -1
  155. package/dist/assets/index-0av6WO9-.css +0 -1
  156. package/dist/assets/index-CT0iYlxV.js +0 -2
  157. package/dist/assets/loader-circle-DtKek-qG.js +0 -1
  158. package/dist/assets/mcp-marketplace-page-4qMwFDpz.js +0 -1
  159. package/dist/assets/mcp-marketplace-page-BbxN5aZF.js +0 -9
  160. package/dist/assets/plus-BwgE-Zwf.js +0 -1
  161. package/dist/assets/providers-config-page-DfiWveEF.js +0 -1
  162. package/dist/assets/remote-BaVECEZw.js +0 -1
  163. package/dist/assets/rotate-cw-CMRZxIkH.js +0 -1
  164. package/dist/assets/runtime-config-page-wygwvMB4.js +0 -1
  165. package/dist/assets/security-config-BPzet76z.js +0 -1
  166. package/dist/assets/use-config-D6yx0lb0.js +0 -1
  167. package/dist/assets/x-B3o1dWR5.js +0 -1
  168. package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +0 -77
  169. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +0 -144
@@ -5,6 +5,9 @@ import { setLanguage } from '@/shared/lib/i18n';
5
5
 
6
6
  const mocks = vi.hoisted(() => ({
7
7
  useServerPathBrowse: vi.fn(),
8
+ useServerPathCreateDirectory: vi.fn(),
9
+ createDirectory: vi.fn(),
10
+ resetCreateDirectory: vi.fn(),
8
11
  refetch: vi.fn(),
9
12
  }));
10
13
 
@@ -12,21 +15,75 @@ vi.mock('@/shared/hooks/use-server-path-browse', () => ({
12
15
  useServerPathBrowse: mocks.useServerPathBrowse,
13
16
  }));
14
17
 
18
+ vi.mock('@/shared/hooks/use-server-path-create-directory', () => ({
19
+ useServerPathCreateDirectory: mocks.useServerPathCreateDirectory,
20
+ }));
21
+
22
+ type PickerOverrides = {
23
+ currentPath?: string;
24
+ defaultWorkspacePath?: string;
25
+ description?: string;
26
+ open?: boolean;
27
+ };
28
+
29
+ const createPickerElement = (overrides?: PickerOverrides) => (
30
+ <ServerPathPickerDialog
31
+ open={overrides?.open ?? true}
32
+ currentPath={overrides?.currentPath ?? '/workspace'}
33
+ defaultWorkspacePath={overrides?.defaultWorkspacePath}
34
+ isSaving={false}
35
+ onOpenChange={() => {}}
36
+ onConfirm={() => {}}
37
+ title="选择目录"
38
+ description={overrides?.description ?? '选择一个目录'}
39
+ pathLabel="目录"
40
+ confirmLabel="确定"
41
+ />
42
+ );
43
+
44
+ const renderPicker = (overrides?: PickerOverrides) => render(createPickerElement(overrides));
45
+
15
46
  describe('ServerPathPickerDialog', () => {
16
47
  beforeEach(() => {
17
48
  setLanguage('zh');
18
49
  mocks.refetch.mockReset();
50
+ mocks.refetch.mockResolvedValue(undefined);
51
+ mocks.createDirectory.mockReset();
52
+ mocks.createDirectory.mockResolvedValue({ path: '/workspace/new-folder' });
53
+ mocks.resetCreateDirectory.mockReset();
54
+ mocks.useServerPathCreateDirectory.mockReturnValue({
55
+ mutateAsync: mocks.createDirectory,
56
+ reset: mocks.resetCreateDirectory,
57
+ isPending: false,
58
+ error: null,
59
+ });
19
60
  mocks.useServerPathBrowse.mockReturnValue({
20
61
  data: {
21
62
  currentPath: '/workspace',
22
63
  homePath: '/Users/peiwang',
23
64
  parentPath: '/',
24
- breadcrumbs: [
25
- { label: 'workspace', path: '/workspace' },
26
- ],
65
+ breadcrumbs: [{ label: 'workspace', path: '/workspace' }],
27
66
  entries: [
28
- { name: 'playground', path: '/workspace/playground' },
29
- { name: 'nextbot', path: '/workspace/nextbot' },
67
+ {
68
+ name: 'playground',
69
+ path: '/workspace/playground',
70
+ kind: 'directory',
71
+ hidden: false,
72
+ },
73
+ {
74
+ name: 'nextbot',
75
+ path: '/workspace/nextbot',
76
+ kind: 'directory',
77
+ hidden: false,
78
+ },
79
+ ],
80
+ locations: [
81
+ { kind: 'desktop', path: '/Users/peiwang/Desktop' },
82
+ { kind: 'documents', path: '/Users/peiwang/Documents' },
83
+ { kind: 'downloads', path: '/Users/peiwang/Downloads' },
84
+ { kind: 'icloud-drive', path: '/Users/peiwang/Library/Mobile Documents/com~apple~CloudDocs' },
85
+ { kind: 'applications', path: '/Applications' },
86
+ { kind: 'volumes', path: '/Volumes' },
30
87
  ],
31
88
  },
32
89
  isLoading: false,
@@ -37,55 +94,248 @@ describe('ServerPathPickerDialog', () => {
37
94
 
38
95
  it('filters entries inside the current directory', async () => {
39
96
  const user = userEvent.setup();
40
-
41
- render(
42
- <ServerPathPickerDialog
43
- open
44
- currentPath="/workspace"
45
- isSaving={false}
46
- onOpenChange={() => {}}
47
- onConfirm={() => {}}
48
- title="选择目录"
49
- description="选择一个目录"
50
- pathLabel="目录"
51
- confirmLabel="确定"
52
- />
53
- );
97
+ renderPicker();
54
98
 
55
99
  expect(screen.getByText('playground')).toBeTruthy();
56
100
  expect(screen.getByText('nextbot')).toBeTruthy();
57
101
 
58
- await user.type(screen.getByPlaceholderText('搜索当前目录'), 'play');
102
+ await user.type(screen.getAllByPlaceholderText('搜索当前目录')[0], 'play');
59
103
 
60
104
  expect(screen.getByText('playground')).toBeTruthy();
61
105
  expect(screen.queryByText('nextbot')).toBeNull();
62
106
  });
63
107
 
64
- it('shows the empty-search hint and keeps the list region scroll-contained', async () => {
108
+ it('shows server-backed Mac locations and opens sidebar locations with one click', async () => {
65
109
  const user = userEvent.setup();
66
- render(
67
- <ServerPathPickerDialog
68
- open
69
- currentPath="/workspace"
70
- isSaving={false}
71
- onOpenChange={() => {}}
72
- onConfirm={() => {}}
73
- title="选择目录"
74
- description="选择一个目录"
75
- pathLabel="目录"
76
- confirmLabel="确定"
77
- />
110
+ renderPicker();
111
+
112
+ expect(screen.getByRole('button', { name: '桌面' })).toBeTruthy();
113
+ expect(screen.getByRole('button', { name: '文稿' })).toBeTruthy();
114
+ expect(screen.getByRole('button', { name: '下载' })).toBeTruthy();
115
+ expect(screen.getByRole('button', { name: 'iCloud 云盘' })).toBeTruthy();
116
+ expect(screen.getByRole('button', { name: '应用程序' })).toBeTruthy();
117
+ expect(screen.getByRole('button', { name: '磁盘与卷' })).toBeTruthy();
118
+
119
+ await user.click(screen.getByRole('button', { name: '下载' }));
120
+
121
+ expect(mocks.useServerPathBrowse).toHaveBeenLastCalledWith({
122
+ path: '/Users/peiwang/Downloads',
123
+ enabled: true,
124
+ });
125
+ });
126
+
127
+ it('keeps the explicit current path when an older home browse result is still visible', () => {
128
+ mocks.useServerPathBrowse.mockReturnValue({
129
+ data: {
130
+ currentPath: '/Users/peiwang',
131
+ homePath: '/Users/peiwang',
132
+ parentPath: '/Users',
133
+ breadcrumbs: [
134
+ { label: 'Users', path: '/Users' },
135
+ { label: 'peiwang', path: '/Users/peiwang' },
136
+ ],
137
+ entries: [],
138
+ },
139
+ isLoading: false,
140
+ error: null,
141
+ refetch: mocks.refetch,
142
+ });
143
+
144
+ renderPicker({ currentPath: '/Users/peiwang/.nextclaw/workspace' });
145
+
146
+ expect((screen.getByLabelText('目录') as HTMLInputElement).value).toBe(
147
+ '/Users/peiwang/.nextclaw/workspace',
78
148
  );
149
+ expect(screen.getByText('/Users/peiwang/.nextclaw/workspace')).toBeTruthy();
150
+ });
151
+
152
+ it('starts at the workdir and keeps the canonical address and selected folder aligned', () => {
153
+ mocks.useServerPathBrowse.mockReturnValue({
154
+ data: {
155
+ currentPath: '/Users/peiwang/.nextclaw/workspace',
156
+ homePath: '/Users/peiwang',
157
+ parentPath: '/Users/peiwang/.nextclaw',
158
+ breadcrumbs: [
159
+ { label: '/', path: '/' },
160
+ { label: 'Users', path: '/Users' },
161
+ { label: 'peiwang', path: '/Users/peiwang' },
162
+ { label: '.nextclaw', path: '/Users/peiwang/.nextclaw' },
163
+ { label: 'workspace', path: '/Users/peiwang/.nextclaw/workspace' },
164
+ ],
165
+ entries: [],
166
+ },
167
+ isLoading: false,
168
+ error: null,
169
+ refetch: mocks.refetch,
170
+ });
79
171
 
80
- await user.type(screen.getByPlaceholderText('搜索当前目录'), 'missing');
172
+ renderPicker({
173
+ currentPath: '',
174
+ defaultWorkspacePath: '~/.nextclaw/workspace',
175
+ description: undefined,
176
+ });
81
177
 
82
- expect(screen.getByText('当前目录下没有匹配结果。')).toBeTruthy();
178
+ expect(mocks.useServerPathBrowse).toHaveBeenLastCalledWith({
179
+ path: '~/.nextclaw/workspace',
180
+ enabled: true,
181
+ });
182
+ expect((screen.getByLabelText('目录') as HTMLInputElement).value).toBe(
183
+ '/Users/peiwang/.nextclaw/workspace',
184
+ );
185
+ expect(screen.getByRole('button', { name: 'NextClaw 工作区' })).toBeTruthy();
186
+ expect(screen.getByText('workspace')).toBeTruthy();
187
+ });
188
+
189
+ it('normalizes a home-relative workdir with Windows separators', () => {
190
+ mocks.useServerPathBrowse.mockReturnValue({
191
+ data: {
192
+ currentPath: 'C:\\Users\\alice\\.nextclaw\\workspace',
193
+ homePath: 'C:\\Users\\alice',
194
+ parentPath: 'C:\\Users\\alice\\.nextclaw',
195
+ breadcrumbs: [
196
+ { label: 'C:', path: 'C:\\' },
197
+ { label: 'Users', path: 'C:\\Users' },
198
+ { label: 'alice', path: 'C:\\Users\\alice' },
199
+ { label: '.nextclaw', path: 'C:\\Users\\alice\\.nextclaw' },
200
+ { label: 'workspace', path: 'C:\\Users\\alice\\.nextclaw\\workspace' },
201
+ ],
202
+ entries: [],
203
+ locations: [],
204
+ },
205
+ isLoading: false,
206
+ error: null,
207
+ refetch: mocks.refetch,
208
+ });
209
+
210
+ renderPicker({ currentPath: '', defaultWorkspacePath: '~/.nextclaw/workspace' });
211
+
212
+ expect((screen.getByLabelText('目录') as HTMLInputElement).value).toBe(
213
+ 'C:\\Users\\alice\\.nextclaw\\workspace',
214
+ );
215
+ expect(screen.getByText('workspace')).toBeTruthy();
216
+ });
217
+
218
+ it('creates a folder in the current directory and selects it without entering it', async () => {
219
+ const user = userEvent.setup();
220
+ renderPicker();
221
+
222
+ await user.click(screen.getByRole('button', { name: '新建文件夹' }));
223
+ await user.type(screen.getByLabelText('文件夹名称'), 'new-folder');
224
+ await user.click(screen.getByRole('button', { name: '创建' }));
225
+
226
+ expect(mocks.createDirectory).toHaveBeenCalledWith({
227
+ parentPath: '/workspace',
228
+ name: 'new-folder',
229
+ });
230
+ expect(mocks.refetch).toHaveBeenCalledOnce();
231
+ expect((screen.getByLabelText('目录') as HTMLInputElement).value).toBe(
232
+ '/workspace/new-folder',
233
+ );
234
+ expect((screen.getByRole('button', { name: '后退' }) as HTMLButtonElement).disabled).toBe(
235
+ true,
236
+ );
237
+ });
238
+
239
+ it('uses single click to select and double click to enter a folder', async () => {
240
+ const user = userEvent.setup();
241
+ renderPicker();
242
+ const playground = screen.getByRole('option', { name: /playground/ });
243
+
244
+ expect(screen.getByText('单击选择,双击进入')).toBeTruthy();
245
+
246
+ await user.click(playground);
247
+
248
+ expect((screen.getByLabelText('目录') as HTMLInputElement).value).toBe(
249
+ '/workspace/playground',
250
+ );
251
+ expect(mocks.useServerPathBrowse).toHaveBeenLastCalledWith({
252
+ path: '/workspace',
253
+ enabled: true,
254
+ });
255
+
256
+ await user.dblClick(playground);
257
+
258
+ expect(mocks.useServerPathBrowse).toHaveBeenLastCalledWith({
259
+ path: '/workspace/playground',
260
+ enabled: true,
261
+ });
262
+ expect((screen.getByRole('button', { name: '后退' }) as HTMLButtonElement).disabled).toBe(
263
+ false,
264
+ );
265
+ });
266
+
267
+ it('starts a fresh selection session when the dialog is reopened', async () => {
268
+ const user = userEvent.setup();
269
+ const view = renderPicker();
83
270
 
271
+ await user.click(screen.getByRole('option', { name: /playground/ }));
272
+ expect((screen.getByLabelText('目录') as HTMLInputElement).value).toBe(
273
+ '/workspace/playground',
274
+ );
275
+
276
+ view.rerender(createPickerElement({ open: false }));
277
+ view.rerender(createPickerElement());
278
+
279
+ expect((screen.getByLabelText('目录') as HTMLInputElement).value).toBe('/workspace');
280
+ expect((screen.getByRole('button', { name: '后退' }) as HTMLButtonElement).disabled).toBe(
281
+ true,
282
+ );
283
+ });
284
+
285
+ it('opens an editable address and records navigation history', async () => {
286
+ const user = userEvent.setup();
287
+ renderPicker();
288
+
289
+ await user.click(screen.getByRole('button', { name: '编辑地址' }));
290
+ const addressInput = screen.getByLabelText('地址');
291
+ await user.clear(addressInput);
292
+ await user.type(addressInput, '/workspace/playground{Enter}');
293
+
294
+ expect((screen.getByLabelText('目录') as HTMLInputElement).value).toBe(
295
+ '/workspace/playground',
296
+ );
297
+ expect((screen.getByRole('button', { name: '后退' }) as HTMLButtonElement).disabled).toBe(
298
+ false,
299
+ );
300
+
301
+ await user.click(screen.getByRole('button', { name: '后退' }));
302
+ expect((screen.getByLabelText('目录') as HTMLInputElement).value).toBe('/workspace');
303
+ expect((screen.getByRole('button', { name: '前进' }) as HTMLButtonElement).disabled).toBe(
304
+ false,
305
+ );
306
+ });
307
+
308
+ it('supports arrow selection and Enter navigation from the directory list', async () => {
309
+ const user = userEvent.setup();
310
+ renderPicker();
311
+ const playground = screen.getByRole('option', { name: /playground/ });
312
+
313
+ await user.click(playground);
314
+ await user.keyboard('{ArrowDown}');
315
+
316
+ expect((screen.getByLabelText('目录') as HTMLInputElement).value).toBe(
317
+ '/workspace/nextbot',
318
+ );
319
+ await user.keyboard('{Enter}');
320
+ expect(mocks.useServerPathBrowse).toHaveBeenLastCalledWith({
321
+ path: '/workspace/nextbot',
322
+ enabled: true,
323
+ });
324
+ });
325
+
326
+ it('shows the empty-search hint and keeps the list region scroll-contained', async () => {
327
+ const user = userEvent.setup();
328
+ renderPicker();
329
+
330
+ await user.type(screen.getAllByPlaceholderText('搜索当前目录')[0], 'missing');
331
+
332
+ expect(screen.getByText('当前目录下没有匹配结果。')).toBeTruthy();
84
333
  const dialog = screen.getByRole('dialog');
85
334
  expect(dialog.className).toContain('overflow-hidden');
86
- expect(dialog.className).toContain('sm:h-[42rem]');
87
-
88
- const scrollArea = screen.getByText('当前目录下没有匹配结果。').closest('.overflow-auto');
335
+ expect(dialog.className).toContain('sm:h-[44rem]');
336
+ const scrollArea = screen
337
+ .getByText('当前目录下没有匹配结果。')
338
+ .closest('.overflow-auto');
89
339
  expect(scrollArea?.className).toContain('flex-1');
90
340
  expect(scrollArea?.className).toContain('min-h-0');
91
341
  });