@nextclaw/ui 0.15.5 → 0.15.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 (147) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/assets/api-Dyb5rgh0.js +13 -0
  3. package/dist/assets/app-presenter-provider-7jV0nPiL.js +106 -0
  4. package/dist/assets/{appearance-settings-page-80fLa4-9.js → appearance-settings-page-DFm3BbEb.js} +1 -1
  5. package/dist/assets/{book-open-CN7Shpij.js → book-open-L6LCuqOr.js} +1 -1
  6. package/dist/assets/{channels-list-page-R7M6pzp6.js → channels-list-page-DOaPKNJA.js} +2 -2
  7. package/dist/assets/{chat-page-152YuwfU.js → chat-page-Cw9uDhD_.js} +1 -1
  8. package/dist/assets/{config-split-page-BlfvfmTm.js → config-split-page-CqlqjnCK.js} +1 -1
  9. package/dist/assets/{confirm-dialog-Dt8Kdbgp.js → confirm-dialog-bAp8NU93.js} +1 -1
  10. package/dist/assets/desktop-update-config-2SjMfCMl.js +1 -0
  11. package/dist/assets/{dist-D5ycbwCo.js → dist-CrtbGLlh.js} +1 -1
  12. package/dist/assets/doc-browser-A5IRyDXQ2.js +1 -0
  13. package/dist/assets/{doc-browser-C9u7N9vH.js → doc-browser-BmnQa30-.js} +1 -1
  14. package/dist/assets/{doc-browser-context-CCdW9TJf.js → doc-browser-context-BlpUUFrR.js} +1 -1
  15. package/dist/assets/{download-CyPgglKr.js → download-BhZZNXD8.js} +1 -1
  16. package/dist/assets/{external-link-BahW1sl3.js → external-link-B9Uoq5J7.js} +1 -1
  17. package/dist/assets/{eye-D8gGXMkD.js → eye-K5knauEM.js} +1 -1
  18. package/dist/assets/{folder-DGWqFDvE.js → folder-BIYZZWJv.js} +1 -1
  19. package/dist/assets/{form-actions-D2imZLnU.js → form-actions-CKprWFeM.js} +1 -1
  20. package/dist/assets/index-BVn702m4.css +1 -0
  21. package/dist/assets/{index-CT0iYlxV.js → index-CFxJ1Fkg.js} +2 -2
  22. package/dist/assets/loader-circle-Bj2RMrCR.js +1 -0
  23. package/dist/assets/{logo-badge-CuPetu-v.js → logo-badge-BQ-XFZod.js} +1 -1
  24. package/dist/assets/mcp-marketplace-page-BfA7A3n4.js +9 -0
  25. package/dist/assets/mcp-marketplace-page-CXHYO95j.js +1 -0
  26. package/dist/assets/{model-config-page-DiJcvBML.js → model-config-page-BbWbW_qM.js} +1 -1
  27. package/dist/assets/plus-TmFdwO3P.js +1 -0
  28. package/dist/assets/{popover-Btp7KVc3.js → popover-CX9zFsuc.js} +1 -1
  29. package/dist/assets/{provider-scoped-model-input-DZMtoAJG.js → provider-scoped-model-input-B-9_DtIP.js} +1 -1
  30. package/dist/assets/providers-config-page-xwIjGur2.js +1 -0
  31. package/dist/assets/{react-BY-U3h3t.js → react-DsgudwpT.js} +1 -1
  32. package/dist/assets/{refresh-cw-Cme5qFej.js → refresh-cw-kXdvbhFf.js} +1 -1
  33. package/dist/assets/remote-B9F2uCk9.js +1 -0
  34. package/dist/assets/rotate-cw-DEc6aihr.js +1 -0
  35. package/dist/assets/runtime-config-page-DTsi7czJ.js +1 -0
  36. package/dist/assets/{save-DNg2yJTS.js → save-C9qpz2_2.js} +1 -1
  37. package/dist/assets/{search-3ykfrAFt.js → search-_kEi29qo.js} +1 -1
  38. package/dist/assets/{search-config-page-DrVKa_Ls.js → search-config-page-H7ug8f4D.js} +1 -1
  39. package/dist/assets/{secrets-config-page-B39uQ5kF.js → secrets-config-page-C1_6rINu.js} +2 -2
  40. package/dist/assets/security-config-Cr2ERKLu.js +1 -0
  41. package/dist/assets/{select-B5EecXJt.js → select-MJKhKAWR.js} +1 -1
  42. package/dist/assets/{setting-row-Qv1Ps79v.js → setting-row-DA3tJuew.js} +1 -1
  43. package/dist/assets/{settings-2-D-3um9Az.js → settings-2-D_y7zXJz.js} +1 -1
  44. package/dist/assets/{skeleton-BgkzQd4U.js → skeleton-0uTLUhm9.js} +1 -1
  45. package/dist/assets/{tabs-BVD8gp3y.js → tabs-h44XPocd.js} +1 -1
  46. package/dist/assets/{tag-chip-0qn-f3Bo.js → tag-chip-bRZ04q3C.js} +1 -1
  47. package/dist/assets/{trash-2-cnsMppRC.js → trash-2-BRpSE-Wt.js} +1 -1
  48. package/dist/assets/use-config-CwDEAl6f.js +1 -0
  49. package/dist/assets/{use-confirm-dialog-C_o0eQeH.js → use-confirm-dialog-BbnWw-8c.js} +1 -1
  50. package/dist/assets/{use-viewport-layout-BWav4KCL.js → use-viewport-layout-CqgIMDQL.js} +1 -1
  51. package/dist/assets/x-DdEDL6ju.js +1 -0
  52. package/dist/index.html +29 -29
  53. package/package.json +8 -8
  54. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +84 -1
  55. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +13 -2
  56. package/src/features/chat/components/layout/__tests__/chat-sidebar-test.utils.ts +22 -0
  57. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +13 -24
  58. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +16 -4
  59. package/src/features/chat/components/layout/chat-sidebar.tsx +38 -2
  60. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +1 -0
  61. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +52 -2
  62. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +22 -0
  63. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +8 -9
  64. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-input-state.test.tsx +55 -0
  65. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +12 -11
  66. package/src/features/chat/features/conversation/hooks/use-session-conversation-preference-actions.ts +94 -0
  67. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +37 -0
  68. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +140 -63
  69. package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +175 -0
  70. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +23 -0
  71. package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +8 -1
  72. package/src/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils.ts +274 -0
  73. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +4 -1
  74. package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +26 -0
  75. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +50 -4
  76. package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +4 -10
  77. package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +29 -0
  78. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +66 -8
  79. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +1 -0
  80. package/src/features/chat/features/ncp/hooks/__tests__/use-hydrated-ncp-agent.test.tsx +41 -3
  81. package/src/features/chat/features/project/components/__tests__/chat-project-create-dialog.test.tsx +67 -0
  82. package/src/features/chat/features/project/components/chat-project-create-dialog.tsx +203 -0
  83. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +7 -7
  84. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-dialog.test.tsx +47 -0
  85. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +6 -3
  86. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -0
  87. package/src/features/chat/features/session/components/session-header/chat-session-project-dialog.tsx +3 -0
  88. package/src/features/chat/features/session/components/session-header/chat-session-switcher-groups.tsx +245 -0
  89. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +81 -127
  90. package/src/features/chat/features/session/utils/__tests__/chat-sidebar-session-groups.utils.test.ts +22 -0
  91. package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +6 -6
  92. package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +12 -1
  93. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +3 -0
  94. package/src/features/chat/features/welcome/components/chat-welcome.tsx +1 -0
  95. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +20 -0
  96. package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +15 -2
  97. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +23 -0
  98. package/src/features/settings/components/config/provider-form.tsx +0 -4
  99. package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +7 -9
  100. package/src/features/settings/utils/provider-form-model.utils.ts +0 -4
  101. package/src/features/system-status/hooks/use-current-version-release-notes-link.ts +33 -0
  102. package/src/features/system-status/index.ts +9 -0
  103. package/src/features/system-status/utils/update-release-notes.utils.ts +44 -3
  104. package/src/index.css +10 -16
  105. package/src/shared/components/common/__tests__/brand-header.test.tsx +110 -3
  106. package/src/shared/components/common/brand-header.tsx +121 -35
  107. package/src/shared/components/path-picker/__tests__/server-path-picker-dialog.test.tsx +288 -38
  108. package/src/shared/components/path-picker/server-path-picker-dialog.tsx +290 -199
  109. package/src/shared/components/path-picker/server-path-picker-directory-list.tsx +103 -0
  110. package/src/shared/components/path-picker/server-path-picker-footer.tsx +58 -0
  111. package/src/shared/components/path-picker/server-path-picker-locations.tsx +148 -0
  112. package/src/shared/components/path-picker/server-path-picker-navigation-bar.tsx +198 -0
  113. package/src/shared/components/path-picker/server-path-picker-new-folder.tsx +59 -0
  114. package/src/shared/components/path-picker/server-path-picker-toolbar.tsx +25 -0
  115. package/src/shared/hooks/use-projects.ts +28 -0
  116. package/src/shared/hooks/use-server-path-create-directory.ts +6 -0
  117. package/src/shared/hooks/use-server-path-search.ts +40 -0
  118. package/src/shared/lib/api/index.ts +1 -0
  119. package/src/shared/lib/api/projects/project.utils.ts +14 -0
  120. package/src/shared/lib/api/types.ts +27 -0
  121. package/src/shared/lib/api/utils/server-path.utils.ts +21 -1
  122. package/src/shared/lib/i18n/locales/en-US/chat.json +35 -2
  123. package/src/shared/lib/i18n/locales/en-US/core.json +2 -3
  124. package/src/shared/lib/i18n/locales/en-US/desktop-update.json +4 -0
  125. package/src/shared/lib/i18n/locales/en-US/path-picker.json +25 -0
  126. package/src/shared/lib/i18n/locales/zh-CN/chat.json +35 -2
  127. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -3
  128. package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +4 -0
  129. package/src/shared/lib/i18n/locales/zh-CN/path-picker.json +25 -0
  130. package/dist/assets/api-IlX_CbwU.js +0 -13
  131. package/dist/assets/app-presenter-provider-Bbl_MVbg.js +0 -106
  132. package/dist/assets/desktop-update-config-DreMJu9z.js +0 -1
  133. package/dist/assets/doc-browser-DJQ5XPsi2.js +0 -1
  134. package/dist/assets/index-0av6WO9-.css +0 -1
  135. package/dist/assets/loader-circle-DtKek-qG.js +0 -1
  136. package/dist/assets/mcp-marketplace-page-4qMwFDpz.js +0 -1
  137. package/dist/assets/mcp-marketplace-page-BbxN5aZF.js +0 -9
  138. package/dist/assets/plus-BwgE-Zwf.js +0 -1
  139. package/dist/assets/providers-config-page-DfiWveEF.js +0 -1
  140. package/dist/assets/remote-BaVECEZw.js +0 -1
  141. package/dist/assets/rotate-cw-CMRZxIkH.js +0 -1
  142. package/dist/assets/runtime-config-page-wygwvMB4.js +0 -1
  143. package/dist/assets/security-config-BPzet76z.js +0 -1
  144. package/dist/assets/use-config-D6yx0lb0.js +0 -1
  145. package/dist/assets/x-B3o1dWR5.js +0 -1
  146. package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +0 -77
  147. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +0 -144
