@nextclaw/ui 0.15.12 → 0.15.13
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.
- package/CHANGELOG.md +22 -0
- package/dist/assets/{api-Bw1oLhwf.js → api-DGG69ZcJ.js} +3 -3
- package/dist/assets/app-presenter-provider-B1RgR5ug.js +106 -0
- package/dist/assets/{appearance-settings-page-BDpels6a.js → appearance-settings-page-BZSd1SrH.js} +1 -1
- package/dist/assets/{book-open-D-oLp0QJ.js → book-open-BhaS5BJv.js} +1 -1
- package/dist/assets/{card-CA3ozZwW.js → card-Bcr5YM8F.js} +1 -1
- package/dist/assets/{channels-list-page-B44EWctT.js → channels-list-page-C_x83ZCT.js} +2 -2
- package/dist/assets/{chat-page-CH8ypOKO.js → chat-page-D2xfR6TK.js} +1 -1
- package/dist/assets/{config-split-page-DskB5yAV.js → config-split-page-RXAoLeNv.js} +1 -1
- package/dist/assets/{confirm-dialog-BRQpFnvD.js → confirm-dialog-o_VPKvVD.js} +1 -1
- package/dist/assets/{desktop-update-config-se0OEu06.js → desktop-update-config-DEOMOsDN.js} +1 -1
- package/dist/assets/{dist-f6aZEsu8.js → dist-C87P4QWA.js} +1 -1
- package/dist/assets/doc-browser-BuGh646I.js +1 -0
- package/dist/assets/{doc-browser-DW7afLXs.js → doc-browser-DQ75iUkA.js} +1 -1
- package/dist/assets/{doc-browser-context-DhTa-zP_.js → doc-browser-context-tE0--wSa.js} +1 -1
- package/dist/assets/{eye-DMGpISEo.js → eye-DL9QkrKg.js} +1 -1
- package/dist/assets/{form-actions-SgP78YnZ.js → form-actions-J0tIaUdK.js} +1 -1
- package/dist/assets/index-B9QEjnoY.js +2 -0
- package/dist/assets/loader-circle-CAnHHSrY.js +1 -0
- package/dist/assets/{logo-badge-BZnxuJYW.js → logo-badge-BYTbZbOF.js} +1 -1
- package/dist/assets/{mcp-marketplace-page-Dm08nf11.js → mcp-marketplace-page-COWlzeP1.js} +2 -2
- package/dist/assets/mcp-marketplace-page-CaUFv37E.js +1 -0
- package/dist/assets/{model-config-page-k_tCjGlK.js → model-config-page-oNqnMXNY.js} +1 -1
- package/dist/assets/{navigation-link-CJ3fpUBN.js → navigation-link-Dk6VdDkc.js} +1 -1
- package/dist/assets/plus-DjdTWPjQ.js +1 -0
- package/dist/assets/{popover-Cq_oB7HI.js → popover-Txwrum65.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-Cwv5zw0Z.js → provider-scoped-model-input-B7rQM4b6.js} +1 -1
- package/dist/assets/{providers-config-page-D82jX13I.js → providers-config-page-CP-OnvLK.js} +1 -1
- package/dist/assets/{react-BjpVlGAu.js → react-B0LbRnAE.js} +1 -1
- package/dist/assets/{refresh-cw-CYyWovQ1.js → refresh-cw-C84mMku5.js} +1 -1
- package/dist/assets/remote-2gZkJSX4.js +1 -0
- package/dist/assets/rotate-cw-DQJhDnjg.js +1 -0
- package/dist/assets/{runtime-config-page-OS6ZT2b0.js → runtime-config-page-u_fuHxc8.js} +1 -1
- package/dist/assets/{save-B0V_M-kz.js → save-R_qyrHpa.js} +1 -1
- package/dist/assets/{search-vFYD1SOf.js → search-UKxiouNi.js} +1 -1
- package/dist/assets/{search-config-page-IzK1hMQX.js → search-config-page-DzkpSGFH.js} +1 -1
- package/dist/assets/{secrets-config-page-Cm3IXYpK.js → secrets-config-page-DhoLYm7Z.js} +2 -2
- package/dist/assets/security-config-yraYoy9s.js +1 -0
- package/dist/assets/{select-BAYdPRVG.js → select-CYHT8P5S.js} +1 -1
- package/dist/assets/{settings-2-QX-FHs_i.js → settings-2-DGh4bGSi.js} +1 -1
- package/dist/assets/skeleton-BQuylYFO.js +1 -0
- package/dist/assets/{tabs-Ddprun22.js → tabs-BCihm1zK.js} +1 -1
- package/dist/assets/{tag-chip-C9SLhljq.js → tag-chip-De0iz4o2.js} +1 -1
- package/dist/assets/{trash-2-CODUwc31.js → trash-2-Bo3DWiju.js} +1 -1
- package/dist/assets/use-config-DCzwgChy.js +1 -0
- package/dist/assets/{use-confirm-dialog-B_ZW-WgC.js → use-confirm-dialog-WZ94CehU.js} +1 -1
- package/dist/assets/{use-viewport-layout-OtzTlg5U.js → use-viewport-layout-CZ-3pMne.js} +1 -1
- package/dist/assets/x-NZxXyqW1.js +1 -0
- package/dist/index.html +27 -27
- package/package.json +9 -9
- package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +1 -0
- package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +4 -4
- package/src/features/chat/components/layout/chat-sidebar.tsx +27 -12
- package/src/features/chat/features/conversation/components/session-conversation-input.tsx +4 -19
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-slash-commands.test.tsx +79 -0
- package/src/features/chat/features/conversation/hooks/use-session-conversation-slash-commands.ts +55 -0
- package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +28 -0
- package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +10 -7
- package/src/features/chat/features/message/components/chat-message-list.container.tsx +1 -0
- package/src/features/chat/features/message/utils/__tests__/chat-message-show-content-tool-card.utils.test.ts +3 -0
- package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +8 -6
- package/src/features/chat/features/message/utils/chat-message-show-content-tool-card.utils.ts +2 -0
- package/src/features/chat/features/project/components/__tests__/chat-project-add-dialog.test.tsx +116 -0
- package/src/features/chat/features/project/components/chat-project-add-dialog.tsx +305 -0
- package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +3 -2
- package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +2 -2
- package/src/features/chat/features/workspace/components/__tests__/workspace-file-preview-error-text.test.ts +22 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +9 -5
- package/src/features/chat/managers/__tests__/chat-ui.manager.test.ts +24 -0
- package/src/features/chat/managers/chat-ui.manager.ts +7 -1
- package/src/features/right-panel-resources/configs/right-panel-resource-routes.config.ts +18 -4
- package/src/features/right-panel-resources/index.ts +1 -0
- package/src/features/right-panel-resources/utils/__tests__/right-panel-resource-route-resolver.utils.test.ts +33 -0
- package/src/features/right-panel-resources/utils/__tests__/right-panel-resource-uri.utils.test.ts +14 -1
- package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.ts +10 -2
- package/src/shared/components/path-picker/__tests__/server-path-picker-dialog.test.tsx +9 -0
- package/src/shared/components/path-picker/server-path-picker-dialog.tsx +8 -2
- package/src/shared/components/path-picker/server-path-picker-toolbar.tsx +8 -4
- package/src/shared/hooks/use-projects.ts +15 -1
- package/src/shared/lib/api/projects/project.utils.ts +7 -0
- package/src/shared/lib/api/types.ts +1 -1
- package/src/shared/lib/api/utils/ncp-session.utils.ts +7 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +26 -8
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +26 -8
- package/src/shared/lib/resource-uri/utils/__tests__/resource-uri-resolver.utils.test.ts +13 -1
- package/src/shared/lib/resource-uri/utils/resource-uri.utils.ts +6 -3
- package/dist/assets/app-presenter-provider-DSN90VJw.js +0 -106
- package/dist/assets/doc-browser-BRiAfy_N.js +0 -1
- package/dist/assets/index-CBD4zQ_A.js +0 -2
- package/dist/assets/loader-circle-DEQ2CflP.js +0 -1
- package/dist/assets/mcp-marketplace-page-B8eWiT9H.js +0 -1
- package/dist/assets/plus-9_9Q74qc.js +0 -1
- package/dist/assets/remote-C2MGMI2j.js +0 -1
- package/dist/assets/rotate-cw-BkGMTudQ.js +0 -1
- package/dist/assets/security-config-DfzQ8txL.js +0 -1
- package/dist/assets/skeleton-2eczb2co.js +0 -1
- package/dist/assets/use-config-C5OKT3lk.js +0 -1
- package/dist/assets/x-Cbfpb-Zm.js +0 -1
- package/src/features/chat/features/project/components/__tests__/chat-project-create-dialog.test.tsx +0 -67
- package/src/features/chat/features/project/components/chat-project-create-dialog.tsx +0 -203
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { FolderOpen } from 'lucide-react';
|
|
3
|
+
import type {
|
|
4
|
+
ProjectAddExistingRequest,
|
|
5
|
+
ProjectCreateRequest,
|
|
6
|
+
ProjectTemplateView,
|
|
7
|
+
} from '@/shared/lib/api';
|
|
8
|
+
import { ServerPathPickerDialog } from '@/shared/components/path-picker/server-path-picker-dialog';
|
|
9
|
+
import { Button } from '@/shared/components/ui/button';
|
|
10
|
+
import {
|
|
11
|
+
Dialog,
|
|
12
|
+
DialogContent,
|
|
13
|
+
DialogDescription,
|
|
14
|
+
DialogFooter,
|
|
15
|
+
DialogHeader,
|
|
16
|
+
DialogTitle,
|
|
17
|
+
} from '@/shared/components/ui/dialog';
|
|
18
|
+
import { Input } from '@/shared/components/ui/input';
|
|
19
|
+
import { Label } from '@/shared/components/ui/label';
|
|
20
|
+
import {
|
|
21
|
+
Select,
|
|
22
|
+
SelectContent,
|
|
23
|
+
SelectItem,
|
|
24
|
+
SelectTrigger,
|
|
25
|
+
SelectValue,
|
|
26
|
+
} from '@/shared/components/ui/select';
|
|
27
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/shared/components/ui/tabs';
|
|
28
|
+
import { t } from '@/shared/lib/i18n';
|
|
29
|
+
|
|
30
|
+
type ProjectTemplateId = NonNullable<ProjectCreateRequest['template']>;
|
|
31
|
+
type ProjectAddMode = 'create' | 'existing';
|
|
32
|
+
|
|
33
|
+
type ChatProjectAddDialogProps = {
|
|
34
|
+
open: boolean;
|
|
35
|
+
defaultWorkspacePath?: string | null;
|
|
36
|
+
templates: readonly ProjectTemplateView[];
|
|
37
|
+
isCreating: boolean;
|
|
38
|
+
isAddingExisting: boolean;
|
|
39
|
+
createErrorMessage?: string | null;
|
|
40
|
+
addExistingErrorMessage?: string | null;
|
|
41
|
+
onOpenChange: (open: boolean) => void;
|
|
42
|
+
onCreate: (input: ProjectCreateRequest) => Promise<void>;
|
|
43
|
+
onAddExisting: (input: ProjectAddExistingRequest) => Promise<void>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function projectTemplateLabel(templateId: ProjectTemplateId): string {
|
|
47
|
+
return t(
|
|
48
|
+
templateId === 'knowledge-base'
|
|
49
|
+
? 'chatProjectTemplateKnowledgeBase'
|
|
50
|
+
: 'chatProjectTemplateEmpty',
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function ChatProjectAddDialog({
|
|
55
|
+
open,
|
|
56
|
+
defaultWorkspacePath,
|
|
57
|
+
templates,
|
|
58
|
+
isCreating,
|
|
59
|
+
isAddingExisting,
|
|
60
|
+
createErrorMessage,
|
|
61
|
+
addExistingErrorMessage,
|
|
62
|
+
onOpenChange,
|
|
63
|
+
onCreate,
|
|
64
|
+
onAddExisting,
|
|
65
|
+
}: ChatProjectAddDialogProps) {
|
|
66
|
+
const [mode, setMode] = useState<ProjectAddMode>('create');
|
|
67
|
+
const [name, setName] = useState('');
|
|
68
|
+
const [createRootPath, setCreateRootPath] = useState('');
|
|
69
|
+
const [existingRootPath, setExistingRootPath] = useState('');
|
|
70
|
+
const [pathPickerOpen, setPathPickerOpen] = useState(false);
|
|
71
|
+
const [template, setTemplate] = useState<ProjectTemplateId>('empty');
|
|
72
|
+
const availableTemplateIds = templates.length > 0
|
|
73
|
+
? templates.map((entry) => entry.id)
|
|
74
|
+
: ['empty' as const];
|
|
75
|
+
const normalizedName = name.trim();
|
|
76
|
+
const normalizedCreateRootPath = createRootPath.trim();
|
|
77
|
+
const normalizedExistingRootPath = existingRootPath.trim();
|
|
78
|
+
const isSubmitting = isCreating || isAddingExisting;
|
|
79
|
+
const submitDisabled = isSubmitting || (
|
|
80
|
+
mode === 'create' ? !normalizedName : !normalizedExistingRootPath
|
|
81
|
+
);
|
|
82
|
+
const errorMessage = mode === 'create' ? createErrorMessage : addExistingErrorMessage;
|
|
83
|
+
|
|
84
|
+
const resetForm = () => {
|
|
85
|
+
setMode('create');
|
|
86
|
+
setName('');
|
|
87
|
+
setCreateRootPath('');
|
|
88
|
+
setExistingRootPath('');
|
|
89
|
+
setTemplate('empty');
|
|
90
|
+
setPathPickerOpen(false);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const closeDialog = () => {
|
|
94
|
+
if (isSubmitting) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
resetForm();
|
|
98
|
+
onOpenChange(false);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<>
|
|
103
|
+
<Dialog
|
|
104
|
+
open={open}
|
|
105
|
+
onOpenChange={(nextOpen) => {
|
|
106
|
+
if (nextOpen) {
|
|
107
|
+
onOpenChange(true);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
closeDialog();
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
<DialogContent className="sm:max-w-lg">
|
|
114
|
+
<DialogHeader>
|
|
115
|
+
<DialogTitle>{t('chatProjectAddTitle')}</DialogTitle>
|
|
116
|
+
<DialogDescription>{t('chatProjectAddDescription')}</DialogDescription>
|
|
117
|
+
</DialogHeader>
|
|
118
|
+
<form
|
|
119
|
+
className="space-y-4"
|
|
120
|
+
onSubmit={(event) => {
|
|
121
|
+
event.preventDefault();
|
|
122
|
+
if (submitDisabled) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const submit = mode === 'create'
|
|
126
|
+
? onCreate({
|
|
127
|
+
name: normalizedName,
|
|
128
|
+
template,
|
|
129
|
+
rootPath: normalizedCreateRootPath || undefined,
|
|
130
|
+
})
|
|
131
|
+
: onAddExisting({ rootPath: normalizedExistingRootPath });
|
|
132
|
+
void submit.then(resetForm).catch(() => undefined);
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
<Tabs
|
|
136
|
+
value={mode}
|
|
137
|
+
onValueChange={(value) => {
|
|
138
|
+
if (!isSubmitting) {
|
|
139
|
+
setMode(value === 'existing' ? 'existing' : 'create');
|
|
140
|
+
}
|
|
141
|
+
}}
|
|
142
|
+
>
|
|
143
|
+
<TabsList className="grid w-full grid-cols-2">
|
|
144
|
+
<TabsTrigger value="create">{t('chatProjectCreateMode')}</TabsTrigger>
|
|
145
|
+
<TabsTrigger value="existing">{t('chatProjectExistingMode')}</TabsTrigger>
|
|
146
|
+
</TabsList>
|
|
147
|
+
|
|
148
|
+
<TabsContent value="create" className="mt-4 space-y-4">
|
|
149
|
+
<p className="text-sm leading-relaxed text-muted-foreground">
|
|
150
|
+
{t('chatProjectCreateModeDescription')}
|
|
151
|
+
</p>
|
|
152
|
+
<div className="space-y-2">
|
|
153
|
+
<Label htmlFor="chat-project-name">{t('chatProjectNameLabel')}</Label>
|
|
154
|
+
<Input
|
|
155
|
+
id="chat-project-name"
|
|
156
|
+
value={name}
|
|
157
|
+
onChange={(event) => setName(event.target.value)}
|
|
158
|
+
placeholder={t('chatProjectNamePlaceholder')}
|
|
159
|
+
autoFocus
|
|
160
|
+
disabled={isSubmitting}
|
|
161
|
+
/>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<div className="space-y-2">
|
|
165
|
+
<Label htmlFor="chat-project-template">{t('chatProjectTemplateLabel')}</Label>
|
|
166
|
+
<Select
|
|
167
|
+
value={template}
|
|
168
|
+
onValueChange={(value) => setTemplate(value as ProjectTemplateId)}
|
|
169
|
+
disabled={isSubmitting}
|
|
170
|
+
>
|
|
171
|
+
<SelectTrigger id="chat-project-template">
|
|
172
|
+
<SelectValue />
|
|
173
|
+
</SelectTrigger>
|
|
174
|
+
<SelectContent>
|
|
175
|
+
{availableTemplateIds.map((templateId) => (
|
|
176
|
+
<SelectItem key={templateId} value={templateId}>
|
|
177
|
+
{projectTemplateLabel(templateId)}
|
|
178
|
+
</SelectItem>
|
|
179
|
+
))}
|
|
180
|
+
</SelectContent>
|
|
181
|
+
</Select>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<div className="space-y-2">
|
|
185
|
+
<Label htmlFor="chat-project-create-root-path">
|
|
186
|
+
{t('chatProjectCreatePathLabel')}
|
|
187
|
+
</Label>
|
|
188
|
+
<div className="flex gap-2">
|
|
189
|
+
<Input
|
|
190
|
+
id="chat-project-create-root-path"
|
|
191
|
+
value={createRootPath}
|
|
192
|
+
onChange={(event) => setCreateRootPath(event.target.value)}
|
|
193
|
+
placeholder={t('chatProjectCreatePathPlaceholder')}
|
|
194
|
+
disabled={isSubmitting}
|
|
195
|
+
/>
|
|
196
|
+
<Button
|
|
197
|
+
type="button"
|
|
198
|
+
variant="outline"
|
|
199
|
+
onClick={() => setPathPickerOpen(true)}
|
|
200
|
+
disabled={isSubmitting}
|
|
201
|
+
>
|
|
202
|
+
<FolderOpen className="mr-2 h-4 w-4" />
|
|
203
|
+
{t('browse')}
|
|
204
|
+
</Button>
|
|
205
|
+
</div>
|
|
206
|
+
<p className="text-xs leading-relaxed text-muted-foreground">
|
|
207
|
+
{t('chatProjectCreatePathHint')}
|
|
208
|
+
</p>
|
|
209
|
+
</div>
|
|
210
|
+
</TabsContent>
|
|
211
|
+
|
|
212
|
+
<TabsContent value="existing" className="mt-4 space-y-4">
|
|
213
|
+
<p className="text-sm leading-relaxed text-muted-foreground">
|
|
214
|
+
{t('chatProjectExistingModeDescription')}
|
|
215
|
+
</p>
|
|
216
|
+
<div className="space-y-2">
|
|
217
|
+
<Label htmlFor="chat-project-existing-root-path">
|
|
218
|
+
{t('chatProjectExistingPathLabel')}
|
|
219
|
+
</Label>
|
|
220
|
+
<div className="flex gap-2">
|
|
221
|
+
<Input
|
|
222
|
+
id="chat-project-existing-root-path"
|
|
223
|
+
value={existingRootPath}
|
|
224
|
+
onChange={(event) => setExistingRootPath(event.target.value)}
|
|
225
|
+
placeholder={t('chatProjectExistingPathPlaceholder')}
|
|
226
|
+
autoFocus
|
|
227
|
+
disabled={isSubmitting}
|
|
228
|
+
/>
|
|
229
|
+
<Button
|
|
230
|
+
type="button"
|
|
231
|
+
variant="outline"
|
|
232
|
+
onClick={() => setPathPickerOpen(true)}
|
|
233
|
+
disabled={isSubmitting}
|
|
234
|
+
>
|
|
235
|
+
<FolderOpen className="mr-2 h-4 w-4" />
|
|
236
|
+
{t('browse')}
|
|
237
|
+
</Button>
|
|
238
|
+
</div>
|
|
239
|
+
<p className="text-xs leading-relaxed text-muted-foreground">
|
|
240
|
+
{t('chatProjectExistingPathHint')}
|
|
241
|
+
</p>
|
|
242
|
+
</div>
|
|
243
|
+
</TabsContent>
|
|
244
|
+
</Tabs>
|
|
245
|
+
|
|
246
|
+
{errorMessage ? (
|
|
247
|
+
<p role="alert" className="text-sm text-destructive">
|
|
248
|
+
{errorMessage}
|
|
249
|
+
</p>
|
|
250
|
+
) : null}
|
|
251
|
+
|
|
252
|
+
<DialogFooter>
|
|
253
|
+
<Button type="button" variant="outline" onClick={closeDialog} disabled={isSubmitting}>
|
|
254
|
+
{t('cancel')}
|
|
255
|
+
</Button>
|
|
256
|
+
<Button type="submit" disabled={submitDisabled}>
|
|
257
|
+
{isSubmitting
|
|
258
|
+
? t('saving')
|
|
259
|
+
: t(mode === 'create' ? 'chatProjectCreateAction' : 'chatProjectExistingAction')}
|
|
260
|
+
</Button>
|
|
261
|
+
</DialogFooter>
|
|
262
|
+
</form>
|
|
263
|
+
</DialogContent>
|
|
264
|
+
</Dialog>
|
|
265
|
+
<ServerPathPickerDialog
|
|
266
|
+
open={pathPickerOpen}
|
|
267
|
+
allowCreateDirectory={mode === 'create'}
|
|
268
|
+
currentPath={mode === 'create' ? createRootPath : existingRootPath}
|
|
269
|
+
defaultWorkspacePath={defaultWorkspacePath}
|
|
270
|
+
isSaving={false}
|
|
271
|
+
onOpenChange={setPathPickerOpen}
|
|
272
|
+
onConfirm={(path) => {
|
|
273
|
+
if (mode === 'create') {
|
|
274
|
+
setCreateRootPath(path);
|
|
275
|
+
} else {
|
|
276
|
+
setExistingRootPath(path);
|
|
277
|
+
}
|
|
278
|
+
setPathPickerOpen(false);
|
|
279
|
+
}}
|
|
280
|
+
title={t(
|
|
281
|
+
mode === 'create'
|
|
282
|
+
? 'chatProjectCreatePathPickerTitle'
|
|
283
|
+
: 'chatProjectExistingPathPickerTitle',
|
|
284
|
+
)}
|
|
285
|
+
description={t(
|
|
286
|
+
mode === 'create'
|
|
287
|
+
? 'chatProjectCreatePathPickerDescription'
|
|
288
|
+
: 'chatProjectExistingPathPickerDescription',
|
|
289
|
+
)}
|
|
290
|
+
pathLabel={t(
|
|
291
|
+
mode === 'create' ? 'chatProjectCreatePathLabel' : 'chatProjectExistingPathLabel',
|
|
292
|
+
)}
|
|
293
|
+
pathPlaceholder={t(
|
|
294
|
+
mode === 'create'
|
|
295
|
+
? 'chatProjectCreatePathPlaceholder'
|
|
296
|
+
: 'chatProjectExistingPathPlaceholder',
|
|
297
|
+
)}
|
|
298
|
+
confirmLabel={t('selectCurrentDirectory')}
|
|
299
|
+
hint={t(
|
|
300
|
+
mode === 'create' ? 'chatProjectCreatePathHint' : 'chatProjectExistingPathHint',
|
|
301
|
+
)}
|
|
302
|
+
/>
|
|
303
|
+
</>
|
|
304
|
+
);
|
|
305
|
+
}
|
package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx
CHANGED
|
@@ -67,10 +67,11 @@ describe("ChatSidebarProjectGroups", () => {
|
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
it("keeps the project header as the full-width folder row and collapses its sessions", () => {
|
|
70
|
-
renderProjectGroups();
|
|
70
|
+
const { container } = renderProjectGroups();
|
|
71
71
|
|
|
72
72
|
const header = screen.getByLabelText("Collapse project");
|
|
73
|
-
expect(
|
|
73
|
+
expect(container.firstElementChild?.className).toBe("space-y-0.5");
|
|
74
|
+
expect(header.parentElement?.className).toContain("h-8");
|
|
74
75
|
expect(header.parentElement?.className).toContain("hover:bg-gray-200/60");
|
|
75
76
|
expect(header.parentElement?.className).not.toContain("focus-within:bg");
|
|
76
77
|
expect(screen.getByText("analysis-project").nextElementSibling?.tagName).toBe("svg");
|
|
@@ -46,7 +46,7 @@ export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
|
|
|
46
46
|
const supportsSessionTypeChoice = sessionTypeOptions.length > 1;
|
|
47
47
|
|
|
48
48
|
return (
|
|
49
|
-
<div className="space-y-
|
|
49
|
+
<div className="space-y-0.5">
|
|
50
50
|
{groups.map((group) => {
|
|
51
51
|
const actionLabel = `${t('chatSidebarNewTask')} · ${group.projectName}`;
|
|
52
52
|
const isCollapsed = collapsedProjectRoots.includes(group.projectRoot);
|
|
@@ -56,7 +56,7 @@ export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
|
|
|
56
56
|
|
|
57
57
|
return (
|
|
58
58
|
<div key={group.projectRoot}>
|
|
59
|
-
<div className="group/project relative h-
|
|
59
|
+
<div className="group/project relative h-8 rounded-lg px-2 text-muted-foreground transition-colors hover:bg-gray-200/60 hover:text-gray-900">
|
|
60
60
|
<button
|
|
61
61
|
type="button"
|
|
62
62
|
aria-expanded={!isCollapsed}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { NextClawClientError } from "@nextclaw/client-sdk";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { resolveWorkspacePreviewErrorText } from "@/features/chat/features/workspace/components/chat-session-workspace-file-preview";
|
|
4
|
+
import { t } from "@/shared/lib/i18n";
|
|
5
|
+
|
|
6
|
+
describe("resolveWorkspacePreviewErrorText", () => {
|
|
7
|
+
it.each([
|
|
8
|
+
[
|
|
9
|
+
new NextClawClientError({
|
|
10
|
+
code: "SERVER_PATH_NOT_FOUND",
|
|
11
|
+
message: "server path does not exist",
|
|
12
|
+
status: 404,
|
|
13
|
+
}),
|
|
14
|
+
"chatWorkspacePreviewNotFound",
|
|
15
|
+
],
|
|
16
|
+
[new Error("private upstream diagnostic"), "chatWorkspacePreviewFailed"],
|
|
17
|
+
] as const)("maps preview errors to %s", (error, translationKey) => {
|
|
18
|
+
const message = resolveWorkspacePreviewErrorText(error);
|
|
19
|
+
expect(message).toBe(t(translationKey));
|
|
20
|
+
expect(message).not.toContain(error.message);
|
|
21
|
+
});
|
|
22
|
+
});
|
package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
ChatFileOpenActionViewModel,
|
|
4
4
|
ChatFileOperationBlockViewModel,
|
|
5
5
|
} from "@nextclaw/agent-chat-ui";
|
|
6
|
+
import { NextClawClientError } from "@nextclaw/client-sdk";
|
|
6
7
|
import {
|
|
7
8
|
ChatMessageMarkdown,
|
|
8
9
|
FileOperationCodeSurface,
|
|
@@ -141,6 +142,13 @@ function WorkspaceFilePreviewStatus({
|
|
|
141
142
|
);
|
|
142
143
|
}
|
|
143
144
|
|
|
145
|
+
export function resolveWorkspacePreviewErrorText(error: unknown): string {
|
|
146
|
+
return error instanceof NextClawClientError &&
|
|
147
|
+
error.code === "SERVER_PATH_NOT_FOUND"
|
|
148
|
+
? t("chatWorkspacePreviewNotFound")
|
|
149
|
+
: t("chatWorkspacePreviewFailed");
|
|
150
|
+
}
|
|
151
|
+
|
|
144
152
|
function WorkspaceDiffBody({
|
|
145
153
|
diffBlock,
|
|
146
154
|
}: {
|
|
@@ -247,11 +255,7 @@ function WorkspacePreviewBody({
|
|
|
247
255
|
return (
|
|
248
256
|
<WorkspaceFilePreviewStatus
|
|
249
257
|
tone="error"
|
|
250
|
-
text={
|
|
251
|
-
previewQuery.error instanceof Error
|
|
252
|
-
? previewQuery.error.message
|
|
253
|
-
: String(previewQuery.error)
|
|
254
|
-
}
|
|
258
|
+
text={resolveWorkspacePreviewErrorText(previewQuery.error)}
|
|
255
259
|
/>
|
|
256
260
|
);
|
|
257
261
|
}
|
|
@@ -125,4 +125,28 @@ describe('ChatUiManager content display', () => {
|
|
|
125
125
|
title: 'Reader',
|
|
126
126
|
});
|
|
127
127
|
});
|
|
128
|
+
|
|
129
|
+
it('opens an explicit external panel app path without resolving the standard catalog', async () => {
|
|
130
|
+
const docBrowserManager = createDocBrowserManager();
|
|
131
|
+
const manager = new ChatUiManager(
|
|
132
|
+
docBrowserManager as unknown as ConstructorParameters<typeof ChatUiManager>[0],
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
await manager.showContent({
|
|
136
|
+
target: {
|
|
137
|
+
type: 'panel_app',
|
|
138
|
+
payload: {
|
|
139
|
+
appId: 'external-reader',
|
|
140
|
+
path: '/tmp/external-reader.panel',
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
title: 'External Reader',
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(listPanelAppsMock).not.toHaveBeenCalled();
|
|
147
|
+
expect(docBrowserManager.open).toHaveBeenCalledWith(
|
|
148
|
+
'nextclaw://panel-app/external-reader?path=%2Ftmp%2Fexternal-reader.panel',
|
|
149
|
+
{ title: 'External Reader' },
|
|
150
|
+
);
|
|
151
|
+
});
|
|
128
152
|
});
|
|
@@ -96,7 +96,13 @@ export class ChatUiManager {
|
|
|
96
96
|
});
|
|
97
97
|
return;
|
|
98
98
|
}
|
|
99
|
-
const { appId } = target.payload;
|
|
99
|
+
const { appId, path } = target.payload;
|
|
100
|
+
if (path) {
|
|
101
|
+
this.docBrowserManager.open(createPanelAppResourceUri(appId, path), {
|
|
102
|
+
title,
|
|
103
|
+
});
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
100
106
|
const entry = await this.resolvePanelAppEntry(appId);
|
|
101
107
|
if (entry) {
|
|
102
108
|
this.docBrowserManager.openTarget(createPanelAppRightPanelResourceTarget(entry), {
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
RightPanelResourceTarget,
|
|
14
14
|
} from '@/features/right-panel-resources/types/right-panel-resource.types';
|
|
15
15
|
import {
|
|
16
|
+
createPanelAppContentPath,
|
|
16
17
|
RIGHT_PANEL_APPS_URL,
|
|
17
18
|
RIGHT_PANEL_APPS_TAB_KIND,
|
|
18
19
|
RIGHT_PANEL_HOME_TAB_KIND,
|
|
@@ -77,12 +78,21 @@ function isPanelAppContentUri(uri: ParsedResourceUri): boolean {
|
|
|
77
78
|
|
|
78
79
|
function resolvePanelAppPlaceholderUrl(uri: ParsedResourceUri): string {
|
|
79
80
|
const appId = getPanelAppId(uri);
|
|
80
|
-
|
|
81
|
+
if (!appId) {
|
|
82
|
+
return 'nextclaw://panel-app';
|
|
83
|
+
}
|
|
84
|
+
const path = uri.searchParams.get('path')?.trim();
|
|
85
|
+
return createPanelAppContentPath(decodeURIComponent(appId), path);
|
|
81
86
|
}
|
|
82
87
|
|
|
83
88
|
function createPanelAppResourceUri(uri: ParsedResourceUri): string {
|
|
84
89
|
const appId = getPanelAppId(uri);
|
|
85
|
-
|
|
90
|
+
if (!appId) {
|
|
91
|
+
return 'nextclaw://panel-app';
|
|
92
|
+
}
|
|
93
|
+
const resourceUri = `nextclaw://panel-app/${encodeURIComponent(decodeURIComponent(appId))}`;
|
|
94
|
+
const path = uri.searchParams.get('path')?.trim();
|
|
95
|
+
return path ? `${resourceUri}?${new URLSearchParams({ path }).toString()}` : resourceUri;
|
|
86
96
|
}
|
|
87
97
|
|
|
88
98
|
function arePanelAppUrlsEquivalent(left: string, right: string): boolean {
|
|
@@ -90,8 +100,10 @@ function arePanelAppUrlsEquivalent(left: string, right: string): boolean {
|
|
|
90
100
|
const rightUri = parseResourceUri(right);
|
|
91
101
|
const leftAppId = getPanelAppId(leftUri);
|
|
92
102
|
const rightAppId = getPanelAppId(rightUri);
|
|
103
|
+
const leftPath = leftUri.searchParams.get('path')?.trim() ?? '';
|
|
104
|
+
const rightPath = rightUri.searchParams.get('path')?.trim() ?? '';
|
|
93
105
|
return leftAppId.length > 0 && rightAppId.length > 0
|
|
94
|
-
? decodeURIComponent(leftAppId) === decodeURIComponent(rightAppId)
|
|
106
|
+
? decodeURIComponent(leftAppId) === decodeURIComponent(rightAppId) && leftPath === rightPath
|
|
95
107
|
: left === right;
|
|
96
108
|
}
|
|
97
109
|
|
|
@@ -171,7 +183,9 @@ export const RIGHT_PANEL_RESOURCE_ROUTE_DEFINITIONS: RightPanelResourceRouteDefi
|
|
|
171
183
|
const url = resolvePanelAppPlaceholderUrl(uri);
|
|
172
184
|
const appId = getPanelAppId(uri);
|
|
173
185
|
return {
|
|
174
|
-
dedupeKey: appId
|
|
186
|
+
dedupeKey: appId
|
|
187
|
+
? `panel-app:${uri.searchParams.get('path')?.trim() || decodeURIComponent(appId)}`
|
|
188
|
+
: undefined,
|
|
175
189
|
historyPolicy: 'managed',
|
|
176
190
|
kind: RIGHT_PANEL_PANEL_APP_TAB_KIND,
|
|
177
191
|
resourceUri: createPanelAppResourceUri(uri),
|
|
@@ -59,4 +59,37 @@ describe('RightPanelResourceRouteResolver', () => {
|
|
|
59
59
|
'panel-app',
|
|
60
60
|
)).toBe(true);
|
|
61
61
|
});
|
|
62
|
+
|
|
63
|
+
it('preserves an explicit panel app source path across resource and content URLs', () => {
|
|
64
|
+
const resolver = new RightPanelResourceRouteResolver();
|
|
65
|
+
const resourceUri = 'nextclaw://panel-app/timer?path=%2Ftmp%2Ftimer.panel';
|
|
66
|
+
const contentUrl = '/api/panel-apps/timer/content?path=%2Ftmp%2Ftimer.panel';
|
|
67
|
+
|
|
68
|
+
expect(resolver.resolve(resourceUri)).toMatchObject({
|
|
69
|
+
dedupeKey: 'panel-app:/tmp/timer.panel',
|
|
70
|
+
kind: 'panel-app',
|
|
71
|
+
resourceUri,
|
|
72
|
+
url: contentUrl,
|
|
73
|
+
});
|
|
74
|
+
expect(resolver.resolveOpenTarget({
|
|
75
|
+
kind: 'panel-app',
|
|
76
|
+
url: contentUrl,
|
|
77
|
+
})).toMatchObject({
|
|
78
|
+
dedupeKey: 'panel-app:/tmp/timer.panel',
|
|
79
|
+
resourceUri,
|
|
80
|
+
url: contentUrl,
|
|
81
|
+
});
|
|
82
|
+
expect(resolver.areUrlsEquivalent(
|
|
83
|
+
resourceUri,
|
|
84
|
+
contentUrl,
|
|
85
|
+
'panel-app',
|
|
86
|
+
'panel-app',
|
|
87
|
+
)).toBe(true);
|
|
88
|
+
expect(resolver.areUrlsEquivalent(
|
|
89
|
+
resourceUri,
|
|
90
|
+
'nextclaw://panel-app/timer?path=%2Ftmp%2Fother.panel',
|
|
91
|
+
'panel-app',
|
|
92
|
+
'panel-app',
|
|
93
|
+
)).toBe(false);
|
|
94
|
+
});
|
|
62
95
|
});
|
package/src/features/right-panel-resources/utils/__tests__/right-panel-resource-uri.utils.test.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import type { PanelAppEntryView } from '@/shared/lib/api';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
createPanelAppContentPath,
|
|
5
|
+
createPanelAppResourceUri,
|
|
6
|
+
createPanelAppRightPanelResourceTarget,
|
|
7
|
+
} from '@/features/right-panel-resources/utils/right-panel-resource-uri.utils';
|
|
4
8
|
|
|
5
9
|
function createPanelAppEntry(overrides: Partial<PanelAppEntryView> = {}): PanelAppEntryView {
|
|
6
10
|
return {
|
|
@@ -22,6 +26,15 @@ function createPanelAppEntry(overrides: Partial<PanelAppEntryView> = {}): PanelA
|
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
describe('right-panel-resource uri utils', () => {
|
|
29
|
+
it('builds matching external panel app resource and content URLs', () => {
|
|
30
|
+
expect(createPanelAppResourceUri('demo', '/tmp/demo.panel')).toBe(
|
|
31
|
+
'nextclaw://panel-app/demo?path=%2Ftmp%2Fdemo.panel',
|
|
32
|
+
);
|
|
33
|
+
expect(createPanelAppContentPath('demo', '/tmp/demo.panel')).toBe(
|
|
34
|
+
'/api/panel-apps/demo/content?path=%2Ftmp%2Fdemo.panel',
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
25
38
|
it('keeps panel app image icons as dock image icons', () => {
|
|
26
39
|
const target = createPanelAppRightPanelResourceTarget(createPanelAppEntry({
|
|
27
40
|
icon: '/api/panel-apps/demo/assets/icon.png',
|
|
@@ -34,8 +34,16 @@ export function normalizeRightPanelAppsUrl(url: string): string {
|
|
|
34
34
|
return createRightPanelAppsUrl(getRightPanelAppsTabFromUrl(url));
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
export function createPanelAppResourceUri(appId: string): string {
|
|
38
|
-
|
|
37
|
+
export function createPanelAppResourceUri(appId: string, sourcePath?: string): string {
|
|
38
|
+
const uri = `nextclaw://panel-app/${encodeURIComponent(appId)}`;
|
|
39
|
+
const path = sourcePath?.trim();
|
|
40
|
+
return path ? `${uri}?${new URLSearchParams({ path }).toString()}` : uri;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createPanelAppContentPath(appId: string, sourcePath?: string): string {
|
|
44
|
+
const url = `/api/panel-apps/${encodeURIComponent(appId)}/content`;
|
|
45
|
+
const path = sourcePath?.trim();
|
|
46
|
+
return path ? `${url}?${new URLSearchParams({ path }).toString()}` : url;
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
function isPanelAppImageIcon(icon: string): boolean {
|
|
@@ -20,6 +20,7 @@ vi.mock('@/shared/hooks/use-server-path-create-directory', () => ({
|
|
|
20
20
|
}));
|
|
21
21
|
|
|
22
22
|
type PickerOverrides = {
|
|
23
|
+
allowCreateDirectory?: boolean;
|
|
23
24
|
currentPath?: string;
|
|
24
25
|
defaultWorkspacePath?: string;
|
|
25
26
|
description?: string;
|
|
@@ -29,6 +30,7 @@ type PickerOverrides = {
|
|
|
29
30
|
const createPickerElement = (overrides?: PickerOverrides) => (
|
|
30
31
|
<ServerPathPickerDialog
|
|
31
32
|
open={overrides?.open ?? true}
|
|
33
|
+
allowCreateDirectory={overrides?.allowCreateDirectory}
|
|
32
34
|
currentPath={overrides?.currentPath ?? '/workspace'}
|
|
33
35
|
defaultWorkspacePath={overrides?.defaultWorkspacePath}
|
|
34
36
|
isSaving={false}
|
|
@@ -236,6 +238,13 @@ describe('ServerPathPickerDialog', () => {
|
|
|
236
238
|
);
|
|
237
239
|
});
|
|
238
240
|
|
|
241
|
+
it('hides directory creation when the caller only allows existing directories', () => {
|
|
242
|
+
renderPicker({ allowCreateDirectory: false });
|
|
243
|
+
|
|
244
|
+
expect(screen.queryByRole('button', { name: '新建文件夹' })).toBeNull();
|
|
245
|
+
expect(screen.getByText('单击选择,双击进入')).toBeTruthy();
|
|
246
|
+
});
|
|
247
|
+
|
|
239
248
|
it('uses single click to select and double click to enter a folder', async () => {
|
|
240
249
|
const user = userEvent.setup();
|
|
241
250
|
renderPicker();
|