@nextclaw/ui 0.11.20 → 0.11.22

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 (125) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/assets/{ChannelsList-DAx7wv0_.js → ChannelsList-Zeys_w43.js} +6 -6
  3. package/dist/assets/ChatPage-DWOU_8P6.js +43 -0
  4. package/dist/assets/DocBrowser-B9OaZjmg.js +1 -0
  5. package/dist/assets/{DocBrowser-DKkE3Y4I.js → DocBrowser-BmtBLFU0.js} +1 -1
  6. package/dist/assets/{DocBrowserContext-BcZRBsCg.js → DocBrowserContext-YIKkPb76.js} +1 -1
  7. package/dist/assets/{LogoBadge-BIPDLEwK.js → LogoBadge-F7ZWdxLT.js} +1 -1
  8. package/dist/assets/MarketplacePage-BfaTTqN6.js +1 -0
  9. package/dist/assets/{MarketplacePage-Dlp5BgCh.js → MarketplacePage-Cd4faegU.js} +2 -2
  10. package/dist/assets/{McpMarketplacePage-CwKtAil8.js → McpMarketplacePage-C09Ngs7O.js} +2 -2
  11. package/dist/assets/ModelConfig-DJgdcgvQ.js +1 -0
  12. package/dist/assets/ProvidersList-w0rVFIBf.js +1 -0
  13. package/dist/assets/RemoteAccessPage-BJ_ckkOV.js +1 -0
  14. package/dist/assets/RuntimeConfig-Cmn2xPQO.js +1 -0
  15. package/dist/assets/{SearchConfig-v46R5a2U.js → SearchConfig-BT13qpR_.js} +1 -1
  16. package/dist/assets/{SecretsConfig-CXvUpbB_.js → SecretsConfig-CvqEVn0B.js} +2 -2
  17. package/dist/assets/{SessionsConfig-7vUHMtOh.js → SessionsConfig-DHHcYznk.js} +2 -2
  18. package/dist/assets/{book-open-DzSduAaw.js → book-open-CXoF5nQC.js} +1 -1
  19. package/dist/assets/chat-session-display-VW6ZMvZP.js +1 -0
  20. package/dist/assets/{chunk-JZWAC4HX-C1vpvW4r.js → chunk-JZWAC4HX-CvRWvTy5.js} +1 -1
  21. package/dist/assets/{config-Df97LeLR.js → config-DJswxxE8.js} +1 -1
  22. package/dist/assets/{createLucideIcon-CcR5wVoU.js → createLucideIcon-CjGHOWb6.js} +1 -1
  23. package/dist/assets/{dist-Dii9v3X9.js → dist-Cl2QB-2y.js} +1 -1
  24. package/dist/assets/{dist-BMlnBah3.js → dist-nqTTbVdA.js} +1 -1
  25. package/dist/assets/{external-link-CnSDrvJE.js → external-link-tIO7zING.js} +1 -1
  26. package/dist/assets/{hash-CAnX6PNt.js → hash-JWUyl1pT.js} +1 -1
  27. package/dist/assets/i18n-CDHMXlRZ.js +1 -0
  28. package/dist/assets/index-BlH4-cBw.css +1 -0
  29. package/dist/assets/{index-B0DzQqwv.js → index-C6d0xmtm.js} +3 -3
  30. package/dist/assets/{label-CtIFj7_6.js → label-BIpeNu4r.js} +1 -1
  31. package/dist/assets/loader-circle-Cs8XVFTw.js +1 -0
  32. package/dist/assets/{logos-3KFNiOej.js → logos-DThdM9lk.js} +1 -1
  33. package/dist/assets/{page-layout-BMwpn87D.js → page-layout-D3Xo605Z.js} +1 -1
  34. package/dist/assets/plus-PHf8q-Ct.js +1 -0
  35. package/dist/assets/{popover-BIzq25oH.js → popover-BJRUGA_H.js} +1 -1
  36. package/dist/assets/provider-models-bz5y28rq.js +1 -0
  37. package/dist/assets/{react-ji6GGP_j.js → react-7ZHqQtEV.js} +1 -1
  38. package/dist/assets/refresh-ccw-CC6-_QuL.js +1 -0
  39. package/dist/assets/{save-CMgYkJ-y.js → save-DJM5RRWW.js} +1 -1
  40. package/dist/assets/search-C91yH_6y.js +1 -0
  41. package/dist/assets/{security-config-Xi5DYW7j.js → security-config-T5zpg16O.js} +1 -1
  42. package/dist/assets/{select-Cz82gl01.js → select-DSkTc61S.js} +1 -1
  43. package/dist/assets/skeleton-Dzg-HOiN.js +1 -0
  44. package/dist/assets/{status-dot-C7q1HvLH.js → status-dot-LNBlDu3q.js} +1 -1
  45. package/dist/assets/{switch-DYswvkYj.js → switch-Bo-Y46HZ.js} +1 -1
  46. package/dist/assets/tabs-custom-DXv507_2.js +1 -0
  47. package/dist/assets/{trash-2-DfXI7-ap.js → trash-2-DFZmW6Gg.js} +1 -1
  48. package/dist/assets/useConfirmDialog-Bs5Ll17m.js +1 -0
  49. package/dist/assets/{useMutation-s2sn2yzh.js → useMutation-DrZrOgVL.js} +1 -1
  50. package/dist/assets/x-D7Q1yqSF.js +1 -0
  51. package/dist/index.html +18 -18
  52. package/package.json +6 -6
  53. package/src/api/ncp-session.test.ts +37 -0
  54. package/src/api/ncp-session.ts +29 -1
  55. package/src/api/server-path.ts +23 -0
  56. package/src/api/types.ts +41 -0
  57. package/src/components/chat/ChatConversationPanel.test.tsx +43 -7
  58. package/src/components/chat/ChatConversationPanel.tsx +23 -17
  59. package/src/components/chat/ChatSidebar.test.tsx +2 -2
  60. package/src/components/chat/ChatSidebar.tsx +2 -2
  61. package/src/components/chat/adapters/chat-input-bar.adapter.test.ts +1 -0
  62. package/src/components/chat/adapters/chat-input-bar.adapter.ts +7 -2
  63. package/src/components/chat/adapters/chat-message-part.adapter.ts +81 -6
  64. package/src/components/chat/adapters/chat-message.adapter.test.ts +393 -3
  65. package/src/components/chat/adapters/chat-message.partial-json.ts +89 -0
  66. package/src/components/chat/adapters/file-operation/card.ts +330 -0
  67. package/src/components/chat/adapters/file-operation/diff.ts +398 -0
  68. package/src/components/chat/adapters/file-operation/line-builder.ts +249 -0
  69. package/src/components/chat/adapters/file-operation/record-readers.ts +233 -0
  70. package/src/components/chat/chat-composer-state.ts +3 -3
  71. package/src/components/chat/chat-session-display.test.ts +21 -0
  72. package/src/components/chat/chat-session-display.ts +6 -1
  73. package/src/components/chat/containers/chat-input-bar.container.tsx +29 -32
  74. package/src/components/chat/containers/chat-message-list.container.tsx +1 -0
  75. package/src/components/chat/hooks/use-chat-session-label.ts +19 -0
  76. package/src/components/chat/hooks/use-chat-session-project.test.tsx +117 -0
  77. package/src/components/chat/hooks/use-chat-session-project.ts +40 -0
  78. package/src/components/chat/{chat-session-label.service.ts → hooks/use-chat-session-update.ts} +11 -7
  79. package/src/components/chat/managers/chat-session-list.manager.ts +5 -1
  80. package/src/components/chat/ncp/NcpChatPage.tsx +55 -17
  81. package/src/components/chat/ncp/ncp-chat-page-data.test.ts +33 -0
  82. package/src/components/chat/ncp/ncp-chat-page-data.ts +21 -15
  83. package/src/components/chat/ncp/ncp-session-adapter.test.ts +176 -0
  84. package/src/components/chat/ncp/ncp-session-adapter.ts +16 -0
  85. package/src/components/chat/session-header/chat-session-header-actions.test.tsx +63 -0
  86. package/src/components/chat/session-header/chat-session-header-actions.tsx +95 -0
  87. package/src/components/chat/session-header/chat-session-header-menu-item.tsx +35 -0
  88. package/src/components/chat/session-header/chat-session-project-badge.test.tsx +66 -0
  89. package/src/components/chat/session-header/chat-session-project-badge.tsx +102 -0
  90. package/src/components/chat/session-header/chat-session-project-dialog.tsx +34 -0
  91. package/src/components/chat/stores/chat-input.store.ts +6 -3
  92. package/src/components/chat/stores/chat-thread.store.ts +6 -2
  93. package/src/components/chat/useNcpAgentRuntime.test.tsx +90 -0
  94. package/src/components/path-picker/server-path-picker-dialog.test.tsx +92 -0
  95. package/src/components/path-picker/server-path-picker-dialog.tsx +282 -0
  96. package/src/hooks/server-path/use-server-path-browse.ts +19 -0
  97. package/src/hooks/useConfig.ts +26 -1
  98. package/src/lib/i18n/i18n-language-owner.ts +94 -0
  99. package/src/lib/i18n/i18n.path-picker.ts +12 -0
  100. package/src/lib/i18n.chat.ts +25 -1
  101. package/src/lib/i18n.ts +21 -84
  102. package/src/lib/session-project/session-project.utils.ts +30 -0
  103. package/src/remote/remote-access-feedback.service.test.ts +18 -0
  104. package/src/remote/remote-access-feedback.service.ts +10 -1
  105. package/dist/assets/ChatPage-l2PYwCeB.js +0 -38
  106. package/dist/assets/DocBrowser-CIHLqoIm.js +0 -1
  107. package/dist/assets/MarketplacePage-TVeyVOuO.js +0 -1
  108. package/dist/assets/ModelConfig-Dg6F3Ldb.js +0 -1
  109. package/dist/assets/ProvidersList-f7bQdRxA.js +0 -1
  110. package/dist/assets/RemoteAccessPage-w_dY7P4T.js +0 -1
  111. package/dist/assets/RuntimeConfig-M4OKjmgU.js +0 -1
  112. package/dist/assets/chat-session-display-CGfXhJoT.js +0 -1
  113. package/dist/assets/i18n-CXBpwAwA.js +0 -1
  114. package/dist/assets/index-BahpXJg8.css +0 -1
  115. package/dist/assets/loader-circle-qgU4zQDw.js +0 -1
  116. package/dist/assets/plus-C9cYVbL-.js +0 -1
  117. package/dist/assets/provider-models-C8JQUd1E.js +0 -1
  118. package/dist/assets/search-sl1OeJFl.js +0 -1
  119. package/dist/assets/skeleton-rgIt7a5q.js +0 -1
  120. package/dist/assets/tabs-custom-DKYQxrx1.js +0 -1
  121. package/dist/assets/useConfirmDialog-CXDAxtRL.js +0 -1
  122. package/dist/assets/x-MIimOGs6.js +0 -1
  123. /package/dist/assets/{config-hints-fGnUjDe9.js → config-hints-WtpHP_DW.js} +0 -0
  124. /package/dist/assets/{config-layout-B-7erZRN.js → config-layout-LQ10ozRC.js} +0 -0
  125. /package/dist/assets/{marketplace-localization-CXeGRf6E.js → marketplace-localization-CxSTG9wr.js} +0 -0
