@nextclaw/ui 0.7.0 → 0.8.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 +14 -0
- package/dist/assets/{ChannelsList-DF2U-LY1.js → ChannelsList-DBcoVJRW.js} +1 -1
- package/dist/assets/ChatPage-CD3cxyyM.js +37 -0
- package/dist/assets/{DocBrowser-B9ws5JL7.js → DocBrowser-DDX2HMXW.js} +1 -1
- package/dist/assets/{LogoBadge-DvGAzkZ3.js → LogoBadge-J53F_3JA.js} +1 -1
- package/dist/assets/{MarketplacePage-DG5mHWJ8.js → MarketplacePage-0BZ4bza0.js} +2 -2
- package/dist/assets/{ModelConfig-BL_HsOsm.js → ModelConfig-Wzq9wGHV.js} +1 -1
- package/dist/assets/{ProvidersList-CH5z00YT.js → ProvidersList-kwzRS8_M.js} +1 -1
- package/dist/assets/RuntimeConfig-N771_AM6.js +1 -0
- package/dist/assets/{SearchConfig-BhaI0fUf.js → SearchConfig-DVt5QVa_.js} +1 -1
- package/dist/assets/{SecretsConfig-CFoimOh9.js → SecretsConfig-CkwauPa8.js} +2 -2
- package/dist/assets/SessionsConfig-C3mnHzkZ.js +2 -0
- package/dist/assets/{session-run-status-TkIuGbVw.js → chat-message-pxr79GDs.js} +3 -3
- package/dist/assets/{index-X5J6Mm--.js → index-BIvFMkN4.js} +1 -1
- package/dist/assets/index-CzkY1reu.js +8 -0
- package/dist/assets/{index-uMsNsQX6.js → index-GdpEEKnz.js} +1 -1
- package/dist/assets/index-RZ0kHHRI.css +1 -0
- package/dist/assets/{label-D8ly4a2P.js → label-CmksBHgc.js} +1 -1
- package/dist/assets/{page-layout-BSYfvwbp.js → page-layout-Db0GbnhS.js} +1 -1
- package/dist/assets/security-config-CjLFME5Q.js +1 -0
- package/dist/assets/skeleton-CkpQeVWN.js +1 -0
- package/dist/assets/{switch-Ce_g9lpN.js → switch-C24d-UJU.js} +1 -1
- package/dist/assets/tabs-custom-D89bh-fc.js +1 -0
- package/dist/assets/{useConfirmDialog-A8Ek8Wu7.js → useConfirmDialog-BeP35LcG.js} +2 -2
- package/dist/assets/{vendor-B7ozqnFC.js → vendor-psXJBy9u.js} +65 -70
- package/dist/index.html +3 -3
- package/package.json +5 -2
- package/src/api/config.ts +38 -0
- package/src/api/types.ts +19 -0
- package/src/components/chat/ChatPage.tsx +10 -324
- package/src/components/chat/adapters/chat-message.adapter.test.ts +1 -0
- package/src/components/chat/chat-chain.test.ts +22 -0
- package/src/components/chat/chat-chain.ts +23 -0
- package/src/components/chat/chat-page-shell.tsx +103 -0
- package/src/components/chat/containers/chat-message-list.container.tsx +5 -1
- package/src/components/chat/legacy/LegacyChatPage.tsx +228 -0
- package/src/components/chat/ncp/NcpChatPage.tsx +349 -0
- package/src/components/chat/ncp/ncp-chat-input.manager.ts +173 -0
- package/src/components/chat/ncp/ncp-chat-page-data.ts +134 -0
- package/src/components/chat/ncp/ncp-chat-thread.manager.ts +89 -0
- package/src/components/chat/ncp/ncp-chat.presenter.ts +33 -0
- package/src/components/chat/ncp/ncp-session-adapter.test.ts +49 -0
- package/src/components/chat/ncp/ncp-session-adapter.ts +194 -0
- package/src/components/chat/presenter/chat-presenter-context.tsx +43 -4
- package/src/hooks/useConfig.ts +42 -0
- package/src/lib/i18n.ts +1 -1
- package/tailwind.config.js +8 -3
- package/tsconfig.json +4 -1
- package/dist/assets/ChatPage-BX39y0U5.js +0 -36
- package/dist/assets/RuntimeConfig-BplBgkwo.js +0 -1
- package/dist/assets/SessionsConfig-BHTAYn9T.js +0 -2
- package/dist/assets/index-BLeJkJ0o.css +0 -1
- package/dist/assets/index-DK4TS5ev.js +0 -8
- package/dist/assets/security-config-DlKEYHNN.js +0 -1
- package/dist/assets/skeleton-CWbsNx2h.js +0 -1
- package/dist/assets/tabs-custom-Cf5azvT5.js +0 -1
package/dist/index.html
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
8
8
|
<title>NextClaw - 系统配置</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-
|
|
11
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-CzkY1reu.js"></script>
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-psXJBy9u.js">
|
|
11
|
+
<link rel="stylesheet" crossorigin href="/assets/index-RZ0kHHRI.css">
|
|
12
12
|
</head>
|
|
13
13
|
|
|
14
14
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,7 +27,10 @@
|
|
|
27
27
|
"tailwind-merge": "^2.5.4",
|
|
28
28
|
"zod": "^3.23.8",
|
|
29
29
|
"zustand": "^5.0.2",
|
|
30
|
-
"@nextclaw/agent-
|
|
30
|
+
"@nextclaw/ncp-http-agent-client": "0.3.0",
|
|
31
|
+
"@nextclaw/ncp-react": "0.3.0",
|
|
32
|
+
"@nextclaw/agent-chat-ui": "0.2.0",
|
|
33
|
+
"@nextclaw/ncp": "0.3.0",
|
|
31
34
|
"@nextclaw/agent-chat": "0.1.1"
|
|
32
35
|
},
|
|
33
36
|
"devDependencies": {
|
package/src/api/config.ts
CHANGED
|
@@ -24,6 +24,8 @@ import type {
|
|
|
24
24
|
ProviderCreateRequest,
|
|
25
25
|
ProviderCreateResult,
|
|
26
26
|
ProviderDeleteResult,
|
|
27
|
+
NcpSessionMessagesView,
|
|
28
|
+
NcpSessionsListView,
|
|
27
29
|
RuntimeConfigUpdate,
|
|
28
30
|
SecretsConfigUpdate,
|
|
29
31
|
SecretsView,
|
|
@@ -369,6 +371,42 @@ export async function deleteSession(key: string): Promise<{ deleted: boolean }>
|
|
|
369
371
|
return response.data;
|
|
370
372
|
}
|
|
371
373
|
|
|
374
|
+
// GET /api/ncp/sessions
|
|
375
|
+
export async function fetchNcpSessions(params?: { limit?: number }): Promise<NcpSessionsListView> {
|
|
376
|
+
const query = new URLSearchParams();
|
|
377
|
+
if (typeof params?.limit === 'number' && Number.isFinite(params.limit)) {
|
|
378
|
+
query.set('limit', String(Math.max(1, Math.trunc(params.limit))));
|
|
379
|
+
}
|
|
380
|
+
const suffix = query.toString();
|
|
381
|
+
const response = await api.get<NcpSessionsListView>(suffix ? `/api/ncp/sessions?${suffix}` : '/api/ncp/sessions');
|
|
382
|
+
if (!response.ok) {
|
|
383
|
+
throw new Error(response.error.message);
|
|
384
|
+
}
|
|
385
|
+
return response.data;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// GET /api/ncp/sessions/:sessionId/messages
|
|
389
|
+
export async function fetchNcpSessionMessages(sessionId: string, limit = 200): Promise<NcpSessionMessagesView> {
|
|
390
|
+
const response = await api.get<NcpSessionMessagesView>(
|
|
391
|
+
`/api/ncp/sessions/${encodeURIComponent(sessionId)}/messages?limit=${Math.max(1, Math.trunc(limit))}`
|
|
392
|
+
);
|
|
393
|
+
if (!response.ok) {
|
|
394
|
+
throw new Error(response.error.message);
|
|
395
|
+
}
|
|
396
|
+
return response.data;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// DELETE /api/ncp/sessions/:sessionId
|
|
400
|
+
export async function deleteNcpSession(sessionId: string): Promise<{ deleted: boolean; sessionId: string }> {
|
|
401
|
+
const response = await api.delete<{ deleted: boolean; sessionId: string }>(
|
|
402
|
+
`/api/ncp/sessions/${encodeURIComponent(sessionId)}`
|
|
403
|
+
);
|
|
404
|
+
if (!response.ok) {
|
|
405
|
+
throw new Error(response.error.message);
|
|
406
|
+
}
|
|
407
|
+
return response.data;
|
|
408
|
+
}
|
|
409
|
+
|
|
372
410
|
// POST /api/chat/turn
|
|
373
411
|
export async function sendChatTurn(data: ChatTurnRequest): Promise<ChatTurnView> {
|
|
374
412
|
const response = await api.post<ChatTurnView>('/api/chat/turn', data);
|
package/src/api/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { NcpMessage, NcpSessionStatus, NcpSessionSummary } from '@nextclaw/ncp';
|
|
2
|
+
|
|
1
3
|
// API Types - matching backend response format
|
|
2
4
|
export type ApiError = {
|
|
3
5
|
code: string;
|
|
@@ -261,6 +263,23 @@ export type SessionHistoryView = {
|
|
|
261
263
|
events: SessionEventView[];
|
|
262
264
|
};
|
|
263
265
|
|
|
266
|
+
export type NcpSessionSummaryView = NcpSessionSummary;
|
|
267
|
+
|
|
268
|
+
export type NcpSessionsListView = {
|
|
269
|
+
sessions: NcpSessionSummaryView[];
|
|
270
|
+
total: number;
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
export type NcpMessageView = NcpMessage;
|
|
274
|
+
|
|
275
|
+
export type NcpSessionMessagesView = {
|
|
276
|
+
sessionId: string;
|
|
277
|
+
messages: NcpMessageView[];
|
|
278
|
+
total: number;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
export type NcpSessionStatusView = NcpSessionStatus;
|
|
282
|
+
|
|
264
283
|
export type SessionPatchUpdate = {
|
|
265
284
|
label?: string | null;
|
|
266
285
|
preferredModel?: string | null;
|
|
@@ -1,330 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { CronConfig } from '@/components/config/CronConfig';
|
|
7
|
-
import { MarketplacePage } from '@/components/marketplace/MarketplacePage';
|
|
8
|
-
import { useSessionRunStatus } from '@/components/chat/chat-page-runtime';
|
|
9
|
-
import { useChatRuntimeController } from '@/components/chat/useChatRuntimeController';
|
|
10
|
-
import { parseSessionKeyFromRoute, resolveAgentIdFromSessionKey } from '@/components/chat/chat-session-route';
|
|
11
|
-
import { useChatPageData, sessionDisplayName } from '@/components/chat/chat-page-data';
|
|
12
|
-
import { ChatPresenterProvider } from '@/components/chat/presenter/chat-presenter-context';
|
|
13
|
-
import { ChatPresenter } from '@/components/chat/presenter/chat.presenter';
|
|
14
|
-
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
15
|
-
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
16
|
-
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
|
17
|
-
|
|
18
|
-
type MainPanelView = 'chat' | 'cron' | 'skills';
|
|
19
|
-
|
|
20
|
-
type ChatPageProps = {
|
|
21
|
-
view: MainPanelView;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
type UseSessionSyncParams = {
|
|
25
|
-
view: MainPanelView;
|
|
26
|
-
routeSessionKey: string | null;
|
|
27
|
-
selectedSessionKey: string | null;
|
|
28
|
-
selectedAgentId: string;
|
|
29
|
-
setSelectedSessionKey: Dispatch<SetStateAction<string | null>>;
|
|
30
|
-
setSelectedAgentId: Dispatch<SetStateAction<string>>;
|
|
31
|
-
selectedSessionKeyRef: MutableRefObject<string | null>;
|
|
32
|
-
resetStreamState: () => void;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
function useChatSessionSync(params: UseSessionSyncParams): void {
|
|
36
|
-
const {
|
|
37
|
-
view,
|
|
38
|
-
routeSessionKey,
|
|
39
|
-
selectedSessionKey,
|
|
40
|
-
selectedAgentId,
|
|
41
|
-
setSelectedSessionKey,
|
|
42
|
-
setSelectedAgentId,
|
|
43
|
-
selectedSessionKeyRef,
|
|
44
|
-
resetStreamState
|
|
45
|
-
} = params;
|
|
46
|
-
|
|
47
|
-
useEffect(() => {
|
|
48
|
-
if (view !== 'chat') {
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
if (routeSessionKey) {
|
|
52
|
-
if (selectedSessionKey !== routeSessionKey) {
|
|
53
|
-
setSelectedSessionKey(routeSessionKey);
|
|
54
|
-
}
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
if (selectedSessionKey !== null) {
|
|
58
|
-
setSelectedSessionKey(null);
|
|
59
|
-
resetStreamState();
|
|
60
|
-
}
|
|
61
|
-
}, [resetStreamState, routeSessionKey, selectedSessionKey, setSelectedSessionKey, view]);
|
|
62
|
-
|
|
63
|
-
useEffect(() => {
|
|
64
|
-
const inferred = selectedSessionKey ? resolveAgentIdFromSessionKey(selectedSessionKey) : null;
|
|
65
|
-
if (!inferred) {
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
if (selectedAgentId !== inferred) {
|
|
69
|
-
setSelectedAgentId(inferred);
|
|
70
|
-
}
|
|
71
|
-
}, [selectedAgentId, selectedSessionKey, setSelectedAgentId]);
|
|
72
|
-
|
|
73
|
-
useEffect(() => {
|
|
74
|
-
selectedSessionKeyRef.current = selectedSessionKey;
|
|
75
|
-
}, [selectedSessionKey, selectedSessionKeyRef]);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
type ChatPageLayoutProps = {
|
|
79
|
-
view: MainPanelView;
|
|
80
|
-
confirmDialog: JSX.Element;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
function ChatPageLayout({ view, confirmDialog }: ChatPageLayoutProps) {
|
|
84
|
-
return (
|
|
85
|
-
<div className="h-full flex">
|
|
86
|
-
<ChatSidebar />
|
|
87
|
-
|
|
88
|
-
{view === 'chat' ? (
|
|
89
|
-
<ChatConversationPanel />
|
|
90
|
-
) : (
|
|
91
|
-
<section className="flex-1 min-h-0 overflow-hidden bg-gradient-to-b from-gray-50/60 to-white">
|
|
92
|
-
{view === 'cron' ? (
|
|
93
|
-
<div className="h-full overflow-auto custom-scrollbar">
|
|
94
|
-
<div className="mx-auto w-full max-w-[min(1120px,100%)] px-6 py-5">
|
|
95
|
-
<CronConfig />
|
|
96
|
-
</div>
|
|
97
|
-
</div>
|
|
98
|
-
) : (
|
|
99
|
-
<div className="h-full overflow-hidden">
|
|
100
|
-
<div className="mx-auto flex h-full min-h-0 w-full max-w-[min(1120px,100%)] flex-col px-6 py-5">
|
|
101
|
-
<MarketplacePage forcedType="skills" />
|
|
102
|
-
</div>
|
|
103
|
-
</div>
|
|
104
|
-
)}
|
|
105
|
-
</section>
|
|
106
|
-
)}
|
|
107
|
-
|
|
108
|
-
{confirmDialog}
|
|
109
|
-
</div>
|
|
110
|
-
);
|
|
111
|
-
}
|
|
1
|
+
import { useLocation } from 'react-router-dom';
|
|
2
|
+
import { resolveChatChain } from '@/components/chat/chat-chain';
|
|
3
|
+
import type { ChatPageProps } from '@/components/chat/chat-page-shell';
|
|
4
|
+
import { LegacyChatPage } from '@/components/chat/legacy/LegacyChatPage';
|
|
5
|
+
import { NcpChatPage } from '@/components/chat/ncp/NcpChatPage';
|
|
112
6
|
|
|
113
7
|
export function ChatPage({ view }: ChatPageProps) {
|
|
114
|
-
const [presenter] = useState(() => new ChatPresenter());
|
|
115
|
-
const query = useChatSessionListStore((state) => state.snapshot.query);
|
|
116
|
-
const selectedSessionKey = useChatSessionListStore((state) => state.snapshot.selectedSessionKey);
|
|
117
|
-
const selectedAgentId = useChatSessionListStore((state) => state.snapshot.selectedAgentId);
|
|
118
|
-
const pendingSessionType = useChatInputStore((state) => state.snapshot.pendingSessionType);
|
|
119
|
-
const { confirm, ConfirmDialog } = useConfirmDialog();
|
|
120
8
|
const location = useLocation();
|
|
121
|
-
const
|
|
122
|
-
const { sessionId: routeSessionIdParam } = useParams<{ sessionId?: string }>();
|
|
123
|
-
const threadRef = useRef<HTMLDivElement | null>(null);
|
|
124
|
-
const selectedSessionKeyRef = useRef<string | null>(selectedSessionKey);
|
|
125
|
-
const thinkingHydratedSessionKeyRef = useRef<string | null>(null);
|
|
126
|
-
const routeSessionKey = useMemo(
|
|
127
|
-
() => parseSessionKeyFromRoute(routeSessionIdParam),
|
|
128
|
-
[routeSessionIdParam]
|
|
129
|
-
);
|
|
130
|
-
const {
|
|
131
|
-
sessionsQuery,
|
|
132
|
-
installedSkillsQuery,
|
|
133
|
-
chatCapabilitiesQuery,
|
|
134
|
-
historyQuery,
|
|
135
|
-
isProviderStateResolved,
|
|
136
|
-
modelOptions,
|
|
137
|
-
sessions,
|
|
138
|
-
skillRecords,
|
|
139
|
-
selectedSession,
|
|
140
|
-
historyMessages,
|
|
141
|
-
selectedSessionThinkingLevel,
|
|
142
|
-
sessionTypeOptions,
|
|
143
|
-
defaultSessionType,
|
|
144
|
-
selectedSessionType,
|
|
145
|
-
canEditSessionType,
|
|
146
|
-
sessionTypeUnavailable,
|
|
147
|
-
sessionTypeUnavailableMessage
|
|
148
|
-
} = useChatPageData({
|
|
149
|
-
query,
|
|
150
|
-
selectedSessionKey,
|
|
151
|
-
selectedAgentId,
|
|
152
|
-
pendingSessionType,
|
|
153
|
-
setPendingSessionType: presenter.chatInputManager.setPendingSessionType,
|
|
154
|
-
setSelectedModel: presenter.chatInputManager.setSelectedModel
|
|
155
|
-
});
|
|
156
|
-
const {
|
|
157
|
-
uiMessages,
|
|
158
|
-
isSending,
|
|
159
|
-
isAwaitingAssistantOutput,
|
|
160
|
-
canStopCurrentRun,
|
|
161
|
-
stopDisabledReason,
|
|
162
|
-
lastSendError,
|
|
163
|
-
activeBackendRunId,
|
|
164
|
-
sendMessage,
|
|
165
|
-
stopCurrentRun,
|
|
166
|
-
resumeRun,
|
|
167
|
-
resetStreamState,
|
|
168
|
-
applyHistoryMessages
|
|
169
|
-
} = useChatRuntimeController(
|
|
170
|
-
{
|
|
171
|
-
selectedSessionKeyRef,
|
|
172
|
-
setSelectedSessionKey: presenter.chatSessionListManager.setSelectedSessionKey,
|
|
173
|
-
setDraft: presenter.chatInputManager.setDraft,
|
|
174
|
-
refetchSessions: sessionsQuery.refetch,
|
|
175
|
-
refetchHistory: historyQuery.refetch
|
|
176
|
-
},
|
|
177
|
-
presenter.chatController
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
console.log('[ChatPage] uiMessages', { uiMessages, historyMessages });
|
|
181
|
-
useEffect(() => {
|
|
182
|
-
presenter.chatStreamActionsManager.bind({
|
|
183
|
-
sendMessage,
|
|
184
|
-
stopCurrentRun,
|
|
185
|
-
resumeRun,
|
|
186
|
-
resetStreamState,
|
|
187
|
-
applyHistoryMessages
|
|
188
|
-
});
|
|
189
|
-
}, [applyHistoryMessages, presenter, resetStreamState, resumeRun, sendMessage, stopCurrentRun]);
|
|
190
|
-
|
|
191
|
-
const { sessionRunStatusByKey } = useSessionRunStatus({
|
|
192
|
-
view,
|
|
193
|
-
selectedSessionKey,
|
|
194
|
-
activeBackendRunId,
|
|
195
|
-
isLocallyRunning: isSending || Boolean(activeBackendRunId),
|
|
196
|
-
resumeRun: presenter.chatStreamActionsManager.resumeRun
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
useChatSessionSync({
|
|
200
|
-
view,
|
|
201
|
-
routeSessionKey,
|
|
202
|
-
selectedSessionKey,
|
|
203
|
-
selectedAgentId,
|
|
204
|
-
setSelectedSessionKey: presenter.chatSessionListManager.setSelectedSessionKey,
|
|
205
|
-
setSelectedAgentId: presenter.chatSessionListManager.setSelectedAgentId,
|
|
206
|
-
selectedSessionKeyRef,
|
|
207
|
-
resetStreamState: presenter.chatStreamActionsManager.resetStreamState
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
useEffect(() => {
|
|
211
|
-
presenter.chatStreamActionsManager.applyHistoryMessages(historyMessages, {
|
|
212
|
-
isLoading: historyQuery.isLoading
|
|
213
|
-
});
|
|
214
|
-
}, [historyMessages, historyQuery.isLoading, presenter]);
|
|
215
|
-
|
|
216
|
-
useEffect(() => {
|
|
217
|
-
presenter.chatUiManager.syncState({
|
|
218
|
-
pathname: location.pathname
|
|
219
|
-
});
|
|
220
|
-
presenter.chatUiManager.bindActions({
|
|
221
|
-
navigate,
|
|
222
|
-
confirm
|
|
223
|
-
});
|
|
224
|
-
}, [confirm, location.pathname, navigate, presenter]);
|
|
225
|
-
const currentSessionDisplayName = selectedSession ? sessionDisplayName(selectedSession) : undefined;
|
|
226
|
-
|
|
227
|
-
useEffect(() => {
|
|
228
|
-
presenter.chatThreadManager.bindActions({
|
|
229
|
-
refetchSessions: sessionsQuery.refetch
|
|
230
|
-
});
|
|
231
|
-
}, [
|
|
232
|
-
presenter,
|
|
233
|
-
sessionsQuery.refetch,
|
|
234
|
-
]);
|
|
235
|
-
|
|
236
|
-
useEffect(() => {
|
|
237
|
-
const shouldHydrateThinkingFromHistory =
|
|
238
|
-
!isSending &&
|
|
239
|
-
!isAwaitingAssistantOutput &&
|
|
240
|
-
!historyQuery.isLoading &&
|
|
241
|
-
selectedSessionKey !== thinkingHydratedSessionKeyRef.current;
|
|
9
|
+
const chatChain = resolveChatChain(location.search);
|
|
242
10
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
canStopGeneration: canStopCurrentRun,
|
|
247
|
-
stopDisabledReason,
|
|
248
|
-
stopSupported: chatCapabilitiesQuery.data?.stopSupported ?? false,
|
|
249
|
-
stopReason: chatCapabilitiesQuery.data?.stopReason,
|
|
250
|
-
sendError: lastSendError,
|
|
251
|
-
isSending,
|
|
252
|
-
modelOptions,
|
|
253
|
-
sessionTypeOptions,
|
|
254
|
-
selectedSessionType,
|
|
255
|
-
...(shouldHydrateThinkingFromHistory ? { selectedThinkingLevel: selectedSessionThinkingLevel } : {}),
|
|
256
|
-
canEditSessionType,
|
|
257
|
-
sessionTypeUnavailable,
|
|
258
|
-
skillRecords,
|
|
259
|
-
isSkillsLoading: installedSkillsQuery.isLoading
|
|
260
|
-
});
|
|
261
|
-
if (shouldHydrateThinkingFromHistory) {
|
|
262
|
-
thinkingHydratedSessionKeyRef.current = selectedSessionKey;
|
|
263
|
-
}
|
|
264
|
-
if (!selectedSessionKey) {
|
|
265
|
-
thinkingHydratedSessionKeyRef.current = null;
|
|
266
|
-
}
|
|
267
|
-
presenter.chatSessionListManager.syncSnapshot({
|
|
268
|
-
sessions,
|
|
269
|
-
query,
|
|
270
|
-
isLoading: sessionsQuery.isLoading
|
|
271
|
-
});
|
|
272
|
-
presenter.chatRunStatusManager.syncSnapshot({
|
|
273
|
-
sessionRunStatusByKey,
|
|
274
|
-
isLocallyRunning: isSending || Boolean(activeBackendRunId),
|
|
275
|
-
activeBackendRunId
|
|
276
|
-
});
|
|
277
|
-
presenter.chatThreadManager.syncSnapshot({
|
|
278
|
-
isProviderStateResolved,
|
|
279
|
-
modelOptions,
|
|
280
|
-
sessionTypeUnavailable,
|
|
281
|
-
sessionTypeUnavailableMessage,
|
|
282
|
-
selectedSessionKey,
|
|
283
|
-
sessionDisplayName: currentSessionDisplayName,
|
|
284
|
-
canDeleteSession: Boolean(selectedSession),
|
|
285
|
-
threadRef,
|
|
286
|
-
isHistoryLoading: historyQuery.isLoading,
|
|
287
|
-
uiMessages,
|
|
288
|
-
isSending,
|
|
289
|
-
isAwaitingAssistantOutput
|
|
290
|
-
});
|
|
291
|
-
}, [
|
|
292
|
-
activeBackendRunId,
|
|
293
|
-
canEditSessionType,
|
|
294
|
-
canStopCurrentRun,
|
|
295
|
-
currentSessionDisplayName,
|
|
296
|
-
chatCapabilitiesQuery.data?.stopReason,
|
|
297
|
-
chatCapabilitiesQuery.data?.stopSupported,
|
|
298
|
-
defaultSessionType,
|
|
299
|
-
historyQuery.isLoading,
|
|
300
|
-
installedSkillsQuery.isLoading,
|
|
301
|
-
isAwaitingAssistantOutput,
|
|
302
|
-
isProviderStateResolved,
|
|
303
|
-
isSending,
|
|
304
|
-
lastSendError,
|
|
305
|
-
uiMessages,
|
|
306
|
-
modelOptions,
|
|
307
|
-
presenter,
|
|
308
|
-
query,
|
|
309
|
-
selectedSession,
|
|
310
|
-
selectedSessionThinkingLevel,
|
|
311
|
-
selectedSessionKey,
|
|
312
|
-
selectedAgentId,
|
|
313
|
-
selectedSessionType,
|
|
314
|
-
sessionRunStatusByKey,
|
|
315
|
-
sessionTypeOptions,
|
|
316
|
-
sessionTypeUnavailable,
|
|
317
|
-
sessionTypeUnavailableMessage,
|
|
318
|
-
sessions,
|
|
319
|
-
sessionsQuery.isLoading,
|
|
320
|
-
stopDisabledReason,
|
|
321
|
-
threadRef,
|
|
322
|
-
skillRecords
|
|
323
|
-
]);
|
|
11
|
+
if (chatChain === 'ncp') {
|
|
12
|
+
return <NcpChatPage view={view} />;
|
|
13
|
+
}
|
|
324
14
|
|
|
325
|
-
return
|
|
326
|
-
<ChatPresenterProvider presenter={presenter}>
|
|
327
|
-
<ChatPageLayout view={view} confirmDialog={<ConfirmDialog />} />
|
|
328
|
-
</ChatPresenterProvider>
|
|
329
|
-
);
|
|
15
|
+
return <LegacyChatPage view={view} />;
|
|
330
16
|
}
|
|
@@ -61,6 +61,7 @@ describe('adaptChatMessages', () => {
|
|
|
61
61
|
expect(adapted[0]?.roleLabel).toBe('Assistant');
|
|
62
62
|
expect(adapted[0]?.timestampLabel).toBe('formatted:2026-03-17T10:00:00.000Z');
|
|
63
63
|
expect(adapted[0]?.parts.map((part) => part.type)).toEqual(['markdown', 'reasoning', 'tool-card']);
|
|
64
|
+
expect(adapted[0]?.parts[1]).toMatchObject({ type: 'reasoning', label: 'Reasoning', text: 'internal reasoning' });
|
|
64
65
|
expect(adapted[0]?.parts[2]).toMatchObject({
|
|
65
66
|
type: 'tool-card',
|
|
66
67
|
card: {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { resolveChatChain } from '@/components/chat/chat-chain';
|
|
3
|
+
|
|
4
|
+
describe('resolveChatChain', () => {
|
|
5
|
+
it('defaults to ncp when no query or env override is provided', () => {
|
|
6
|
+
vi.stubEnv('VITE_CHAT_CHAIN', '');
|
|
7
|
+
|
|
8
|
+
expect(resolveChatChain('')).toBe('ncp');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('allows explicit legacy rollback from query string', () => {
|
|
12
|
+
vi.stubEnv('VITE_CHAT_CHAIN', 'ncp');
|
|
13
|
+
|
|
14
|
+
expect(resolveChatChain('?chatChain=legacy')).toBe('legacy');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('accepts env override when query string is absent', () => {
|
|
18
|
+
vi.stubEnv('VITE_CHAT_CHAIN', 'legacy');
|
|
19
|
+
|
|
20
|
+
expect(resolveChatChain('')).toBe('legacy');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type ChatChain = 'legacy' | 'ncp';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_CHAT_CHAIN: ChatChain = 'ncp';
|
|
4
|
+
|
|
5
|
+
function normalizeChatChain(value: string | null | undefined): ChatChain | null {
|
|
6
|
+
if (typeof value !== 'string') {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
const normalized = value.trim().toLowerCase();
|
|
10
|
+
if (normalized === 'legacy' || normalized === 'ncp') {
|
|
11
|
+
return normalized;
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function resolveChatChain(search: string): ChatChain {
|
|
17
|
+
const fromSearch = normalizeChatChain(new URLSearchParams(search).get('chatChain'));
|
|
18
|
+
if (fromSearch) {
|
|
19
|
+
return fromSearch;
|
|
20
|
+
}
|
|
21
|
+
const fromEnv = normalizeChatChain(import.meta.env.VITE_CHAT_CHAIN);
|
|
22
|
+
return fromEnv ?? DEFAULT_CHAT_CHAIN;
|
|
23
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
|
3
|
+
import { ChatSidebar } from '@/components/chat/ChatSidebar';
|
|
4
|
+
import { ChatConversationPanel } from '@/components/chat/ChatConversationPanel';
|
|
5
|
+
import { CronConfig } from '@/components/config/CronConfig';
|
|
6
|
+
import { MarketplacePage } from '@/components/marketplace/MarketplacePage';
|
|
7
|
+
|
|
8
|
+
export type MainPanelView = 'chat' | 'cron' | 'skills';
|
|
9
|
+
|
|
10
|
+
export type ChatPageProps = {
|
|
11
|
+
view: MainPanelView;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type UseChatSessionSyncParams = {
|
|
15
|
+
view: MainPanelView;
|
|
16
|
+
routeSessionKey: string | null;
|
|
17
|
+
selectedSessionKey: string | null;
|
|
18
|
+
selectedAgentId: string;
|
|
19
|
+
setSelectedSessionKey: Dispatch<SetStateAction<string | null>>;
|
|
20
|
+
setSelectedAgentId: Dispatch<SetStateAction<string>>;
|
|
21
|
+
selectedSessionKeyRef: MutableRefObject<string | null>;
|
|
22
|
+
resetStreamState: () => void;
|
|
23
|
+
resolveAgentIdFromSessionKey: (sessionKey: string) => string | null;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function useChatSessionSync(params: UseChatSessionSyncParams): void {
|
|
27
|
+
const {
|
|
28
|
+
view,
|
|
29
|
+
routeSessionKey,
|
|
30
|
+
selectedSessionKey,
|
|
31
|
+
selectedAgentId,
|
|
32
|
+
setSelectedSessionKey,
|
|
33
|
+
setSelectedAgentId,
|
|
34
|
+
selectedSessionKeyRef,
|
|
35
|
+
resetStreamState,
|
|
36
|
+
resolveAgentIdFromSessionKey
|
|
37
|
+
} = params;
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (view !== 'chat') {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (routeSessionKey) {
|
|
44
|
+
if (selectedSessionKey !== routeSessionKey) {
|
|
45
|
+
setSelectedSessionKey(routeSessionKey);
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (selectedSessionKey !== null) {
|
|
50
|
+
setSelectedSessionKey(null);
|
|
51
|
+
resetStreamState();
|
|
52
|
+
}
|
|
53
|
+
}, [resetStreamState, routeSessionKey, selectedSessionKey, setSelectedSessionKey, view]);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const inferred = selectedSessionKey ? resolveAgentIdFromSessionKey(selectedSessionKey) : null;
|
|
57
|
+
if (!inferred) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (selectedAgentId !== inferred) {
|
|
61
|
+
setSelectedAgentId(inferred);
|
|
62
|
+
}
|
|
63
|
+
}, [resolveAgentIdFromSessionKey, selectedAgentId, selectedSessionKey, setSelectedAgentId]);
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
selectedSessionKeyRef.current = selectedSessionKey;
|
|
67
|
+
}, [selectedSessionKey, selectedSessionKeyRef]);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type ChatPageLayoutProps = {
|
|
71
|
+
view: MainPanelView;
|
|
72
|
+
confirmDialog: JSX.Element;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export function ChatPageLayout({ view, confirmDialog }: ChatPageLayoutProps) {
|
|
76
|
+
return (
|
|
77
|
+
<div className="h-full flex">
|
|
78
|
+
<ChatSidebar />
|
|
79
|
+
|
|
80
|
+
{view === 'chat' ? (
|
|
81
|
+
<ChatConversationPanel />
|
|
82
|
+
) : (
|
|
83
|
+
<section className="flex-1 min-h-0 overflow-hidden bg-gradient-to-b from-gray-50/60 to-white">
|
|
84
|
+
{view === 'cron' ? (
|
|
85
|
+
<div className="h-full overflow-auto custom-scrollbar">
|
|
86
|
+
<div className="mx-auto w-full max-w-[min(1120px,100%)] px-6 py-5">
|
|
87
|
+
<CronConfig />
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
) : (
|
|
91
|
+
<div className="h-full overflow-hidden">
|
|
92
|
+
<div className="mx-auto flex h-full min-h-0 w-full max-w-[min(1120px,100%)] flex-col px-6 py-5">
|
|
93
|
+
<MarketplacePage forcedType="skills" />
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
</section>
|
|
98
|
+
)}
|
|
99
|
+
|
|
100
|
+
{confirmDialog}
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -55,7 +55,11 @@ export function ChatMessageListContainer(props: ChatMessageListContainerProps) {
|
|
|
55
55
|
<ChatMessageList
|
|
56
56
|
messages={messages}
|
|
57
57
|
isSending={props.isSending}
|
|
58
|
-
|
|
58
|
+
hasAssistantDraft={props.uiMessages.some(
|
|
59
|
+
(message) =>
|
|
60
|
+
message.role === 'assistant' &&
|
|
61
|
+
(message.meta?.status === 'streaming' || message.meta?.status === 'pending')
|
|
62
|
+
)}
|
|
59
63
|
className={props.className}
|
|
60
64
|
texts={{
|
|
61
65
|
copyCodeLabel: t('chatCodeCopy'),
|