@nextclaw/ui 0.11.21 → 0.11.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/assets/{ChannelsList-ByHWHkQS.js → ChannelsList-Zeys_w43.js} +6 -6
- package/dist/assets/ChatPage-DWOU_8P6.js +43 -0
- package/dist/assets/DocBrowser-B9OaZjmg.js +1 -0
- package/dist/assets/{DocBrowser-3y_NHZ71.js → DocBrowser-BmtBLFU0.js} +1 -1
- package/dist/assets/{DocBrowserContext-CVJuwCcw.js → DocBrowserContext-YIKkPb76.js} +1 -1
- package/dist/assets/{LogoBadge-D8fyilO-.js → LogoBadge-F7ZWdxLT.js} +1 -1
- package/dist/assets/MarketplacePage-BfaTTqN6.js +1 -0
- package/dist/assets/{MarketplacePage-CmhsZXr1.js → MarketplacePage-Cd4faegU.js} +2 -2
- package/dist/assets/{McpMarketplacePage-C7PkCYbp.js → McpMarketplacePage-C09Ngs7O.js} +2 -2
- package/dist/assets/ModelConfig-DJgdcgvQ.js +1 -0
- package/dist/assets/ProvidersList-w0rVFIBf.js +1 -0
- package/dist/assets/RemoteAccessPage-BJ_ckkOV.js +1 -0
- package/dist/assets/RuntimeConfig-Cmn2xPQO.js +1 -0
- package/dist/assets/{SearchConfig-Dm7r2yfp.js → SearchConfig-BT13qpR_.js} +1 -1
- package/dist/assets/{SecretsConfig-BBP_mbQh.js → SecretsConfig-CvqEVn0B.js} +2 -2
- package/dist/assets/{SessionsConfig-6wNJloZN.js → SessionsConfig-DHHcYznk.js} +2 -2
- package/dist/assets/{book-open-B26jGBjY.js → book-open-CXoF5nQC.js} +1 -1
- package/dist/assets/chat-session-display-VW6ZMvZP.js +1 -0
- package/dist/assets/{chunk-JZWAC4HX-B-4B29RN.js → chunk-JZWAC4HX-CvRWvTy5.js} +1 -1
- package/dist/assets/{config-BaC29Qf-.js → config-DJswxxE8.js} +1 -1
- package/dist/assets/{createLucideIcon-DiFAvXmK.js → createLucideIcon-CjGHOWb6.js} +1 -1
- package/dist/assets/{dist-pCfWPG1A.js → dist-Cl2QB-2y.js} +1 -1
- package/dist/assets/{dist-kW_O3kyZ.js → dist-nqTTbVdA.js} +1 -1
- package/dist/assets/{external-link-D5-p-Gmm.js → external-link-tIO7zING.js} +1 -1
- package/dist/assets/{hash-BlwrSV0q.js → hash-JWUyl1pT.js} +1 -1
- package/dist/assets/i18n-CDHMXlRZ.js +1 -0
- package/dist/assets/index-BlH4-cBw.css +1 -0
- package/dist/assets/{index-DvKS3L9j.js → index-C6d0xmtm.js} +3 -3
- package/dist/assets/{label-RyXfZqkP.js → label-BIpeNu4r.js} +1 -1
- package/dist/assets/loader-circle-Cs8XVFTw.js +1 -0
- package/dist/assets/{logos-Bpl8QTgI.js → logos-DThdM9lk.js} +1 -1
- package/dist/assets/{page-layout--S0YBU0W.js → page-layout-D3Xo605Z.js} +1 -1
- package/dist/assets/plus-PHf8q-Ct.js +1 -0
- package/dist/assets/{popover-BEjfbEwy.js → popover-BJRUGA_H.js} +1 -1
- package/dist/assets/provider-models-bz5y28rq.js +1 -0
- package/dist/assets/{react-BuSP2-8B.js → react-7ZHqQtEV.js} +1 -1
- package/dist/assets/refresh-ccw-CC6-_QuL.js +1 -0
- package/dist/assets/{save-DPPPpD_c.js → save-DJM5RRWW.js} +1 -1
- package/dist/assets/search-C91yH_6y.js +1 -0
- package/dist/assets/{security-config-6t78Ph-I.js → security-config-T5zpg16O.js} +1 -1
- package/dist/assets/{select-CT50pzod.js → select-DSkTc61S.js} +1 -1
- package/dist/assets/skeleton-Dzg-HOiN.js +1 -0
- package/dist/assets/{status-dot-BbBqRHfh.js → status-dot-LNBlDu3q.js} +1 -1
- package/dist/assets/{switch-D3l6AcCk.js → switch-Bo-Y46HZ.js} +1 -1
- package/dist/assets/tabs-custom-DXv507_2.js +1 -0
- package/dist/assets/{trash-2-B2_AGVE3.js → trash-2-DFZmW6Gg.js} +1 -1
- package/dist/assets/useConfirmDialog-Bs5Ll17m.js +1 -0
- package/dist/assets/{useMutation-BzCrO8j-.js → useMutation-DrZrOgVL.js} +1 -1
- package/dist/assets/x-D7Q1yqSF.js +1 -0
- package/dist/index.html +18 -18
- package/package.json +3 -3
- package/src/api/ncp-session.test.ts +37 -0
- package/src/api/ncp-session.ts +29 -1
- package/src/api/server-path.ts +23 -0
- package/src/api/types.ts +41 -0
- package/src/components/chat/ChatConversationPanel.test.tsx +43 -7
- package/src/components/chat/ChatConversationPanel.tsx +23 -17
- package/src/components/chat/ChatSidebar.test.tsx +2 -2
- package/src/components/chat/ChatSidebar.tsx +2 -2
- package/src/components/chat/adapters/chat-input-bar.adapter.test.ts +1 -0
- package/src/components/chat/adapters/chat-input-bar.adapter.ts +7 -2
- package/src/components/chat/adapters/chat-message-part.adapter.ts +13 -9
- package/src/components/chat/adapters/chat-message.adapter.test.ts +76 -4
- package/src/components/chat/adapters/{chat-message.file-operation-card.ts → file-operation/card.ts} +74 -181
- package/src/components/chat/adapters/{chat-message.file-operation-diff.ts → file-operation/diff.ts} +178 -188
- package/src/components/chat/adapters/file-operation/line-builder.ts +249 -0
- package/src/components/chat/adapters/file-operation/record-readers.ts +233 -0
- package/src/components/chat/chat-composer-state.ts +3 -3
- package/src/components/chat/chat-session-display.test.ts +21 -0
- package/src/components/chat/chat-session-display.ts +6 -1
- package/src/components/chat/containers/chat-input-bar.container.tsx +21 -24
- package/src/components/chat/hooks/use-chat-session-label.ts +19 -0
- package/src/components/chat/hooks/use-chat-session-project.test.tsx +117 -0
- package/src/components/chat/hooks/use-chat-session-project.ts +40 -0
- package/src/components/chat/{chat-session-label.service.ts → hooks/use-chat-session-update.ts} +11 -7
- package/src/components/chat/managers/chat-session-list.manager.ts +5 -1
- package/src/components/chat/ncp/NcpChatPage.tsx +55 -17
- package/src/components/chat/ncp/ncp-chat-page-data.test.ts +33 -0
- package/src/components/chat/ncp/ncp-chat-page-data.ts +21 -15
- package/src/components/chat/ncp/ncp-session-adapter.test.ts +3 -0
- package/src/components/chat/ncp/ncp-session-adapter.ts +16 -0
- package/src/components/chat/session-header/chat-session-header-actions.test.tsx +63 -0
- package/src/components/chat/session-header/chat-session-header-actions.tsx +95 -0
- package/src/components/chat/session-header/chat-session-header-menu-item.tsx +35 -0
- package/src/components/chat/session-header/chat-session-project-badge.test.tsx +66 -0
- package/src/components/chat/session-header/chat-session-project-badge.tsx +102 -0
- package/src/components/chat/session-header/chat-session-project-dialog.tsx +34 -0
- package/src/components/chat/stores/chat-input.store.ts +6 -3
- package/src/components/chat/stores/chat-thread.store.ts +6 -2
- package/src/components/path-picker/server-path-picker-dialog.test.tsx +92 -0
- package/src/components/path-picker/server-path-picker-dialog.tsx +282 -0
- package/src/hooks/server-path/use-server-path-browse.ts +19 -0
- package/src/hooks/useConfig.ts +26 -1
- package/src/lib/i18n/i18n-language-owner.ts +94 -0
- package/src/lib/i18n/i18n.path-picker.ts +12 -0
- package/src/lib/i18n.chat.ts +23 -0
- package/src/lib/i18n.ts +21 -84
- package/src/lib/session-project/session-project.utils.ts +30 -0
- package/dist/assets/ChatPage-FdT3pDnw.js +0 -42
- package/dist/assets/DocBrowser-CMdPdbZj.js +0 -1
- package/dist/assets/MarketplacePage-9oKmxN2n.js +0 -1
- package/dist/assets/ModelConfig-DmCY6jWM.js +0 -1
- package/dist/assets/ProvidersList-ClT-34aX.js +0 -1
- package/dist/assets/RemoteAccessPage-B6hUZl1O.js +0 -1
- package/dist/assets/RuntimeConfig-C5aqliGk.js +0 -1
- package/dist/assets/chat-session-display-Bjmn4aIZ.js +0 -1
- package/dist/assets/i18n-CSytxMFI.js +0 -1
- package/dist/assets/index-CUy6doWo.css +0 -1
- package/dist/assets/loader-circle-B2J777gj.js +0 -1
- package/dist/assets/plus-CM9XJ0Tf.js +0 -1
- package/dist/assets/provider-models-C8JQUd1E.js +0 -1
- package/dist/assets/search-Ctaw34Kp.js +0 -1
- package/dist/assets/skeleton-Bycyb0zU.js +0 -1
- package/dist/assets/tabs-custom-TZQ5WPWP.js +0 -1
- package/dist/assets/useConfirmDialog-BDpdjfIO.js +0 -1
- package/dist/assets/x-CHOBE-63.js +0 -1
- /package/dist/assets/{config-hints-fGnUjDe9.js → config-hints-WtpHP_DW.js} +0 -0
- /package/dist/assets/{config-layout-B-7erZRN.js → config-layout-LQ10ozRC.js} +0 -0
- /package/dist/assets/{marketplace-localization-CXeGRf6E.js → marketplace-localization-CxSTG9wr.js} +0 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { act, renderHook } from '@testing-library/react';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { toast } from 'sonner';
|
|
4
|
+
import { useChatSessionProject } from '@/components/chat/hooks/use-chat-session-project';
|
|
5
|
+
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
6
|
+
|
|
7
|
+
const mocks = vi.hoisted(() => ({
|
|
8
|
+
updateSession: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock('sonner', () => ({
|
|
12
|
+
toast: {
|
|
13
|
+
success: vi.fn(),
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock('@/components/chat/hooks/use-chat-session-update', () => ({
|
|
18
|
+
useChatSessionUpdate: () => mocks.updateSession,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
describe('useChatSessionProject', () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
useChatInputStore.setState((state) => ({
|
|
24
|
+
snapshot: {
|
|
25
|
+
...state.snapshot,
|
|
26
|
+
pendingProjectRoot: null,
|
|
27
|
+
pendingProjectRootSessionKey: null,
|
|
28
|
+
},
|
|
29
|
+
}));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
vi.clearAllMocks();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('stores the draft project root locally when the session does not exist yet', async () => {
|
|
37
|
+
const { result } = renderHook(() => useChatSessionProject());
|
|
38
|
+
|
|
39
|
+
await act(async () => {
|
|
40
|
+
await result.current({
|
|
41
|
+
sessionKey: 'draft-session-1',
|
|
42
|
+
projectRoot: '/tmp/project-alpha',
|
|
43
|
+
persistToServer: false,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(mocks.updateSession).not.toHaveBeenCalled();
|
|
48
|
+
expect(useChatInputStore.getState().snapshot).toMatchObject({
|
|
49
|
+
pendingProjectRoot: '/tmp/project-alpha',
|
|
50
|
+
pendingProjectRootSessionKey: 'draft-session-1',
|
|
51
|
+
});
|
|
52
|
+
expect(toast.success).toHaveBeenCalledTimes(1);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('keeps an explicit draft override when clearing the project root locally', async () => {
|
|
56
|
+
const { result } = renderHook(() => useChatSessionProject());
|
|
57
|
+
|
|
58
|
+
await act(async () => {
|
|
59
|
+
await result.current({
|
|
60
|
+
sessionKey: 'draft-session-1',
|
|
61
|
+
projectRoot: null,
|
|
62
|
+
persistToServer: false,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(mocks.updateSession).not.toHaveBeenCalled();
|
|
67
|
+
expect(useChatInputStore.getState().snapshot).toMatchObject({
|
|
68
|
+
pendingProjectRoot: null,
|
|
69
|
+
pendingProjectRootSessionKey: 'draft-session-1',
|
|
70
|
+
});
|
|
71
|
+
expect(toast.success).toHaveBeenCalledTimes(1);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('persists to the server and mirrors the updated project override locally for an existing session', async () => {
|
|
75
|
+
const { result } = renderHook(() => useChatSessionProject());
|
|
76
|
+
|
|
77
|
+
await act(async () => {
|
|
78
|
+
await result.current({
|
|
79
|
+
sessionKey: 'session-1',
|
|
80
|
+
projectRoot: '/tmp/project-beta',
|
|
81
|
+
persistToServer: true,
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(mocks.updateSession).toHaveBeenCalledWith({
|
|
86
|
+
sessionKey: 'session-1',
|
|
87
|
+
patch: { projectRoot: '/tmp/project-beta' },
|
|
88
|
+
successMessage: 'Project directory updated',
|
|
89
|
+
});
|
|
90
|
+
expect(useChatInputStore.getState().snapshot).toMatchObject({
|
|
91
|
+
pendingProjectRoot: '/tmp/project-beta',
|
|
92
|
+
pendingProjectRootSessionKey: 'session-1',
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('persists clearing to the server and keeps the cleared override until session state catches up', async () => {
|
|
97
|
+
const { result } = renderHook(() => useChatSessionProject());
|
|
98
|
+
|
|
99
|
+
await act(async () => {
|
|
100
|
+
await result.current({
|
|
101
|
+
sessionKey: 'session-1',
|
|
102
|
+
projectRoot: null,
|
|
103
|
+
persistToServer: true,
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(mocks.updateSession).toHaveBeenCalledWith({
|
|
108
|
+
sessionKey: 'session-1',
|
|
109
|
+
patch: { projectRoot: null },
|
|
110
|
+
successMessage: 'Project directory cleared',
|
|
111
|
+
});
|
|
112
|
+
expect(useChatInputStore.getState().snapshot).toMatchObject({
|
|
113
|
+
pendingProjectRoot: null,
|
|
114
|
+
pendingProjectRootSessionKey: 'session-1',
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { toast } from 'sonner';
|
|
2
|
+
import { t } from '@/lib/i18n';
|
|
3
|
+
import { useChatSessionUpdate } from '@/components/chat/hooks/use-chat-session-update';
|
|
4
|
+
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
5
|
+
|
|
6
|
+
type UpdateChatSessionProjectParams = {
|
|
7
|
+
sessionKey: string;
|
|
8
|
+
projectRoot: string | null;
|
|
9
|
+
persistToServer: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function useChatSessionProject() {
|
|
13
|
+
const updateSession = useChatSessionUpdate();
|
|
14
|
+
|
|
15
|
+
return async (params: UpdateChatSessionProjectParams): Promise<void> => {
|
|
16
|
+
const successMessage = params.projectRoot
|
|
17
|
+
? t('chatSessionProjectUpdated')
|
|
18
|
+
: t('chatSessionProjectCleared');
|
|
19
|
+
|
|
20
|
+
if (!params.persistToServer) {
|
|
21
|
+
useChatInputStore.getState().setSnapshot({
|
|
22
|
+
pendingProjectRoot: params.projectRoot,
|
|
23
|
+
pendingProjectRootSessionKey: params.sessionKey
|
|
24
|
+
});
|
|
25
|
+
toast.success(successMessage);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await updateSession({
|
|
30
|
+
sessionKey: params.sessionKey,
|
|
31
|
+
patch: { projectRoot: params.projectRoot },
|
|
32
|
+
successMessage,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
useChatInputStore.getState().setSnapshot({
|
|
36
|
+
pendingProjectRoot: params.projectRoot,
|
|
37
|
+
pendingProjectRootSessionKey: params.sessionKey,
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
}
|
package/src/components/chat/{chat-session-label.service.ts → hooks/use-chat-session-update.ts}
RENAMED
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
import { useQueryClient } from '@tanstack/react-query';
|
|
2
2
|
import { toast } from 'sonner';
|
|
3
|
+
import type { SessionPatchUpdate } from '@/api/types';
|
|
3
4
|
import { upsertNcpSessionSummaryInQueryClient } from '@/api/ncp-session-query-cache';
|
|
4
5
|
import { updateNcpSession } from '@/api/ncp-session';
|
|
5
6
|
import { t } from '@/lib/i18n';
|
|
6
7
|
|
|
7
|
-
type
|
|
8
|
+
type UpdateChatSessionParams = {
|
|
8
9
|
sessionKey: string;
|
|
9
|
-
|
|
10
|
+
patch: SessionPatchUpdate;
|
|
11
|
+
successMessage?: string;
|
|
10
12
|
};
|
|
11
13
|
|
|
12
|
-
export function
|
|
14
|
+
export function useChatSessionUpdate() {
|
|
13
15
|
const queryClient = useQueryClient();
|
|
14
16
|
|
|
15
|
-
return async (params:
|
|
17
|
+
return async (params: UpdateChatSessionParams): Promise<void> => {
|
|
16
18
|
try {
|
|
17
|
-
const updated = await updateNcpSession(params.sessionKey,
|
|
19
|
+
const updated = await updateNcpSession(params.sessionKey, params.patch);
|
|
18
20
|
upsertNcpSessionSummaryInQueryClient(queryClient, updated);
|
|
19
|
-
toast.success(t('configSavedApplied'));
|
|
21
|
+
toast.success(params.successMessage ?? t('configSavedApplied'));
|
|
20
22
|
} catch (error) {
|
|
21
|
-
toast.error(
|
|
23
|
+
toast.error(
|
|
24
|
+
t('configSaveFailed') + ': ' + (error instanceof Error ? error.message : String(error)),
|
|
25
|
+
);
|
|
22
26
|
throw error;
|
|
23
27
|
}
|
|
24
28
|
};
|
|
@@ -44,7 +44,11 @@ export class ChatSessionListManager {
|
|
|
44
44
|
? sessionType.trim()
|
|
45
45
|
: defaultSessionType;
|
|
46
46
|
this.streamActionsManager.resetStreamState();
|
|
47
|
-
useChatInputStore.getState().setSnapshot({
|
|
47
|
+
useChatInputStore.getState().setSnapshot({
|
|
48
|
+
pendingSessionType: nextSessionType,
|
|
49
|
+
pendingProjectRoot: null,
|
|
50
|
+
pendingProjectRootSessionKey: null
|
|
51
|
+
});
|
|
48
52
|
this.uiManager.goToChatRoot();
|
|
49
53
|
};
|
|
50
54
|
|
|
@@ -23,12 +23,14 @@ import { useChatSessionListStore } from '@/components/chat/stores/chat-session-l
|
|
|
23
23
|
import { resolveSessionTypeLabel } from '@/components/chat/useChatSessionTypeState';
|
|
24
24
|
import { useConfirmDialog } from '@/hooks/useConfirmDialog';
|
|
25
25
|
import { normalizeRequestedSkills } from '@/lib/chat-runtime-utils';
|
|
26
|
+
import { getSessionProjectName, normalizeSessionProjectRootValue } from '@/lib/session-project/session-project.utils';
|
|
26
27
|
import { appClient } from '@/transport';
|
|
27
28
|
|
|
28
|
-
function buildNcpSendMetadata(payload: {
|
|
29
|
+
export function buildNcpSendMetadata(payload: {
|
|
29
30
|
model?: string;
|
|
30
31
|
thinkingLevel?: string;
|
|
31
32
|
sessionType?: string;
|
|
33
|
+
projectRoot?: string | null;
|
|
32
34
|
requestedSkills?: string[];
|
|
33
35
|
composerNodes?: Parameters<typeof buildInlineSkillTokensFromComposer>[0];
|
|
34
36
|
}): Record<string, unknown> {
|
|
@@ -44,9 +46,13 @@ function buildNcpSendMetadata(payload: {
|
|
|
44
46
|
if (payload.sessionType?.trim()) {
|
|
45
47
|
metadata.session_type = payload.sessionType.trim();
|
|
46
48
|
}
|
|
49
|
+
const projectRoot = normalizeSessionProjectRootValue(payload.projectRoot);
|
|
50
|
+
if (projectRoot) {
|
|
51
|
+
metadata.project_root = projectRoot;
|
|
52
|
+
}
|
|
47
53
|
const requestedSkills = normalizeRequestedSkills(payload.requestedSkills);
|
|
48
54
|
if (requestedSkills.length > 0) {
|
|
49
|
-
metadata.
|
|
55
|
+
metadata.requested_skill_refs = requestedSkills;
|
|
50
56
|
}
|
|
51
57
|
const inlineSkillTokens = payload.composerNodes
|
|
52
58
|
? buildInlineSkillTokensFromComposer(payload.composerNodes)
|
|
@@ -71,6 +77,8 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
71
77
|
const selectedSessionKey = useChatSessionListStore((state) => state.snapshot.selectedSessionKey);
|
|
72
78
|
const selectedAgentId = useChatSessionListStore((state) => state.snapshot.selectedAgentId);
|
|
73
79
|
const pendingSessionType = useChatInputStore((state) => state.snapshot.pendingSessionType);
|
|
80
|
+
const pendingProjectRoot = useChatInputStore((state) => state.snapshot.pendingProjectRoot);
|
|
81
|
+
const pendingProjectRootSessionKey = useChatInputStore((state) => state.snapshot.pendingProjectRootSessionKey);
|
|
74
82
|
const currentSelectedModel = useChatInputStore((state) => state.snapshot.selectedModel);
|
|
75
83
|
const { confirm, ConfirmDialog } = useConfirmDialog();
|
|
76
84
|
const location = useLocation();
|
|
@@ -83,8 +91,11 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
83
91
|
() => parseSessionKeyFromRoute(routeSessionIdParam),
|
|
84
92
|
[routeSessionIdParam]
|
|
85
93
|
);
|
|
94
|
+
const sessionKey = selectedSessionKey ?? draftSessionId;
|
|
95
|
+
const hasSessionProjectRootOverride = pendingProjectRootSessionKey === sessionKey;
|
|
96
|
+
const sessionProjectRootOverride = hasSessionProjectRootOverride ? pendingProjectRoot : undefined;
|
|
86
97
|
const {
|
|
87
|
-
|
|
98
|
+
sessionSkillsQuery,
|
|
88
99
|
isProviderStateResolved,
|
|
89
100
|
modelOptions,
|
|
90
101
|
sessionSummaries,
|
|
@@ -98,7 +109,8 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
98
109
|
sessionTypeUnavailableMessage
|
|
99
110
|
} = useNcpChatPageData({
|
|
100
111
|
query,
|
|
101
|
-
|
|
112
|
+
sessionKey,
|
|
113
|
+
projectRootOverride: sessionProjectRootOverride,
|
|
102
114
|
currentSelectedModel,
|
|
103
115
|
pendingSessionType,
|
|
104
116
|
setPendingSessionType: presenter.chatInputManager.setPendingSessionType,
|
|
@@ -106,7 +118,6 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
106
118
|
setSelectedThinkingLevel: presenter.chatInputManager.setSelectedThinkingLevel
|
|
107
119
|
});
|
|
108
120
|
|
|
109
|
-
const activeSessionId = selectedSessionKey ?? draftSessionId;
|
|
110
121
|
const sessionSummariesRef = useRef(sessionSummaries);
|
|
111
122
|
useEffect(() => {
|
|
112
123
|
sessionSummariesRef.current = sessionSummaries;
|
|
@@ -141,7 +152,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
141
152
|
}, []);
|
|
142
153
|
|
|
143
154
|
const agent = useHydratedNcpAgent({
|
|
144
|
-
sessionId:
|
|
155
|
+
sessionId: sessionKey,
|
|
145
156
|
client: ncpClient,
|
|
146
157
|
loadSeed
|
|
147
158
|
});
|
|
@@ -158,6 +169,9 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
158
169
|
}
|
|
159
170
|
}, [presenter, selectedSessionKey]);
|
|
160
171
|
|
|
172
|
+
const effectiveSessionProjectRoot = hasSessionProjectRootOverride ? pendingProjectRoot : selectedSession?.projectRoot ?? null;
|
|
173
|
+
const effectiveSessionProjectName = hasSessionProjectRootOverride ? getSessionProjectName(effectiveSessionProjectRoot) : selectedSession?.projectName ?? getSessionProjectName(effectiveSessionProjectRoot);
|
|
174
|
+
|
|
161
175
|
const isSending = agent.isSending || agent.isRunning;
|
|
162
176
|
const isAwaitingAssistantOutput = agent.isRunning;
|
|
163
177
|
const canStopCurrentRun = agent.isRunning;
|
|
@@ -175,7 +189,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
175
189
|
|
|
176
190
|
sessionStreamAttachInFlightRef.current = true;
|
|
177
191
|
void ncpClient
|
|
178
|
-
.stream({ sessionId:
|
|
192
|
+
.stream({ sessionId: sessionKey })
|
|
179
193
|
.catch(() => undefined)
|
|
180
194
|
.finally(() => {
|
|
181
195
|
sessionStreamAttachInFlightRef.current = false;
|
|
@@ -185,24 +199,28 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
185
199
|
return appClient.subscribe((event) => {
|
|
186
200
|
if (
|
|
187
201
|
event.type === 'session.run-status' &&
|
|
188
|
-
event.payload.sessionKey ===
|
|
202
|
+
event.payload.sessionKey === sessionKey &&
|
|
189
203
|
event.payload.status === 'running'
|
|
190
204
|
) {
|
|
191
205
|
attachRealtimeSessionStream();
|
|
192
206
|
}
|
|
193
207
|
});
|
|
194
|
-
}, [
|
|
208
|
+
}, [agent.isHydrating, agent.isRunning, agent.isSending, ncpClient, sessionKey]);
|
|
195
209
|
|
|
196
210
|
useEffect(() => {
|
|
197
211
|
presenter.chatStreamActionsManager.bind({
|
|
198
212
|
sendMessage: async (payload) => {
|
|
199
|
-
if (payload.sessionKey !==
|
|
213
|
+
if (payload.sessionKey !== sessionKey) {
|
|
200
214
|
return;
|
|
201
215
|
}
|
|
202
216
|
const metadata = buildNcpSendMetadata({
|
|
203
217
|
model: payload.model,
|
|
204
218
|
thinkingLevel: payload.thinkingLevel,
|
|
205
219
|
sessionType: payload.sessionType,
|
|
220
|
+
projectRoot:
|
|
221
|
+
payload.sessionKey === pendingProjectRootSessionKey
|
|
222
|
+
? pendingProjectRoot
|
|
223
|
+
: selectedSession?.projectRoot ?? null,
|
|
206
224
|
requestedSkills: payload.requestedSkills,
|
|
207
225
|
composerNodes: payload.composerNodes
|
|
208
226
|
});
|
|
@@ -238,7 +256,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
238
256
|
await agent.abort();
|
|
239
257
|
},
|
|
240
258
|
resumeRun: async (run: ResumeRunParams) => {
|
|
241
|
-
if (run.sessionKey !==
|
|
259
|
+
if (run.sessionKey !== sessionKey) {
|
|
242
260
|
return;
|
|
243
261
|
}
|
|
244
262
|
await agent.streamRun();
|
|
@@ -248,7 +266,24 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
248
266
|
},
|
|
249
267
|
applyHistoryMessages: () => {}
|
|
250
268
|
});
|
|
251
|
-
}, [
|
|
269
|
+
}, [
|
|
270
|
+
agent,
|
|
271
|
+
pendingProjectRoot,
|
|
272
|
+
pendingProjectRootSessionKey,
|
|
273
|
+
presenter,
|
|
274
|
+
selectedSession?.projectRoot,
|
|
275
|
+
sessionKey
|
|
276
|
+
]);
|
|
277
|
+
|
|
278
|
+
useEffect(() => {
|
|
279
|
+
if (!selectedSession || pendingProjectRootSessionKey !== selectedSession.key || (selectedSession.projectRoot ?? null) !== pendingProjectRoot) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
useChatInputStore.getState().setSnapshot({
|
|
283
|
+
pendingProjectRoot: null,
|
|
284
|
+
pendingProjectRootSessionKey: null
|
|
285
|
+
});
|
|
286
|
+
}, [pendingProjectRoot, pendingProjectRootSessionKey, selectedSession]);
|
|
252
287
|
|
|
253
288
|
useChatSessionSync({
|
|
254
289
|
view,
|
|
@@ -293,7 +328,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
293
328
|
canEditSessionType,
|
|
294
329
|
sessionTypeUnavailable,
|
|
295
330
|
skillRecords,
|
|
296
|
-
isSkillsLoading:
|
|
331
|
+
isSkillsLoading: sessionSkillsQuery.isLoading
|
|
297
332
|
});
|
|
298
333
|
presenter.chatThreadManager.syncSnapshot({
|
|
299
334
|
isProviderStateResolved,
|
|
@@ -301,8 +336,10 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
301
336
|
sessionTypeUnavailable,
|
|
302
337
|
sessionTypeUnavailableMessage,
|
|
303
338
|
sessionTypeLabel: currentSessionTypeLabel,
|
|
304
|
-
|
|
339
|
+
sessionKey,
|
|
305
340
|
sessionDisplayName: currentSessionDisplayName,
|
|
341
|
+
sessionProjectRoot: effectiveSessionProjectRoot,
|
|
342
|
+
sessionProjectName: effectiveSessionProjectName,
|
|
306
343
|
canDeleteSession: Boolean(selectedSession),
|
|
307
344
|
threadRef,
|
|
308
345
|
isHistoryLoading: agent.isHydrating,
|
|
@@ -317,16 +354,17 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
317
354
|
currentSessionDisplayName,
|
|
318
355
|
currentSessionTypeLabel,
|
|
319
356
|
defaultSessionType,
|
|
320
|
-
|
|
357
|
+
sessionSkillsQuery.isLoading,
|
|
321
358
|
isAwaitingAssistantOutput,
|
|
322
359
|
isProviderStateResolved,
|
|
323
360
|
isSending,
|
|
324
361
|
lastSendError,
|
|
325
|
-
modelOptions.length,
|
|
326
362
|
modelOptions,
|
|
327
363
|
presenter,
|
|
364
|
+
effectiveSessionProjectName,
|
|
365
|
+
effectiveSessionProjectRoot,
|
|
328
366
|
selectedSession,
|
|
329
|
-
|
|
367
|
+
sessionKey,
|
|
330
368
|
selectedSessionType,
|
|
331
369
|
sessionTypeOptions,
|
|
332
370
|
sessionTypeUnavailable,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { buildNcpSendMetadata } from '@/components/chat/ncp/NcpChatPage';
|
|
2
3
|
import { filterModelOptionsBySessionType } from '@/components/chat/ncp/ncp-chat-page-data';
|
|
3
4
|
import type { ChatModelOption } from '@/components/chat/chat-input.types';
|
|
4
5
|
|
|
@@ -42,3 +43,35 @@ describe('filterModelOptionsBySessionType', () => {
|
|
|
42
43
|
).toEqual(modelOptions);
|
|
43
44
|
});
|
|
44
45
|
});
|
|
46
|
+
|
|
47
|
+
describe('buildNcpSendMetadata', () => {
|
|
48
|
+
it('includes the project root in the first-message metadata when present', () => {
|
|
49
|
+
expect(
|
|
50
|
+
buildNcpSendMetadata({
|
|
51
|
+
sessionType: 'codex',
|
|
52
|
+
projectRoot: ' /tmp/project-alpha ',
|
|
53
|
+
}),
|
|
54
|
+
).toMatchObject({
|
|
55
|
+
session_type: 'codex',
|
|
56
|
+
project_root: '/tmp/project-alpha',
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('omits project_root when the input is blank', () => {
|
|
61
|
+
expect(
|
|
62
|
+
buildNcpSendMetadata({
|
|
63
|
+
projectRoot: ' ',
|
|
64
|
+
}),
|
|
65
|
+
).not.toHaveProperty('project_root');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('sends requested skill refs instead of legacy requested skill names', () => {
|
|
69
|
+
expect(
|
|
70
|
+
buildNcpSendMetadata({
|
|
71
|
+
requestedSkills: ['project:/tmp/project-alpha/.agents/skills/review'],
|
|
72
|
+
}),
|
|
73
|
+
).toMatchObject({
|
|
74
|
+
requested_skill_refs: ['project:/tmp/project-alpha/.agents/skills/review'],
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -14,15 +14,16 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
useConfig,
|
|
16
16
|
useConfigMeta,
|
|
17
|
+
useNcpSessionSkills,
|
|
17
18
|
useNcpSessions
|
|
18
19
|
} from '@/hooks/useConfig';
|
|
19
20
|
import { useNcpChatSessionTypes } from '@/hooks/use-ncp-chat-session-types';
|
|
20
|
-
import { useMarketplaceInstalled } from '@/hooks/useMarketplace';
|
|
21
21
|
import { buildProviderModelCatalog, composeProviderModel, resolveModelThinkingCapability } from '@/lib/provider-models';
|
|
22
22
|
|
|
23
23
|
type UseNcpChatPageDataParams = {
|
|
24
24
|
query: string;
|
|
25
|
-
|
|
25
|
+
sessionKey: string;
|
|
26
|
+
projectRootOverride?: string | null;
|
|
26
27
|
currentSelectedModel: string;
|
|
27
28
|
pendingSessionType: string;
|
|
28
29
|
setPendingSessionType: Dispatch<SetStateAction<string>>;
|
|
@@ -51,7 +52,12 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
|
|
|
51
52
|
const configMetaQuery = useConfigMeta();
|
|
52
53
|
const sessionsQuery = useNcpSessions({ limit: 200 });
|
|
53
54
|
const sessionTypesQuery = useNcpChatSessionTypes();
|
|
54
|
-
const
|
|
55
|
+
const sessionSkillsQuery = useNcpSessionSkills({
|
|
56
|
+
sessionId: params.sessionKey,
|
|
57
|
+
...(Object.prototype.hasOwnProperty.call(params, 'projectRootOverride')
|
|
58
|
+
? { projectRoot: params.projectRootOverride ?? null }
|
|
59
|
+
: {})
|
|
60
|
+
});
|
|
55
61
|
const isProviderStateResolved =
|
|
56
62
|
(configQuery.isFetched || configQuery.isSuccess) &&
|
|
57
63
|
(configMetaQuery.isFetched || configMetaQuery.isSuccess);
|
|
@@ -101,16 +107,16 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
|
|
|
101
107
|
[allSessions, params.query]
|
|
102
108
|
);
|
|
103
109
|
const selectedSession = useMemo(
|
|
104
|
-
() => allSessions.find((session) => session.key === params.
|
|
105
|
-
[allSessions, params.
|
|
110
|
+
() => allSessions.find((session) => session.key === params.sessionKey) ?? null,
|
|
111
|
+
[allSessions, params.sessionKey]
|
|
106
112
|
);
|
|
107
113
|
const skillRecords = useMemo(
|
|
108
|
-
() =>
|
|
109
|
-
[
|
|
114
|
+
() => sessionSkillsQuery.data?.records ?? [],
|
|
115
|
+
[sessionSkillsQuery.data?.records]
|
|
110
116
|
);
|
|
111
117
|
const sessionTypeState = useChatSessionTypeState({
|
|
112
118
|
selectedSession,
|
|
113
|
-
selectedSessionKey: params.
|
|
119
|
+
selectedSessionKey: params.sessionKey,
|
|
114
120
|
pendingSessionType: params.pendingSessionType,
|
|
115
121
|
setPendingSessionType: params.setPendingSessionType,
|
|
116
122
|
sessionTypesData: sessionTypesQuery.data
|
|
@@ -127,10 +133,10 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
|
|
|
127
133
|
() =>
|
|
128
134
|
resolveRecentSessionPreferredModel({
|
|
129
135
|
sessions: allSessions,
|
|
130
|
-
selectedSessionKey: params.
|
|
136
|
+
selectedSessionKey: params.sessionKey,
|
|
131
137
|
sessionType: sessionTypeState.selectedSessionType
|
|
132
138
|
}),
|
|
133
|
-
[allSessions, params.
|
|
139
|
+
[allSessions, params.sessionKey, sessionTypeState.selectedSessionType]
|
|
134
140
|
);
|
|
135
141
|
const currentModelOption = useMemo(
|
|
136
142
|
() => filteredModelOptions.find((option) => option.value === params.currentSelectedModel),
|
|
@@ -148,15 +154,15 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
|
|
|
148
154
|
() =>
|
|
149
155
|
resolveRecentSessionPreferredThinking({
|
|
150
156
|
sessions: allSessions,
|
|
151
|
-
selectedSessionKey: params.
|
|
157
|
+
selectedSessionKey: params.sessionKey,
|
|
152
158
|
sessionType: sessionTypeState.selectedSessionType
|
|
153
159
|
}),
|
|
154
|
-
[allSessions, params.
|
|
160
|
+
[allSessions, params.sessionKey, sessionTypeState.selectedSessionType]
|
|
155
161
|
);
|
|
156
162
|
|
|
157
163
|
useSyncSelectedModel({
|
|
158
164
|
modelOptions: filteredModelOptions,
|
|
159
|
-
selectedSessionKey: params.
|
|
165
|
+
selectedSessionKey: params.sessionKey,
|
|
160
166
|
selectedSessionExists: Boolean(selectedSession),
|
|
161
167
|
selectedSessionPreferredModel: selectedSession?.preferredModel,
|
|
162
168
|
fallbackPreferredModel: sessionTypeState.selectedSessionTypeOption?.recommendedModel ?? recentSessionPreferredModel,
|
|
@@ -165,7 +171,7 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
|
|
|
165
171
|
});
|
|
166
172
|
useSyncSelectedThinking({
|
|
167
173
|
supportedThinkingLevels,
|
|
168
|
-
selectedSessionKey: params.
|
|
174
|
+
selectedSessionKey: params.sessionKey,
|
|
169
175
|
selectedSessionExists: Boolean(selectedSession),
|
|
170
176
|
selectedSessionPreferredThinking: selectedSession?.preferredThinking ?? null,
|
|
171
177
|
fallbackPreferredThinking: recentSessionPreferredThinking ?? null,
|
|
@@ -178,7 +184,7 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
|
|
|
178
184
|
configMetaQuery,
|
|
179
185
|
sessionsQuery,
|
|
180
186
|
sessionTypesQuery,
|
|
181
|
-
|
|
187
|
+
sessionSkillsQuery,
|
|
182
188
|
isProviderStateResolved,
|
|
183
189
|
modelOptions: filteredModelOptions,
|
|
184
190
|
sessionSummaries,
|
|
@@ -24,6 +24,7 @@ describe('adaptNcpSessionSummary', () => {
|
|
|
24
24
|
label: 'NCP Planning Thread',
|
|
25
25
|
model: 'openai/gpt-5',
|
|
26
26
|
preferred_thinking: 'medium',
|
|
27
|
+
project_root: '/Users/demo/workspace/project-alpha',
|
|
27
28
|
session_type: 'native'
|
|
28
29
|
}
|
|
29
30
|
})
|
|
@@ -34,6 +35,8 @@ describe('adaptNcpSessionSummary', () => {
|
|
|
34
35
|
label: 'NCP Planning Thread',
|
|
35
36
|
preferredModel: 'openai/gpt-5',
|
|
36
37
|
preferredThinking: 'medium',
|
|
38
|
+
projectRoot: '/Users/demo/workspace/project-alpha',
|
|
39
|
+
projectName: 'project-alpha',
|
|
37
40
|
sessionType: 'native',
|
|
38
41
|
sessionTypeMutable: false,
|
|
39
42
|
messageCount: 3
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { ToolInvocationStatus, type UIMessage } from '@nextclaw/agent-chat';
|
|
2
2
|
import type { NcpMessagePart } from '@nextclaw/ncp';
|
|
3
3
|
import type { NcpMessageView, NcpSessionSummaryView, SessionEntryView, ThinkingLevel } from '@/api/types';
|
|
4
|
+
import {
|
|
5
|
+
getSessionProjectName,
|
|
6
|
+
normalizeSessionProjectRootValue,
|
|
7
|
+
} from '@/lib/session-project/session-project.utils';
|
|
4
8
|
|
|
5
9
|
const THINKING_LEVEL_SET = new Set<string>(['off', 'minimal', 'low', 'medium', 'high', 'adaptive', 'xhigh']);
|
|
6
10
|
|
|
@@ -68,6 +72,14 @@ function readNcpSessionLabel(summary: NcpSessionSummaryView): string | null {
|
|
|
68
72
|
return readOptionalString(metadata.label);
|
|
69
73
|
}
|
|
70
74
|
|
|
75
|
+
function readNcpSessionProjectRoot(summary: NcpSessionSummaryView): string | null {
|
|
76
|
+
const metadata = readMetadata(summary);
|
|
77
|
+
if (!metadata) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return normalizeSessionProjectRootValue(metadata.project_root ?? metadata.projectRoot);
|
|
81
|
+
}
|
|
82
|
+
|
|
71
83
|
function readNcpSessionType(summary: NcpSessionSummaryView): string {
|
|
72
84
|
const metadata = readMetadata(summary);
|
|
73
85
|
if (!metadata) {
|
|
@@ -207,6 +219,8 @@ export function adaptNcpSessionSummary(summary: NcpSessionSummaryView): SessionE
|
|
|
207
219
|
const label = readNcpSessionLabel(summary);
|
|
208
220
|
const preferredModel = readNcpSessionPreferredModel(summary);
|
|
209
221
|
const preferredThinking = readNcpSessionPreferredThinking(summary);
|
|
222
|
+
const projectRoot = readNcpSessionProjectRoot(summary);
|
|
223
|
+
const projectName = getSessionProjectName(projectRoot);
|
|
210
224
|
const context = parseSessionContext(summary.sessionId);
|
|
211
225
|
return {
|
|
212
226
|
key: summary.sessionId,
|
|
@@ -216,6 +230,8 @@ export function adaptNcpSessionSummary(summary: NcpSessionSummaryView): SessionE
|
|
|
216
230
|
...context,
|
|
217
231
|
...(preferredModel ? { preferredModel } : {}),
|
|
218
232
|
...(preferredThinking ? { preferredThinking } : {}),
|
|
233
|
+
...(projectRoot ? { projectRoot } : {}),
|
|
234
|
+
...(projectName ? { projectName } : {}),
|
|
219
235
|
sessionType: readNcpSessionType(summary),
|
|
220
236
|
sessionTypeMutable: false,
|
|
221
237
|
messageCount: summary.messageCount
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { ChatSessionHeaderActions } from '@/components/chat/session-header/chat-session-header-actions';
|
|
5
|
+
|
|
6
|
+
const mocks = vi.hoisted(() => ({
|
|
7
|
+
updateSessionProject: vi.fn(),
|
|
8
|
+
onDeleteSession: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock('@/components/chat/hooks/use-chat-session-project', () => ({
|
|
12
|
+
useChatSessionProject: () => mocks.updateSessionProject,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock('@/components/chat/session-header/chat-session-project-dialog', () => ({
|
|
16
|
+
ChatSessionProjectDialog: () => null,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
describe('ChatSessionHeaderActions', () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
mocks.updateSessionProject.mockReset();
|
|
22
|
+
mocks.onDeleteSession.mockReset();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('keeps only the set-project action in the more-actions menu when a project is already attached', async () => {
|
|
26
|
+
const user = userEvent.setup();
|
|
27
|
+
|
|
28
|
+
render(
|
|
29
|
+
<ChatSessionHeaderActions
|
|
30
|
+
sessionKey="session-1"
|
|
31
|
+
canDeleteSession
|
|
32
|
+
isDeletePending={false}
|
|
33
|
+
projectRoot="/tmp/project-alpha"
|
|
34
|
+
onDeleteSession={mocks.onDeleteSession}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
await user.click(screen.getByRole('button', { name: 'More actions' }));
|
|
39
|
+
|
|
40
|
+
expect(screen.getByText('Set Project Directory')).toBeTruthy();
|
|
41
|
+
expect(screen.queryByText('Clear Project Directory')).toBeNull();
|
|
42
|
+
expect(screen.getByText('Delete Session')).toBeTruthy();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('keeps the set-project entry in the more-actions menu when no project is attached', async () => {
|
|
46
|
+
const user = userEvent.setup();
|
|
47
|
+
|
|
48
|
+
render(
|
|
49
|
+
<ChatSessionHeaderActions
|
|
50
|
+
sessionKey="draft-session"
|
|
51
|
+
canDeleteSession={false}
|
|
52
|
+
isDeletePending={false}
|
|
53
|
+
projectRoot={null}
|
|
54
|
+
onDeleteSession={mocks.onDeleteSession}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
await user.click(screen.getByRole('button', { name: 'More actions' }));
|
|
59
|
+
|
|
60
|
+
expect(screen.getByText('Set Project Directory')).toBeTruthy();
|
|
61
|
+
expect(screen.queryByText('Clear Project Directory')).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
});
|