@nextclaw/ui 0.11.0 → 0.11.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 +8 -2
- package/dist/assets/{ChannelsList-BqsOYnXz.js → ChannelsList-CVPqrxns.js} +4 -4
- package/dist/assets/ChatPage-BO1VUrAY.js +37 -0
- package/dist/assets/{DocBrowser-BmL0QXBZ.js → DocBrowser-FBwg8iji.js} +1 -1
- package/dist/assets/{LogoBadge-C1HiPZPf.js → LogoBadge-BCmJfRT8.js} +1 -1
- package/dist/assets/MarketplacePage-DWxXUOCx.js +49 -0
- package/dist/assets/{McpMarketplacePage-CLHFnNBd.js → McpMarketplacePage-Bth9X_hu.js} +2 -2
- package/dist/assets/{ModelConfig-LQSR58tc.js → ModelConfig-PkSp_ioc.js} +1 -1
- package/dist/assets/ProvidersList-DVDge8wa.js +1 -0
- package/dist/assets/RemoteAccessPage-BVkzfEaL.js +1 -0
- package/dist/assets/RuntimeConfig-ByJs3khh.js +1 -0
- package/dist/assets/{SearchConfig-Chzo_JGs.js → SearchConfig-KZUAqYJN.js} +1 -1
- package/dist/assets/{SecretsConfig-CEIbjZYA.js → SecretsConfig-qwB_Y_Ka.js} +2 -2
- package/dist/assets/SessionsConfig-CGCl4UTr.js +2 -0
- package/dist/assets/index-CrilScMo.css +1 -0
- package/dist/assets/{index-j6A_-1b6.js → index-D41ntvb7.js} +6 -6
- package/dist/assets/{label-GACO2RzW.js → label-7JEFhkur.js} +1 -1
- package/dist/assets/ncp-session-adapter-BOqhkrc-.js +1 -0
- package/dist/assets/{page-layout-DjXaK3A3.js → page-layout-B7q511TE.js} +1 -1
- package/dist/assets/popover-CywJGmPr.js +1 -0
- package/dist/assets/security-config-zi2UxN5r.js +1 -0
- package/dist/assets/skeleton-qUJZQ03S.js +1 -0
- package/dist/assets/{status-dot-IWEBezqb.js → status-dot-BilwNdTT.js} +1 -1
- package/dist/assets/{switch-DCHAJSrA.js → switch-BLp2Pno1.js} +1 -1
- package/dist/assets/tabs-custom-CgIdQMGC.js +1 -0
- package/dist/assets/useConfirmDialog-BitswAkv.js +1 -0
- package/dist/assets/{vendor-CNhxtHCf.js → vendor-D_JxmsLV.js} +87 -87
- package/dist/index.html +3 -3
- package/package.json +4 -4
- package/src/App.test.tsx +42 -10
- package/src/App.tsx +5 -40
- package/src/api/api-base.test.ts +37 -0
- package/src/api/api-base.ts +0 -4
- package/src/api/config.ts +2 -270
- package/src/api/types.ts +0 -117
- package/src/components/chat/ChatPage.tsx +1 -11
- package/src/components/chat/ChatSidebar.test.tsx +1 -50
- package/src/components/chat/ChatSidebar.tsx +0 -5
- package/src/components/chat/README.md +2 -0
- package/src/components/chat/chat-attachment-upload-limit.test.ts +41 -0
- package/src/components/chat/chat-session-display.ts +9 -0
- package/src/components/chat/chat-session-label.service.ts +3 -12
- package/src/components/chat/chat-session-preference-sync.test.ts +10 -13
- package/src/components/chat/chat-stream/types.ts +4 -57
- package/src/components/chat/ncp/NcpChatPage.tsx +3 -3
- package/src/components/chat/useHydratedNcpAgent.test.tsx +77 -0
- package/src/components/config/README.md +2 -0
- package/src/components/config/SessionsConfig.tsx +152 -132
- package/src/hooks/use-auth.test.ts +3 -3
- package/src/hooks/use-auth.ts +16 -4
- package/src/hooks/use-realtime-query-bridge.ts +0 -24
- package/src/hooks/useConfig.ts +10 -137
- package/src/lib/session-run-status.ts +1 -63
- package/src/vite-env.d.ts +1 -0
- package/vite.config.ts +4 -4
- package/dist/assets/ChatPage-CJBYKR-Y.js +0 -38
- package/dist/assets/MarketplacePage-BIRP0NRS.js +0 -49
- package/dist/assets/ProvidersList-CwI-mxah.js +0 -1
- package/dist/assets/RemoteAccessPage-Cw5BqZb6.js +0 -1
- package/dist/assets/RuntimeConfig-DbowSRAb.js +0 -1
- package/dist/assets/SessionsConfig-BR8GfGWL.js +0 -2
- package/dist/assets/chat-message-CPG7zxRR.js +0 -3
- package/dist/assets/index-kaPUhd-8.css +0 -1
- package/dist/assets/popover-DTaFiTmU.js +0 -1
- package/dist/assets/security-config-Dk-yoKvK.js +0 -1
- package/dist/assets/skeleton-Dm2xOBSA.js +0 -1
- package/dist/assets/tabs-custom-DKSbDSB9.js +0 -1
- package/dist/assets/useConfirmDialog-ByJ8A8n7.js +0 -1
- package/src/api/config.stream.test.ts +0 -115
- package/src/components/chat/chat-chain.test.ts +0 -22
- package/src/components/chat/chat-chain.ts +0 -23
- package/src/components/chat/chat-page-data.ts +0 -171
- package/src/components/chat/chat-page-runtime.ts +0 -190
- package/src/components/chat/chat-stream/nextbot-parsers.ts +0 -52
- package/src/components/chat/chat-stream/nextbot-runtime-agent.ts +0 -413
- package/src/components/chat/chat-stream/stream-event-adapter.ts +0 -98
- package/src/components/chat/chat-stream/transport.ts +0 -253
- package/src/components/chat/legacy/LegacyChatPage.tsx +0 -223
- package/src/components/chat/managers/chat-input.manager.ts +0 -228
- package/src/components/chat/managers/chat-thread.manager.ts +0 -87
- package/src/components/chat/presenter/chat.presenter.ts +0 -32
- package/src/components/chat/useChatRuntimeController.ts +0 -134
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
import { fetchChatRuns, stopChatTurn } from '@/api/config';
|
|
2
|
-
import { appClient } from '@/transport';
|
|
3
|
-
import type { ActiveRunState, SendMessageParams, StreamDeltaEvent, StreamReadyEvent, StreamSessionEvent } from './types';
|
|
4
|
-
|
|
5
|
-
function buildSendTurnPayload(item: SendMessageParams, requestedSkills: string[]) {
|
|
6
|
-
const metadata: Record<string, unknown> = {};
|
|
7
|
-
if (item.sessionType) {
|
|
8
|
-
metadata.session_type = item.sessionType;
|
|
9
|
-
}
|
|
10
|
-
if (item.thinkingLevel) {
|
|
11
|
-
metadata.thinking = item.thinkingLevel;
|
|
12
|
-
}
|
|
13
|
-
if (requestedSkills.length > 0) {
|
|
14
|
-
metadata.requested_skills = requestedSkills;
|
|
15
|
-
}
|
|
16
|
-
return {
|
|
17
|
-
message: item.message,
|
|
18
|
-
...(item.runId ? { runId: item.runId } : {}),
|
|
19
|
-
sessionKey: item.sessionKey,
|
|
20
|
-
agentId: item.agentId,
|
|
21
|
-
...(item.model ? { model: item.model } : {}),
|
|
22
|
-
...(Object.keys(metadata).length > 0 ? { metadata } : {}),
|
|
23
|
-
channel: 'ui',
|
|
24
|
-
chatId: 'web-ui'
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export async function openSendTurnStream(params: {
|
|
29
|
-
item: SendMessageParams;
|
|
30
|
-
requestedSkills: string[];
|
|
31
|
-
signal: AbortSignal;
|
|
32
|
-
onReady: (event: StreamReadyEvent) => void;
|
|
33
|
-
onDelta: (event: StreamDeltaEvent) => void;
|
|
34
|
-
onSessionEvent: (event: StreamSessionEvent) => void;
|
|
35
|
-
}) {
|
|
36
|
-
let readySessionKey = '';
|
|
37
|
-
let finalResult: { sessionKey: string; reply: string } | null = null;
|
|
38
|
-
const session = appClient.openStream<{ reply?: string; sessionKey?: string }>({
|
|
39
|
-
method: 'POST',
|
|
40
|
-
path: '/api/chat/turn/stream',
|
|
41
|
-
body: buildSendTurnPayload(params.item, params.requestedSkills),
|
|
42
|
-
signal: params.signal,
|
|
43
|
-
onEvent: (event) => {
|
|
44
|
-
if (event.name === 'ready') {
|
|
45
|
-
const payload = (event.payload ?? {}) as StreamReadyEvent;
|
|
46
|
-
if (typeof payload.sessionKey === 'string' && payload.sessionKey.trim()) {
|
|
47
|
-
readySessionKey = payload.sessionKey;
|
|
48
|
-
}
|
|
49
|
-
params.onReady(payload);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
if (event.name === 'delta') {
|
|
53
|
-
params.onDelta((event.payload ?? { delta: '' }) as StreamDeltaEvent);
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
if (event.name === 'session_event') {
|
|
57
|
-
params.onSessionEvent({ data: event.payload as StreamSessionEvent['data'] });
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
if (event.name === 'final') {
|
|
61
|
-
const payload = (event.payload ?? {}) as { sessionKey?: string; reply?: string };
|
|
62
|
-
finalResult = {
|
|
63
|
-
sessionKey:
|
|
64
|
-
typeof payload.sessionKey === 'string' && payload.sessionKey.trim()
|
|
65
|
-
? payload.sessionKey
|
|
66
|
-
: readySessionKey,
|
|
67
|
-
reply: typeof payload.reply === 'string' ? payload.reply : ''
|
|
68
|
-
};
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
if (event.name === 'error') {
|
|
72
|
-
const payload = (event.payload ?? {}) as { message?: string };
|
|
73
|
-
throw new Error(payload.message?.trim() || 'chat stream failed');
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
const result = await session.finished as { reply?: string; sessionKey?: string } | undefined;
|
|
78
|
-
if (finalResult !== null) {
|
|
79
|
-
return finalResult;
|
|
80
|
-
}
|
|
81
|
-
return {
|
|
82
|
-
sessionKey: typeof result?.sessionKey === 'string' && result.sessionKey.trim()
|
|
83
|
-
? result.sessionKey
|
|
84
|
-
: readySessionKey,
|
|
85
|
-
reply: typeof result?.reply === 'string' ? result.reply : ''
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export async function openResumeRunStream(params: {
|
|
90
|
-
runId: string;
|
|
91
|
-
fromEventIndex?: number;
|
|
92
|
-
signal: AbortSignal;
|
|
93
|
-
onReady: (event: StreamReadyEvent) => void;
|
|
94
|
-
onDelta: (event: StreamDeltaEvent) => void;
|
|
95
|
-
onSessionEvent: (event: StreamSessionEvent) => void;
|
|
96
|
-
}) {
|
|
97
|
-
let readySessionKey = '';
|
|
98
|
-
let finalResult: { sessionKey: string; reply: string } | null = null;
|
|
99
|
-
const query = new URLSearchParams();
|
|
100
|
-
if (typeof params.fromEventIndex === 'number') {
|
|
101
|
-
query.set('fromEventIndex', String(Math.max(0, Math.trunc(params.fromEventIndex))));
|
|
102
|
-
}
|
|
103
|
-
const path =
|
|
104
|
-
`/api/chat/runs/${encodeURIComponent(params.runId)}/stream`
|
|
105
|
-
+ (query.size > 0 ? `?${query.toString()}` : '');
|
|
106
|
-
const session = appClient.openStream<{ reply?: string; sessionKey?: string }>({
|
|
107
|
-
method: 'GET',
|
|
108
|
-
path,
|
|
109
|
-
signal: params.signal,
|
|
110
|
-
onEvent: (event) => {
|
|
111
|
-
if (event.name === 'ready') {
|
|
112
|
-
const payload = (event.payload ?? {}) as StreamReadyEvent;
|
|
113
|
-
if (typeof payload.sessionKey === 'string' && payload.sessionKey.trim()) {
|
|
114
|
-
readySessionKey = payload.sessionKey;
|
|
115
|
-
}
|
|
116
|
-
params.onReady(payload);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
if (event.name === 'delta') {
|
|
120
|
-
params.onDelta((event.payload ?? { delta: '' }) as StreamDeltaEvent);
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
if (event.name === 'session_event') {
|
|
124
|
-
params.onSessionEvent({ data: event.payload as StreamSessionEvent['data'] });
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
if (event.name === 'final') {
|
|
128
|
-
const payload = (event.payload ?? {}) as { sessionKey?: string; reply?: string };
|
|
129
|
-
finalResult = {
|
|
130
|
-
sessionKey:
|
|
131
|
-
typeof payload.sessionKey === 'string' && payload.sessionKey.trim()
|
|
132
|
-
? payload.sessionKey
|
|
133
|
-
: readySessionKey,
|
|
134
|
-
reply: typeof payload.reply === 'string' ? payload.reply : ''
|
|
135
|
-
};
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
if (event.name === 'error') {
|
|
139
|
-
const payload = (event.payload ?? {}) as { message?: string };
|
|
140
|
-
throw new Error(payload.message?.trim() || 'chat stream failed');
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
const result = await session.finished as { reply?: string; sessionKey?: string } | undefined;
|
|
145
|
-
if (finalResult !== null) {
|
|
146
|
-
return finalResult;
|
|
147
|
-
}
|
|
148
|
-
return {
|
|
149
|
-
sessionKey: typeof result?.sessionKey === 'string' && result.sessionKey.trim()
|
|
150
|
-
? result.sessionKey
|
|
151
|
-
: readySessionKey,
|
|
152
|
-
reply: typeof result?.reply === 'string' ? result.reply : ''
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export async function requestStopRun(activeRun: ActiveRunState): Promise<void> {
|
|
157
|
-
if (!activeRun.backendStopSupported) {
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
const attemptedRunIds = new Set<string>();
|
|
163
|
-
const knownRunId = activeRun.backendRunId?.trim();
|
|
164
|
-
if (knownRunId) {
|
|
165
|
-
attemptedRunIds.add(knownRunId);
|
|
166
|
-
const stopped = await stopRunById(knownRunId);
|
|
167
|
-
if (stopped) {
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const candidateRunIds = await resolveStopCandidateRunIds(activeRun);
|
|
173
|
-
for (const runId of candidateRunIds) {
|
|
174
|
-
if (attemptedRunIds.has(runId)) {
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
attemptedRunIds.add(runId);
|
|
178
|
-
const stopped = await stopRunById(runId);
|
|
179
|
-
if (stopped) {
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
if (knownRunId) {
|
|
184
|
-
const stopped = await stopRunById(knownRunId);
|
|
185
|
-
if (stopped) {
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
} catch {
|
|
190
|
-
// Keep local abort as fallback even if stop API fails.
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
async function stopRunById(runId: string): Promise<boolean> {
|
|
195
|
-
const normalizedRunId = runId.trim();
|
|
196
|
-
if (!normalizedRunId) {
|
|
197
|
-
return false;
|
|
198
|
-
}
|
|
199
|
-
try {
|
|
200
|
-
const result = await stopChatTurn({
|
|
201
|
-
runId: normalizedRunId
|
|
202
|
-
});
|
|
203
|
-
return result.stopped === true;
|
|
204
|
-
} catch {
|
|
205
|
-
return false;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
async function resolveStopCandidateRunIds(activeRun: ActiveRunState): Promise<string[]> {
|
|
210
|
-
const sessionKey = activeRun.sessionKey?.trim();
|
|
211
|
-
if (!sessionKey) {
|
|
212
|
-
return [];
|
|
213
|
-
}
|
|
214
|
-
const attempts = 8;
|
|
215
|
-
const delayMs = 120;
|
|
216
|
-
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
217
|
-
const runIds = await listActiveRunIdsBySession(activeRun, sessionKey);
|
|
218
|
-
if (runIds.length > 0) {
|
|
219
|
-
return runIds;
|
|
220
|
-
}
|
|
221
|
-
if (attempt < attempts - 1) {
|
|
222
|
-
await sleep(delayMs);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
return [];
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
async function listActiveRunIdsBySession(activeRun: ActiveRunState, sessionKey: string): Promise<string[]> {
|
|
229
|
-
try {
|
|
230
|
-
const response = await fetchChatRuns({
|
|
231
|
-
sessionKey,
|
|
232
|
-
states: ['queued', 'running'],
|
|
233
|
-
limit: 50
|
|
234
|
-
});
|
|
235
|
-
const primary = response.runs
|
|
236
|
-
.filter((run) => run.runId?.trim() && run.sessionKey === sessionKey && run.agentId === activeRun.agentId)
|
|
237
|
-
.map((run) => run.runId.trim());
|
|
238
|
-
if (primary.length > 0) {
|
|
239
|
-
return primary;
|
|
240
|
-
}
|
|
241
|
-
return response.runs
|
|
242
|
-
.filter((run) => run.runId?.trim() && run.sessionKey === sessionKey)
|
|
243
|
-
.map((run) => run.runId.trim());
|
|
244
|
-
} catch {
|
|
245
|
-
return [];
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function sleep(ms: number): Promise<void> {
|
|
250
|
-
return new Promise((resolve) => {
|
|
251
|
-
window.setTimeout(resolve, ms);
|
|
252
|
-
});
|
|
253
|
-
}
|
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
-
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
|
3
|
-
import { useConfirmDialog } from '@/hooks/useConfirmDialog';
|
|
4
|
-
import { useSessionRunStatus } from '@/components/chat/chat-page-runtime';
|
|
5
|
-
import { useChatPageData, sessionDisplayName } from '@/components/chat/chat-page-data';
|
|
6
|
-
import { ChatPageLayout, type ChatPageProps, useChatSessionSync } from '@/components/chat/chat-page-shell';
|
|
7
|
-
import { parseSessionKeyFromRoute, resolveAgentIdFromSessionKey } from '@/components/chat/chat-session-route';
|
|
8
|
-
import { ChatPresenterProvider } from '@/components/chat/presenter/chat-presenter-context';
|
|
9
|
-
import { ChatPresenter } from '@/components/chat/presenter/chat.presenter';
|
|
10
|
-
import { useChatRuntimeController } from '@/components/chat/useChatRuntimeController';
|
|
11
|
-
import { resolveSessionTypeLabel } from '@/components/chat/useChatSessionTypeState';
|
|
12
|
-
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
13
|
-
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
14
|
-
|
|
15
|
-
export function LegacyChatPage({ view }: ChatPageProps) {
|
|
16
|
-
const [presenter] = useState(() => new ChatPresenter());
|
|
17
|
-
const query = useChatSessionListStore((state) => state.snapshot.query);
|
|
18
|
-
const selectedSessionKey = useChatSessionListStore((state) => state.snapshot.selectedSessionKey);
|
|
19
|
-
const selectedAgentId = useChatSessionListStore((state) => state.snapshot.selectedAgentId);
|
|
20
|
-
const pendingSessionType = useChatInputStore((state) => state.snapshot.pendingSessionType);
|
|
21
|
-
const currentSelectedModel = useChatInputStore((state) => state.snapshot.selectedModel);
|
|
22
|
-
const { confirm, ConfirmDialog } = useConfirmDialog();
|
|
23
|
-
const location = useLocation();
|
|
24
|
-
const navigate = useNavigate();
|
|
25
|
-
const { sessionId: routeSessionIdParam } = useParams<{ sessionId?: string }>();
|
|
26
|
-
const threadRef = useRef<HTMLDivElement | null>(null);
|
|
27
|
-
const selectedSessionKeyRef = useRef<string | null>(selectedSessionKey);
|
|
28
|
-
const routeSessionKey = useMemo(
|
|
29
|
-
() => parseSessionKeyFromRoute(routeSessionIdParam),
|
|
30
|
-
[routeSessionIdParam]
|
|
31
|
-
);
|
|
32
|
-
const {
|
|
33
|
-
sessionsQuery,
|
|
34
|
-
installedSkillsQuery,
|
|
35
|
-
chatCapabilitiesQuery,
|
|
36
|
-
historyQuery,
|
|
37
|
-
isProviderStateResolved,
|
|
38
|
-
modelOptions,
|
|
39
|
-
sessions,
|
|
40
|
-
skillRecords,
|
|
41
|
-
selectedSession,
|
|
42
|
-
historyMessages,
|
|
43
|
-
sessionTypeOptions,
|
|
44
|
-
defaultSessionType,
|
|
45
|
-
selectedSessionType,
|
|
46
|
-
canEditSessionType,
|
|
47
|
-
sessionTypeUnavailable,
|
|
48
|
-
sessionTypeUnavailableMessage
|
|
49
|
-
} = useChatPageData({
|
|
50
|
-
query,
|
|
51
|
-
selectedSessionKey,
|
|
52
|
-
selectedAgentId,
|
|
53
|
-
currentSelectedModel,
|
|
54
|
-
pendingSessionType,
|
|
55
|
-
setPendingSessionType: presenter.chatInputManager.setPendingSessionType,
|
|
56
|
-
setSelectedModel: presenter.chatInputManager.setSelectedModel,
|
|
57
|
-
setSelectedThinkingLevel: presenter.chatInputManager.setSelectedThinkingLevel
|
|
58
|
-
});
|
|
59
|
-
const {
|
|
60
|
-
uiMessages,
|
|
61
|
-
isSending,
|
|
62
|
-
isAwaitingAssistantOutput,
|
|
63
|
-
canStopCurrentRun,
|
|
64
|
-
stopDisabledReason,
|
|
65
|
-
lastSendError,
|
|
66
|
-
activeBackendRunId,
|
|
67
|
-
sendMessage,
|
|
68
|
-
stopCurrentRun,
|
|
69
|
-
resumeRun,
|
|
70
|
-
resetStreamState,
|
|
71
|
-
applyHistoryMessages
|
|
72
|
-
} = useChatRuntimeController(
|
|
73
|
-
{
|
|
74
|
-
selectedSessionKeyRef,
|
|
75
|
-
setSelectedSessionKey: presenter.chatSessionListManager.setSelectedSessionKey,
|
|
76
|
-
setDraft: presenter.chatInputManager.setDraft,
|
|
77
|
-
setComposerNodes: presenter.chatInputManager.setComposerNodes,
|
|
78
|
-
refetchSessions: sessionsQuery.refetch,
|
|
79
|
-
refetchHistory: historyQuery.refetch
|
|
80
|
-
},
|
|
81
|
-
presenter.chatController
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
useEffect(() => {
|
|
85
|
-
presenter.chatStreamActionsManager.bind({
|
|
86
|
-
sendMessage,
|
|
87
|
-
stopCurrentRun,
|
|
88
|
-
resumeRun,
|
|
89
|
-
resetStreamState,
|
|
90
|
-
applyHistoryMessages
|
|
91
|
-
});
|
|
92
|
-
}, [applyHistoryMessages, presenter, resetStreamState, resumeRun, sendMessage, stopCurrentRun]);
|
|
93
|
-
|
|
94
|
-
const { sessionRunStatusByKey } = useSessionRunStatus({
|
|
95
|
-
view,
|
|
96
|
-
selectedSessionKey,
|
|
97
|
-
activeBackendRunId,
|
|
98
|
-
isLocallyRunning: isSending || Boolean(activeBackendRunId),
|
|
99
|
-
resumeRun: presenter.chatStreamActionsManager.resumeRun
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
useChatSessionSync({
|
|
103
|
-
view,
|
|
104
|
-
routeSessionKey,
|
|
105
|
-
selectedSessionKey,
|
|
106
|
-
selectedAgentId,
|
|
107
|
-
setSelectedSessionKey: presenter.chatSessionListManager.setSelectedSessionKey,
|
|
108
|
-
setSelectedAgentId: presenter.chatSessionListManager.setSelectedAgentId,
|
|
109
|
-
selectedSessionKeyRef,
|
|
110
|
-
resetStreamState: presenter.chatStreamActionsManager.resetStreamState,
|
|
111
|
-
resolveAgentIdFromSessionKey
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
useEffect(() => {
|
|
115
|
-
presenter.chatStreamActionsManager.applyHistoryMessages(historyMessages, {
|
|
116
|
-
isLoading: historyQuery.isLoading
|
|
117
|
-
});
|
|
118
|
-
}, [historyMessages, historyQuery.isLoading, presenter]);
|
|
119
|
-
|
|
120
|
-
useEffect(() => {
|
|
121
|
-
presenter.chatUiManager.syncState({
|
|
122
|
-
pathname: location.pathname
|
|
123
|
-
});
|
|
124
|
-
presenter.chatUiManager.bindActions({
|
|
125
|
-
navigate,
|
|
126
|
-
confirm
|
|
127
|
-
});
|
|
128
|
-
}, [confirm, location.pathname, navigate, presenter]);
|
|
129
|
-
|
|
130
|
-
const currentSessionDisplayName = selectedSession ? sessionDisplayName(selectedSession) : undefined;
|
|
131
|
-
const currentSessionTypeLabel =
|
|
132
|
-
sessionTypeOptions.find((option) => option.value === selectedSessionType)?.label ??
|
|
133
|
-
resolveSessionTypeLabel(selectedSessionType);
|
|
134
|
-
|
|
135
|
-
useEffect(() => {
|
|
136
|
-
presenter.chatThreadManager.bindActions({
|
|
137
|
-
refetchSessions: sessionsQuery.refetch
|
|
138
|
-
});
|
|
139
|
-
}, [presenter, sessionsQuery.refetch]);
|
|
140
|
-
|
|
141
|
-
useEffect(() => {
|
|
142
|
-
presenter.chatInputManager.syncSnapshot({
|
|
143
|
-
isProviderStateResolved,
|
|
144
|
-
defaultSessionType,
|
|
145
|
-
canStopGeneration: canStopCurrentRun,
|
|
146
|
-
stopDisabledReason,
|
|
147
|
-
stopSupported: chatCapabilitiesQuery.data?.stopSupported ?? false,
|
|
148
|
-
stopReason: chatCapabilitiesQuery.data?.stopReason,
|
|
149
|
-
sendError: lastSendError,
|
|
150
|
-
isSending,
|
|
151
|
-
modelOptions,
|
|
152
|
-
sessionTypeOptions,
|
|
153
|
-
selectedSessionType,
|
|
154
|
-
canEditSessionType,
|
|
155
|
-
sessionTypeUnavailable,
|
|
156
|
-
skillRecords,
|
|
157
|
-
isSkillsLoading: installedSkillsQuery.isLoading
|
|
158
|
-
});
|
|
159
|
-
presenter.chatSessionListManager.syncSnapshot({
|
|
160
|
-
sessions,
|
|
161
|
-
query,
|
|
162
|
-
isLoading: sessionsQuery.isLoading
|
|
163
|
-
});
|
|
164
|
-
presenter.chatRunStatusManager.syncSnapshot({
|
|
165
|
-
sessionRunStatusByKey,
|
|
166
|
-
isLocallyRunning: isSending || Boolean(activeBackendRunId),
|
|
167
|
-
activeBackendRunId
|
|
168
|
-
});
|
|
169
|
-
presenter.chatThreadManager.syncSnapshot({
|
|
170
|
-
isProviderStateResolved,
|
|
171
|
-
modelOptions,
|
|
172
|
-
sessionTypeUnavailable,
|
|
173
|
-
sessionTypeUnavailableMessage,
|
|
174
|
-
sessionTypeLabel: currentSessionTypeLabel,
|
|
175
|
-
selectedSessionKey,
|
|
176
|
-
sessionDisplayName: currentSessionDisplayName,
|
|
177
|
-
canDeleteSession: Boolean(selectedSession),
|
|
178
|
-
threadRef,
|
|
179
|
-
isHistoryLoading: historyQuery.isLoading,
|
|
180
|
-
uiMessages,
|
|
181
|
-
isSending,
|
|
182
|
-
isAwaitingAssistantOutput
|
|
183
|
-
});
|
|
184
|
-
}, [
|
|
185
|
-
activeBackendRunId,
|
|
186
|
-
canEditSessionType,
|
|
187
|
-
canStopCurrentRun,
|
|
188
|
-
currentSessionDisplayName,
|
|
189
|
-
currentSessionTypeLabel,
|
|
190
|
-
chatCapabilitiesQuery.data?.stopReason,
|
|
191
|
-
chatCapabilitiesQuery.data?.stopSupported,
|
|
192
|
-
defaultSessionType,
|
|
193
|
-
historyQuery.isLoading,
|
|
194
|
-
installedSkillsQuery.isLoading,
|
|
195
|
-
isAwaitingAssistantOutput,
|
|
196
|
-
isProviderStateResolved,
|
|
197
|
-
isSending,
|
|
198
|
-
lastSendError,
|
|
199
|
-
modelOptions.length,
|
|
200
|
-
modelOptions,
|
|
201
|
-
presenter,
|
|
202
|
-
query,
|
|
203
|
-
selectedSession,
|
|
204
|
-
selectedSessionKey,
|
|
205
|
-
selectedSessionType,
|
|
206
|
-
sessionRunStatusByKey,
|
|
207
|
-
sessionTypeOptions,
|
|
208
|
-
sessionTypeUnavailable,
|
|
209
|
-
sessionTypeUnavailableMessage,
|
|
210
|
-
sessions,
|
|
211
|
-
sessionsQuery.isLoading,
|
|
212
|
-
skillRecords,
|
|
213
|
-
stopDisabledReason,
|
|
214
|
-
threadRef,
|
|
215
|
-
uiMessages
|
|
216
|
-
]);
|
|
217
|
-
|
|
218
|
-
return (
|
|
219
|
-
<ChatPresenterProvider presenter={presenter}>
|
|
220
|
-
<ChatPageLayout view={view} confirmDialog={<ConfirmDialog />} />
|
|
221
|
-
</ChatPresenterProvider>
|
|
222
|
-
);
|
|
223
|
-
}
|
|
@@ -1,228 +0,0 @@
|
|
|
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';
|
|
9
|
-
import { updateSession } from '@/api/config';
|
|
10
|
-
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
11
|
-
import { buildNewSessionKey } from '@/components/chat/chat-session-route';
|
|
12
|
-
import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
|
|
13
|
-
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
14
|
-
import { normalizeSessionType } from '@/components/chat/useChatSessionTypeState';
|
|
15
|
-
import type { ChatInputSnapshot } from '@/components/chat/stores/chat-input.store';
|
|
16
|
-
import { ChatSessionPreferenceSync } from '@/components/chat/chat-session-preference-sync';
|
|
17
|
-
import type { SetStateAction } from 'react';
|
|
18
|
-
import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
|
|
19
|
-
import type { ThinkingLevel } from '@/api/types';
|
|
20
|
-
import type { ChatModelOption } from '@/components/chat/chat-input.types';
|
|
21
|
-
|
|
22
|
-
export class ChatInputManager {
|
|
23
|
-
private readonly sessionPreferenceSync = new ChatSessionPreferenceSync(updateSession);
|
|
24
|
-
|
|
25
|
-
constructor(
|
|
26
|
-
private uiManager: ChatUiManager,
|
|
27
|
-
private streamActionsManager: ChatStreamActionsManager
|
|
28
|
-
) {}
|
|
29
|
-
|
|
30
|
-
private hasSnapshotChanges = (patch: Partial<ChatInputSnapshot>): boolean => {
|
|
31
|
-
const current = useChatInputStore.getState().snapshot;
|
|
32
|
-
for (const [key, value] of Object.entries(patch) as Array<[keyof ChatInputSnapshot, ChatInputSnapshot[keyof ChatInputSnapshot]]>) {
|
|
33
|
-
if (!Object.is(current[key], value)) {
|
|
34
|
-
return true;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return false;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
private resolveUpdateValue = <T>(prev: T, next: SetStateAction<T>): T => {
|
|
41
|
-
if (typeof next === 'function') {
|
|
42
|
-
return (next as (value: T) => T)(prev);
|
|
43
|
-
}
|
|
44
|
-
return next;
|
|
45
|
-
};
|
|
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
|
-
|
|
58
|
-
syncSnapshot = (patch: Partial<ChatInputSnapshot>) => {
|
|
59
|
-
if (!this.hasSnapshotChanges(patch)) {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
useChatInputStore.getState().setSnapshot(patch);
|
|
63
|
-
if (
|
|
64
|
-
Object.prototype.hasOwnProperty.call(patch, 'modelOptions') ||
|
|
65
|
-
Object.prototype.hasOwnProperty.call(patch, 'selectedModel') ||
|
|
66
|
-
Object.prototype.hasOwnProperty.call(patch, 'selectedThinkingLevel')
|
|
67
|
-
) {
|
|
68
|
-
const { selectedModel } = useChatInputStore.getState().snapshot;
|
|
69
|
-
this.reconcileThinkingForModel(selectedModel);
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
setDraft = (next: SetStateAction<string>) => {
|
|
74
|
-
const prev = useChatInputStore.getState().snapshot.draft;
|
|
75
|
-
const value = this.resolveUpdateValue(prev, next);
|
|
76
|
-
if (value === prev) {
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
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);
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
setPendingSessionType = (next: SetStateAction<string>) => {
|
|
92
|
-
const prev = useChatInputStore.getState().snapshot.pendingSessionType;
|
|
93
|
-
const value = this.resolveUpdateValue(prev, next);
|
|
94
|
-
if (value === prev) {
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
useChatInputStore.getState().setSnapshot({ pendingSessionType: value });
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
send = async () => {
|
|
101
|
-
const inputSnapshot = useChatInputStore.getState().snapshot;
|
|
102
|
-
const sessionSnapshot = useChatSessionListStore.getState().snapshot;
|
|
103
|
-
const message = inputSnapshot.draft.trim();
|
|
104
|
-
if (!message) {
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
const { selectedSkills: requestedSkills, composerNodes } = inputSnapshot;
|
|
108
|
-
const hasSelectedSession = Boolean(sessionSnapshot.selectedSessionKey);
|
|
109
|
-
const sessionKey = sessionSnapshot.selectedSessionKey ?? buildNewSessionKey(sessionSnapshot.selectedAgentId);
|
|
110
|
-
if (!hasSelectedSession) {
|
|
111
|
-
this.uiManager.goToSession(sessionKey, { replace: true });
|
|
112
|
-
}
|
|
113
|
-
this.setComposerNodes(createInitialChatComposerNodes());
|
|
114
|
-
await this.streamActionsManager.sendMessage({
|
|
115
|
-
message,
|
|
116
|
-
sessionKey,
|
|
117
|
-
agentId: sessionSnapshot.selectedAgentId,
|
|
118
|
-
sessionType: inputSnapshot.selectedSessionType,
|
|
119
|
-
model: inputSnapshot.selectedModel || undefined,
|
|
120
|
-
thinkingLevel: inputSnapshot.selectedThinkingLevel ?? undefined,
|
|
121
|
-
stopSupported: inputSnapshot.stopSupported,
|
|
122
|
-
stopReason: inputSnapshot.stopReason,
|
|
123
|
-
requestedSkills,
|
|
124
|
-
restoreDraftOnError: true,
|
|
125
|
-
composerNodes
|
|
126
|
-
});
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
stop = async () => {
|
|
130
|
-
await this.streamActionsManager.stopCurrentRun();
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
goToProviders = () => {
|
|
134
|
-
this.uiManager.goToProviders();
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
setSelectedModel = (next: SetStateAction<string>) => {
|
|
138
|
-
const prev = useChatInputStore.getState().snapshot.selectedModel;
|
|
139
|
-
const value = this.resolveUpdateValue(prev, next);
|
|
140
|
-
if (value === prev) {
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
useChatInputStore.getState().setSnapshot({ selectedModel: value });
|
|
144
|
-
this.reconcileThinkingForModel(value);
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
setSelectedThinkingLevel = (next: SetStateAction<ThinkingLevel | null>) => {
|
|
148
|
-
const prev = useChatInputStore.getState().snapshot.selectedThinkingLevel;
|
|
149
|
-
const value = this.resolveUpdateValue(prev, next);
|
|
150
|
-
if (value === prev) {
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
useChatInputStore.getState().setSnapshot({ selectedThinkingLevel: value });
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
selectSessionType = (value: string) => {
|
|
157
|
-
const normalized = normalizeSessionType(value);
|
|
158
|
-
useChatInputStore.getState().setSnapshot({ selectedSessionType: normalized, pendingSessionType: normalized });
|
|
159
|
-
void this.syncRemoteSessionType(normalized);
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
setSelectedSkills = (next: SetStateAction<string[]>) => {
|
|
163
|
-
const snapshot = useChatInputStore.getState().snapshot;
|
|
164
|
-
const { selectedSkills: prev } = snapshot;
|
|
165
|
-
const value = this.resolveUpdateValue(prev, next);
|
|
166
|
-
if (this.isSameStringArray(value, prev)) {
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
this.syncComposerSnapshot(syncComposerSkills(snapshot.composerNodes, value, snapshot.skillRecords));
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
selectModel = (value: string) => {
|
|
173
|
-
this.setSelectedModel(value);
|
|
174
|
-
this.sessionPreferenceSync.syncSelectedSessionPreferences();
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
selectThinkingLevel = (value: ThinkingLevel) => {
|
|
178
|
-
this.setSelectedThinkingLevel(value);
|
|
179
|
-
this.sessionPreferenceSync.syncSelectedSessionPreferences();
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
selectSkills = (next: string[]) => {
|
|
183
|
-
this.setSelectedSkills(next);
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
private resolveThinkingForModel(modelOption: ChatModelOption | undefined, current: ThinkingLevel | null): ThinkingLevel | null {
|
|
187
|
-
const capability = modelOption?.thinkingCapability;
|
|
188
|
-
if (!capability || capability.supported.length === 0) {
|
|
189
|
-
return null;
|
|
190
|
-
}
|
|
191
|
-
if (current === 'off') {
|
|
192
|
-
return 'off';
|
|
193
|
-
}
|
|
194
|
-
if (current && capability.supported.includes(current)) {
|
|
195
|
-
return current;
|
|
196
|
-
}
|
|
197
|
-
if (capability.default && capability.supported.includes(capability.default)) {
|
|
198
|
-
return capability.default;
|
|
199
|
-
}
|
|
200
|
-
return 'off';
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
private reconcileThinkingForModel(model: string): void {
|
|
204
|
-
const snapshot = useChatInputStore.getState().snapshot;
|
|
205
|
-
const modelOption = snapshot.modelOptions.find((option) => option.value === model);
|
|
206
|
-
const { selectedThinkingLevel } = snapshot;
|
|
207
|
-
const nextThinking = this.resolveThinkingForModel(modelOption, selectedThinkingLevel);
|
|
208
|
-
if (nextThinking !== selectedThinkingLevel) {
|
|
209
|
-
useChatInputStore.getState().setSnapshot({ selectedThinkingLevel: nextThinking });
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
private syncRemoteSessionType = async (normalizedType: string) => {
|
|
214
|
-
const sessionSnapshot = useChatSessionListStore.getState().snapshot;
|
|
215
|
-
const { selectedSessionKey } = sessionSnapshot;
|
|
216
|
-
if (!selectedSessionKey) {
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
const selectedSession = sessionSnapshot.sessions.find((session) => session.key === selectedSessionKey);
|
|
220
|
-
if (!selectedSession?.sessionTypeMutable) {
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
if (normalizeSessionType(selectedSession.sessionType) === normalizedType) {
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
await updateSession(selectedSessionKey, { sessionType: normalizedType });
|
|
227
|
-
};
|
|
228
|
-
}
|