@nextclaw/ui 0.8.0 → 0.9.0
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 +16 -0
- package/dist/assets/ChannelsList-C7F_As4r.js +1 -0
- package/dist/assets/ChatPage-Oo7-OUsx.js +37 -0
- package/dist/assets/{DocBrowser-DDX2HMXW.js → DocBrowser-Dsd8Dlq8.js} +1 -1
- package/dist/assets/{LogoBadge-J53F_3JA.js → LogoBadge-2ChEc_oz.js} +1 -1
- package/dist/assets/{MarketplacePage-0BZ4bza0.js → MarketplacePage-BXck6-X3.js} +3 -3
- package/dist/assets/{ModelConfig-Wzq9wGHV.js → ModelConfig-CgHRSD0b.js} +1 -1
- package/dist/assets/ProvidersList-PPfZucvS.js +1 -0
- package/dist/assets/{RuntimeConfig-N771_AM6.js → RuntimeConfig-ClLEKNTN.js} +1 -1
- package/dist/assets/{SearchConfig-DVt5QVa_.js → SearchConfig-CuXVCbrf.js} +1 -1
- package/dist/assets/{SecretsConfig-CkwauPa8.js → SecretsConfig-udJz6Ake.js} +1 -1
- package/dist/assets/{SessionsConfig-C3mnHzkZ.js → SessionsConfig-C1XnFfiC.js} +2 -2
- package/dist/assets/{chat-message-pxr79GDs.js → chat-message-BETwXLD4.js} +1 -1
- package/dist/assets/{index-GdpEEKnz.js → index-COJdlL0e.js} +1 -1
- package/dist/assets/index-CsvP4CER.js +8 -0
- package/dist/assets/index-D-bXl7qL.css +1 -0
- package/dist/assets/{label-CmksBHgc.js → label-BGL-ztxh.js} +1 -1
- package/dist/assets/{page-layout-Db0GbnhS.js → page-layout-aw88k7tG.js} +1 -1
- package/dist/assets/popover-DyEvzhmV.js +1 -0
- package/dist/assets/{security-config-CjLFME5Q.js → security-config-BuPAQn82.js} +1 -1
- package/dist/assets/skeleton-drzO_tdU.js +1 -0
- package/dist/assets/{switch-C24d-UJU.js → switch-BK8jIzto.js} +1 -1
- package/dist/assets/tabs-custom-Da3cEOji.js +1 -0
- package/dist/assets/{useConfirmDialog-BeP35LcG.js → useConfirmDialog-z0CE92iS.js} +1 -1
- package/dist/assets/{vendor-psXJBy9u.js → vendor-CkJHmX1g.js} +1 -1
- package/dist/index.html +3 -3
- package/package.json +2 -2
- package/src/api/config.ts +9 -38
- package/src/api/ncp-session.ts +50 -0
- package/src/api/types.ts +1 -0
- package/src/components/chat/ChatConversationPanel.test.tsx +65 -0
- package/src/components/chat/ChatConversationPanel.tsx +21 -12
- package/src/components/chat/ChatSidebar.test.tsx +203 -0
- package/src/components/chat/ChatSidebar.tsx +97 -7
- package/src/components/chat/adapters/chat-message.adapter.test.ts +132 -82
- package/src/components/chat/adapters/chat-message.adapter.ts +27 -9
- package/src/components/chat/chat-page-data.ts +30 -1
- package/src/components/chat/chat-page-runtime.test.ts +181 -0
- package/src/components/chat/chat-page-runtime.ts +101 -15
- package/src/components/chat/chat-session-preference-sync.test.ts +62 -0
- package/src/components/chat/chat-session-preference-sync.ts +75 -0
- package/src/components/chat/containers/chat-input-bar.container.tsx +0 -22
- package/src/components/chat/containers/chat-message-list.container.tsx +31 -27
- package/src/components/chat/legacy/LegacyChatPage.tsx +24 -0
- package/src/components/chat/managers/chat-input.manager.ts +5 -0
- package/src/components/chat/managers/chat-session-list.manager.test.ts +39 -0
- package/src/components/chat/managers/chat-session-list.manager.ts +9 -3
- package/src/components/chat/ncp/NcpChatPage.tsx +42 -10
- package/src/components/chat/ncp/ncp-chat-input.manager.ts +6 -0
- package/src/components/chat/ncp/ncp-chat-page-data.ts +34 -2
- package/src/components/chat/ncp/ncp-chat-thread.manager.ts +1 -1
- package/src/components/chat/ncp/ncp-session-adapter.test.ts +27 -1
- package/src/components/chat/ncp/ncp-session-adapter.ts +20 -0
- package/src/components/chat/stores/chat-thread.store.ts +2 -0
- package/src/components/chat/useChatSessionTypeState.test.tsx +58 -0
- package/src/components/chat/useChatSessionTypeState.ts +25 -8
- package/src/hooks/use-ncp-chat-session-types.ts +11 -0
- package/src/hooks/useConfig.ts +2 -4
- package/src/hooks/useMarketplace.ts +7 -4
- package/src/hooks/useWebSocket.ts +23 -2
- package/dist/assets/ChannelsList-DBcoVJRW.js +0 -1
- package/dist/assets/ChatPage-CD3cxyyM.js +0 -37
- package/dist/assets/ProvidersList-kwzRS8_M.js +0 -1
- package/dist/assets/index-BIvFMkN4.js +0 -1
- package/dist/assets/index-CzkY1reu.js +0 -8
- package/dist/assets/index-RZ0kHHRI.css +0 -1
- package/dist/assets/skeleton-CkpQeVWN.js +0 -1
- package/dist/assets/tabs-custom-D89bh-fc.js +0 -1
|
@@ -8,6 +8,7 @@ import { parseSessionKeyFromRoute, resolveAgentIdFromSessionKey } from '@/compon
|
|
|
8
8
|
import { ChatPresenterProvider } from '@/components/chat/presenter/chat-presenter-context';
|
|
9
9
|
import { ChatPresenter } from '@/components/chat/presenter/chat.presenter';
|
|
10
10
|
import { useChatRuntimeController } from '@/components/chat/useChatRuntimeController';
|
|
11
|
+
import { resolveSessionTypeLabel } from '@/components/chat/useChatSessionTypeState';
|
|
11
12
|
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
12
13
|
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
13
14
|
|
|
@@ -23,6 +24,7 @@ export function LegacyChatPage({ view }: ChatPageProps) {
|
|
|
23
24
|
const { sessionId: routeSessionIdParam } = useParams<{ sessionId?: string }>();
|
|
24
25
|
const threadRef = useRef<HTMLDivElement | null>(null);
|
|
25
26
|
const selectedSessionKeyRef = useRef<string | null>(selectedSessionKey);
|
|
27
|
+
const modelHydratedSessionKeyRef = useRef<string | null>(null);
|
|
26
28
|
const thinkingHydratedSessionKeyRef = useRef<string | null>(null);
|
|
27
29
|
const routeSessionKey = useMemo(
|
|
28
30
|
() => parseSessionKeyFromRoute(routeSessionIdParam),
|
|
@@ -38,6 +40,7 @@ export function LegacyChatPage({ view }: ChatPageProps) {
|
|
|
38
40
|
sessions,
|
|
39
41
|
skillRecords,
|
|
40
42
|
selectedSession,
|
|
43
|
+
hydratedSessionModel,
|
|
41
44
|
historyMessages,
|
|
42
45
|
selectedSessionThinkingLevel,
|
|
43
46
|
sessionTypeOptions,
|
|
@@ -125,6 +128,9 @@ export function LegacyChatPage({ view }: ChatPageProps) {
|
|
|
125
128
|
}, [confirm, location.pathname, navigate, presenter]);
|
|
126
129
|
|
|
127
130
|
const currentSessionDisplayName = selectedSession ? sessionDisplayName(selectedSession) : undefined;
|
|
131
|
+
const currentSessionTypeLabel =
|
|
132
|
+
sessionTypeOptions.find((option) => option.value === selectedSessionType)?.label ??
|
|
133
|
+
resolveSessionTypeLabel(selectedSessionType);
|
|
128
134
|
|
|
129
135
|
useEffect(() => {
|
|
130
136
|
presenter.chatThreadManager.bindActions({
|
|
@@ -133,10 +139,19 @@ export function LegacyChatPage({ view }: ChatPageProps) {
|
|
|
133
139
|
}, [presenter, sessionsQuery.refetch]);
|
|
134
140
|
|
|
135
141
|
useEffect(() => {
|
|
142
|
+
const shouldHydrateModelFromSession =
|
|
143
|
+
!isSending &&
|
|
144
|
+
!isAwaitingAssistantOutput &&
|
|
145
|
+
!sessionsQuery.isLoading &&
|
|
146
|
+
isProviderStateResolved &&
|
|
147
|
+
modelOptions.length > 0 &&
|
|
148
|
+
selectedSessionKey !== modelHydratedSessionKeyRef.current;
|
|
136
149
|
const shouldHydrateThinkingFromHistory =
|
|
137
150
|
!isSending &&
|
|
138
151
|
!isAwaitingAssistantOutput &&
|
|
139
152
|
!historyQuery.isLoading &&
|
|
153
|
+
isProviderStateResolved &&
|
|
154
|
+
modelOptions.length > 0 &&
|
|
140
155
|
selectedSessionKey !== thinkingHydratedSessionKeyRef.current;
|
|
141
156
|
|
|
142
157
|
presenter.chatInputManager.syncSnapshot({
|
|
@@ -149,6 +164,7 @@ export function LegacyChatPage({ view }: ChatPageProps) {
|
|
|
149
164
|
sendError: lastSendError,
|
|
150
165
|
isSending,
|
|
151
166
|
modelOptions,
|
|
167
|
+
...(shouldHydrateModelFromSession ? { selectedModel: hydratedSessionModel } : {}),
|
|
152
168
|
sessionTypeOptions,
|
|
153
169
|
selectedSessionType,
|
|
154
170
|
...(shouldHydrateThinkingFromHistory ? { selectedThinkingLevel: selectedSessionThinkingLevel } : {}),
|
|
@@ -157,10 +173,14 @@ export function LegacyChatPage({ view }: ChatPageProps) {
|
|
|
157
173
|
skillRecords,
|
|
158
174
|
isSkillsLoading: installedSkillsQuery.isLoading
|
|
159
175
|
});
|
|
176
|
+
if (shouldHydrateModelFromSession) {
|
|
177
|
+
modelHydratedSessionKeyRef.current = selectedSessionKey;
|
|
178
|
+
}
|
|
160
179
|
if (shouldHydrateThinkingFromHistory) {
|
|
161
180
|
thinkingHydratedSessionKeyRef.current = selectedSessionKey;
|
|
162
181
|
}
|
|
163
182
|
if (!selectedSessionKey) {
|
|
183
|
+
modelHydratedSessionKeyRef.current = null;
|
|
164
184
|
thinkingHydratedSessionKeyRef.current = null;
|
|
165
185
|
}
|
|
166
186
|
presenter.chatSessionListManager.syncSnapshot({
|
|
@@ -178,6 +198,7 @@ export function LegacyChatPage({ view }: ChatPageProps) {
|
|
|
178
198
|
modelOptions,
|
|
179
199
|
sessionTypeUnavailable,
|
|
180
200
|
sessionTypeUnavailableMessage,
|
|
201
|
+
sessionTypeLabel: currentSessionTypeLabel,
|
|
181
202
|
selectedSessionKey,
|
|
182
203
|
sessionDisplayName: currentSessionDisplayName,
|
|
183
204
|
canDeleteSession: Boolean(selectedSession),
|
|
@@ -192,15 +213,18 @@ export function LegacyChatPage({ view }: ChatPageProps) {
|
|
|
192
213
|
canEditSessionType,
|
|
193
214
|
canStopCurrentRun,
|
|
194
215
|
currentSessionDisplayName,
|
|
216
|
+
currentSessionTypeLabel,
|
|
195
217
|
chatCapabilitiesQuery.data?.stopReason,
|
|
196
218
|
chatCapabilitiesQuery.data?.stopSupported,
|
|
197
219
|
defaultSessionType,
|
|
198
220
|
historyQuery.isLoading,
|
|
199
221
|
installedSkillsQuery.isLoading,
|
|
200
222
|
isAwaitingAssistantOutput,
|
|
223
|
+
hydratedSessionModel,
|
|
201
224
|
isProviderStateResolved,
|
|
202
225
|
isSending,
|
|
203
226
|
lastSendError,
|
|
227
|
+
modelOptions.length,
|
|
204
228
|
modelOptions,
|
|
205
229
|
presenter,
|
|
206
230
|
query,
|
|
@@ -5,12 +5,15 @@ import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
|
|
|
5
5
|
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
6
6
|
import { normalizeSessionType } from '@/components/chat/useChatSessionTypeState';
|
|
7
7
|
import type { ChatInputSnapshot } from '@/components/chat/stores/chat-input.store';
|
|
8
|
+
import { ChatSessionPreferenceSync } from '@/components/chat/chat-session-preference-sync';
|
|
8
9
|
import type { SetStateAction } from 'react';
|
|
9
10
|
import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
|
|
10
11
|
import type { ThinkingLevel } from '@/api/types';
|
|
11
12
|
import type { ChatModelOption } from '@/components/chat/chat-input.types';
|
|
12
13
|
|
|
13
14
|
export class ChatInputManager {
|
|
15
|
+
private readonly sessionPreferenceSync = new ChatSessionPreferenceSync(updateSession);
|
|
16
|
+
|
|
14
17
|
constructor(
|
|
15
18
|
private uiManager: ChatUiManager,
|
|
16
19
|
private streamActionsManager: ChatStreamActionsManager
|
|
@@ -139,10 +142,12 @@ export class ChatInputManager {
|
|
|
139
142
|
|
|
140
143
|
selectModel = (value: string) => {
|
|
141
144
|
this.setSelectedModel(value);
|
|
145
|
+
this.sessionPreferenceSync.syncSelectedSessionPreferences();
|
|
142
146
|
};
|
|
143
147
|
|
|
144
148
|
selectThinkingLevel = (value: ThinkingLevel) => {
|
|
145
149
|
this.setSelectedThinkingLevel(value);
|
|
150
|
+
this.sessionPreferenceSync.syncSelectedSessionPreferences();
|
|
146
151
|
};
|
|
147
152
|
|
|
148
153
|
selectSkills = (next: string[]) => {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { ChatSessionListManager } from '@/components/chat/managers/chat-session-list.manager';
|
|
3
|
+
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
4
|
+
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
5
|
+
|
|
6
|
+
describe('ChatSessionListManager', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
useChatInputStore.setState({
|
|
9
|
+
snapshot: {
|
|
10
|
+
...useChatInputStore.getState().snapshot,
|
|
11
|
+
defaultSessionType: 'native',
|
|
12
|
+
pendingSessionType: 'native'
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
useChatSessionListStore.setState({
|
|
16
|
+
snapshot: {
|
|
17
|
+
...useChatSessionListStore.getState().snapshot,
|
|
18
|
+
selectedSessionKey: 'session-1'
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('applies the requested session type when creating a session', () => {
|
|
24
|
+
const uiManager = {
|
|
25
|
+
goToChatRoot: vi.fn()
|
|
26
|
+
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[0];
|
|
27
|
+
const streamActionsManager = {
|
|
28
|
+
resetStreamState: vi.fn()
|
|
29
|
+
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[1];
|
|
30
|
+
|
|
31
|
+
const manager = new ChatSessionListManager(uiManager, streamActionsManager);
|
|
32
|
+
manager.createSession('codex');
|
|
33
|
+
|
|
34
|
+
expect(streamActionsManager.resetStreamState).toHaveBeenCalledTimes(1);
|
|
35
|
+
expect(uiManager.goToChatRoot).toHaveBeenCalledTimes(1);
|
|
36
|
+
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
|
|
37
|
+
expect(useChatInputStore.getState().snapshot.pendingSessionType).toBe('codex');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -53,11 +53,17 @@ export class ChatSessionListManager {
|
|
|
53
53
|
useChatSessionListStore.getState().setSnapshot({ selectedSessionKey: value });
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
-
createSession = () => {
|
|
57
|
-
const
|
|
56
|
+
createSession = (sessionType?: string) => {
|
|
57
|
+
const { snapshot } = useChatInputStore.getState();
|
|
58
|
+
const { defaultSessionType: configuredDefaultSessionType } = snapshot;
|
|
59
|
+
const defaultSessionType = configuredDefaultSessionType || 'native';
|
|
60
|
+
const nextSessionType =
|
|
61
|
+
typeof sessionType === 'string' && sessionType.trim().length > 0
|
|
62
|
+
? sessionType.trim()
|
|
63
|
+
: defaultSessionType;
|
|
58
64
|
this.streamActionsManager.resetStreamState();
|
|
59
65
|
this.setSelectedSessionKey(null);
|
|
60
|
-
useChatInputStore.getState().setSnapshot({ pendingSessionType:
|
|
66
|
+
useChatInputStore.getState().setSnapshot({ pendingSessionType: nextSessionType });
|
|
61
67
|
this.uiManager.goToChatRoot();
|
|
62
68
|
};
|
|
63
69
|
|
|
@@ -3,17 +3,18 @@ import { NcpHttpAgentClientEndpoint } from '@nextclaw/ncp-http-agent-client';
|
|
|
3
3
|
import { useHydratedNcpAgent, type NcpConversationSeed } from '@nextclaw/ncp-react';
|
|
4
4
|
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
|
5
5
|
import { API_BASE } from '@/api/client';
|
|
6
|
-
import { fetchNcpSessionMessages } from '@/api/
|
|
6
|
+
import { fetchNcpSessionMessages } from '@/api/ncp-session';
|
|
7
7
|
import type { ChatRunView } from '@/api/types';
|
|
8
8
|
import { sessionDisplayName } from '@/components/chat/chat-page-data';
|
|
9
9
|
import { ChatPageLayout, type ChatPageProps, useChatSessionSync } from '@/components/chat/chat-page-shell';
|
|
10
10
|
import { parseSessionKeyFromRoute, resolveAgentIdFromSessionKey } from '@/components/chat/chat-session-route';
|
|
11
11
|
import { useNcpChatPageData } from '@/components/chat/ncp/ncp-chat-page-data';
|
|
12
12
|
import { NcpChatPresenter } from '@/components/chat/ncp/ncp-chat.presenter';
|
|
13
|
-
import { adaptNcpMessagesToUiMessages, createNcpSessionId } from '@/components/chat/ncp/ncp-session-adapter';
|
|
13
|
+
import { adaptNcpMessagesToUiMessages, buildNcpSessionRunStatusByKey, createNcpSessionId } from '@/components/chat/ncp/ncp-session-adapter';
|
|
14
14
|
import { ChatPresenterProvider } from '@/components/chat/presenter/chat-presenter-context';
|
|
15
15
|
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
16
16
|
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
17
|
+
import { resolveSessionTypeLabel } from '@/components/chat/useChatSessionTypeState';
|
|
17
18
|
import { useConfirmDialog } from '@/hooks/useConfirmDialog';
|
|
18
19
|
import { normalizeRequestedSkills } from '@/lib/chat-runtime-utils';
|
|
19
20
|
|
|
@@ -70,6 +71,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
70
71
|
const { sessionId: routeSessionIdParam } = useParams<{ sessionId?: string }>();
|
|
71
72
|
const threadRef = useRef<HTMLDivElement | null>(null);
|
|
72
73
|
const selectedSessionKeyRef = useRef<string | null>(selectedSessionKey);
|
|
74
|
+
const modelHydratedSessionKeyRef = useRef<string | null>(null);
|
|
73
75
|
const thinkingHydratedSessionKeyRef = useRef<string | null>(null);
|
|
74
76
|
const routeSessionKey = useMemo(
|
|
75
77
|
() => parseSessionKeyFromRoute(routeSessionIdParam),
|
|
@@ -84,6 +86,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
84
86
|
sessions,
|
|
85
87
|
skillRecords,
|
|
86
88
|
selectedSession,
|
|
89
|
+
hydratedSessionModel,
|
|
87
90
|
selectedSessionThinkingLevel,
|
|
88
91
|
sessionTypeOptions,
|
|
89
92
|
defaultSessionType,
|
|
@@ -98,6 +101,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
98
101
|
setPendingSessionType: presenter.chatInputManager.setPendingSessionType,
|
|
99
102
|
setSelectedModel: presenter.chatInputManager.setSelectedModel
|
|
100
103
|
});
|
|
104
|
+
const refetchSessions = sessionsQuery.refetch;
|
|
101
105
|
|
|
102
106
|
const activeSessionId = selectedSessionKey ?? draftSessionId;
|
|
103
107
|
const sessionSummariesRef = useRef(sessionSummaries);
|
|
@@ -161,15 +165,22 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
161
165
|
const stopDisabledReason = agent.isRunning ? null : '__preparing__';
|
|
162
166
|
const lastSendError = agent.hydrateError?.message ?? agent.snapshot.error?.message ?? null;
|
|
163
167
|
const activeBackendRunId = agent.activeRunId;
|
|
164
|
-
const sessionRunStatusByKey = useMemo(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
168
|
+
const sessionRunStatusByKey = useMemo(
|
|
169
|
+
() =>
|
|
170
|
+
buildNcpSessionRunStatusByKey({
|
|
171
|
+
summaries: sessionSummaries,
|
|
172
|
+
activeSessionId,
|
|
173
|
+
isLocallyRunning: isSending || Boolean(activeBackendRunId)
|
|
174
|
+
}),
|
|
175
|
+
[activeBackendRunId, activeSessionId, isSending, sessionSummaries]
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
if (!isSending && !activeBackendRunId) {
|
|
180
|
+
return;
|
|
170
181
|
}
|
|
171
|
-
|
|
172
|
-
}, [
|
|
182
|
+
void refetchSessions();
|
|
183
|
+
}, [activeBackendRunId, isSending, refetchSessions]);
|
|
173
184
|
|
|
174
185
|
useEffect(() => {
|
|
175
186
|
presenter.chatStreamActionsManager.bind({
|
|
@@ -248,6 +259,9 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
248
259
|
}, [confirm, location.pathname, navigate, presenter]);
|
|
249
260
|
|
|
250
261
|
const currentSessionDisplayName = selectedSession ? sessionDisplayName(selectedSession) : undefined;
|
|
262
|
+
const currentSessionTypeLabel =
|
|
263
|
+
sessionTypeOptions.find((option) => option.value === selectedSessionType)?.label ??
|
|
264
|
+
resolveSessionTypeLabel(selectedSessionType);
|
|
251
265
|
|
|
252
266
|
useEffect(() => {
|
|
253
267
|
presenter.chatThreadManager.bindActions({
|
|
@@ -256,10 +270,19 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
256
270
|
}, [presenter, sessionsQuery.refetch]);
|
|
257
271
|
|
|
258
272
|
useEffect(() => {
|
|
273
|
+
const shouldHydrateModelFromSession =
|
|
274
|
+
!isSending &&
|
|
275
|
+
!isAwaitingAssistantOutput &&
|
|
276
|
+
!sessionsQuery.isLoading &&
|
|
277
|
+
isProviderStateResolved &&
|
|
278
|
+
modelOptions.length > 0 &&
|
|
279
|
+
selectedSessionKey !== modelHydratedSessionKeyRef.current;
|
|
259
280
|
const shouldHydrateThinkingFromSession =
|
|
260
281
|
!isSending &&
|
|
261
282
|
!isAwaitingAssistantOutput &&
|
|
262
283
|
!agent.isHydrating &&
|
|
284
|
+
isProviderStateResolved &&
|
|
285
|
+
modelOptions.length > 0 &&
|
|
263
286
|
selectedSessionKey !== thinkingHydratedSessionKeyRef.current;
|
|
264
287
|
|
|
265
288
|
presenter.chatInputManager.syncSnapshot({
|
|
@@ -272,6 +295,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
272
295
|
sendError: lastSendError,
|
|
273
296
|
isSending,
|
|
274
297
|
modelOptions,
|
|
298
|
+
...(shouldHydrateModelFromSession ? { selectedModel: hydratedSessionModel } : {}),
|
|
275
299
|
sessionTypeOptions,
|
|
276
300
|
selectedSessionType,
|
|
277
301
|
...(shouldHydrateThinkingFromSession ? { selectedThinkingLevel: selectedSessionThinkingLevel } : {}),
|
|
@@ -280,10 +304,14 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
280
304
|
skillRecords,
|
|
281
305
|
isSkillsLoading: installedSkillsQuery.isLoading
|
|
282
306
|
});
|
|
307
|
+
if (shouldHydrateModelFromSession) {
|
|
308
|
+
modelHydratedSessionKeyRef.current = selectedSessionKey;
|
|
309
|
+
}
|
|
283
310
|
if (shouldHydrateThinkingFromSession) {
|
|
284
311
|
thinkingHydratedSessionKeyRef.current = selectedSessionKey;
|
|
285
312
|
}
|
|
286
313
|
if (!selectedSessionKey) {
|
|
314
|
+
modelHydratedSessionKeyRef.current = null;
|
|
287
315
|
thinkingHydratedSessionKeyRef.current = null;
|
|
288
316
|
}
|
|
289
317
|
presenter.chatSessionListManager.syncSnapshot({
|
|
@@ -301,6 +329,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
301
329
|
modelOptions,
|
|
302
330
|
sessionTypeUnavailable,
|
|
303
331
|
sessionTypeUnavailableMessage,
|
|
332
|
+
sessionTypeLabel: currentSessionTypeLabel,
|
|
304
333
|
selectedSessionKey,
|
|
305
334
|
sessionDisplayName: currentSessionDisplayName,
|
|
306
335
|
canDeleteSession: Boolean(selectedSession),
|
|
@@ -316,12 +345,15 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
316
345
|
canEditSessionType,
|
|
317
346
|
canStopCurrentRun,
|
|
318
347
|
currentSessionDisplayName,
|
|
348
|
+
currentSessionTypeLabel,
|
|
319
349
|
defaultSessionType,
|
|
320
350
|
installedSkillsQuery.isLoading,
|
|
321
351
|
isAwaitingAssistantOutput,
|
|
352
|
+
hydratedSessionModel,
|
|
322
353
|
isProviderStateResolved,
|
|
323
354
|
isSending,
|
|
324
355
|
lastSendError,
|
|
356
|
+
modelOptions.length,
|
|
325
357
|
modelOptions,
|
|
326
358
|
presenter,
|
|
327
359
|
query,
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import type { SetStateAction } from 'react';
|
|
2
2
|
import type { ThinkingLevel } from '@/api/types';
|
|
3
|
+
import { updateNcpSession } from '@/api/ncp-session';
|
|
3
4
|
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
4
5
|
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
5
6
|
import type { ChatInputSnapshot } from '@/components/chat/stores/chat-input.store';
|
|
6
7
|
import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
|
|
7
8
|
import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
|
|
9
|
+
import { ChatSessionPreferenceSync } from '@/components/chat/chat-session-preference-sync';
|
|
8
10
|
import type { ChatModelOption } from '@/components/chat/chat-input.types';
|
|
9
11
|
import { normalizeSessionType } from '@/components/chat/useChatSessionTypeState';
|
|
10
12
|
|
|
11
13
|
export class NcpChatInputManager {
|
|
14
|
+
private readonly sessionPreferenceSync = new ChatSessionPreferenceSync(updateNcpSession);
|
|
15
|
+
|
|
12
16
|
constructor(
|
|
13
17
|
private uiManager: ChatUiManager,
|
|
14
18
|
private streamActionsManager: ChatStreamActionsManager,
|
|
@@ -135,10 +139,12 @@ export class NcpChatInputManager {
|
|
|
135
139
|
|
|
136
140
|
selectModel = (value: string) => {
|
|
137
141
|
this.setSelectedModel(value);
|
|
142
|
+
this.sessionPreferenceSync.syncSelectedSessionPreferences();
|
|
138
143
|
};
|
|
139
144
|
|
|
140
145
|
selectThinkingLevel = (value: ThinkingLevel) => {
|
|
141
146
|
this.setSelectedThinkingLevel(value);
|
|
147
|
+
this.sessionPreferenceSync.syncSelectedSessionPreferences();
|
|
142
148
|
};
|
|
143
149
|
|
|
144
150
|
selectSkills = (next: string[]) => {
|
|
@@ -7,12 +7,17 @@ import {
|
|
|
7
7
|
readNcpSessionPreferredThinking
|
|
8
8
|
} from '@/components/chat/ncp/ncp-session-adapter';
|
|
9
9
|
import { useChatSessionTypeState } from '@/components/chat/useChatSessionTypeState';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
resolveSelectedModelValue,
|
|
12
|
+
resolveRecentSessionPreferredModel,
|
|
13
|
+
useSyncSelectedModel
|
|
14
|
+
} from '@/components/chat/chat-page-runtime';
|
|
11
15
|
import {
|
|
12
16
|
useConfig,
|
|
13
17
|
useConfigMeta,
|
|
14
18
|
useNcpSessions
|
|
15
19
|
} from '@/hooks/useConfig';
|
|
20
|
+
import { useNcpChatSessionTypes } from '@/hooks/use-ncp-chat-session-types';
|
|
16
21
|
import { useMarketplaceInstalled } from '@/hooks/useMarketplace';
|
|
17
22
|
import { buildProviderModelCatalog, composeProviderModel, resolveModelThinkingCapability } from '@/lib/provider-models';
|
|
18
23
|
|
|
@@ -36,6 +41,7 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
|
|
|
36
41
|
const configQuery = useConfig();
|
|
37
42
|
const configMetaQuery = useConfigMeta();
|
|
38
43
|
const sessionsQuery = useNcpSessions({ limit: 200 });
|
|
44
|
+
const sessionTypesQuery = useNcpChatSessionTypes();
|
|
39
45
|
const installedSkillsQuery = useMarketplaceInstalled('skill');
|
|
40
46
|
const isProviderStateResolved =
|
|
41
47
|
(configQuery.isFetched || configQuery.isSuccess) &&
|
|
@@ -107,20 +113,45 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
|
|
|
107
113
|
selectedSessionKey: params.selectedSessionKey,
|
|
108
114
|
pendingSessionType: params.pendingSessionType,
|
|
109
115
|
setPendingSessionType: params.setPendingSessionType,
|
|
110
|
-
sessionTypesData:
|
|
116
|
+
sessionTypesData: sessionTypesQuery.data
|
|
111
117
|
});
|
|
118
|
+
const recentSessionPreferredModel = useMemo(
|
|
119
|
+
() =>
|
|
120
|
+
resolveRecentSessionPreferredModel({
|
|
121
|
+
sessions: allSessions,
|
|
122
|
+
selectedSessionKey: params.selectedSessionKey,
|
|
123
|
+
sessionType: sessionTypeState.selectedSessionType
|
|
124
|
+
}),
|
|
125
|
+
[allSessions, params.selectedSessionKey, sessionTypeState.selectedSessionType]
|
|
126
|
+
);
|
|
112
127
|
|
|
113
128
|
useSyncSelectedModel({
|
|
114
129
|
modelOptions,
|
|
130
|
+
selectedSessionKey: params.selectedSessionKey,
|
|
115
131
|
selectedSessionPreferredModel: selectedSession?.preferredModel,
|
|
132
|
+
fallbackPreferredModel: recentSessionPreferredModel,
|
|
116
133
|
defaultModel: configQuery.data?.agents.defaults.model,
|
|
117
134
|
setSelectedModel: params.setSelectedModel
|
|
118
135
|
});
|
|
119
136
|
|
|
137
|
+
const hydratedSessionModel = useMemo(
|
|
138
|
+
() =>
|
|
139
|
+
resolveSelectedModelValue({
|
|
140
|
+
currentSelectedModel: '',
|
|
141
|
+
modelOptions,
|
|
142
|
+
selectedSessionPreferredModel: selectedSession?.preferredModel,
|
|
143
|
+
fallbackPreferredModel: recentSessionPreferredModel,
|
|
144
|
+
defaultModel: configQuery.data?.agents.defaults.model,
|
|
145
|
+
preferSessionPreferredModel: true
|
|
146
|
+
}),
|
|
147
|
+
[configQuery.data?.agents.defaults.model, modelOptions, recentSessionPreferredModel, selectedSession?.preferredModel]
|
|
148
|
+
);
|
|
149
|
+
|
|
120
150
|
return {
|
|
121
151
|
configQuery,
|
|
122
152
|
configMetaQuery,
|
|
123
153
|
sessionsQuery,
|
|
154
|
+
sessionTypesQuery,
|
|
124
155
|
installedSkillsQuery,
|
|
125
156
|
isProviderStateResolved,
|
|
126
157
|
modelOptions,
|
|
@@ -128,6 +159,7 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
|
|
|
128
159
|
sessions,
|
|
129
160
|
skillRecords,
|
|
130
161
|
selectedSession,
|
|
162
|
+
hydratedSessionModel,
|
|
131
163
|
selectedSessionThinkingLevel,
|
|
132
164
|
...sessionTypeState
|
|
133
165
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { deleteNcpSession as deleteNcpSessionApi } from '@/api/
|
|
1
|
+
import { deleteNcpSession as deleteNcpSessionApi } from '@/api/ncp-session';
|
|
2
2
|
import type { ChatSessionListManager } from '@/components/chat/managers/chat-session-list.manager';
|
|
3
3
|
import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
|
|
4
4
|
import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
adaptNcpSessionSummary,
|
|
3
|
+
buildNcpSessionRunStatusByKey,
|
|
4
|
+
readNcpSessionPreferredThinking
|
|
5
|
+
} from '@/components/chat/ncp/ncp-session-adapter';
|
|
2
6
|
import type { NcpSessionSummaryView } from '@/api/types';
|
|
3
7
|
|
|
4
8
|
function createSummary(partial: Partial<NcpSessionSummaryView> = {}): NcpSessionSummaryView {
|
|
@@ -47,3 +51,25 @@ describe('readNcpSessionPreferredThinking', () => {
|
|
|
47
51
|
expect(thinking).toBe('high');
|
|
48
52
|
});
|
|
49
53
|
});
|
|
54
|
+
|
|
55
|
+
describe('buildNcpSessionRunStatusByKey', () => {
|
|
56
|
+
it('marks the active local session as running before the server summary catches up', () => {
|
|
57
|
+
const statuses = buildNcpSessionRunStatusByKey({
|
|
58
|
+
summaries: [createSummary({ sessionId: 'ncp-session-1', status: 'idle' })],
|
|
59
|
+
activeSessionId: 'ncp-session-1',
|
|
60
|
+
isLocallyRunning: true
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(statuses.get('ncp-session-1')).toBe('running');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('keeps persisted running sessions marked as running', () => {
|
|
67
|
+
const statuses = buildNcpSessionRunStatusByKey({
|
|
68
|
+
summaries: [createSummary({ sessionId: 'ncp-session-2', status: 'running' })],
|
|
69
|
+
activeSessionId: null,
|
|
70
|
+
isLocallyRunning: false
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(statuses.get('ncp-session-2')).toBe('running');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
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 type { SessionRunStatus } from '@/lib/session-run-status';
|
|
4
5
|
|
|
5
6
|
const THINKING_LEVEL_SET = new Set<string>(['off', 'minimal', 'low', 'medium', 'high', 'adaptive', 'xhigh']);
|
|
6
7
|
|
|
@@ -167,6 +168,7 @@ export function adaptNcpMessageToUiMessage(message: NcpMessageView): UIMessage {
|
|
|
167
168
|
}
|
|
168
169
|
|
|
169
170
|
export function adaptNcpMessagesToUiMessages(messages: readonly NcpMessageView[]): UIMessage[] {
|
|
171
|
+
console.log('[adaptNcpMessagesToUiMessages]', { messages });
|
|
170
172
|
return messages.map(adaptNcpMessageToUiMessage);
|
|
171
173
|
}
|
|
172
174
|
|
|
@@ -189,6 +191,24 @@ export function adaptNcpSessionSummaries(summaries: NcpSessionSummaryView[]): Se
|
|
|
189
191
|
return summaries.map(adaptNcpSessionSummary);
|
|
190
192
|
}
|
|
191
193
|
|
|
194
|
+
export function buildNcpSessionRunStatusByKey(params: {
|
|
195
|
+
summaries: readonly NcpSessionSummaryView[];
|
|
196
|
+
activeSessionId?: string | null;
|
|
197
|
+
isLocallyRunning?: boolean;
|
|
198
|
+
}): Map<string, SessionRunStatus> {
|
|
199
|
+
const map = new Map<string, SessionRunStatus>();
|
|
200
|
+
for (const summary of params.summaries) {
|
|
201
|
+
if (summary.status === 'running') {
|
|
202
|
+
map.set(summary.sessionId, 'running');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const activeSessionId = readOptionalString(params.activeSessionId);
|
|
206
|
+
if (params.isLocallyRunning && activeSessionId) {
|
|
207
|
+
map.set(activeSessionId, 'running');
|
|
208
|
+
}
|
|
209
|
+
return map;
|
|
210
|
+
}
|
|
211
|
+
|
|
192
212
|
export function createNcpSessionId(): string {
|
|
193
213
|
return `ncp-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
194
214
|
}
|
|
@@ -8,6 +8,7 @@ export type ChatThreadSnapshot = {
|
|
|
8
8
|
modelOptions: ChatModelOption[];
|
|
9
9
|
sessionTypeUnavailable: boolean;
|
|
10
10
|
sessionTypeUnavailableMessage?: string | null;
|
|
11
|
+
sessionTypeLabel?: string | null;
|
|
11
12
|
selectedSessionKey: string | null;
|
|
12
13
|
sessionDisplayName?: string;
|
|
13
14
|
canDeleteSession: boolean;
|
|
@@ -29,6 +30,7 @@ const initialSnapshot: ChatThreadSnapshot = {
|
|
|
29
30
|
modelOptions: [],
|
|
30
31
|
sessionTypeUnavailable: false,
|
|
31
32
|
sessionTypeUnavailableMessage: null,
|
|
33
|
+
sessionTypeLabel: null,
|
|
32
34
|
selectedSessionKey: null,
|
|
33
35
|
sessionDisplayName: undefined,
|
|
34
36
|
canDeleteSession: false,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { resolveSessionTypeLabel, useChatSessionTypeState } from '@/components/chat/useChatSessionTypeState';
|
|
4
|
+
|
|
5
|
+
vi.mock('@/lib/i18n', () => ({
|
|
6
|
+
t: (key: string) => key
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
describe('useChatSessionTypeState', () => {
|
|
10
|
+
it('formats non-native runtime labels generically when no explicit label is provided', () => {
|
|
11
|
+
expect(resolveSessionTypeLabel('workspace-agent')).toBe('Workspace Agent');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('preserves an explicitly selected draft session type instead of resetting to the default', () => {
|
|
15
|
+
const setPendingSessionType = vi.fn();
|
|
16
|
+
|
|
17
|
+
const { result } = renderHook(() =>
|
|
18
|
+
useChatSessionTypeState({
|
|
19
|
+
selectedSession: null,
|
|
20
|
+
selectedSessionKey: null,
|
|
21
|
+
pendingSessionType: 'codex-sdk',
|
|
22
|
+
setPendingSessionType,
|
|
23
|
+
sessionTypesData: {
|
|
24
|
+
defaultType: 'native',
|
|
25
|
+
options: [
|
|
26
|
+
{ value: 'native', label: 'Native' },
|
|
27
|
+
{ value: 'codex-sdk', label: 'Codex' }
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
expect(result.current.selectedSessionType).toBe('codex-sdk');
|
|
34
|
+
expect(setPendingSessionType).not.toHaveBeenCalled();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('hydrates the draft session type from the runtime default when no explicit type exists', () => {
|
|
38
|
+
const setPendingSessionType = vi.fn();
|
|
39
|
+
|
|
40
|
+
renderHook(() =>
|
|
41
|
+
useChatSessionTypeState({
|
|
42
|
+
selectedSession: null,
|
|
43
|
+
selectedSessionKey: null,
|
|
44
|
+
pendingSessionType: '',
|
|
45
|
+
setPendingSessionType,
|
|
46
|
+
sessionTypesData: {
|
|
47
|
+
defaultType: 'codex-sdk',
|
|
48
|
+
options: [
|
|
49
|
+
{ value: 'native', label: 'Native' },
|
|
50
|
+
{ value: 'codex-sdk', label: 'Codex' }
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
expect(setPendingSessionType).toHaveBeenCalledWith('codex-sdk');
|
|
57
|
+
});
|
|
58
|
+
});
|