@nextclaw/ui 0.15.11 → 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 +41 -0
- package/dist/assets/{api-pbx14YAL.js → api-DGG69ZcJ.js} +3 -3
- package/dist/assets/app-presenter-provider-B1RgR5ug.js +106 -0
- package/dist/assets/{appearance-settings-page-D9wJdf-0.js → appearance-settings-page-BZSd1SrH.js} +1 -1
- package/dist/assets/{book-open-DZyssdCc.js → book-open-BhaS5BJv.js} +1 -1
- package/dist/assets/{card-D7l-gWkY.js → card-Bcr5YM8F.js} +1 -1
- package/dist/assets/{channels-list-page-B3qW3T-B.js → channels-list-page-C_x83ZCT.js} +2 -2
- package/dist/assets/{chat-page-DUI_Jm6r.js → chat-page-D2xfR6TK.js} +1 -1
- package/dist/assets/{config-split-page-DFEKStD7.js → config-split-page-RXAoLeNv.js} +1 -1
- package/dist/assets/{confirm-dialog-7wpxfMCp.js → confirm-dialog-o_VPKvVD.js} +1 -1
- package/dist/assets/{desktop-update-config-DP6IbxBO.js → desktop-update-config-DEOMOsDN.js} +1 -1
- package/dist/assets/{dist-B9UN0HrS.js → dist-C87P4QWA.js} +1 -1
- package/dist/assets/doc-browser-BuGh646I.js +1 -0
- package/dist/assets/{doc-browser-D7klPMHs.js → doc-browser-DQ75iUkA.js} +1 -1
- package/dist/assets/{doc-browser-context-Bw9m83Jp.js → doc-browser-context-tE0--wSa.js} +1 -1
- package/dist/assets/{eye-IVMB0Rrz.js → eye-DL9QkrKg.js} +1 -1
- package/dist/assets/{form-actions-DrFAgO58.js → form-actions-J0tIaUdK.js} +1 -1
- package/dist/assets/index-B2YF-7Yw.css +1 -0
- package/dist/assets/index-B9QEjnoY.js +2 -0
- package/dist/assets/loader-circle-CAnHHSrY.js +1 -0
- package/dist/assets/{logo-badge-DoZOVuPr.js → logo-badge-BYTbZbOF.js} +1 -1
- package/dist/assets/{mcp-marketplace-page-C151EGP-.js → mcp-marketplace-page-COWlzeP1.js} +2 -2
- package/dist/assets/mcp-marketplace-page-CaUFv37E.js +1 -0
- package/dist/assets/{model-config-page-BmfQzc52.js → model-config-page-oNqnMXNY.js} +1 -1
- package/dist/assets/{navigation-link-CJw6FLOj.js → navigation-link-Dk6VdDkc.js} +1 -1
- package/dist/assets/plus-DjdTWPjQ.js +1 -0
- package/dist/assets/{popover-DitLJE6k.js → popover-Txwrum65.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-CuAtduIc.js → provider-scoped-model-input-B7rQM4b6.js} +1 -1
- package/dist/assets/{providers-config-page-CrmuY4DC.js → providers-config-page-CP-OnvLK.js} +1 -1
- package/dist/assets/{react-CTDaaQ9V.js → react-B0LbRnAE.js} +1 -1
- package/dist/assets/{refresh-cw-E5qTlQY6.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-1fPJJvo5.js → runtime-config-page-u_fuHxc8.js} +1 -1
- package/dist/assets/{save-CTiZvAd2.js → save-R_qyrHpa.js} +1 -1
- package/dist/assets/{search-CztGmpnR.js → search-UKxiouNi.js} +1 -1
- package/dist/assets/{search-config-page-D3jVALZU.js → search-config-page-DzkpSGFH.js} +1 -1
- package/dist/assets/{secrets-config-page-CxWLQCjZ.js → secrets-config-page-DhoLYm7Z.js} +2 -2
- package/dist/assets/security-config-yraYoy9s.js +1 -0
- package/dist/assets/{select-BPVI2dQ_.js → select-CYHT8P5S.js} +1 -1
- package/dist/assets/{settings-2-Cd7QaBsq.js → settings-2-DGh4bGSi.js} +1 -1
- package/dist/assets/skeleton-BQuylYFO.js +1 -0
- package/dist/assets/{tabs-DrUmYwEQ.js → tabs-BCihm1zK.js} +1 -1
- package/dist/assets/{tag-chip-CmViAZKK.js → tag-chip-De0iz4o2.js} +1 -1
- package/dist/assets/{trash-2-DMRJ63AB.js → trash-2-Bo3DWiju.js} +1 -1
- package/dist/assets/use-config-DCzwgChy.js +1 -0
- package/dist/assets/{use-confirm-dialog-Du33bHPz.js → use-confirm-dialog-WZ94CehU.js} +1 -1
- package/dist/assets/{use-viewport-layout-D80O1d90.js → use-viewport-layout-CZ-3pMne.js} +1 -1
- package/dist/assets/x-NZxXyqW1.js +1 -0
- package/dist/index.html +28 -28
- 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/__tests__/chat-message-list.container.test.tsx +11 -12
- 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-execution-summary.utils.test.ts +27 -15
- 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-execution-summary.utils.ts +1 -6
- package/src/features/chat/features/message/utils/chat-message-show-content-tool-card.utils.ts +2 -0
- package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +0 -1
- 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/cron/components/cron-task-discovery.tsx +1 -6
- 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 -9
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +26 -9
- 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-Q6u2xBAn.js +0 -106
- package/dist/assets/doc-browser-wgEeLkn1.js +0 -1
- package/dist/assets/index-D297NGOb.js +0 -2
- package/dist/assets/index-DqZoodZL.css +0 -1
- package/dist/assets/loader-circle-DDgFo4Kv.js +0 -1
- package/dist/assets/mcp-marketplace-page-BtNExfMn.js +0 -1
- package/dist/assets/plus-CYxLSfLh.js +0 -1
- package/dist/assets/remote-YzPpOPmK.js +0 -1
- package/dist/assets/rotate-cw-BXeiDT6C.js +0 -1
- package/dist/assets/security-config-DEyj4WBh.js +0 -1
- package/dist/assets/skeleton-CUpAXGoe.js +0 -1
- package/dist/assets/use-config-gpXy2Iyc.js +0 -1
- package/dist/assets/x-CsLMuNFe.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
|
package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx
CHANGED
|
@@ -358,7 +358,7 @@ it("derives completed assistant process duration from message lifecycle", () =>
|
|
|
358
358
|
});
|
|
359
359
|
});
|
|
360
360
|
|
|
361
|
-
it("
|
|
361
|
+
it("keeps unavailable ai usage in metadata but out of the footer", () => {
|
|
362
362
|
const assistantMessage = {
|
|
363
363
|
id: "assistant-execution",
|
|
364
364
|
sessionId: "session-1",
|
|
@@ -375,13 +375,13 @@ it("projects ai execution metadata into the assistant message footer summary", (
|
|
|
375
375
|
requestedModel: null,
|
|
376
376
|
outcome: "completed",
|
|
377
377
|
usage: {
|
|
378
|
-
inputTokens:
|
|
379
|
-
outputTokens:
|
|
380
|
-
cachedInputTokens:
|
|
381
|
-
totalTokens:
|
|
382
|
-
modelCallCount:
|
|
383
|
-
reportedModelCallCount:
|
|
384
|
-
status: "
|
|
378
|
+
inputTokens: null,
|
|
379
|
+
outputTokens: null,
|
|
380
|
+
cachedInputTokens: null,
|
|
381
|
+
totalTokens: null,
|
|
382
|
+
modelCallCount: null,
|
|
383
|
+
reportedModelCallCount: null,
|
|
384
|
+
status: "unavailable",
|
|
385
385
|
},
|
|
386
386
|
},
|
|
387
387
|
},
|
|
@@ -397,8 +397,7 @@ it("projects ai execution metadata into the assistant message footer summary", (
|
|
|
397
397
|
const renderedMessages =
|
|
398
398
|
captures.renders[captures.renders.length - 1]?.messages ?? [];
|
|
399
399
|
expect(renderedMessages[0]).toMatchObject({
|
|
400
|
-
executionSummaryLabel:
|
|
401
|
-
"openai/gpt-5 · 1.2k chatAiExecutionInput / 200 chatAiExecutionOutput",
|
|
400
|
+
executionSummaryLabel: "openai/gpt-5",
|
|
402
401
|
moreActions: {
|
|
403
402
|
triggerLabel: "chatMessageMoreActions",
|
|
404
403
|
items: [
|
|
@@ -408,8 +407,8 @@ it("projects ai execution metadata into the assistant message footer summary", (
|
|
|
408
407
|
dialog: {
|
|
409
408
|
rows: expect.arrayContaining([
|
|
410
409
|
{
|
|
411
|
-
label: "
|
|
412
|
-
value: "
|
|
410
|
+
label: "chatAiExecutionUsageStatus",
|
|
411
|
+
value: "chatAiExecutionUsageUnavailable",
|
|
413
412
|
},
|
|
414
413
|
]),
|
|
415
414
|
},
|
|
@@ -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 = () => {
|
|
@@ -10,7 +10,6 @@ const labels = {
|
|
|
10
10
|
output: "output",
|
|
11
11
|
tokens: "tokens",
|
|
12
12
|
partial: "partial usage",
|
|
13
|
-
unavailable: "Token usage unavailable",
|
|
14
13
|
};
|
|
15
14
|
|
|
16
15
|
const presentationLabels = {
|
|
@@ -85,7 +84,7 @@ describe("buildChatMessageExecutionSummary", () => {
|
|
|
85
84
|
).toBe("openai/gpt-5 · 143k input / 307 output");
|
|
86
85
|
});
|
|
87
86
|
|
|
88
|
-
it("shows partial and unavailable
|
|
87
|
+
it("shows partial usage and keeps unavailable usage out of the footer", () => {
|
|
89
88
|
const base = {
|
|
90
89
|
version: 1,
|
|
91
90
|
runId: "run-1",
|
|
@@ -111,23 +110,36 @@ describe("buildChatMessageExecutionSummary", () => {
|
|
|
111
110
|
labels,
|
|
112
111
|
}),
|
|
113
112
|
).toBe("gpt-5 · 2k input · partial usage");
|
|
113
|
+
const unavailableExecution = {
|
|
114
|
+
...base,
|
|
115
|
+
usage: {
|
|
116
|
+
inputTokens: null,
|
|
117
|
+
outputTokens: null,
|
|
118
|
+
cachedInputTokens: null,
|
|
119
|
+
totalTokens: null,
|
|
120
|
+
modelCallCount: null,
|
|
121
|
+
reportedModelCallCount: null,
|
|
122
|
+
status: "unavailable" as const,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
114
125
|
expect(
|
|
115
126
|
buildChatMessageExecutionSummary({
|
|
116
|
-
message: createMessage(
|
|
117
|
-
...base,
|
|
118
|
-
usage: {
|
|
119
|
-
inputTokens: null,
|
|
120
|
-
outputTokens: null,
|
|
121
|
-
cachedInputTokens: null,
|
|
122
|
-
totalTokens: null,
|
|
123
|
-
modelCallCount: null,
|
|
124
|
-
reportedModelCallCount: null,
|
|
125
|
-
status: "unavailable",
|
|
126
|
-
},
|
|
127
|
-
}),
|
|
127
|
+
message: createMessage(unavailableExecution),
|
|
128
128
|
labels,
|
|
129
129
|
}),
|
|
130
|
-
).toBe("gpt-5
|
|
130
|
+
).toBe("gpt-5");
|
|
131
|
+
|
|
132
|
+
const presentation = buildChatMessageExecutionPresentation({
|
|
133
|
+
message: createMessage(unavailableExecution),
|
|
134
|
+
labels: presentationLabels,
|
|
135
|
+
});
|
|
136
|
+
expect(presentation?.summaryLabel).toBe("gpt-5");
|
|
137
|
+
expect(presentation?.moreActions.items[0]?.dialog?.rows).toEqual(
|
|
138
|
+
expect.arrayContaining([
|
|
139
|
+
{ label: "Input tokens", value: "Not available" },
|
|
140
|
+
{ label: "Usage status", value: "Unavailable" },
|
|
141
|
+
]),
|
|
142
|
+
);
|
|
131
143
|
});
|
|
132
144
|
|
|
133
145
|
it("keeps cached and exact token counts in the metadata detail action", () => {
|
|
@@ -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
|
}
|
|
@@ -10,7 +10,6 @@ export type ChatMessageExecutionSummaryLabels = {
|
|
|
10
10
|
output: string;
|
|
11
11
|
tokens: string;
|
|
12
12
|
partial: string;
|
|
13
|
-
unavailable: string;
|
|
14
13
|
};
|
|
15
14
|
|
|
16
15
|
export type ChatMessageExecutionPresentationLabels =
|
|
@@ -69,10 +68,6 @@ function buildExecutionSummary(
|
|
|
69
68
|
execution: NcpAiExecutionMetadata,
|
|
70
69
|
labels: ChatMessageExecutionSummaryLabels,
|
|
71
70
|
): string {
|
|
72
|
-
if (execution.usage.status === "unavailable") {
|
|
73
|
-
return `${execution.model} · ${labels.unavailable}`;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
71
|
const tokenSegments: string[] = [];
|
|
77
72
|
if (execution.usage.inputTokens !== null) {
|
|
78
73
|
tokenSegments.push(
|
|
@@ -90,7 +85,7 @@ function buildExecutionSummary(
|
|
|
90
85
|
);
|
|
91
86
|
}
|
|
92
87
|
if (tokenSegments.length === 0) {
|
|
93
|
-
|
|
88
|
+
return execution.model;
|
|
94
89
|
}
|
|
95
90
|
const summary = `${execution.model} · ${tokenSegments.join(" / ")}`;
|
|
96
91
|
return execution.usage.status === "partial"
|
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
|
}
|
|
@@ -107,7 +107,6 @@ export function buildChatMessageExecutionLabels(
|
|
|
107
107
|
output: t("chatAiExecutionOutput"),
|
|
108
108
|
tokens: t("chatAiExecutionTokens"),
|
|
109
109
|
partial: t("chatAiExecutionPartial"),
|
|
110
|
-
unavailable: t("chatAiExecutionUnavailable"),
|
|
111
110
|
moreActions: t("chatMessageMoreActions"),
|
|
112
111
|
viewMetadata: t("chatAiExecutionViewMetadata"),
|
|
113
112
|
metadataTitle: t("chatAiExecutionMetadataTitle"),
|