@@ -0,0 +1,282 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import { ChevronRight, Folder, FolderUp, Home, RefreshCcw, Search } from 'lucide-react';
3
+ import { useServerPathBrowse } from '@/hooks/server-path/use-server-path-browse';
4
+ import { Button } from '@/components/ui/button';
5
+ import {
6
+ Dialog,
7
+ DialogContent,
8
+ DialogDescription,
9
+ DialogFooter,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ } from '@/components/ui/dialog';
13
+ import { Input } from '@/components/ui/input';
14
+ import { Label } from '@/components/ui/label';
15
+ import { ScrollArea } from '@/components/ui/scroll-area';
16
+ import { t } from '@/lib/i18n';
17
+
18
+ type ServerPathPickerDialogProps = {
19
+ open: boolean;
20
+ currentPath?: string | null;
21
+ isSaving: boolean;
22
+ onOpenChange: (open: boolean) => void;
23
+ onConfirm: (path: string) => Promise<void> | void;
24
+ title: string;
25
+ description?: string;
26
+ pathLabel: string;
27
+ pathPlaceholder?: string;
28
+ confirmLabel: string;
29
+ hint?: string;
30
+ };
31
+
32
+ export function ServerPathPickerDialog({
33
+ open,
34
+ currentPath,
35
+ isSaving,
36
+ onOpenChange,
37
+ onConfirm,
38
+ title,
39
+ description,
40
+ pathLabel,
41
+ pathPlaceholder,
42
+ confirmLabel,
43
+ hint,
44
+ }: ServerPathPickerDialogProps) {
45
+ const [draftPath, setDraftPath] = useState('');
46
+ const [browsePath, setBrowsePath] = useState<string | null>(null);
47
+ const [searchText, setSearchText] = useState('');
48
+
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]);
58
+
59
+ const browseQuery = useServerPathBrowse({
60
+ path: browsePath,
61
+ enabled: open,
62
+ });
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;
75
+ const errorMessage = useMemo(() => {
76
+ if (!browseQuery.error) {
77
+ return null;
78
+ }
79
+ return browseQuery.error instanceof Error
80
+ ? browseQuery.error.message
81
+ : String(browseQuery.error);
82
+ }, [browseQuery.error]);
83
+
84
+ const normalizedSearchText = searchText.trim().toLowerCase();
85
+ const filteredEntries = useMemo(() => {
86
+ const entries = browseQuery.data?.entries ?? [];
87
+ if (normalizedSearchText.length === 0) {
88
+ return entries;
89
+ }
90
+ return entries.filter((entry) => {
91
+ const normalizedName = entry.name.toLowerCase();
92
+ const normalizedPath = entry.path.toLowerCase();
93
+ return (
94
+ normalizedName.includes(normalizedSearchText) ||
95
+ normalizedPath.includes(normalizedSearchText)
96
+ );
97
+ });
98
+ }, [browseQuery.data?.entries, normalizedSearchText]);
99
+
100
+ const navigateTo = (path: string | null) => {
101
+ setBrowsePath(path);
102
+ setSearchText('');
103
+ if (path) {
104
+ setDraftPath(path);
105
+ }
106
+ };
107
+
108
+ return (
109
+ <Dialog
110
+ open={open}
111
+ onOpenChange={(nextOpen) => {
112
+ if (isSaving) {
113
+ return;
114
+ }
115
+ onOpenChange(nextOpen);
116
+ }}
117
+ >
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>
192
+
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>
215
+
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>
259
+ </div>
260
+
261
+ {hint ? (
262
+ <p className="text-xs leading-relaxed text-gray-500">{hint}</p>
263
+ ) : null}
264
+
265
+ <DialogFooter>
266
+ <Button
267
+ type="button"
268
+ variant="outline"
269
+ onClick={() => onOpenChange(false)}
270
+ 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>
281
+ );
282
+ }
@@ -0,0 +1,19 @@
1
+ import { useQuery } from '@tanstack/react-query';
2
+ import { fetchServerPathBrowse } from '@/api/server-path';
3
+
4
+ export function useServerPathBrowse(params: {
5
+ path?: string | null;
6
+ includeFiles?: boolean;
7
+ enabled?: boolean;
8
+ }) {
9
+ return useQuery({
10
+ queryKey: ['server-path-browse', params.path ?? null, params.includeFiles ?? false],
11
+ queryFn: () =>
12
+ fetchServerPathBrowse({
13
+ path: params.path,
14
+ includeFiles: params.includeFiles,
15
+ }),
16
+ enabled: params.enabled ?? true,
17
+ staleTime: 0,
18
+ });
19
+ }
@@ -26,7 +26,13 @@ import {
26
26
  setCronJobEnabled,
27
27
  runCronJob
28
28
  } from '@/api/config';
