@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
|
@@ -16,8 +16,12 @@ import { useChatSessionListStore } from "@/features/chat/stores/chat-session-lis
|
|
|
16
16
|
import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
|
|
17
17
|
import { useSystemStatus } from "@/features/system-status";
|
|
18
18
|
import { useAgents } from "@/shared/hooks/use-agents";
|
|
19
|
-
import {
|
|
20
|
-
|
|
19
|
+
import {
|
|
20
|
+
useAddExistingProject,
|
|
21
|
+
useCreateProject,
|
|
22
|
+
useProjects,
|
|
23
|
+
} from "@/shared/hooks/use-projects";
|
|
24
|
+
import type { ProjectAddExistingRequest, ProjectCreateRequest } from "@/shared/lib/api";
|
|
21
25
|
import { normalizeSessionProjectRootValue } from "@/shared/lib/session-project";
|
|
22
26
|
import { cn } from "@/shared/lib/utils";
|
|
23
27
|
import { LANGUAGE_OPTIONS, t, type I18nLanguage } from "@/shared/lib/i18n";
|
|
@@ -45,7 +49,7 @@ import {
|
|
|
45
49
|
import { useChatNewSessionTypePreference } from "@/features/chat/features/session-type/hooks/use-chat-new-session-type-preference";
|
|
46
50
|
import { useViewportLayoutStore } from "@/app/stores/viewport-layout.store";
|
|
47
51
|
import { SIDEBAR_RAIL_WIDTH_CLASS } from "@/app/components/layout/sidebar-rail.styles";
|
|
48
|
-
import {
|
|
52
|
+
import { ChatProjectAddDialog } from "@/features/chat/features/project/components/chat-project-add-dialog";
|
|
49
53
|
|
|
50
54
|
type ChatSidebarVariant = "desktop" | "mobile";
|
|
51
55
|
|
|
@@ -98,7 +102,7 @@ export function ChatSidebar({
|
|
|
98
102
|
(state) => state.isSidebarCollapsed,
|
|
99
103
|
);
|
|
100
104
|
const [isCreateMenuOpen, setIsCreateMenuOpen] = useState(false);
|
|
101
|
-
const [
|
|
105
|
+
const [isProjectAddOpen, setIsProjectAddOpen] = useState(false);
|
|
102
106
|
const [isUtilityMenuOpen, setIsUtilityMenuOpen] = useState(false);
|
|
103
107
|
const listSnapshot = useChatSessionListStore((state) => state.snapshot);
|
|
104
108
|
const sessionTypesData = useChatQueryStore(
|
|
@@ -111,6 +115,7 @@ export function ChatSidebar({
|
|
|
111
115
|
const agentsQuery = useAgents();
|
|
112
116
|
const projectsQuery = useProjects();
|
|
113
117
|
const projectCreateMutation = useCreateProject();
|
|
118
|
+
const projectAddExistingMutation = useAddExistingProject();
|
|
114
119
|
const { isLoading, items } = useNcpSessionListView();
|
|
115
120
|
const { language, setLanguage } = useI18n();
|
|
116
121
|
const { theme, setTheme } = useTheme();
|
|
@@ -246,13 +251,20 @@ export function ChatSidebar({
|
|
|
246
251
|
sessionType,
|
|
247
252
|
});
|
|
248
253
|
};
|
|
249
|
-
const
|
|
254
|
+
const openProjectAdd = () => {
|
|
250
255
|
projectCreateMutation.reset();
|
|
251
|
-
|
|
256
|
+
projectAddExistingMutation.reset();
|
|
257
|
+
setIsProjectAddOpen(true);
|
|
252
258
|
};
|
|
253
259
|
const createProjectFromSidebar = async (input: ProjectCreateRequest): Promise<void> => {
|
|
254
260
|
await projectCreateMutation.mutateAsync(input);
|
|
255
|
-
|
|
261
|
+
setIsProjectAddOpen(false);
|
|
262
|
+
};
|
|
263
|
+
const addExistingProjectFromSidebar = async (
|
|
264
|
+
input: ProjectAddExistingRequest,
|
|
265
|
+
): Promise<void> => {
|
|
266
|
+
await projectAddExistingMutation.mutateAsync(input);
|
|
267
|
+
setIsProjectAddOpen(false);
|
|
256
268
|
};
|
|
257
269
|
|
|
258
270
|
return (
|
|
@@ -324,7 +336,7 @@ export function ChatSidebar({
|
|
|
324
336
|
isCollapsed={shouldCollapse}
|
|
325
337
|
isLoading={isLoading}
|
|
326
338
|
isProjectFirstView={isProjectFirstView}
|
|
327
|
-
|
|
339
|
+
onAddProject={openProjectAdd}
|
|
328
340
|
onSelectMode={presenter.chatSessionListManager.setListMode}
|
|
329
341
|
projectGroups={projectGroups}
|
|
330
342
|
renderSessionItem={renderSessionItem}
|
|
@@ -354,16 +366,19 @@ export function ChatSidebar({
|
|
|
354
366
|
/>
|
|
355
367
|
) : null}
|
|
356
368
|
|
|
357
|
-
<
|
|
358
|
-
open={
|
|
369
|
+
<ChatProjectAddDialog
|
|
370
|
+
open={isProjectAddOpen}
|
|
359
371
|
defaultWorkspacePath={normalizeSessionProjectRootValue(
|
|
360
372
|
config?.agents.defaults.workspace,
|
|
361
373
|
)}
|
|
362
374
|
templates={projectsQuery.data?.templates ?? []}
|
|
363
375
|
isCreating={projectCreateMutation.isPending}
|
|
364
|
-
|
|
365
|
-
|
|
376
|
+
isAddingExisting={projectAddExistingMutation.isPending}
|
|
377
|
+
createErrorMessage={projectCreateMutation.error?.message}
|
|
378
|
+
addExistingErrorMessage={projectAddExistingMutation.error?.message}
|
|
379
|
+
onOpenChange={setIsProjectAddOpen}
|
|
366
380
|
onCreate={createProjectFromSidebar}
|
|
381
|
+
onAddExisting={addExistingProjectFromSidebar}
|
|
367
382
|
/>
|
|
368
383
|
</aside>
|
|
369
384
|
);
|
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
} from '@/features/chat/managers/chat-recent-skills.manager';
|
|
50
50
|
|
|
51
51
|
import { useSessionConversationInputAttachments } from '@/features/chat/features/conversation/hooks/use-session-conversation-input-attachments';
|
|
52
|
+
import { useSessionConversationSlashCommands } from '@/features/chat/features/conversation/hooks/use-session-conversation-slash-commands';
|
|
52
53
|
import { ChatConversationTrack } from '@/features/chat/components/conversation/chat-conversation-track';
|
|
53
54
|
import { useChatMessageLayoutStore } from '@/features/chat/stores/chat-message-layout.store';
|
|
54
55
|
import type { useSessionConversationInputQuery } from '@/features/chat/features/conversation/hooks/use-session-conversation-input-query';
|
|
@@ -222,26 +223,10 @@ export const SessionConversationInput = memo(function SessionConversationInput(p
|
|
|
222
223
|
skillGroupLabels: labels.skillGroupLabels,
|
|
223
224
|
skillScopeLabels: labels.skillScopeLabels,
|
|
224
225
|
});
|
|
225
|
-
const slashCommands =
|
|
226
|
-
const parentSessionKey = inputQuery.selectedSessionKey?.trim();
|
|
227
|
-
if (!parentSessionKey) {
|
|
228
|
-
return [];
|
|
229
|
-
}
|
|
230
|
-
return [
|
|
231
|
-
{
|
|
232
|
-
key: 'side-chat',
|
|
233
|
-
title: t('chatSlashCommandSideChatTitle', language),
|
|
234
|
-
description: t('chatSlashCommandSideChatDescription', language),
|
|
235
|
-
detailLines: [t('chatSlashCommandSideChatDetail', language)],
|
|
236
|
-
keywords: ['side', 'chat', 'child', 'branch', 'new'],
|
|
237
|
-
onSelect: () => presenter.chatThreadManager.openSideChatDraft(parentSessionKey),
|
|
238
|
-
},
|
|
239
|
-
];
|
|
240
|
-
}, [
|
|
241
|
-
inputQuery.selectedSessionKey,
|
|
226
|
+
const slashCommands = useSessionConversationSlashCommands({
|
|
242
227
|
language,
|
|
243
|
-
|
|
244
|
-
|
|
228
|
+
selectedSessionKey: inputQuery.selectedSessionKey,
|
|
229
|
+
});
|
|
245
230
|
const handleSlashPanelAppSelect = useCallback(
|
|
246
231
|
(appId: string) =>
|
|
247
232
|
void presenter.chatUiManager.showContent({
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { act, renderHook, waitFor } from '@testing-library/react';
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { toast } from 'sonner';
|
|
4
|
+
import { compactNcpSessionContext } from '@/shared/lib/api';
|
|
5
|
+
import { useSessionConversationSlashCommands } from '@/features/chat/features/conversation/hooks/use-session-conversation-slash-commands';
|
|
6
|
+
|
|
7
|
+
const mocks = vi.hoisted(() => ({
|
|
8
|
+
compactNcpSessionContext: vi.fn(),
|
|
9
|
+
openSideChatDraft: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock('sonner', () => ({
|
|
13
|
+
toast: { error: vi.fn(), success: vi.fn() },
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock('@/shared/lib/api', () => ({
|
|
17
|
+
compactNcpSessionContext: mocks.compactNcpSessionContext,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock('@/features/chat/components/providers/chat-presenter.provider', () => ({
|
|
21
|
+
usePresenter: () => ({
|
|
22
|
+
chatThreadManager: { openSideChatDraft: mocks.openSideChatDraft },
|
|
23
|
+
}),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
describe('useSessionConversationSlashCommands', () => {
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
vi.clearAllMocks();
|
|
29
|
+
mocks.compactNcpSessionContext.mockResolvedValue({
|
|
30
|
+
compacted: true,
|
|
31
|
+
sessionId: 'session-1',
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('exposes the runtime compaction command for the selected session', async () => {
|
|
36
|
+
const { result } = renderHook(() => useSessionConversationSlashCommands({
|
|
37
|
+
language: 'en',
|
|
38
|
+
selectedSessionKey: ' session-1 ',
|
|
39
|
+
}));
|
|
40
|
+
const command = result.current.find((entry) => entry.key === 'compact-context');
|
|
41
|
+
|
|
42
|
+
act(() => command?.onSelect());
|
|
43
|
+
|
|
44
|
+
await waitFor(() => expect(compactNcpSessionContext).toHaveBeenCalledWith('session-1'));
|
|
45
|
+
expect(toast.success).toHaveBeenCalledOnce();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('reports runtime errors and suppresses duplicate in-flight requests', async () => {
|
|
49
|
+
let rejectRequest!: (error: Error) => void;
|
|
50
|
+
mocks.compactNcpSessionContext.mockReturnValue(new Promise((_resolve, reject) => {
|
|
51
|
+
rejectRequest = reject;
|
|
52
|
+
}));
|
|
53
|
+
const { result } = renderHook(() => useSessionConversationSlashCommands({
|
|
54
|
+
language: 'en',
|
|
55
|
+
selectedSessionKey: 'session-1',
|
|
56
|
+
}));
|
|
57
|
+
const command = result.current.find((entry) => entry.key === 'compact-context');
|
|
58
|
+
|
|
59
|
+
act(() => {
|
|
60
|
+
command?.onSelect();
|
|
61
|
+
command?.onSelect();
|
|
62
|
+
});
|
|
63
|
+
expect(compactNcpSessionContext).toHaveBeenCalledOnce();
|
|
64
|
+
rejectRequest(new Error('session is busy'));
|
|
65
|
+
|
|
66
|
+
await waitFor(() => expect(toast.error).toHaveBeenCalledWith(
|
|
67
|
+
expect.stringContaining('session is busy'),
|
|
68
|
+
));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('does not expose session commands before a session exists', () => {
|
|
72
|
+
const { result } = renderHook(() => useSessionConversationSlashCommands({
|
|
73
|
+
language: 'en',
|
|
74
|
+
selectedSessionKey: null,
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
expect(result.current).toEqual([]);
|
|
78
|
+
});
|
|
79
|
+
});
|
package/src/features/chat/features/conversation/hooks/use-session-conversation-slash-commands.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useCallback, useMemo, useRef } from 'react';
|
|
2
|
+
import { toast } from 'sonner';
|
|
3
|
+
import { compactNcpSessionContext } from '@/shared/lib/api';
|
|
4
|
+
import { t, type I18nLanguage } from '@/shared/lib/i18n';
|
|
5
|
+
import { usePresenter } from '@/features/chat/components/providers/chat-presenter.provider';
|
|
6
|
+
import type { ChatSlashCommandDescriptor } from '@/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils';
|
|
7
|
+
|
|
8
|
+
export function useSessionConversationSlashCommands(params: {
|
|
9
|
+
language: I18nLanguage;
|
|
10
|
+
selectedSessionKey?: string | null;
|
|
11
|
+
}): readonly ChatSlashCommandDescriptor[] {
|
|
12
|
+
const { language, selectedSessionKey } = params;
|
|
13
|
+
const presenter = usePresenter();
|
|
14
|
+
const compactingSessionIdsRef = useRef(new Set<string>());
|
|
15
|
+
const compactContext = useCallback(async (sessionId: string) => {
|
|
16
|
+
if (compactingSessionIdsRef.current.has(sessionId)) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
compactingSessionIdsRef.current.add(sessionId);
|
|
20
|
+
try {
|
|
21
|
+
await compactNcpSessionContext(sessionId);
|
|
22
|
+
toast.success(t('chatSlashCommandCompactContextSuccess', language));
|
|
23
|
+
} catch (error) {
|
|
24
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
25
|
+
toast.error(`${t('chatSlashCommandCompactContextFailed', language)}: ${message}`);
|
|
26
|
+
} finally {
|
|
27
|
+
compactingSessionIdsRef.current.delete(sessionId);
|
|
28
|
+
}
|
|
29
|
+
}, [language]);
|
|
30
|
+
|
|
31
|
+
return useMemo(() => {
|
|
32
|
+
const sessionId = selectedSessionKey?.trim();
|
|
33
|
+
if (!sessionId) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
return [
|
|
37
|
+
{
|
|
38
|
+
key: 'side-chat',
|
|
39
|
+
title: t('chatSlashCommandSideChatTitle', language),
|
|
40
|
+
description: t('chatSlashCommandSideChatDescription', language),
|
|
41
|
+
detailLines: [t('chatSlashCommandSideChatDetail', language)],
|
|
42
|
+
keywords: ['side', 'chat', 'child', 'branch', 'new'],
|
|
43
|
+
onSelect: () => presenter.chatThreadManager.openSideChatDraft(sessionId),
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
key: 'compact-context',
|
|
47
|
+
title: t('chatSlashCommandCompactContextTitle', language),
|
|
48
|
+
description: t('chatSlashCommandCompactContextDescription', language),
|
|
49
|
+
detailLines: [t('chatSlashCommandCompactContextDetail', language)],
|
|
50
|
+
keywords: ['compact', 'compress', 'context', 'summary', '压缩', '上下文'],
|
|
51
|
+
onSelect: () => void compactContext(sessionId),
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
}, [compactContext, language, presenter.chatThreadManager, selectedSessionKey]);
|
|
55
|
+
}
|
package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx
CHANGED
|
@@ -126,6 +126,34 @@ it("renders panel apps through the shared adaptive inline surface", () => {
|
|
|
126
126
|
);
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
+
it("renders and expands a panel app from an explicit external path", () => {
|
|
130
|
+
render(
|
|
131
|
+
<ChatInlinePanelAppCard
|
|
132
|
+
panelApp={{
|
|
133
|
+
appId: "weather-card",
|
|
134
|
+
path: "/tmp/weather-card.panel",
|
|
135
|
+
title: "External Weather",
|
|
136
|
+
}}
|
|
137
|
+
/>,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
expect(screen.getByTitle("External Weather").getAttribute("src")).toBe(
|
|
141
|
+
"/api/panel-apps/weather-card/content?path=%2Ftmp%2Fweather-card.panel&nextclawDisplayMode=card&nextclawPlacement=inline",
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
fireEvent.click(screen.getByLabelText("chatPanelCardExpand"));
|
|
145
|
+
|
|
146
|
+
expect(mocks.openTarget).not.toHaveBeenCalled();
|
|
147
|
+
expect(mocks.open).toHaveBeenCalledWith(
|
|
148
|
+
"/api/panel-apps/weather-card/content?path=%2Ftmp%2Fweather-card.panel",
|
|
149
|
+
{
|
|
150
|
+
dedupeKey: "panel-app:/tmp/weather-card.panel",
|
|
151
|
+
kind: "panel-app",
|
|
152
|
+
title: "External Weather",
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
129
157
|
it("ignores inline height messages from a different window", () => {
|
|
130
158
|
const { container } = render(
|
|
131
159
|
<ChatInlinePanelAppCard
|
|
@@ -22,7 +22,7 @@ import { t } from "@/shared/lib/i18n";
|
|
|
22
22
|
|
|
23
23
|
type ChatInlinePanelAppDescriptor = Pick<
|
|
24
24
|
ChatPanelAppCardViewModel,
|
|
25
|
-
"appId" | "title"
|
|
25
|
+
"appId" | "path" | "title"
|
|
26
26
|
>;
|
|
27
27
|
|
|
28
28
|
export function ChatInlinePanelAppCard({
|
|
@@ -39,17 +39,19 @@ export function ChatInlinePanelAppCard({
|
|
|
39
39
|
const panelApps = usePanelApps();
|
|
40
40
|
const entry = useMemo(
|
|
41
41
|
() =>
|
|
42
|
-
findPanelAppEntryByDisplayId(
|
|
42
|
+
panelApp.path ? undefined : findPanelAppEntryByDisplayId(
|
|
43
43
|
panelApps.data?.entries ?? [],
|
|
44
44
|
panelApp.appId,
|
|
45
45
|
),
|
|
46
|
-
[panelApp.appId, panelApps.data?.entries],
|
|
46
|
+
[panelApp.appId, panelApp.path, panelApps.data?.entries],
|
|
47
47
|
);
|
|
48
48
|
const target = entry ? createPanelAppRightPanelResourceTarget(entry) : null;
|
|
49
49
|
const title = panelApp.title ?? target?.title ?? panelApp.appId;
|
|
50
|
-
const url =
|
|
51
|
-
?
|
|
52
|
-
:
|
|
50
|
+
const url = panelApp.path
|
|
51
|
+
? createFallbackPanelAppContentPath(panelApp.appId, panelApp.path)
|
|
52
|
+
: panelApps.isLoading
|
|
53
|
+
? ""
|
|
54
|
+
: (target?.url ?? createFallbackPanelAppContentPath(panelApp.appId));
|
|
53
55
|
const cardUrl = useMemo(
|
|
54
56
|
() => (url ? createInlinePanelAppCardUrl(url) : ""),
|
|
55
57
|
[url],
|
|
@@ -58,10 +60,11 @@ export function ChatInlinePanelAppCard({
|
|
|
58
60
|
() =>
|
|
59
61
|
createInlinePanelAppTab({
|
|
60
62
|
appId: entry?.id ?? panelApp.appId,
|
|
63
|
+
path: panelApp.path,
|
|
61
64
|
title,
|
|
62
65
|
url,
|
|
63
66
|
}),
|
|
64
|
-
[entry?.id, panelApp.appId, title, url],
|
|
67
|
+
[entry?.id, panelApp.appId, panelApp.path, title, url],
|
|
65
68
|
);
|
|
66
69
|
const iframeInstanceId = `${tab.id}:${tab.navVersion}:${iframeId}`;
|
|
67
70
|
const openExpanded = () => {
|
|
@@ -149,6 +149,7 @@ describe("buildShowContentToolCard", () => {
|
|
|
149
149
|
type: "panel_app",
|
|
150
150
|
payload: {
|
|
151
151
|
appId: "reader",
|
|
152
|
+
path: "/tmp/reader.panel",
|
|
152
153
|
},
|
|
153
154
|
},
|
|
154
155
|
title: "Reader",
|
|
@@ -164,6 +165,7 @@ describe("buildShowContentToolCard", () => {
|
|
|
164
165
|
expect(card).toMatchObject({
|
|
165
166
|
panelApp: {
|
|
166
167
|
appId: "reader",
|
|
168
|
+
path: "/tmp/reader.panel",
|
|
167
169
|
title: "Reader",
|
|
168
170
|
action: {
|
|
169
171
|
kind: "show-content",
|
|
@@ -173,6 +175,7 @@ describe("buildShowContentToolCard", () => {
|
|
|
173
175
|
type: "panel_app",
|
|
174
176
|
payload: {
|
|
175
177
|
appId: "reader",
|
|
178
|
+
path: "/tmp/reader.panel",
|
|
176
179
|
},
|
|
177
180
|
},
|
|
178
181
|
title: "Reader",
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
+
createPanelAppContentPath,
|
|
2
3
|
createPanelAppResourceUri,
|
|
3
4
|
RIGHT_PANEL_PANEL_APP_TAB_KIND,
|
|
4
5
|
} from "@/features/right-panel-resources";
|
|
5
6
|
import { PANEL_APP_INLINE_HOST_CONTRACT } from "@nextclaw/shared";
|
|
6
7
|
import type { DocBrowserTab } from "@/shared/components/doc-browser/doc-browser-context";
|
|
7
8
|
|
|
8
|
-
export function createFallbackPanelAppContentPath(appId: string): string {
|
|
9
|
-
return
|
|
9
|
+
export function createFallbackPanelAppContentPath(appId: string, sourcePath?: string): string {
|
|
10
|
+
return createPanelAppContentPath(appId, sourcePath);
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export function createInlinePanelAppCardUrl(url: string): string {
|
|
@@ -35,19 +36,20 @@ export function readInlinePanelAppContentHeight(value: unknown): number | null {
|
|
|
35
36
|
|
|
36
37
|
export function createInlinePanelAppTab(params: {
|
|
37
38
|
appId: string;
|
|
39
|
+
path?: string;
|
|
38
40
|
title: string;
|
|
39
41
|
url: string;
|
|
40
42
|
}): DocBrowserTab {
|
|
41
|
-
const { appId, title, url } = params;
|
|
43
|
+
const { appId, path, title, url } = params;
|
|
42
44
|
return {
|
|
43
45
|
currentUrl: url,
|
|
44
|
-
dedupeKey: `panel-app:${appId}`,
|
|
46
|
+
dedupeKey: `panel-app:${path ?? appId}`,
|
|
45
47
|
history: [url],
|
|
46
48
|
historyIndex: 0,
|
|
47
|
-
id: `inline-panel-app:${appId}`,
|
|
49
|
+
id: `inline-panel-app:${path ?? appId}`,
|
|
48
50
|
kind: RIGHT_PANEL_PANEL_APP_TAB_KIND,
|
|
49
51
|
navVersion: 0,
|
|
50
|
-
resourceUri: createPanelAppResourceUri(appId),
|
|
52
|
+
resourceUri: createPanelAppResourceUri(appId, path),
|
|
51
53
|
title,
|
|
52
54
|
};
|
|
53
55
|
}
|
package/src/features/chat/features/message/utils/chat-message-show-content-tool-card.utils.ts
CHANGED
|
@@ -121,6 +121,7 @@ function readShowContentRequest(value: unknown): ChatUiShowContentRequest | null
|
|
|
121
121
|
type: "panel_app",
|
|
122
122
|
payload: {
|
|
123
123
|
appId,
|
|
124
|
+
path: readOptionalString(payload.path),
|
|
124
125
|
},
|
|
125
126
|
},
|
|
126
127
|
title,
|
|
@@ -178,6 +179,7 @@ export function buildShowContentToolCard(params: {
|
|
|
178
179
|
const panelApp = request.target.type === "panel_app" && request.placement === "inline"
|
|
179
180
|
? {
|
|
180
181
|
appId: request.target.payload.appId,
|
|
182
|
+
path: request.target.payload.path,
|
|
181
183
|
title: request.title,
|
|
182
184
|
action: showContentAction,
|
|
183
185
|
}
|
package/src/features/chat/features/project/components/__tests__/chat-project-add-dialog.test.tsx
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import type { ComponentProps } from 'react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { ChatProjectAddDialog } from '@/features/chat/features/project/components/chat-project-add-dialog';
|
|
6
|
+
import { setLanguage } from '@/shared/lib/i18n';
|
|
7
|
+
|
|
8
|
+
const pathPickerSpy = vi.fn();
|
|
9
|
+
|
|
10
|
+
vi.mock('@/shared/components/path-picker/server-path-picker-dialog', () => ({
|
|
11
|
+
ServerPathPickerDialog: (props: unknown) => pathPickerSpy(props),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
function renderDialog(overrides: Partial<ComponentProps<typeof ChatProjectAddDialog>> = {}) {
|
|
15
|
+
const onCreate = vi.fn(async () => undefined);
|
|
16
|
+
const onAddExisting = vi.fn(async () => undefined);
|
|
17
|
+
render(
|
|
18
|
+
<ChatProjectAddDialog
|
|
19
|
+
open
|
|
20
|
+
defaultWorkspacePath="/workspace"
|
|
21
|
+
templates={[
|
|
22
|
+
{ id: 'empty', name: 'Empty', description: 'Empty project' },
|
|
23
|
+
{
|
|
24
|
+
id: 'knowledge-base',
|
|
25
|
+
name: 'Knowledge base',
|
|
26
|
+
description: 'Knowledge base project',
|
|
27
|
+
},
|
|
28
|
+
]}
|
|
29
|
+
isCreating={false}
|
|
30
|
+
isAddingExisting={false}
|
|
31
|
+
onOpenChange={() => {}}
|
|
32
|
+
onCreate={onCreate}
|
|
33
|
+
onAddExisting={onAddExisting}
|
|
34
|
+
{...overrides}
|
|
35
|
+
/>,
|
|
36
|
+
);
|
|
37
|
+
return { onCreate, onAddExisting };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('ChatProjectAddDialog', () => {
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
setLanguage('zh');
|
|
43
|
+
pathPickerSpy.mockClear();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('creates a new project with an optional project directory and template', async () => {
|
|
47
|
+
const user = userEvent.setup();
|
|
48
|
+
const { onCreate, onAddExisting } = renderDialog();
|
|
49
|
+
|
|
50
|
+
expect(screen.getByRole('heading', { name: '添加项目' })).toBeTruthy();
|
|
51
|
+
expect(screen.getByRole('button', { name: '新建项目' }).getAttribute('aria-pressed')).toBe(
|
|
52
|
+
'true',
|
|
53
|
+
);
|
|
54
|
+
await user.type(screen.getByLabelText('项目名称'), '个人知识库');
|
|
55
|
+
fireEvent.change(document.querySelector('select')!, {
|
|
56
|
+
target: { value: 'knowledge-base' },
|
|
57
|
+
});
|
|
58
|
+
await user.click(screen.getByRole('button', { name: '浏览' }));
|
|
59
|
+
expect(pathPickerSpy).toHaveBeenLastCalledWith(
|
|
60
|
+
expect.objectContaining({
|
|
61
|
+
open: true,
|
|
62
|
+
allowCreateDirectory: true,
|
|
63
|
+
currentPath: '',
|
|
64
|
+
defaultWorkspacePath: '/workspace',
|
|
65
|
+
title: '选择或新建项目目录',
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
const pickerProps = pathPickerSpy.mock.lastCall?.[0] as {
|
|
69
|
+
onConfirm: (path: string) => void;
|
|
70
|
+
};
|
|
71
|
+
act(() => pickerProps.onConfirm('/srv/knowledge'));
|
|
72
|
+
expect((screen.getByLabelText('项目目录(可选)') as HTMLInputElement).value).toBe(
|
|
73
|
+
'/srv/knowledge',
|
|
74
|
+
);
|
|
75
|
+
await user.click(screen.getByRole('button', { name: '创建项目' }));
|
|
76
|
+
|
|
77
|
+
await waitFor(() => {
|
|
78
|
+
expect(onCreate).toHaveBeenCalledWith({
|
|
79
|
+
name: '个人知识库',
|
|
80
|
+
rootPath: '/srv/knowledge',
|
|
81
|
+
template: 'knowledge-base',
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
expect(onAddExisting).not.toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('adds an existing directory without exposing project initialization', async () => {
|
|
88
|
+
const user = userEvent.setup();
|
|
89
|
+
const { onCreate, onAddExisting } = renderDialog();
|
|
90
|
+
|
|
91
|
+
await user.click(screen.getByRole('button', { name: '添加已有目录' }));
|
|
92
|
+
expect(screen.queryByLabelText('项目名称')).toBeNull();
|
|
93
|
+
expect(screen.queryByLabelText('项目模板')).toBeNull();
|
|
94
|
+
expect(screen.getByText(
|
|
95
|
+
'选择服务端机器上的已有目录。项目名称取目录名,不会修改或初始化目录内容。',
|
|
96
|
+
)).toBeTruthy();
|
|
97
|
+
await user.click(screen.getByRole('button', { name: '浏览' }));
|
|
98
|
+
expect(pathPickerSpy).toHaveBeenLastCalledWith(
|
|
99
|
+
expect.objectContaining({
|
|
100
|
+
open: true,
|
|
101
|
+
allowCreateDirectory: false,
|
|
102
|
+
title: '选择已有项目目录',
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
const pickerProps = pathPickerSpy.mock.lastCall?.[0] as {
|
|
106
|
+
onConfirm: (path: string) => void;
|
|
107
|
+
};
|
|
108
|
+
act(() => pickerProps.onConfirm('/srv/existing-project'));
|
|
109
|
+
await user.click(screen.getByRole('button', { name: '添加到项目列表' }));
|
|
110
|
+
|
|
111
|
+
await waitFor(() => {
|
|
112
|
+
expect(onAddExisting).toHaveBeenCalledWith({ rootPath: '/srv/existing-project' });
|
|
113
|
+
});
|
|
114
|
+
expect(onCreate).not.toHaveBeenCalled();
|
|
115
|
+
});
|
|
116
|
+
});
|