@@ -1,23 +1,27 @@
1
- import { useEffect, useMemo, useState } from 'react';
2
- import { ChevronRight, Folder, FolderUp, Home, RefreshCcw, Search } from 'lucide-react';
1
+ import { useMemo, useState } from 'react';
2
+ import { Search } from 'lucide-react';
3
3
  import { useServerPathBrowse } from '@/shared/hooks/use-server-path-browse';
4
- import { Button } from '@/shared/components/ui/button';
4
+ import { useServerPathCreateDirectory } from '@/shared/hooks/use-server-path-create-directory';
5
+ import { ServerPathPickerDirectoryList } from '@/shared/components/path-picker/server-path-picker-directory-list';
6
+ import { ServerPathPickerFooter } from '@/shared/components/path-picker/server-path-picker-footer';
7
+ import { ServerPathPickerLocations } from '@/shared/components/path-picker/server-path-picker-locations';
8
+ import { ServerPathPickerNavigationBar } from '@/shared/components/path-picker/server-path-picker-navigation-bar';
9
+ import { ServerPathPickerNewFolder } from '@/shared/components/path-picker/server-path-picker-new-folder';
10
+ import { ServerPathPickerToolbar } from '@/shared/components/path-picker/server-path-picker-toolbar';
5
11
  import {
6
12
  Dialog,
7
13
  DialogContent,
8
14
  DialogDescription,
9
- DialogFooter,
10
15
  DialogHeader,
11
16
  DialogTitle,
12
17
  } from '@/shared/components/ui/dialog';
