@nextclaw/ui 0.8.0 → 0.9.1
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 +24 -0
- package/dist/assets/ChannelsList-DhvjpZcs.js +1 -0
- package/dist/assets/ChatPage-B8VBaMQm.js +38 -0
- package/dist/assets/{DocBrowser-DDX2HMXW.js → DocBrowser-LpzGe8An.js} +1 -1
- package/dist/assets/{LogoBadge-J53F_3JA.js → LogoBadge-Be4lktJN.js} +1 -1
- package/dist/assets/{MarketplacePage-0BZ4bza0.js → MarketplacePage-Cx9AI3_h.js} +3 -3
- package/dist/assets/{ModelConfig-Wzq9wGHV.js → ModelConfig-DuImUHIX.js} +1 -1
- package/dist/assets/ProvidersList-Ccleg25k.js +1 -0
- package/dist/assets/{RuntimeConfig-N771_AM6.js → RuntimeConfig-C6iqpJR_.js} +1 -1
- package/dist/assets/{SearchConfig-DVt5QVa_.js → SearchConfig-Dvp1TAXu.js} +1 -1
- package/dist/assets/{SecretsConfig-CkwauPa8.js → SecretsConfig-D5Ymlvt9.js} +1 -1
- package/dist/assets/{SessionsConfig-C3mnHzkZ.js → SessionsConfig-CIA_jA1P.js} +2 -2
- package/dist/assets/{chat-message-pxr79GDs.js → chat-message-B60Fh9kI.js} +1 -1
- package/dist/assets/index-BiPDnzv0.js +8 -0
- package/dist/assets/index-C8GsgIUn.css +1 -0
- package/dist/assets/{index-GdpEEKnz.js → index-CPDASUXh.js} +1 -1
- package/dist/assets/{label-CmksBHgc.js → label-D4fGx6Wb.js} +1 -1
- package/dist/assets/{page-layout-Db0GbnhS.js → page-layout-twy8gmBE.js} +1 -1
- package/dist/assets/popover-DYbYpt1j.js +1 -0
- package/dist/assets/{security-config-CjLFME5Q.js → security-config-BcIZ4rpb.js} +1 -1
- package/dist/assets/skeleton-DypBy7jp.js +1 -0
- package/dist/assets/{switch-C24d-UJU.js → switch-DqA6r5XR.js} +1 -1
- package/dist/assets/tabs-custom-C6enKKs1.js +1 -0
- package/dist/assets/{useConfirmDialog-BeP35LcG.js → useConfirmDialog-CHBf5Of7.js} +1 -1
- package/dist/assets/{vendor-psXJBy9u.js → vendor-DKBNiC31.js} +1 -1
- package/dist/index.html +3 -3
- package/package.json +6 -6
- 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-composer-state.ts +53 -0
- 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/chat-stream/types.ts +3 -0
- package/src/components/chat/containers/chat-input-bar.container.tsx +12 -63
- package/src/components/chat/containers/chat-message-list.container.tsx +31 -27
- package/src/components/chat/legacy/LegacyChatPage.tsx +25 -0
- package/src/components/chat/managers/chat-input.manager.ts +48 -13
- 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 +53 -13
- package/src/components/chat/ncp/ncp-chat-input.manager.ts +48 -12
- 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/presenter/chat-presenter-context.tsx +2 -0
- package/src/components/chat/stores/chat-input.store.ts +4 -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
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { useMemo } from
|
|
2
|
-
import { type UiMessage } from
|
|
3
|
-
import { ChatMessageList } from
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { type UiMessage } from "@nextclaw/agent-chat";
|
|
3
|
+
import { ChatMessageList } from "@nextclaw/agent-chat-ui";
|
|
4
|
+
import {
|
|
5
|
+
adaptChatMessages,
|
|
6
|
+
type ChatMessageSource,
|
|
7
|
+
} from "@/components/chat/adapters/chat-message.adapter";
|
|
8
|
+
import { useI18n } from "@/components/providers/I18nProvider";
|
|
9
|
+
import { formatDateTime, t } from "@/lib/i18n";
|
|
7
10
|
|
|
8
11
|
type ChatMessageListContainerProps = {
|
|
9
12
|
uiMessages: UiMessage[];
|
|
@@ -20,11 +23,11 @@ export function ChatMessageListContainer(props: ChatMessageListContainerProps) {
|
|
|
20
23
|
role: message.role,
|
|
21
24
|
meta: {
|
|
22
25
|
timestamp: message.meta?.timestamp,
|
|
23
|
-
status: message.meta?.status
|
|
26
|
+
status: message.meta?.status,
|
|
24
27
|
},
|
|
25
|
-
parts: message.parts as unknown as ChatMessageSource[
|
|
28
|
+
parts: message.parts as unknown as ChatMessageSource["parts"],
|
|
26
29
|
})),
|
|
27
|
-
[props.uiMessages]
|
|
30
|
+
[props.uiMessages],
|
|
28
31
|
);
|
|
29
32
|
|
|
30
33
|
const messages = useMemo(
|
|
@@ -34,21 +37,21 @@ export function ChatMessageListContainer(props: ChatMessageListContainerProps) {
|
|
|
34
37
|
formatTimestamp: (value) => formatDateTime(value, language),
|
|
35
38
|
texts: {
|
|
36
39
|
roleLabels: {
|
|
37
|
-
user: t(
|
|
38
|
-
assistant: t(
|
|
39
|
-
tool: t(
|
|
40
|
-
system: t(
|
|
41
|
-
fallback: t(
|
|
40
|
+
user: t("chatRoleUser"),
|
|
41
|
+
assistant: t("chatRoleAssistant"),
|
|
42
|
+
tool: t("chatRoleTool"),
|
|
43
|
+
system: t("chatRoleSystem"),
|
|
44
|
+
fallback: t("chatRoleMessage"),
|
|
42
45
|
},
|
|
43
|
-
reasoningLabel: t(
|
|
44
|
-
toolCallLabel: t(
|
|
45
|
-
toolResultLabel: t(
|
|
46
|
-
toolNoOutputLabel: t(
|
|
47
|
-
toolOutputLabel: t(
|
|
48
|
-
unknownPartLabel: t(
|
|
49
|
-
}
|
|
46
|
+
reasoningLabel: t("chatReasoning"),
|
|
47
|
+
toolCallLabel: t("chatToolCall"),
|
|
48
|
+
toolResultLabel: t("chatToolResult"),
|
|
49
|
+
toolNoOutputLabel: t("chatToolNoOutput"),
|
|
50
|
+
toolOutputLabel: t("chatToolOutput"),
|
|
51
|
+
unknownPartLabel: t("chatUnknownPart"),
|
|
52
|
+
},
|
|
50
53
|
}),
|
|
51
|
-
[language, sourceMessages]
|
|
54
|
+
[language, sourceMessages],
|
|
52
55
|
);
|
|
53
56
|
|
|
54
57
|
return (
|
|
@@ -57,14 +60,15 @@ export function ChatMessageListContainer(props: ChatMessageListContainerProps) {
|
|
|
57
60
|
isSending={props.isSending}
|
|
58
61
|
hasAssistantDraft={props.uiMessages.some(
|
|
59
62
|
(message) =>
|
|
60
|
-
message.role ===
|
|
61
|
-
(message.meta?.status ===
|
|
63
|
+
message.role === "assistant" &&
|
|
64
|
+
(message.meta?.status === "streaming" ||
|
|
65
|
+
message.meta?.status === "pending"),
|
|
62
66
|
)}
|
|
63
67
|
className={props.className}
|
|
64
68
|
texts={{
|
|
65
|
-
copyCodeLabel: t(
|
|
66
|
-
copiedCodeLabel: t(
|
|
67
|
-
typingLabel: t(
|
|
69
|
+
copyCodeLabel: t("chatCodeCopy"),
|
|
70
|
+
copiedCodeLabel: t("chatCodeCopied"),
|
|
71
|
+
typingLabel: t("chatTyping"),
|
|
68
72
|
}}
|
|
69
73
|
/>
|
|
70
74
|
);
|
|
@@ -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,
|
|
@@ -72,6 +75,7 @@ export function LegacyChatPage({ view }: ChatPageProps) {
|
|
|
72
75
|
selectedSessionKeyRef,
|
|
73
76
|
setSelectedSessionKey: presenter.chatSessionListManager.setSelectedSessionKey,
|
|
74
77
|
setDraft: presenter.chatInputManager.setDraft,
|
|
78
|
+
setComposerNodes: presenter.chatInputManager.setComposerNodes,
|
|
75
79
|
refetchSessions: sessionsQuery.refetch,
|
|
76
80
|
refetchHistory: historyQuery.refetch
|
|
77
81
|
},
|
|
@@ -125,6 +129,9 @@ export function LegacyChatPage({ view }: ChatPageProps) {
|
|
|
125
129
|
}, [confirm, location.pathname, navigate, presenter]);
|
|
126
130
|
|
|
127
131
|
const currentSessionDisplayName = selectedSession ? sessionDisplayName(selectedSession) : undefined;
|
|
132
|
+
const currentSessionTypeLabel =
|
|
133
|
+
sessionTypeOptions.find((option) => option.value === selectedSessionType)?.label ??
|
|
134
|
+
resolveSessionTypeLabel(selectedSessionType);
|
|
128
135
|
|
|
129
136
|
useEffect(() => {
|
|
130
137
|
presenter.chatThreadManager.bindActions({
|
|
@@ -133,10 +140,19 @@ export function LegacyChatPage({ view }: ChatPageProps) {
|
|
|
133
140
|
}, [presenter, sessionsQuery.refetch]);
|
|
134
141
|
|
|
135
142
|
useEffect(() => {
|
|
143
|
+
const shouldHydrateModelFromSession =
|
|
144
|
+
!isSending &&
|
|
145
|
+
!isAwaitingAssistantOutput &&
|
|
146
|
+
!sessionsQuery.isLoading &&
|
|
147
|
+
isProviderStateResolved &&
|
|
148
|
+
modelOptions.length > 0 &&
|
|
149
|
+
selectedSessionKey !== modelHydratedSessionKeyRef.current;
|
|
136
150
|
const shouldHydrateThinkingFromHistory =
|
|
137
151
|
!isSending &&
|
|
138
152
|
!isAwaitingAssistantOutput &&
|
|
139
153
|
!historyQuery.isLoading &&
|
|
154
|
+
isProviderStateResolved &&
|
|
155
|
+
modelOptions.length > 0 &&
|
|
140
156
|
selectedSessionKey !== thinkingHydratedSessionKeyRef.current;
|
|
141
157
|
|
|
142
158
|
presenter.chatInputManager.syncSnapshot({
|
|
@@ -149,6 +165,7 @@ export function LegacyChatPage({ view }: ChatPageProps) {
|
|
|
149
165
|
sendError: lastSendError,
|
|
150
166
|
isSending,
|
|
151
167
|
modelOptions,
|
|
168
|
+
...(shouldHydrateModelFromSession ? { selectedModel: hydratedSessionModel } : {}),
|
|
152
169
|
sessionTypeOptions,
|
|
153
170
|
selectedSessionType,
|
|
154
171
|
...(shouldHydrateThinkingFromHistory ? { selectedThinkingLevel: selectedSessionThinkingLevel } : {}),
|
|
@@ -157,10 +174,14 @@ export function LegacyChatPage({ view }: ChatPageProps) {
|
|
|
157
174
|
skillRecords,
|
|
158
175
|
isSkillsLoading: installedSkillsQuery.isLoading
|
|
159
176
|
});
|
|
177
|
+
if (shouldHydrateModelFromSession) {
|
|
178
|
+
modelHydratedSessionKeyRef.current = selectedSessionKey;
|
|
179
|
+
}
|
|
160
180
|
if (shouldHydrateThinkingFromHistory) {
|
|
161
181
|
thinkingHydratedSessionKeyRef.current = selectedSessionKey;
|
|
162
182
|
}
|
|
163
183
|
if (!selectedSessionKey) {
|
|
184
|
+
modelHydratedSessionKeyRef.current = null;
|
|
164
185
|
thinkingHydratedSessionKeyRef.current = null;
|
|
165
186
|
}
|
|
166
187
|
presenter.chatSessionListManager.syncSnapshot({
|
|
@@ -178,6 +199,7 @@ export function LegacyChatPage({ view }: ChatPageProps) {
|
|
|
178
199
|
modelOptions,
|
|
179
200
|
sessionTypeUnavailable,
|
|
180
201
|
sessionTypeUnavailableMessage,
|
|
202
|
+
sessionTypeLabel: currentSessionTypeLabel,
|
|
181
203
|
selectedSessionKey,
|
|
182
204
|
sessionDisplayName: currentSessionDisplayName,
|
|
183
205
|
canDeleteSession: Boolean(selectedSession),
|
|
@@ -192,15 +214,18 @@ export function LegacyChatPage({ view }: ChatPageProps) {
|
|
|
192
214
|
canEditSessionType,
|
|
193
215
|
canStopCurrentRun,
|
|
194
216
|
currentSessionDisplayName,
|
|
217
|
+
currentSessionTypeLabel,
|
|
195
218
|
chatCapabilitiesQuery.data?.stopReason,
|
|
196
219
|
chatCapabilitiesQuery.data?.stopSupported,
|
|
197
220
|
defaultSessionType,
|
|
198
221
|
historyQuery.isLoading,
|
|
199
222
|
installedSkillsQuery.isLoading,
|
|
200
223
|
isAwaitingAssistantOutput,
|
|
224
|
+
hydratedSessionModel,
|
|
201
225
|
isProviderStateResolved,
|
|
202
226
|
isSending,
|
|
203
227
|
lastSendError,
|
|
228
|
+
modelOptions.length,
|
|
204
229
|
modelOptions,
|
|
205
230
|
presenter,
|
|
206
231
|
query,
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import type { ChatComposerNode } from '@nextclaw/agent-chat-ui';
|
|
2
|
+
import {
|
|
3
|
+
createInitialChatComposerNodes,
|
|
4
|
+
createChatComposerNodesFromDraft,
|
|
5
|
+
deriveChatComposerDraft,
|
|
6
|
+
deriveSelectedSkillsFromComposer,
|
|
7
|
+
syncComposerSkills
|
|
8
|
+
} from '@/components/chat/chat-composer-state';
|
|
1
9
|
import { updateSession } from '@/api/config';
|
|
2
10
|
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
3
11
|
import { buildNewSessionKey } from '@/components/chat/chat-session-route';
|
|
@@ -5,12 +13,15 @@ import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
|
|
|
5
13
|
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
6
14
|
import { normalizeSessionType } from '@/components/chat/useChatSessionTypeState';
|
|
7
15
|
import type { ChatInputSnapshot } from '@/components/chat/stores/chat-input.store';
|
|
16
|
+
import { ChatSessionPreferenceSync } from '@/components/chat/chat-session-preference-sync';
|
|
8
17
|
import type { SetStateAction } from 'react';
|
|
9
18
|
import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
|
|
10
19
|
import type { ThinkingLevel } from '@/api/types';
|
|
11
20
|
import type { ChatModelOption } from '@/components/chat/chat-input.types';
|
|
12
21
|
|
|
13
22
|
export class ChatInputManager {
|
|
23
|
+
private readonly sessionPreferenceSync = new ChatSessionPreferenceSync(updateSession);
|
|
24
|
+
|
|
14
25
|
constructor(
|
|
15
26
|
private uiManager: ChatUiManager,
|
|
16
27
|
private streamActionsManager: ChatStreamActionsManager
|
|
@@ -33,6 +44,17 @@ export class ChatInputManager {
|
|
|
33
44
|
return next;
|
|
34
45
|
};
|
|
35
46
|
|
|
47
|
+
private isSameStringArray = (left: string[], right: string[]): boolean =>
|
|
48
|
+
left.length === right.length && left.every((value, index) => value === right[index]);
|
|
49
|
+
|
|
50
|
+
private syncComposerSnapshot = (nodes: ChatComposerNode[]) => {
|
|
51
|
+
useChatInputStore.getState().setSnapshot({
|
|
52
|
+
composerNodes: nodes,
|
|
53
|
+
draft: deriveChatComposerDraft(nodes),
|
|
54
|
+
selectedSkills: deriveSelectedSkillsFromComposer(nodes)
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
36
58
|
syncSnapshot = (patch: Partial<ChatInputSnapshot>) => {
|
|
37
59
|
if (!this.hasSnapshotChanges(patch)) {
|
|
38
60
|
return;
|
|
@@ -43,8 +65,8 @@ export class ChatInputManager {
|
|
|
43
65
|
Object.prototype.hasOwnProperty.call(patch, 'selectedModel') ||
|
|
44
66
|
Object.prototype.hasOwnProperty.call(patch, 'selectedThinkingLevel')
|
|
45
67
|
) {
|
|
46
|
-
const
|
|
47
|
-
this.reconcileThinkingForModel(
|
|
68
|
+
const { selectedModel } = useChatInputStore.getState().snapshot;
|
|
69
|
+
this.reconcileThinkingForModel(selectedModel);
|
|
48
70
|
}
|
|
49
71
|
};
|
|
50
72
|
|
|
@@ -54,7 +76,16 @@ export class ChatInputManager {
|
|
|
54
76
|
if (value === prev) {
|
|
55
77
|
return;
|
|
56
78
|
}
|
|
57
|
-
|
|
79
|
+
this.syncComposerSnapshot(createChatComposerNodesFromDraft(value));
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
setComposerNodes = (next: SetStateAction<ChatComposerNode[]>) => {
|
|
83
|
+
const prev = useChatInputStore.getState().snapshot.composerNodes;
|
|
84
|
+
const value = this.resolveUpdateValue(prev, next);
|
|
85
|
+
if (Object.is(value, prev)) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
this.syncComposerSnapshot(value);
|
|
58
89
|
};
|
|
59
90
|
|
|
60
91
|
setPendingSessionType = (next: SetStateAction<string>) => {
|
|
@@ -73,14 +104,13 @@ export class ChatInputManager {
|
|
|
73
104
|
if (!message) {
|
|
74
105
|
return;
|
|
75
106
|
}
|
|
76
|
-
const requestedSkills = inputSnapshot
|
|
107
|
+
const { selectedSkills: requestedSkills, composerNodes } = inputSnapshot;
|
|
77
108
|
const hasSelectedSession = Boolean(sessionSnapshot.selectedSessionKey);
|
|
78
109
|
const sessionKey = sessionSnapshot.selectedSessionKey ?? buildNewSessionKey(sessionSnapshot.selectedAgentId);
|
|
79
110
|
if (!hasSelectedSession) {
|
|
80
111
|
this.uiManager.goToSession(sessionKey, { replace: true });
|
|
81
112
|
}
|
|
82
|
-
this.
|
|
83
|
-
this.setSelectedSkills([]);
|
|
113
|
+
this.setComposerNodes(createInitialChatComposerNodes());
|
|
84
114
|
await this.streamActionsManager.sendMessage({
|
|
85
115
|
message,
|
|
86
116
|
sessionKey,
|
|
@@ -91,7 +121,8 @@ export class ChatInputManager {
|
|
|
91
121
|
stopSupported: inputSnapshot.stopSupported,
|
|
92
122
|
stopReason: inputSnapshot.stopReason,
|
|
93
123
|
requestedSkills,
|
|
94
|
-
restoreDraftOnError: true
|
|
124
|
+
restoreDraftOnError: true,
|
|
125
|
+
composerNodes
|
|
95
126
|
});
|
|
96
127
|
};
|
|
97
128
|
|
|
@@ -129,20 +160,23 @@ export class ChatInputManager {
|
|
|
129
160
|
};
|
|
130
161
|
|
|
131
162
|
setSelectedSkills = (next: SetStateAction<string[]>) => {
|
|
132
|
-
const
|
|
163
|
+
const snapshot = useChatInputStore.getState().snapshot;
|
|
164
|
+
const { selectedSkills: prev } = snapshot;
|
|
133
165
|
const value = this.resolveUpdateValue(prev, next);
|
|
134
|
-
if (
|
|
166
|
+
if (this.isSameStringArray(value, prev)) {
|
|
135
167
|
return;
|
|
136
168
|
}
|
|
137
|
-
|
|
169
|
+
this.syncComposerSnapshot(syncComposerSkills(snapshot.composerNodes, value, snapshot.skillRecords));
|
|
138
170
|
};
|
|
139
171
|
|
|
140
172
|
selectModel = (value: string) => {
|
|
141
173
|
this.setSelectedModel(value);
|
|
174
|
+
this.sessionPreferenceSync.syncSelectedSessionPreferences();
|
|
142
175
|
};
|
|
143
176
|
|
|
144
177
|
selectThinkingLevel = (value: ThinkingLevel) => {
|
|
145
178
|
this.setSelectedThinkingLevel(value);
|
|
179
|
+
this.sessionPreferenceSync.syncSelectedSessionPreferences();
|
|
146
180
|
};
|
|
147
181
|
|
|
148
182
|
selectSkills = (next: string[]) => {
|
|
@@ -169,15 +203,16 @@ export class ChatInputManager {
|
|
|
169
203
|
private reconcileThinkingForModel(model: string): void {
|
|
170
204
|
const snapshot = useChatInputStore.getState().snapshot;
|
|
171
205
|
const modelOption = snapshot.modelOptions.find((option) => option.value === model);
|
|
172
|
-
const
|
|
173
|
-
|
|
206
|
+
const { selectedThinkingLevel } = snapshot;
|
|
207
|
+
const nextThinking = this.resolveThinkingForModel(modelOption, selectedThinkingLevel);
|
|
208
|
+
if (nextThinking !== selectedThinkingLevel) {
|
|
174
209
|
useChatInputStore.getState().setSnapshot({ selectedThinkingLevel: nextThinking });
|
|
175
210
|
}
|
|
176
211
|
}
|
|
177
212
|
|
|
178
213
|
private syncRemoteSessionType = async (normalizedType: string) => {
|
|
179
214
|
const sessionSnapshot = useChatSessionListStore.getState().snapshot;
|
|
180
|
-
const selectedSessionKey = sessionSnapshot
|
|
215
|
+
const { selectedSessionKey } = sessionSnapshot;
|
|
181
216
|
if (!selectedSessionKey) {
|
|
182
217
|
return;
|
|
183
218
|
}
|
|
@@ -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({
|
|
@@ -201,9 +212,17 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
201
212
|
await sessionsQuery.refetch();
|
|
202
213
|
} catch (error) {
|
|
203
214
|
if (payload.restoreDraftOnError) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
215
|
+
if (payload.composerNodes && payload.composerNodes.length > 0) {
|
|
216
|
+
presenter.chatInputManager.setComposerNodes((currentNodes) =>
|
|
217
|
+
currentNodes.length === 1 && currentNodes[0]?.type === 'text' && currentNodes[0].text.length === 0
|
|
218
|
+
? payload.composerNodes ?? currentNodes
|
|
219
|
+
: currentNodes
|
|
220
|
+
);
|
|
221
|
+
} else {
|
|
222
|
+
presenter.chatInputManager.setDraft((currentDraft) =>
|
|
223
|
+
currentDraft.trim().length === 0 ? payload.message : currentDraft
|
|
224
|
+
);
|
|
225
|
+
}
|
|
207
226
|
}
|
|
208
227
|
throw error;
|
|
209
228
|
}
|
|
@@ -248,6 +267,9 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
248
267
|
}, [confirm, location.pathname, navigate, presenter]);
|
|
249
268
|
|
|
250
269
|
const currentSessionDisplayName = selectedSession ? sessionDisplayName(selectedSession) : undefined;
|
|
270
|
+
const currentSessionTypeLabel =
|
|
271
|
+
sessionTypeOptions.find((option) => option.value === selectedSessionType)?.label ??
|
|
272
|
+
resolveSessionTypeLabel(selectedSessionType);
|
|
251
273
|
|
|
252
274
|
useEffect(() => {
|
|
253
275
|
presenter.chatThreadManager.bindActions({
|
|
@@ -256,10 +278,19 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
256
278
|
}, [presenter, sessionsQuery.refetch]);
|
|
257
279
|
|
|
258
280
|
useEffect(() => {
|
|
281
|
+
const shouldHydrateModelFromSession =
|
|
282
|
+
!isSending &&
|
|
283
|
+
!isAwaitingAssistantOutput &&
|
|
284
|
+
!sessionsQuery.isLoading &&
|
|
285
|
+
isProviderStateResolved &&
|
|
286
|
+
modelOptions.length > 0 &&
|
|
287
|
+
selectedSessionKey !== modelHydratedSessionKeyRef.current;
|
|
259
288
|
const shouldHydrateThinkingFromSession =
|
|
260
289
|
!isSending &&
|
|
261
290
|
!isAwaitingAssistantOutput &&
|
|
262
291
|
!agent.isHydrating &&
|
|
292
|
+
isProviderStateResolved &&
|
|
293
|
+
modelOptions.length > 0 &&
|
|
263
294
|
selectedSessionKey !== thinkingHydratedSessionKeyRef.current;
|
|
264
295
|
|
|
265
296
|
presenter.chatInputManager.syncSnapshot({
|
|
@@ -272,6 +303,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
272
303
|
sendError: lastSendError,
|
|
273
304
|
isSending,
|
|
274
305
|
modelOptions,
|
|
306
|
+
...(shouldHydrateModelFromSession ? { selectedModel: hydratedSessionModel } : {}),
|
|
275
307
|
sessionTypeOptions,
|
|
276
308
|
selectedSessionType,
|
|
277
309
|
...(shouldHydrateThinkingFromSession ? { selectedThinkingLevel: selectedSessionThinkingLevel } : {}),
|
|
@@ -280,10 +312,14 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
280
312
|
skillRecords,
|
|
281
313
|
isSkillsLoading: installedSkillsQuery.isLoading
|
|
282
314
|
});
|
|
315
|
+
if (shouldHydrateModelFromSession) {
|
|
316
|
+
modelHydratedSessionKeyRef.current = selectedSessionKey;
|
|
317
|
+
}
|
|
283
318
|
if (shouldHydrateThinkingFromSession) {
|
|
284
319
|
thinkingHydratedSessionKeyRef.current = selectedSessionKey;
|
|
285
320
|
}
|
|
286
321
|
if (!selectedSessionKey) {
|
|
322
|
+
modelHydratedSessionKeyRef.current = null;
|
|
287
323
|
thinkingHydratedSessionKeyRef.current = null;
|
|
288
324
|
}
|
|
289
325
|
presenter.chatSessionListManager.syncSnapshot({
|
|
@@ -301,6 +337,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
301
337
|
modelOptions,
|
|
302
338
|
sessionTypeUnavailable,
|
|
303
339
|
sessionTypeUnavailableMessage,
|
|
340
|
+
sessionTypeLabel: currentSessionTypeLabel,
|
|
304
341
|
selectedSessionKey,
|
|
305
342
|
sessionDisplayName: currentSessionDisplayName,
|
|
306
343
|
canDeleteSession: Boolean(selectedSession),
|
|
@@ -316,12 +353,15 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
316
353
|
canEditSessionType,
|
|
317
354
|
canStopCurrentRun,
|
|
318
355
|
currentSessionDisplayName,
|
|
356
|
+
currentSessionTypeLabel,
|
|
319
357
|
defaultSessionType,
|
|
320
358
|
installedSkillsQuery.isLoading,
|
|
321
359
|
isAwaitingAssistantOutput,
|
|
360
|
+
hydratedSessionModel,
|
|
322
361
|
isProviderStateResolved,
|
|
323
362
|
isSending,
|
|
324
363
|
lastSendError,
|
|
364
|
+
modelOptions.length,
|
|
325
365
|
modelOptions,
|
|
326
366
|
presenter,
|
|
327
367
|
query,
|