29
- import { deleteNcpSession, fetchNcpSessionMessages, fetchNcpSessions, updateNcpSession } from '@/api/ncp-session';
29
+ import {
30
+ deleteNcpSession,
31
+ fetchNcpSessionMessages,
32
+ fetchNcpSessionSkills,
33
+ fetchNcpSessions,
34
+ updateNcpSession
35
+ } from '@/api/ncp-session';
30
36
  import { toast } from 'sonner';
31
37
  import { t } from '@/lib/i18n';
32
38
 
@@ -248,6 +254,24 @@ export function useNcpSessionMessages(sessionId: string | null, limit = 200) {
248
254
  });
249
255
  }
250
256
 
257
+ export function useNcpSessionSkills(params: {
258
+ sessionId: string | null;
259
+ projectRoot?: string | null;
260
+ }) {
261
+ return useQuery({
262
+ queryKey: ['ncp-session-skills', params.sessionId, params.projectRoot ?? null],
263
+ queryFn: () =>
264
+ fetchNcpSessionSkills(params.sessionId as string, {
265
+ ...(Object.prototype.hasOwnProperty.call(params, 'projectRoot')
266
+ ? { projectRoot: params.projectRoot ?? null }
267
+ : {})
268
+ }),
269
+ enabled: Boolean(params.sessionId),
270
+ staleTime: 5_000,
271
+ retry: false
272
+ });
273
+ }
274
+
251
275
  export function useDeleteNcpSession() {
252
276
  const queryClient = useQueryClient();
253
277
 
@@ -272,6 +296,7 @@ export function useUpdateNcpSession() {
272
296
  updateNcpSession(sessionId, data),
273
297
  onSuccess: (data) => {
274
298
  upsertNcpSessionSummaryInQueryClient(queryClient, data);
299
+ queryClient.invalidateQueries({ queryKey: ['ncp-session-skills', data.sessionId] });
275
300
  toast.success(t('configSavedApplied'));
276
301
  },
277
302
  onError: (error: Error) => {
@@ -0,0 +1,94 @@
1
+ export type I18nLanguage = 'zh' | 'en';
2
+
3
+ const I18N_STORAGE_KEY = 'nextclaw.ui.language';
4
+ const LANGUAGE_TO_LOCALE: Record<I18nLanguage, string> = {
5
+ en: 'en-US',
6
+ zh: 'zh-CN'
7
+ };
8
+
9
+ export const LANGUAGE_OPTIONS: Array<{ value: I18nLanguage; label: string }> = [
10
+ { value: 'en', label: 'English' },
11
+ { value: 'zh', label: '中文' }
12
+ ];
13
+
14
+ class I18nLanguageOwner {
15
+ private activeLanguage: I18nLanguage = 'en';
16
+ private initialized = false;
17
+ private readonly listeners = new Set<(lang: I18nLanguage) => void>();
18
+
19
+ private isLanguage = (value: unknown): value is I18nLanguage => {
20
+ return value === 'en' || value === 'zh';
21
+ };
22
+
23
+ private detectBrowserLanguage = (): I18nLanguage => {
24
+ if (typeof navigator === 'undefined') {
25
+ return 'en';
26
+ }
27
+ const preferred = navigator.language?.toLowerCase() ?? 'en';
28
+ return preferred.startsWith('zh') ? 'zh' : 'en';
29
+ };
30
+
31
+ resolveInitialLanguage = (): I18nLanguage => {
32
+ if (typeof window === 'undefined') {
33
+ return 'en';
34
+ }
35
+
36
+ try {
37
+ const saved = window.localStorage.getItem(I18N_STORAGE_KEY);
38
+ if (this.isLanguage(saved)) {
39
+ return saved;
40
+ }
41
+ } catch {
42
+ // ignore storage failures
43
+ }
44
+
45
+ return this.detectBrowserLanguage();
46
+ };
47
+
48
+ initialize = (): I18nLanguage => {
49
+ if (!this.initialized) {
50
+ this.activeLanguage = this.resolveInitialLanguage();
51
+ this.initialized = true;
52
+ }
53
+ return this.activeLanguage;
54
+ };
55
+
56
+ getLanguage = (): I18nLanguage => (this.initialized ? this.activeLanguage : this.initialize());
57
+
58
+ setLanguage = (lang: I18nLanguage): void => {
59
+ this.initialize();
60
+ if (this.activeLanguage === lang) {
61
+ return;
62
+ }
63
+
64
+ this.activeLanguage = lang;
65
+
66
+ if (typeof window !== 'undefined') {
67
+ try {
68
+ window.localStorage.setItem(I18N_STORAGE_KEY, lang);
69
+ } catch {
70
+ // ignore storage failures
71
+ }
72
+ }
73
+
74
+ this.listeners.forEach((listener) => listener(lang));
75
+ };
76
+
77
+ subscribeLanguageChange = (listener: (lang: I18nLanguage) => void): (() => void) => {
78
+ this.listeners.add(listener);
79
+ return () => {
80
+ this.listeners.delete(listener);
81
+ };
82
+ };
83
+
84
+ getLocale = (lang: I18nLanguage = this.getLanguage()): string => LANGUAGE_TO_LOCALE[lang];
85
+ }
86
+
87
+ const owner = new I18nLanguageOwner();
88
+
89
+ export const resolveInitialLanguage = owner.resolveInitialLanguage;
90
+ export const initializeI18n = owner.initialize;
91
+ export const getLanguage = owner.getLanguage;
92
+ export const setLanguage = owner.setLanguage;
93
+ export const subscribeLanguageChange = owner.subscribeLanguageChange;
94
+ export const getLocale = owner.getLocale;
@@ -0,0 +1,12 @@
1
+ export const PATH_PICKER_LABELS: Record<string, { zh: string; en: string }> = {
2
+ browse: { zh: '浏览', en: 'Browse' },
3
+ openPath: { zh: '打开', en: 'Open' },
4
+ homeDirectory: { zh: '主目录', en: 'Home' },
5
+ currentDirectory: { zh: '当前目录', en: 'Current Directory' },
6
+ parentDirectory: { zh: '上一级', en: 'Up' },
7
+ pathPickerSearchPlaceholder: { zh: '搜索当前目录', en: 'Search current directory' },
8
+ pathPickerSearchEmpty: { zh: '当前目录下没有匹配结果。', en: 'No matching entries in the current directory.' },
9
+ selectCurrentDirectory: { zh: '选择当前目录', en: 'Select Current Directory' },
10
+ emptyDirectory: { zh: '当前目录下暂无可浏览项。', en: 'No entries in the current directory.' },
11
+ pathBrowseFailed: { zh: '读取目录失败', en: 'Failed to load directory' },
12
+ };
@@ -50,11 +50,18 @@ export const CHAT_LABELS: Record<string, { zh: string; en: string }> = {
50
50
  chatSlashTypeCommand: { zh: '命令', en: 'Command' },
51
51
  chatSlashTypeSkill: { zh: '技能', en: 'Skill' },
52
52
  chatSlashSkillSpec: { zh: '标识', en: 'Spec' },
53
+ chatSlashSkillScope: { zh: '作用域', en: 'Scope' },
53
54
  chatSlashLoading: { zh: '加载命令与技能中…', en: 'Loading commands and skills…' },
54
55
  chatSlashNoResult: { zh: '无匹配项', en: 'No matches' },
55
56
  chatSlashHint: { zh: '输入 / 触发命令或技能选择', en: 'Type / to access commands and skills' },
56
57
  chatSlashCommandHint: { zh: '回车插入命令,继续输入参数后发送。', en: 'Press Enter to insert command, then add args and send.' },
57
58
  chatSlashSkillHint: { zh: '回车把该技能加入本轮请求。', en: 'Press Enter to add this skill for the next turn.' },
59
+ chatSkillScopeProject: { zh: '项目', en: 'Project' },
60
+ chatSkillScopeWorkspace: { zh: '工作区', en: 'Workspace' },
61
+ chatPickerRecent: { zh: '最近使用', en: 'Recent' },
62
+ chatPickerRecentModels: { zh: '最近选择', en: 'Recent' },
63
+ chatPickerAllModels: { zh: '全部模型', en: 'All models' },
64
+ chatPickerAllSkills: { zh: '全部技能', en: 'All skills' },
58
65
  chatSend: { zh: '发送', en: 'Send' },
59
66
  chatStop: { zh: '停止', en: 'Stop' },
60
67
  chatStopPreparing: { zh: '正在建立可停止会话,请稍候…', en: 'Preparing stoppable run…' },
@@ -66,6 +73,22 @@ export const CHAT_LABELS: Record<string, { zh: string; en: string }> = {
66
73
  chatQueueMoveFirst: { zh: '置顶到下一条', en: 'Move to Next' },
67
74
  chatDeleteSession: { zh: '删除会话', en: 'Delete Session' },
68
75
  chatDeleteSessionConfirm: { zh: '确认删除当前会话?', en: 'Delete the current session?' },
76
+ chatSessionMoreActions: { zh: '更多操作', en: 'More actions' },
77
+ chatSessionSetProject: { zh: '设置项目目录', en: 'Set Project Directory' },
78
+ chatSessionClearProject: { zh: '清除项目目录', en: 'Clear Project Directory' },
79
+ chatSessionProjectDialogTitle: { zh: '设置项目目录', en: 'Set Project Directory' },
80
+ chatSessionProjectDialogDescription: {
81
+ zh: '把当前会话绑定到运行 NextClaw 服务的那台机器上的一个目录,让后续消息和工具执行围绕这个目录展开。',
82
+ en: 'Bind this session to a directory on the machine running the NextClaw service so future messages and tool runs work around that directory.'
83
+ },
84
+ chatSessionProjectPathLabel: { zh: '项目目录路径', en: 'Project Directory Path' },
85
+ chatSessionProjectPathPlaceholder: { zh: '/path/to/project', en: '/path/to/project' },
86
+ chatSessionProjectUpdateHint: {
87
+ zh: '这里浏览和选择的是服务端机器上的目录。修改项目目录只影响后续消息和后续工具执行,不会改写历史会话。',
88
+ en: 'The browser lists directories from the server machine. Changing the project directory only affects future messages and tool runs. History stays unchanged.'
89
+ },
90
+ chatSessionProjectUpdated: { zh: '项目目录已更新', en: 'Project directory updated' },
91
+ chatSessionProjectCleared: { zh: '项目目录已清除', en: 'Project directory cleared' },
69
92
  chatSendFailed: { zh: '发送消息失败', en: 'Failed to send message' },
70
93
  chatRoleUser: { zh: '你', en: 'You' },
71
94
  chatRoleAssistant: { zh: '助手', en: 'Assistant' },
@@ -74,9 +97,10 @@ export const CHAT_LABELS: Record<string, { zh: string; en: string }> = {
74
97
  chatRoleMessage: { zh: '消息', en: 'Message' },
75
98
  chatToolCall: { zh: '工具调用', en: 'Tool Call' },
76
99
  chatToolResult: { zh: '工具结果', en: 'Tool Result' },
100
+ chatToolInput: { zh: '输入', en: 'Input' },
77
101
  chatToolWorkflow: { zh: '工具工作流', en: 'Tool Workflow' },
78
102
  chatToolWorkflowDetails: { zh: '展开查看参数和结果', en: 'Expand to view params and results' },
79
- chatToolOutput: { zh: '查看输出', en: 'View Output' },
103
+ chatToolOutput: { zh: '输出', en: 'Output' },
80
104
  chatToolNoOutput: { zh: '无输出(执行完成)', en: 'No output (completed)' },
81
105
  chatToolStatusPreparing: { zh: '准备中', en: 'Preparing' },
82
106
  chatToolStatusRunning: { zh: '执行中', en: 'Running' },
package/src/lib/i18n.ts CHANGED
@@ -1,94 +1,30 @@
1
1
  import { CHANNEL_LABELS } from './i18n.channels';
2
2
  import { CHANNEL_AUTH_LABELS } from './i18n.channel-auth';
3
3
  import { CHAT_LABELS } from './i18n.chat';
4
+ import {
5
+ getLanguage,
6
+ getLocale,
7
+ initializeI18n,
8
+ LANGUAGE_OPTIONS,
9
+ resolveInitialLanguage,
10
+ setLanguage,
11
+ subscribeLanguageChange,
12
+ type I18nLanguage
13
+ } from './i18n/i18n-language-owner';
4
14
  import { MARKETPLACE_LABELS } from './i18n.marketplace';
15
+ import { PATH_PICKER_LABELS } from './i18n/i18n.path-picker';
5
16
  import { REMOTE_LABELS } from './i18n.remote';
6
- export type I18nLanguage = 'zh' | 'en';
7
- const I18N_STORAGE_KEY = 'nextclaw.ui.language';
8
- export const LANGUAGE_OPTIONS: Array<{ value: I18nLanguage; label: string }> = [
9
- { value: 'en', label: 'English' },
10
- { value: 'zh', label: '中文' }
11
- ];
12
- const LANGUAGE_TO_LOCALE: Record<I18nLanguage, string> = {
13
- en: 'en-US',
14
- zh: 'zh-CN'
17
+ export type { I18nLanguage };
18
+ export {
19
+ getLanguage,
20
+ getLocale,
21
+ initializeI18n,
22
+ LANGUAGE_OPTIONS,
23
+ resolveInitialLanguage,
24
+ setLanguage,
25
+ subscribeLanguageChange
15
26
  };
16
27
 
17
- let activeLanguage: I18nLanguage = 'en';
18
- let initialized = false;
19
- const listeners = new Set<(lang: I18nLanguage) => void>();
20
-
21
- function isLanguage(value: unknown): value is I18nLanguage {
22
- return value === 'en' || value === 'zh';
23
- }
24
-
25
- function detectBrowserLanguage(): I18nLanguage {
26
- if (typeof navigator === 'undefined') {
27
- return 'en';
28
- }
29
- const preferred = navigator.language?.toLowerCase() ?? 'en';
30
- return preferred.startsWith('zh') ? 'zh' : 'en';
31
- }
32
-
33
- export function resolveInitialLanguage(): I18nLanguage {
34
- if (typeof window === 'undefined') {
35
- return 'en';
36
- }
37
-
38
- try {
39
- const saved = window.localStorage.getItem(I18N_STORAGE_KEY);
40
- if (isLanguage(saved)) {
41
- return saved;
42
- }
43
- } catch {
44
- // ignore storage failures
45
- }
46
-
47
- return detectBrowserLanguage();
48
- }
49
-
50
- export function initializeI18n(): I18nLanguage {
51
- if (!initialized) {
52
- activeLanguage = resolveInitialLanguage();
53
- initialized = true;
54
- }
55
- return activeLanguage;
56
- }
57
-
58
- export function getLanguage(): I18nLanguage {
59
- return initialized ? activeLanguage : initializeI18n();
60
- }
61
-
62
- export function setLanguage(lang: I18nLanguage): void {
63
- initializeI18n();
64
- if (activeLanguage === lang) {
65
- return;
66
- }
67
-
68
- activeLanguage = lang;
69
-
70
- if (typeof window !== 'undefined') {
71
- try {
72
- window.localStorage.setItem(I18N_STORAGE_KEY, lang);
73
- } catch {
74
- // ignore storage failures
75
- }
76
- }
77
-
78
- listeners.forEach((listener) => listener(lang));
79
- }
80
-
81
- export function subscribeLanguageChange(listener: (lang: I18nLanguage) => void): () => void {
82
- listeners.add(listener);
83
- return () => {
84
- listeners.delete(listener);
85
- };
86
- }
87
-
88
- export function getLocale(lang: I18nLanguage = getLanguage()): string {
89
- return LANGUAGE_TO_LOCALE[lang];
90
- }
91
-
92
28
  export function formatDateTime(value?: string | Date, lang: I18nLanguage = getLanguage()): string {
93
29
  if (!value) {
94
30
  return '-';
@@ -624,6 +560,7 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
624
560
  docBrowserNewTab: { zh: '新建标签', en: 'New Tab' },
625
561
  docBrowserCloseTab: { zh: '关闭标签', en: 'Close Tab' },
626
562
  docBrowserTabUntitled: { zh: '未命名', en: 'Untitled' },
563
+ ...PATH_PICKER_LABELS,
627
564
  ...CHANNEL_AUTH_LABELS,
628
565
  };
629
566
 
@@ -0,0 +1,30 @@
1
+ function trimOptionalString(value: unknown): string | null {
2
+ if (typeof value !== "string") {
3
+ return null;
4
+ }
5
+ const trimmed = value.trim();
6
+ return trimmed.length > 0 ? trimmed : null;
7
+ }
8
+
9
+ export function normalizeSessionProjectRootValue(value: unknown): string | null {
10
+ return trimOptionalString(value);
11
+ }
12
+
13
+ export function getSessionProjectName(
14
+ projectRoot: string | null | undefined,
15
+ ): string | null {
16
+ const normalizedProjectRoot = trimOptionalString(projectRoot);
17
+ if (!normalizedProjectRoot) {
18
+ return null;
19
+ }
20
+
21
+ const trimmedTrailingSeparators = normalizedProjectRoot.replace(/[\\/]+$/, "");
22
+ if (!trimmedTrailingSeparators) {
23
+ return normalizedProjectRoot;
24
+ }
25
+
26
+ const pathSegments = trimmedTrailingSeparators
27
+ .split(/[\\/]/)
28
+ .filter(Boolean);
29
+ return pathSegments[pathSegments.length - 1] ?? trimmedTrailingSeparators;
30
+ }