13
18
  import { Input } from '@/shared/components/ui/input';
14
- import { Label } from '@/shared/components/ui/label';
15
- import { ScrollArea } from '@/shared/components/ui/scroll-area';
16
19
  import { t } from '@/shared/lib/i18n';
17
20
 
18
21
  type ServerPathPickerDialogProps = {
19
22
  open: boolean;
20
23
  currentPath?: string | null;
24
+ defaultWorkspacePath?: string | null;
21
25
  isSaving: boolean;
22
26
  onOpenChange: (open: boolean) => void;
23
27
  onConfirm: (path: string) => Promise<void> | void;
@@ -29,9 +33,26 @@ type ServerPathPickerDialogProps = {
29
33
  hint?: string;
30
34
  };
31
35
 
36
+ function resolveHomeRelativePath(path: string, homePath?: string | null): string {
37
+ if (!homePath) {
38
+ return path;
39
+ }
40
+ if (path === '~') {
41
+ return homePath;
42
+ }
43
+ const windowsHome = homePath.includes('\\');
44
+ if (path.startsWith('~/') || (windowsHome && path.startsWith('~\\'))) {
45
+ const separator = windowsHome ? '\\' : '/';
46
+ const relativePath = path.slice(2).replaceAll(separator === '\\' ? '/' : '\\', separator);
47
+ return `${homePath.replace(/[\\/]+$/, '')}${separator}${relativePath}`;
48
+ }
49
+ return path;
50
+ }
51
+
32
52
  export function ServerPathPickerDialog({
33
53
  open,
34
54
  currentPath,
55
+ defaultWorkspacePath,
35
56
  isSaving,
36
57
  onOpenChange,
37
58
  onConfirm,
@@ -42,36 +63,101 @@ export function ServerPathPickerDialog({
42
63
  confirmLabel,
43
64
  hint,
44
65
  }: ServerPathPickerDialogProps) {
45
- const [draftPath, setDraftPath] = useState('');
46
- const [browsePath, setBrowsePath] = useState<string | null>(null);
47
- const [searchText, setSearchText] = useState('');
66
+ const normalizedCurrentPath = currentPath?.trim() ?? '';
67
+ const normalizedDefaultWorkspacePath = defaultWorkspacePath?.trim() ?? '';
68
+ const sessionKey = `${normalizedCurrentPath}\u0000${normalizedDefaultWorkspacePath}`;
48
69
 
49
- useEffect(() => {
50
- if (!open) {
51
- return;
52
- }
53
- const nextPath = currentPath?.trim() || null;
54
- setDraftPath(nextPath ?? '');
55
- setBrowsePath(nextPath);
56
- setSearchText('');
57
- }, [currentPath, open]);
70
+ return (
71
+ <Dialog
72
+ open={open}
73
+ onOpenChange={(nextOpen) => {
74
+ if (isSaving) {
75
+ return;
76
+ }
77
+ onOpenChange(nextOpen);
78
+ }}
79
+ >
80
+ <DialogContent className="max-h-[92vh] overflow-hidden p-0 sm:h-[44rem] sm:max-w-5xl sm:grid-rows-[auto_minmax(0,1fr)]">
81
+ <DialogHeader className="border-b border-border px-5 py-4 pr-12">
82
+ <DialogTitle>{title}</DialogTitle>
83
+ <DialogDescription className={description ? undefined : 'sr-only'}>
84
+ {description ?? title}
85
+ </DialogDescription>
86
+ </DialogHeader>
87
+
88
+ {open ? (
89
+ <ServerPathPickerSession
90
+ key={sessionKey}
91
+ currentPath={normalizedCurrentPath}
92
+ defaultWorkspacePath={normalizedDefaultWorkspacePath}
93
+ isSaving={isSaving}
94
+ onCancel={() => onOpenChange(false)}
95
+ onConfirm={onConfirm}
96
+ pathLabel={pathLabel}
97
+ pathPlaceholder={pathPlaceholder}
98
+ confirmLabel={confirmLabel}
99
+ hint={hint}
100
+ />
101
+ ) : null}
102
+ </DialogContent>
103
+ </Dialog>
104
+ );
105
+ }
106
+
107
+ type ServerPathPickerSessionProps = {
108
+ currentPath: string;
109
+ defaultWorkspacePath: string;
110
+ isSaving: boolean;
111
+ onCancel: () => void;
112
+ onConfirm: (path: string) => Promise<void> | void;
113
+ pathLabel: string;
114
+ pathPlaceholder?: string;
115
+ confirmLabel: string;
116
+ hint?: string;
117
+ };
118
+
119
+ function ServerPathPickerSession({
120
+ currentPath,
121
+ defaultWorkspacePath,
122
+ isSaving,
123
+ onCancel,
124
+ onConfirm,
125
+ pathLabel,
126
+ pathPlaceholder,
127
+ confirmLabel,
128
+ hint,
129
+ }: ServerPathPickerSessionProps) {
130
+ const initialPath = currentPath || defaultWorkspacePath;
131
+ const [selectedPath, setSelectedPath] = useState<string | null>(initialPath || null);
132
+ const [browsePath, setBrowsePath] = useState<string | null>(initialPath || null);
133
+ const [backPaths, setBackPaths] = useState<string[]>([]);
134
+ const [forwardPaths, setForwardPaths] = useState<string[]>([]);
135
+ const [newFolderName, setNewFolderName] = useState('');
136
+ const [newFolderVisible, setNewFolderVisible] = useState(false);
137
+ const [searchText, setSearchText] = useState('');
58
138
 
59
139
  const browseQuery = useServerPathBrowse({
60
140
  path: browsePath,
61
- enabled: open,
141
+ enabled: true,
62
142
  });
63
-
64
- useEffect(() => {
65
- if (!open || !browseQuery.data) {
66
- return;
67
- }
68
- if (draftPath.trim().length === 0) {
69
- setDraftPath(browseQuery.data.currentPath);
70
- }
71
- }, [browseQuery.data, draftPath, open]);
72
-
73
- const normalizedDraftPath = draftPath.trim();
74
- const submitDisabled = normalizedDraftPath.length === 0 || isSaving;
143
+ const createDirectoryMutation = useServerPathCreateDirectory();
144
+ const resolvedBrowsePath = resolveHomeRelativePath(
145
+ browsePath?.trim() ?? '',
146
+ browseQuery.data?.homePath,
147
+ );
148
+ const canonicalSelectedPath =
149
+ selectedPath === null
150
+ ? (browseQuery.data?.currentPath ?? '')
151
+ : selectedPath === browsePath && browseQuery.data?.currentPath === resolvedBrowsePath
152
+ ? browseQuery.data.currentPath
153
+ : selectedPath;
154
+ const normalizedSelectedPath = canonicalSelectedPath.trim();
155
+ const submitDisabled = normalizedSelectedPath.length === 0 || isSaving;
156
+ const addressPath = resolvedBrowsePath || browseQuery.data?.currentPath || '';
157
+ const visibleBreadcrumbs =
158
+ !browsePath || browseQuery.data?.currentPath === addressPath
159
+ ? (browseQuery.data?.breadcrumbs ?? [])
160
+ : [];
75
161
  const errorMessage = useMemo(() => {
76
162
  if (!browseQuery.error) {
77
163
  return null;
@@ -80,14 +166,25 @@ export function ServerPathPickerDialog({
80
166
  ? browseQuery.error.message
81
167
  : String(browseQuery.error);
82
168
  }, [browseQuery.error]);
169
+ const createDirectoryErrorMessage = useMemo(() => {
170
+ if (!createDirectoryMutation.error) {
171
+ return null;
172
+ }
173
+ return createDirectoryMutation.error instanceof Error
174
+ ? createDirectoryMutation.error.message
175
+ : String(createDirectoryMutation.error);
176
+ }, [createDirectoryMutation.error]);
83
177
 
84
178
  const normalizedSearchText = searchText.trim().toLowerCase();
179
+ const directoryEntries = useMemo(
180
+ () => (browseQuery.data?.entries ?? []).filter((entry) => entry.kind === 'directory'),
181
+ [browseQuery.data?.entries],
182
+ );
85
183
  const filteredEntries = useMemo(() => {
86
- const entries = browseQuery.data?.entries ?? [];
87
184
  if (normalizedSearchText.length === 0) {
88
- return entries;
185
+ return directoryEntries;
89
186
  }
90
- return entries.filter((entry) => {
187
+ return directoryEntries.filter((entry) => {
91
188
  const normalizedName = entry.name.toLowerCase();
92
189
  const normalizedPath = entry.path.toLowerCase();
93
190
  return (
@@ -95,188 +192,182 @@ export function ServerPathPickerDialog({
95
192
  normalizedPath.includes(normalizedSearchText)
96
193
  );
97
194
  });
98
- }, [browseQuery.data?.entries, normalizedSearchText]);
195
+ }, [directoryEntries, normalizedSearchText]);
99
196
 
100
- const navigateTo = (path: string | null) => {
197
+ const showPath = (path: string) => {
101
198
  setBrowsePath(path);
199
+ setSelectedPath(path);
102
200
  setSearchText('');
103
- if (path) {
104
- setDraftPath(path);
201
+ setNewFolderVisible(false);
202
+ setNewFolderName('');
203
+ createDirectoryMutation.reset();
204
+ };
205
+
206
+ const navigateTo = (path: string) => {
207
+ const nextPath = path.trim();
208
+ if (!nextPath) {
209
+ return;
210
+ }
211
+ const visiblePath = addressPath;
212
+ if (visiblePath && visiblePath !== nextPath) {
213
+ setBackPaths((paths) => [...paths, visiblePath].slice(-50));
214
+ }
215
+ setForwardPaths([]);
216
+ showPath(nextPath);
217
+ };
218
+
219
+ const navigateBack = () => {
220
+ const targetPath = backPaths.at(-1);
221
+ if (!targetPath) {
222
+ return;
223
+ }
224
+ const visiblePath = addressPath;
225
+ setBackPaths((paths) => paths.slice(0, -1));
226
+ if (visiblePath) {
227
+ setForwardPaths((paths) => [visiblePath, ...paths].slice(0, 50));
228
+ }
229
+ showPath(targetPath);
230
+ };
231
+
232
+ const navigateForward = () => {
233
+ const targetPath = forwardPaths[0];
234
+ if (!targetPath) {
235
+ return;
236
+ }
237
+ const visiblePath = addressPath;
238
+ setForwardPaths((paths) => paths.slice(1));
239
+ if (visiblePath) {
240
+ setBackPaths((paths) => [...paths, visiblePath].slice(-50));
105
241
  }
242
+ showPath(targetPath);
106
243
  };
107
244
 
245
+ const createDirectory = async () => {
246
+ const parentPath = browseQuery.data?.currentPath;
247
+ const name = newFolderName.trim();
248
+ if (!parentPath || !name || createDirectoryMutation.isPending) {
249
+ return;
250
+ }
251
+ try {
252
+ const created = await createDirectoryMutation.mutateAsync({ parentPath, name });
253
+ await browseQuery.refetch();
254
+ setSelectedPath(created.path);
255
+ setNewFolderName('');
256
+ setNewFolderVisible(false);
257
+ } catch {
258
+ // The mutation owns the user-visible error state rendered below the input.
259
+ }
260
+ };
261
+
262
+ const emptyMessage = browseQuery.isLoading
263
+ ? t('loading')
264
+ : errorMessage
265
+ ? `${t('pathBrowseFailed')}: ${errorMessage}`
266
+ : directoryEntries.length > 0
267
+ ? t('pathPickerSearchEmpty')
268
+ : t('emptyDirectory');
269
+ const navigationDisabled = isSaving || browseQuery.isLoading;
270
+
108
271
  return (
109
- <Dialog
110
- open={open}
111
- onOpenChange={(nextOpen) => {
112
- if (isSaving) {
272
+ <form
273
+ className="flex min-h-0 min-w-0 flex-col overflow-hidden"
274
+ onSubmit={(event) => {
275
+ event.preventDefault();
276
+ if (submitDisabled) {
113
277
  return;
114
278
  }
115
- onOpenChange(nextOpen);
279
+ void onConfirm(normalizedSelectedPath);
116
280
  }}
117
281
  >
118
- <DialogContent className="max-h-[85vh] overflow-hidden sm:h-[42rem] sm:max-w-2xl sm:grid-rows-[auto_minmax(0,1fr)]">
119
- <DialogHeader>
120
- <DialogTitle>{title}</DialogTitle>
121
- {description ? (
122
- <DialogDescription>{description}</DialogDescription>
123
- ) : null}
124
- </DialogHeader>
125
- <form
126
- className="flex min-h-0 flex-col gap-4"
127
- onSubmit={(event) => {
128
- event.preventDefault();
129
- if (submitDisabled) {
130
- return;
131
- }
132
- void onConfirm(normalizedDraftPath);
133
- }}
134
- >
135
- <div className="space-y-2">
136
- <Label htmlFor="server-path-picker-input">{pathLabel}</Label>
137
- <div className="flex gap-2">
138
- <Input
139
- id="server-path-picker-input"
140
- value={draftPath}
141
- onChange={(event) => setDraftPath(event.target.value)}
142
- placeholder={pathPlaceholder}
143
- autoFocus
144
- disabled={isSaving}
145
- />
146
- <Button
147
- type="button"
148
- variant="outline"
149
- onClick={() => navigateTo(normalizedDraftPath || null)}
150
- disabled={isSaving}
151
- >
152
- {t('openPath')}
153
- </Button>
154
- </div>
155
- </div>
156
-
157
- <div className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-2xl border border-gray-200 bg-gray-50/70">
158
- <div className="flex flex-wrap items-center gap-2 border-b border-gray-200 px-3 py-2">
159
- <Button
160
- type="button"
161
- variant="ghost"
162
- size="sm"
163
- onClick={() => navigateTo(browseQuery.data?.homePath ?? null)}
164
- disabled={isSaving || browseQuery.isLoading}
165
- >
166
- <Home className="mr-1 h-4 w-4" />
167
- {t('homeDirectory')}
168
- </Button>
169
- <Button
170
- type="button"
171
- variant="ghost"
172
- size="sm"
173
- onClick={() => navigateTo(browseQuery.data?.parentPath ?? null)}
174
- disabled={!browseQuery.data?.parentPath || isSaving || browseQuery.isLoading}
175
- >
176
- <FolderUp className="mr-1 h-4 w-4" />
177
- {t('parentDirectory')}
178
- </Button>
179
- <Button
180
- type="button"
181
- variant="ghost"
182
- size="sm"
183
- onClick={() => {
184
- void browseQuery.refetch();
185
- }}
186
- disabled={isSaving || browseQuery.isLoading}
187
- >
188
- <RefreshCcw className="mr-1 h-4 w-4" />
189
- {t('chatRefresh')}
190
- </Button>
191
- </div>
282
+ <ServerPathPickerNavigationBar
283
+ addressPath={addressPath}
284
+ breadcrumbs={visibleBreadcrumbs}
285
+ canGoBack={backPaths.length > 0}
286
+ canGoForward={forwardPaths.length > 0}
287
+ canGoUp={Boolean(browseQuery.data?.parentPath)}
288
+ disabled={navigationDisabled}
289
+ onBack={navigateBack}
290
+ onForward={navigateForward}
291
+ onNavigate={navigateTo}
292
+ onRefresh={() => void browseQuery.refetch()}
293
+ onSearchChange={setSearchText}
294
+ onUp={() => {
295
+ const parentPath = browseQuery.data?.parentPath;
296
+ if (parentPath) {
297
+ navigateTo(parentPath);
298
+ }
299
+ }}
300
+ searchText={searchText}
301
+ />
192
302
 
193
- <div className="border-b border-gray-200 px-3 py-2">
194
- <div className="mb-2 text-xs font-medium text-gray-500">
195
- {t('currentDirectory')}
196
- </div>
197
- <div className="flex flex-wrap items-center gap-1 text-xs text-gray-600">
198
- {browseQuery.data?.breadcrumbs.map((breadcrumb, index) => (
199
- <div key={breadcrumb.path} className="flex items-center gap-1">
200
- <button
201
- type="button"
202
- className="rounded px-1.5 py-0.5 hover:bg-gray-200"
203
- onClick={() => navigateTo(breadcrumb.path)}
204
- disabled={isSaving}
205
- >
206
- {breadcrumb.label}
207
- </button>
208
- {index < browseQuery.data.breadcrumbs.length - 1 ? (
209
- <ChevronRight className="h-3 w-3 text-gray-400" />
210
- ) : null}
211
- </div>
212
- ))}
213
- </div>
214
- </div>
303
+ <ServerPathPickerToolbar
304
+ disabled={navigationDisabled || !browseQuery.data?.currentPath}
305
+ onNewFolder={() => {
306
+ setNewFolderVisible(true);
307
+ setNewFolderName('');
308
+ createDirectoryMutation.reset();
309
+ }}
310
+ />
215
311
 
216
- <div className="border-b border-gray-200 px-3 py-2">
217
- <div className="relative">
218
- <Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
219
- <Input
220
- value={searchText}
221
- onChange={(event) => setSearchText(event.target.value)}
222
- placeholder={t('pathPickerSearchPlaceholder')}
223
- disabled={isSaving || browseQuery.isLoading}
224
- className="pl-9"
225
- />
226
- </div>
227
- </div>
228
-
229
- <ScrollArea className="min-h-0 flex-1 px-2 py-2">
230
- {browseQuery.isLoading ? (
231
- <div className="px-2 py-6 text-sm text-gray-500">{t('loading')}</div>
232
- ) : errorMessage ? (
233
- <div className="px-2 py-4 text-sm text-destructive">
234
- {t('pathBrowseFailed')}: {errorMessage}
235
- </div>
236
- ) : browseQuery.data && filteredEntries.length > 0 ? (
237
- <div className="space-y-1">
238
- {filteredEntries.map((entry) => (
239
- <button
240
- key={entry.path}
241
- type="button"
242
- className="flex w-full items-center gap-2 rounded-xl px-3 py-2 text-left text-sm text-gray-700 transition-colors hover:bg-white"
243
- onClick={() => navigateTo(entry.path)}
244
- disabled={isSaving}
245
- >
246
- <Folder className="h-4 w-4 shrink-0 text-emerald-600" />
247
- <span className="truncate">{entry.name}</span>
248
- </button>
249
- ))}
250
- </div>
251
- ) : browseQuery.data && browseQuery.data.entries.length > 0 ? (
252
- <div className="px-2 py-6 text-sm text-gray-500">
253
- {t('pathPickerSearchEmpty')}
254
- </div>
255
- ) : (
256
- <div className="px-2 py-6 text-sm text-gray-500">{t('emptyDirectory')}</div>
257
- )}
258
- </ScrollArea>
312
+ <div className="relative border-b border-border p-2 sm:hidden">
313
+ <Search className="pointer-events-none absolute left-5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
314
+ <Input
315
+ value={searchText}
316
+ onChange={(event) => setSearchText(event.target.value)}
317
+ placeholder={t('pathPickerSearchPlaceholder')}
318
+ disabled={navigationDisabled}
319
+ className="h-9 pl-9"
320
+ />
259
321
  </div>
260
322
 
261
- {hint ? (
262
- <p className="text-xs leading-relaxed text-gray-500">{hint}</p>
323
+ {newFolderVisible ? (
324
+ <ServerPathPickerNewFolder
325
+ errorMessage={createDirectoryErrorMessage}
326
+ isCreating={createDirectoryMutation.isPending}
327
+ name={newFolderName}
328
+ onCancel={() => setNewFolderVisible(false)}
329
+ onCreate={() => void createDirectory()}
330
+ onNameChange={(name) => {
331
+ setNewFolderName(name);
332
+ createDirectoryMutation.reset();
333
+ }}
334
+ />
263
335
  ) : null}
264
336
 
265
- <DialogFooter>
266
- <Button
267
- type="button"
268
- variant="outline"
269
- onClick={() => onOpenChange(false)}
337
+ <div className="flex min-h-0 min-w-0 flex-1 overflow-hidden">
338
+ <ServerPathPickerLocations
339
+ currentPath={addressPath}
340
+ defaultWorkspacePath={
341
+ resolveHomeRelativePath(defaultWorkspacePath, browseQuery.data?.homePath) ||
342
+ null
343
+ }
344
+ disabled={navigationDisabled}
345
+ homePath={browseQuery.data?.homePath}
346
+ locations={browseQuery.data?.locations ?? []}
347
+ rootPath={browseQuery.data?.breadcrumbs[0]?.path}
348
+ onNavigate={navigateTo}
349
+ />
350
+ <ServerPathPickerDirectoryList
351
+ entries={filteredEntries}
352
+ emptyMessage={emptyMessage}
270
353
  disabled={isSaving}
271
- >
272
- {t('cancel')}
273
- </Button>
274
- <Button type="submit" disabled={submitDisabled}>
275
- {isSaving ? t('saving') : confirmLabel}
276
- </Button>
277
- </DialogFooter>
278
- </form>
279
- </DialogContent>
280
- </Dialog>
354
+ onOpen={navigateTo}
355
+ onSelect={setSelectedPath}
356
+ selectedPath={normalizedSelectedPath}
357
+ />
358
+ </div>
359
+
360
+ <ServerPathPickerFooter
361
+ confirmLabel={confirmLabel}
362
+ disabled={submitDisabled}
363
+ hint={hint}
364
+ isSaving={isSaving}
365
+ onCancel={onCancel}
366
+ onSelectedPathChange={setSelectedPath}
367
+ pathLabel={pathLabel}
368
+ pathPlaceholder={pathPlaceholder}
369
+ selectedPath={canonicalSelectedPath}
370
+ />
371
+ </form>
281
372
  );
282
373
  }
@@ -0,0 +1,103 @@
1
+ import type { KeyboardEvent } from 'react';
2
+ import { Folder } from 'lucide-react';
3
+ import type { ServerPathEntryView } from '@/shared/lib/api';
4
+ import { ScrollArea } from '@/shared/components/ui/scroll-area';
5
+ import { cn } from '@/shared/lib/utils';
6
+ import { t } from '@/shared/lib/i18n';
7
+
8
+ type ServerPathPickerDirectoryListProps = {
9
+ entries: readonly ServerPathEntryView[];
10
+ emptyMessage: string;
11
+ disabled: boolean;
12
+ onOpen: (path: string) => void;
13
+ onSelect: (path: string) => void;
14
+ selectedPath: string;
15
+ };
16
+
17
+ export function ServerPathPickerDirectoryList({
18
+ entries,
19
+ emptyMessage,
20
+ disabled,
21
+ onOpen,
22
+ onSelect,
23
+ selectedPath,
24
+ }: ServerPathPickerDirectoryListProps) {
25
+ const moveSelection = (
26
+ event: KeyboardEvent<HTMLButtonElement>,
27
+ direction: -1 | 1,
28
+ ) => {
29
+ event.preventDefault();
30
+ const rows = Array.from(
31
+ event.currentTarget.parentElement?.querySelectorAll<HTMLButtonElement>(
32
+ '[data-server-path-row]',
33
+ ) ?? [],
34
+ );
35
+ const currentIndex = rows.indexOf(event.currentTarget);
36
+ const nextRow = rows[currentIndex + direction];
37
+ if (!nextRow) {
38
+ return;
39
+ }
40
+ nextRow.focus();
41
+ nextRow.click();
42
+ };
43
+
44
+ return (
45
+ <div className="flex min-h-0 flex-1 flex-col bg-background">
46
+ <div className="grid grid-cols-[minmax(0,1fr)_8rem] border-b border-border bg-muted/20 px-3 py-1.5 text-xs text-muted-foreground">
47
+ <span>{t('pathPickerNameColumn')}</span>
48
+ <span>{t('pathPickerTypeColumn')}</span>
49
+ </div>
50
+ <ScrollArea className="min-h-0 flex-1">
51
+ {entries.length > 0 ? (
52
+ <div role="listbox" aria-label={t('pathPickerDirectoryList')} className="p-1.5">
53
+ {entries.map((entry) => {
54
+ const selected = selectedPath === entry.path;
55
+ return (
56
+ <button
57
+ key={entry.path}
58
+ type="button"
59
+ role="option"
60
+ aria-selected={selected}
61
+ data-server-path-row
62
+ className={cn(
63
+ 'grid w-full grid-cols-[minmax(0,1fr)_8rem] items-center rounded-sm border border-transparent px-2 py-1.5 text-left text-sm text-foreground outline-none transition-colors',
64
+ selected
65
+ ? 'border-primary/25 bg-primary/15'
66
+ : 'hover:bg-accent/70 focus-visible:border-primary/35 focus-visible:bg-accent/70',
67
+ )}
68
+ onClick={() => onSelect(entry.path)}
69
+ onDoubleClick={() => onOpen(entry.path)}
70
+ onKeyDown={(event) => {
71
+ if (event.key === 'Enter') {
72
+ event.preventDefault();
73
+ onOpen(entry.path);
74
+ }
75
+ if (event.key === 'ArrowDown') {
76
+ moveSelection(event, 1);
77
+ }
78
+ if (event.key === 'ArrowUp') {
79
+ moveSelection(event, -1);
80
+ }
81
+ }}
82
+ disabled={disabled}
83
+ >
84
+ <span className="flex min-w-0 items-center gap-2">
85
+ <Folder className="h-4 w-4 shrink-0 fill-amber-300 text-amber-500" />
86
+ <span className="truncate">{entry.name}</span>
87
+ </span>
88
+ <span className="truncate text-xs text-muted-foreground">
89
+ {t('pathPickerDirectoryType')}
90
+ </span>
91
+ </button>
92
+ );
93
+ })}
94
+ </div>
95
+ ) : (
96
+ <div className="grid h-full min-h-48 place-items-center px-6 text-center text-sm text-muted-foreground">
97
+ {emptyMessage}
98
+ </div>
99
+ )}
100
+ </ScrollArea>
101
+ </div>
102
+ );
103
+ }