@nextclaw/ui 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/assets/ChannelsList-C7F_As4r.js +1 -0
  3. package/dist/assets/ChatPage-Oo7-OUsx.js +37 -0
  4. package/dist/assets/{DocBrowser-B9ws5JL7.js → DocBrowser-Dsd8Dlq8.js} +1 -1
  5. package/dist/assets/{LogoBadge-DvGAzkZ3.js → LogoBadge-2ChEc_oz.js} +1 -1
  6. package/dist/assets/MarketplacePage-BXck6-X3.js +49 -0
  7. package/dist/assets/{ModelConfig-BL_HsOsm.js → ModelConfig-CgHRSD0b.js} +1 -1
  8. package/dist/assets/ProvidersList-PPfZucvS.js +1 -0
  9. package/dist/assets/RuntimeConfig-ClLEKNTN.js +1 -0
  10. package/dist/assets/{SearchConfig-BhaI0fUf.js → SearchConfig-CuXVCbrf.js} +1 -1
  11. package/dist/assets/{SecretsConfig-CFoimOh9.js → SecretsConfig-udJz6Ake.js} +2 -2
  12. package/dist/assets/SessionsConfig-C1XnFfiC.js +2 -0
  13. package/dist/assets/{session-run-status-TkIuGbVw.js → chat-message-BETwXLD4.js} +3 -3
  14. package/dist/assets/{index-uMsNsQX6.js → index-COJdlL0e.js} +1 -1
  15. package/dist/assets/index-CsvP4CER.js +8 -0
  16. package/dist/assets/index-D-bXl7qL.css +1 -0
  17. package/dist/assets/{label-D8ly4a2P.js → label-BGL-ztxh.js} +1 -1
  18. package/dist/assets/{page-layout-BSYfvwbp.js → page-layout-aw88k7tG.js} +1 -1
  19. package/dist/assets/popover-DyEvzhmV.js +1 -0
  20. package/dist/assets/security-config-BuPAQn82.js +1 -0
  21. package/dist/assets/skeleton-drzO_tdU.js +1 -0
  22. package/dist/assets/{switch-Ce_g9lpN.js → switch-BK8jIzto.js} +1 -1
  23. package/dist/assets/{tabs-custom-Cf5azvT5.js → tabs-custom-Da3cEOji.js} +1 -1
  24. package/dist/assets/{useConfirmDialog-A8Ek8Wu7.js → useConfirmDialog-z0CE92iS.js} +2 -2
  25. package/dist/assets/{vendor-B7ozqnFC.js → vendor-CkJHmX1g.js} +65 -70
  26. package/dist/index.html +3 -3
  27. package/package.json +5 -2
  28. package/src/api/config.ts +9 -0
  29. package/src/api/ncp-session.ts +50 -0
  30. package/src/api/types.ts +20 -0
  31. package/src/components/chat/ChatConversationPanel.test.tsx +65 -0
  32. package/src/components/chat/ChatConversationPanel.tsx +21 -12
  33. package/src/components/chat/ChatPage.tsx +10 -324
  34. package/src/components/chat/ChatSidebar.test.tsx +203 -0
  35. package/src/components/chat/ChatSidebar.tsx +97 -7
  36. package/src/components/chat/adapters/chat-message.adapter.test.ts +132 -81
  37. package/src/components/chat/adapters/chat-message.adapter.ts +27 -9
  38. package/src/components/chat/chat-chain.test.ts +22 -0
  39. package/src/components/chat/chat-chain.ts +23 -0
  40. package/src/components/chat/chat-page-data.ts +30 -1
  41. package/src/components/chat/chat-page-runtime.test.ts +181 -0
  42. package/src/components/chat/chat-page-runtime.ts +101 -15
  43. package/src/components/chat/chat-page-shell.tsx +103 -0
  44. package/src/components/chat/chat-session-preference-sync.test.ts +62 -0
  45. package/src/components/chat/chat-session-preference-sync.ts +75 -0
  46. package/src/components/chat/containers/chat-input-bar.container.tsx +0 -22
  47. package/src/components/chat/containers/chat-message-list.container.tsx +34 -26
  48. package/src/components/chat/legacy/LegacyChatPage.tsx +252 -0
  49. package/src/components/chat/managers/chat-input.manager.ts +5 -0
  50. package/src/components/chat/managers/chat-session-list.manager.test.ts +39 -0
  51. package/src/components/chat/managers/chat-session-list.manager.ts +9 -3
  52. package/src/components/chat/ncp/NcpChatPage.tsx +381 -0
  53. package/src/components/chat/ncp/ncp-chat-input.manager.ts +179 -0
  54. package/src/components/chat/ncp/ncp-chat-page-data.ts +166 -0
  55. package/src/components/chat/ncp/ncp-chat-thread.manager.ts +89 -0
  56. package/src/components/chat/ncp/ncp-chat.presenter.ts +33 -0
  57. package/src/components/chat/ncp/ncp-session-adapter.test.ts +75 -0
  58. package/src/components/chat/ncp/ncp-session-adapter.ts +214 -0
  59. package/src/components/chat/presenter/chat-presenter-context.tsx +43 -4
  60. package/src/components/chat/stores/chat-thread.store.ts +2 -0
  61. package/src/components/chat/useChatSessionTypeState.test.tsx +58 -0
  62. package/src/components/chat/useChatSessionTypeState.ts +25 -8
  63. package/src/hooks/use-ncp-chat-session-types.ts +11 -0
  64. package/src/hooks/useConfig.ts +41 -1
  65. package/src/hooks/useMarketplace.ts +7 -4
  66. package/src/hooks/useWebSocket.ts +23 -2
  67. package/src/lib/i18n.ts +1 -1
  68. package/tailwind.config.js +8 -3
  69. package/tsconfig.json +4 -1
  70. package/dist/assets/ChannelsList-DF2U-LY1.js +0 -1
  71. package/dist/assets/ChatPage-BX39y0U5.js +0 -36
  72. package/dist/assets/MarketplacePage-DG5mHWJ8.js +0 -49
  73. package/dist/assets/ProvidersList-CH5z00YT.js +0 -1
  74. package/dist/assets/RuntimeConfig-BplBgkwo.js +0 -1
  75. package/dist/assets/SessionsConfig-BHTAYn9T.js +0 -2
  76. package/dist/assets/index-BLeJkJ0o.css +0 -1
  77. package/dist/assets/index-DK4TS5ev.js +0 -8
  78. package/dist/assets/index-X5J6Mm--.js +0 -1
  79. package/dist/assets/security-config-DlKEYHNN.js +0 -1
  80. package/dist/assets/skeleton-CWbsNx2h.js +0 -1
@@ -0,0 +1,381 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { NcpHttpAgentClientEndpoint } from '@nextclaw/ncp-http-agent-client';
3
+ import { useHydratedNcpAgent, type NcpConversationSeed } from '@nextclaw/ncp-react';
4
+ import { useLocation, useNavigate, useParams } from 'react-router-dom';
5
+ import { API_BASE } from '@/api/client';
6
+ import { fetchNcpSessionMessages } from '@/api/ncp-session';
7
+ import type { ChatRunView } from '@/api/types';
8
+ import { sessionDisplayName } from '@/components/chat/chat-page-data';
9
+ import { ChatPageLayout, type ChatPageProps, useChatSessionSync } from '@/components/chat/chat-page-shell';
10
+ import { parseSessionKeyFromRoute, resolveAgentIdFromSessionKey } from '@/components/chat/chat-session-route';
11
+ import { useNcpChatPageData } from '@/components/chat/ncp/ncp-chat-page-data';
12
+ import { NcpChatPresenter } from '@/components/chat/ncp/ncp-chat.presenter';
13
+ import { adaptNcpMessagesToUiMessages, buildNcpSessionRunStatusByKey, createNcpSessionId } from '@/components/chat/ncp/ncp-session-adapter';
14
+ import { ChatPresenterProvider } from '@/components/chat/presenter/chat-presenter-context';
15
+ import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
16
+ import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
17
+ import { resolveSessionTypeLabel } from '@/components/chat/useChatSessionTypeState';
18
+ import { useConfirmDialog } from '@/hooks/useConfirmDialog';
19
+ import { normalizeRequestedSkills } from '@/lib/chat-runtime-utils';
20
+
21
+ function createFetchWithCredentials(): typeof fetch {
22
+ return (input, init) =>
23
+ fetch(input, {
24
+ credentials: 'include',
25
+ ...init
26
+ });
27
+ }
28
+
29
+ function buildNcpSendMetadata(payload: {
30
+ model?: string;
31
+ thinkingLevel?: string;
32
+ sessionType?: string;
33
+ requestedSkills?: string[];
34
+ }): Record<string, unknown> {
35
+ const metadata: Record<string, unknown> = {};
36
+ if (payload.model?.trim()) {
37
+ metadata.model = payload.model.trim();
38
+ metadata.preferred_model = payload.model.trim();
39
+ }
40
+ if (payload.thinkingLevel?.trim()) {
41
+ metadata.thinking = payload.thinkingLevel.trim();
42
+ metadata.preferred_thinking = payload.thinkingLevel.trim();
43
+ }
44
+ if (payload.sessionType?.trim()) {
45
+ metadata.session_type = payload.sessionType.trim();
46
+ }
47
+ const requestedSkills = normalizeRequestedSkills(payload.requestedSkills);
48
+ if (requestedSkills.length > 0) {
49
+ metadata.requested_skills = requestedSkills;
50
+ }
51
+ return metadata;
52
+ }
53
+
54
+ function isMissingNcpSessionError(error: unknown): boolean {
55
+ if (!(error instanceof Error)) {
56
+ return false;
57
+ }
58
+ return error.message.includes('ncp session not found:');
59
+ }
60
+
61
+ export function NcpChatPage({ view }: ChatPageProps) {
62
+ const [presenter] = useState(() => new NcpChatPresenter());
63
+ const [draftSessionId, setDraftSessionId] = useState(() => createNcpSessionId());
64
+ const query = useChatSessionListStore((state) => state.snapshot.query);
65
+ const selectedSessionKey = useChatSessionListStore((state) => state.snapshot.selectedSessionKey);
66
+ const selectedAgentId = useChatSessionListStore((state) => state.snapshot.selectedAgentId);
67
+ const pendingSessionType = useChatInputStore((state) => state.snapshot.pendingSessionType);
68
+ const { confirm, ConfirmDialog } = useConfirmDialog();
69
+ const location = useLocation();
70
+ const navigate = useNavigate();
71
+ const { sessionId: routeSessionIdParam } = useParams<{ sessionId?: string }>();
72
+ const threadRef = useRef<HTMLDivElement | null>(null);
73
+ const selectedSessionKeyRef = useRef<string | null>(selectedSessionKey);
74
+ const modelHydratedSessionKeyRef = useRef<string | null>(null);
75
+ const thinkingHydratedSessionKeyRef = useRef<string | null>(null);
76
+ const routeSessionKey = useMemo(
77
+ () => parseSessionKeyFromRoute(routeSessionIdParam),
78
+ [routeSessionIdParam]
79
+ );
80
+ const {
81
+ sessionsQuery,
82
+ installedSkillsQuery,
83
+ isProviderStateResolved,
84
+ modelOptions,
85
+ sessionSummaries,
86
+ sessions,
87
+ skillRecords,
88
+ selectedSession,
89
+ hydratedSessionModel,
90
+ selectedSessionThinkingLevel,
91
+ sessionTypeOptions,
92
+ defaultSessionType,
93
+ selectedSessionType,
94
+ canEditSessionType,
95
+ sessionTypeUnavailable,
96
+ sessionTypeUnavailableMessage
97
+ } = useNcpChatPageData({
98
+ query,
99
+ selectedSessionKey,
100
+ pendingSessionType,
101
+ setPendingSessionType: presenter.chatInputManager.setPendingSessionType,
102
+ setSelectedModel: presenter.chatInputManager.setSelectedModel
103
+ });
104
+ const refetchSessions = sessionsQuery.refetch;
105
+
106
+ const activeSessionId = selectedSessionKey ?? draftSessionId;
107
+ const sessionSummariesRef = useRef(sessionSummaries);
108
+ useEffect(() => {
109
+ sessionSummariesRef.current = sessionSummaries;
110
+ }, [sessionSummaries]);
111
+
112
+ const [ncpClient] = useState(
113
+ () =>
114
+ new NcpHttpAgentClientEndpoint({
115
+ baseUrl: API_BASE,
116
+ basePath: '/api/ncp/agent',
117
+ fetchImpl: createFetchWithCredentials()
118
+ })
119
+ );
120
+
121
+ const loadSeed = useCallback(async (sessionId: string, signal: AbortSignal): Promise<NcpConversationSeed> => {
122
+ signal.throwIfAborted();
123
+ let history: Awaited<ReturnType<typeof fetchNcpSessionMessages>> | null = null;
124
+ try {
125
+ history = await fetchNcpSessionMessages(sessionId, 300);
126
+ } catch (error) {
127
+ if (!isMissingNcpSessionError(error)) {
128
+ throw error;
129
+ }
130
+ }
131
+ signal.throwIfAborted();
132
+
133
+ const sessionSummary = sessionSummariesRef.current.find((item) => item.sessionId === sessionId) ?? null;
134
+ return {
135
+ messages: history?.messages ?? [],
136
+ status: sessionSummary?.status === 'running' ? 'running' : 'idle'
137
+ };
138
+ }, []);
139
+
140
+ const agent = useHydratedNcpAgent({
141
+ sessionId: activeSessionId,
142
+ client: ncpClient,
143
+ loadSeed
144
+ });
145
+
146
+ useEffect(() => {
147
+ presenter.setDraftSessionId(draftSessionId);
148
+ }, [draftSessionId, presenter]);
149
+
150
+ useEffect(() => {
151
+ if (selectedSessionKey === null) {
152
+ const nextDraftSessionId = createNcpSessionId();
153
+ setDraftSessionId(nextDraftSessionId);
154
+ presenter.setDraftSessionId(nextDraftSessionId);
155
+ }
156
+ }, [presenter, selectedSessionKey]);
157
+
158
+ const uiMessages = useMemo(
159
+ () => adaptNcpMessagesToUiMessages(agent.visibleMessages),
160
+ [agent.visibleMessages]
161
+ );
162
+ const isSending = agent.isSending || agent.isRunning;
163
+ const isAwaitingAssistantOutput = agent.isRunning;
164
+ const canStopCurrentRun = agent.isRunning;
165
+ const stopDisabledReason = agent.isRunning ? null : '__preparing__';
166
+ const lastSendError = agent.hydrateError?.message ?? agent.snapshot.error?.message ?? null;
167
+ const activeBackendRunId = agent.activeRunId;
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;
181
+ }
182
+ void refetchSessions();
183
+ }, [activeBackendRunId, isSending, refetchSessions]);
184
+
185
+ useEffect(() => {
186
+ presenter.chatStreamActionsManager.bind({
187
+ sendMessage: async (payload) => {
188
+ if (payload.sessionKey !== activeSessionId) {
189
+ return;
190
+ }
191
+ const metadata = buildNcpSendMetadata({
192
+ model: payload.model,
193
+ thinkingLevel: payload.thinkingLevel,
194
+ sessionType: payload.sessionType,
195
+ requestedSkills: payload.requestedSkills
196
+ });
197
+ try {
198
+ void sessionsQuery.refetch();
199
+ await agent.send({
200
+ sessionId: payload.sessionKey,
201
+ message: {
202
+ id: `user-${Date.now().toString(36)}`,
203
+ sessionId: payload.sessionKey,
204
+ role: 'user',
205
+ status: 'final',
206
+ parts: [{ type: 'text', text: payload.message }],
207
+ timestamp: new Date().toISOString(),
208
+ ...(Object.keys(metadata).length > 0 ? { metadata } : {})
209
+ },
210
+ ...(Object.keys(metadata).length > 0 ? { metadata } : {})
211
+ });
212
+ await sessionsQuery.refetch();
213
+ } catch (error) {
214
+ if (payload.restoreDraftOnError) {
215
+ presenter.chatInputManager.setDraft((currentDraft) =>
216
+ currentDraft.trim().length === 0 ? payload.message : currentDraft
217
+ );
218
+ }
219
+ throw error;
220
+ }
221
+ },
222
+ stopCurrentRun: async () => {
223
+ await agent.abort();
224
+ await sessionsQuery.refetch();
225
+ },
226
+ resumeRun: async (run: ChatRunView) => {
227
+ if (run.sessionKey !== activeSessionId) {
228
+ return;
229
+ }
230
+ await agent.streamRun();
231
+ },
232
+ resetStreamState: () => {
233
+ selectedSessionKeyRef.current = null;
234
+ },
235
+ applyHistoryMessages: () => {}
236
+ });
237
+ }, [activeSessionId, agent, presenter, sessionsQuery]);
238
+
239
+ useChatSessionSync({
240
+ view,
241
+ routeSessionKey,
242
+ selectedSessionKey,
243
+ selectedAgentId,
244
+ setSelectedSessionKey: presenter.chatSessionListManager.setSelectedSessionKey,
245
+ setSelectedAgentId: presenter.chatSessionListManager.setSelectedAgentId,
246
+ selectedSessionKeyRef,
247
+ resetStreamState: presenter.chatStreamActionsManager.resetStreamState,
248
+ resolveAgentIdFromSessionKey
249
+ });
250
+
251
+ useEffect(() => {
252
+ presenter.chatUiManager.syncState({
253
+ pathname: location.pathname
254
+ });
255
+ presenter.chatUiManager.bindActions({
256
+ navigate,
257
+ confirm
258
+ });
259
+ }, [confirm, location.pathname, navigate, presenter]);
260
+
261
+ const currentSessionDisplayName = selectedSession ? sessionDisplayName(selectedSession) : undefined;
262
+ const currentSessionTypeLabel =
263
+ sessionTypeOptions.find((option) => option.value === selectedSessionType)?.label ??
264
+ resolveSessionTypeLabel(selectedSessionType);
265
+
266
+ useEffect(() => {
267
+ presenter.chatThreadManager.bindActions({
268
+ refetchSessions: sessionsQuery.refetch
269
+ });
270
+ }, [presenter, sessionsQuery.refetch]);
271
+
272
+ useEffect(() => {
273
+ const shouldHydrateModelFromSession =
274
+ !isSending &&
275
+ !isAwaitingAssistantOutput &&
276
+ !sessionsQuery.isLoading &&
277
+ isProviderStateResolved &&
278
+ modelOptions.length > 0 &&
279
+ selectedSessionKey !== modelHydratedSessionKeyRef.current;
280
+ const shouldHydrateThinkingFromSession =
281
+ !isSending &&
282
+ !isAwaitingAssistantOutput &&
283
+ !agent.isHydrating &&
284
+ isProviderStateResolved &&
285
+ modelOptions.length > 0 &&
286
+ selectedSessionKey !== thinkingHydratedSessionKeyRef.current;
287
+
288
+ presenter.chatInputManager.syncSnapshot({
289
+ isProviderStateResolved,
290
+ defaultSessionType,
291
+ canStopGeneration: canStopCurrentRun,
292
+ stopDisabledReason,
293
+ stopSupported: true,
294
+ stopReason: undefined,
295
+ sendError: lastSendError,
296
+ isSending,
297
+ modelOptions,
298
+ ...(shouldHydrateModelFromSession ? { selectedModel: hydratedSessionModel } : {}),
299
+ sessionTypeOptions,
300
+ selectedSessionType,
301
+ ...(shouldHydrateThinkingFromSession ? { selectedThinkingLevel: selectedSessionThinkingLevel } : {}),
302
+ canEditSessionType,
303
+ sessionTypeUnavailable,
304
+ skillRecords,
305
+ isSkillsLoading: installedSkillsQuery.isLoading
306
+ });
307
+ if (shouldHydrateModelFromSession) {
308
+ modelHydratedSessionKeyRef.current = selectedSessionKey;
309
+ }
310
+ if (shouldHydrateThinkingFromSession) {
311
+ thinkingHydratedSessionKeyRef.current = selectedSessionKey;
312
+ }
313
+ if (!selectedSessionKey) {
314
+ modelHydratedSessionKeyRef.current = null;
315
+ thinkingHydratedSessionKeyRef.current = null;
316
+ }
317
+ presenter.chatSessionListManager.syncSnapshot({
318
+ sessions,
319
+ query,
320
+ isLoading: sessionsQuery.isLoading
321
+ });
322
+ presenter.chatRunStatusManager.syncSnapshot({
323
+ sessionRunStatusByKey,
324
+ isLocallyRunning: isSending || Boolean(activeBackendRunId),
325
+ activeBackendRunId
326
+ });
327
+ presenter.chatThreadManager.syncSnapshot({
328
+ isProviderStateResolved,
329
+ modelOptions,
330
+ sessionTypeUnavailable,
331
+ sessionTypeUnavailableMessage,
332
+ sessionTypeLabel: currentSessionTypeLabel,
333
+ selectedSessionKey,
334
+ sessionDisplayName: currentSessionDisplayName,
335
+ canDeleteSession: Boolean(selectedSession),
336
+ threadRef,
337
+ isHistoryLoading: agent.isHydrating,
338
+ uiMessages,
339
+ isSending,
340
+ isAwaitingAssistantOutput
341
+ });
342
+ }, [
343
+ activeBackendRunId,
344
+ agent.isHydrating,
345
+ canEditSessionType,
346
+ canStopCurrentRun,
347
+ currentSessionDisplayName,
348
+ currentSessionTypeLabel,
349
+ defaultSessionType,
350
+ installedSkillsQuery.isLoading,
351
+ isAwaitingAssistantOutput,
352
+ hydratedSessionModel,
353
+ isProviderStateResolved,
354
+ isSending,
355
+ lastSendError,
356
+ modelOptions.length,
357
+ modelOptions,
358
+ presenter,
359
+ query,
360
+ selectedSession,
361
+ selectedSessionKey,
362
+ selectedSessionThinkingLevel,
363
+ selectedSessionType,
364
+ sessionRunStatusByKey,
365
+ sessionTypeOptions,
366
+ sessionTypeUnavailable,
367
+ sessionTypeUnavailableMessage,
368
+ sessions,
369
+ sessionsQuery.isLoading,
370
+ skillRecords,
371
+ stopDisabledReason,
372
+ threadRef,
373
+ uiMessages
374
+ ]);
375
+
376
+ return (
377
+ <ChatPresenterProvider presenter={presenter}>
378
+ <ChatPageLayout view={view} confirmDialog={<ConfirmDialog />} />
379
+ </ChatPresenterProvider>
380
+ );
381
+ }
@@ -0,0 +1,179 @@
1
+ import type { SetStateAction } from 'react';
2
+ import type { ThinkingLevel } from '@/api/types';
3
+ import { updateNcpSession } from '@/api/ncp-session';
4
+ import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
5
+ import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
6
+ import type { ChatInputSnapshot } from '@/components/chat/stores/chat-input.store';
7
+ import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
8
+ import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
9
+ import { ChatSessionPreferenceSync } from '@/components/chat/chat-session-preference-sync';
10
+ import type { ChatModelOption } from '@/components/chat/chat-input.types';
11
+ import { normalizeSessionType } from '@/components/chat/useChatSessionTypeState';
12
+
13
+ export class NcpChatInputManager {
14
+ private readonly sessionPreferenceSync = new ChatSessionPreferenceSync(updateNcpSession);
15
+
16
+ constructor(
17
+ private uiManager: ChatUiManager,
18
+ private streamActionsManager: ChatStreamActionsManager,
19
+ private getDraftSessionId: () => string
20
+ ) {}
21
+
22
+ private hasSnapshotChanges = (patch: Partial<ChatInputSnapshot>): boolean => {
23
+ const current = useChatInputStore.getState().snapshot;
24
+ for (const [key, value] of Object.entries(patch) as Array<[keyof ChatInputSnapshot, ChatInputSnapshot[keyof ChatInputSnapshot]]>) {
25
+ if (!Object.is(current[key], value)) {
26
+ return true;
27
+ }
28
+ }
29
+ return false;
30
+ };
31
+
32
+ private resolveUpdateValue = <T>(prev: T, next: SetStateAction<T>): T => {
33
+ if (typeof next === 'function') {
34
+ return (next as (value: T) => T)(prev);
35
+ }
36
+ return next;
37
+ };
38
+
39
+ syncSnapshot = (patch: Partial<ChatInputSnapshot>) => {
40
+ if (!this.hasSnapshotChanges(patch)) {
41
+ return;
42
+ }
43
+ useChatInputStore.getState().setSnapshot(patch);
44
+ if (
45
+ Object.prototype.hasOwnProperty.call(patch, 'modelOptions') ||
46
+ Object.prototype.hasOwnProperty.call(patch, 'selectedModel') ||
47
+ Object.prototype.hasOwnProperty.call(patch, 'selectedThinkingLevel')
48
+ ) {
49
+ const snapshot = useChatInputStore.getState().snapshot;
50
+ this.reconcileThinkingForModel(snapshot.selectedModel);
51
+ }
52
+ };
53
+
54
+ setDraft = (next: SetStateAction<string>) => {
55
+ const prev = useChatInputStore.getState().snapshot.draft;
56
+ const value = this.resolveUpdateValue(prev, next);
57
+ if (value === prev) {
58
+ return;
59
+ }
60
+ useChatInputStore.getState().setSnapshot({ draft: value });
61
+ };
62
+
63
+ setPendingSessionType = (next: SetStateAction<string>) => {
64
+ const prev = useChatInputStore.getState().snapshot.pendingSessionType;
65
+ const value = this.resolveUpdateValue(prev, next);
66
+ if (value === prev) {
67
+ return;
68
+ }
69
+ useChatInputStore.getState().setSnapshot({ pendingSessionType: value });
70
+ };
71
+
72
+ send = async () => {
73
+ const inputSnapshot = useChatInputStore.getState().snapshot;
74
+ const sessionSnapshot = useChatSessionListStore.getState().snapshot;
75
+ const message = inputSnapshot.draft.trim();
76
+ if (!message) {
77
+ return;
78
+ }
79
+ const requestedSkills = inputSnapshot.selectedSkills;
80
+ const sessionKey = sessionSnapshot.selectedSessionKey ?? this.getDraftSessionId();
81
+ if (!sessionSnapshot.selectedSessionKey) {
82
+ this.uiManager.goToSession(sessionKey, { replace: true });
83
+ }
84
+ this.setDraft('');
85
+ this.setSelectedSkills([]);
86
+ await this.streamActionsManager.sendMessage({
87
+ message,
88
+ sessionKey,
89
+ agentId: sessionSnapshot.selectedAgentId,
90
+ sessionType: inputSnapshot.selectedSessionType,
91
+ model: inputSnapshot.selectedModel || undefined,
92
+ thinkingLevel: inputSnapshot.selectedThinkingLevel ?? undefined,
93
+ stopSupported: true,
94
+ requestedSkills,
95
+ restoreDraftOnError: true
96
+ });
97
+ };
98
+
99
+ stop = async () => {
100
+ await this.streamActionsManager.stopCurrentRun();
101
+ };
102
+
103
+ goToProviders = () => {
104
+ this.uiManager.goToProviders();
105
+ };
106
+
107
+ setSelectedModel = (next: SetStateAction<string>) => {
108
+ const prev = useChatInputStore.getState().snapshot.selectedModel;
109
+ const value = this.resolveUpdateValue(prev, next);
110
+ if (value === prev) {
111
+ return;
112
+ }
113
+ useChatInputStore.getState().setSnapshot({ selectedModel: value });
114
+ this.reconcileThinkingForModel(value);
115
+ };
116
+
117
+ setSelectedThinkingLevel = (next: SetStateAction<ThinkingLevel | null>) => {
118
+ const prev = useChatInputStore.getState().snapshot.selectedThinkingLevel;
119
+ const value = this.resolveUpdateValue(prev, next);
120
+ if (value === prev) {
121
+ return;
122
+ }
123
+ useChatInputStore.getState().setSnapshot({ selectedThinkingLevel: value });
124
+ };
125
+
126
+ selectSessionType = (value: string) => {
127
+ const normalized = normalizeSessionType(value);
128
+ useChatInputStore.getState().setSnapshot({ selectedSessionType: normalized, pendingSessionType: normalized });
129
+ };
130
+
131
+ setSelectedSkills = (next: SetStateAction<string[]>) => {
132
+ const prev = useChatInputStore.getState().snapshot.selectedSkills;
133
+ const value = this.resolveUpdateValue(prev, next);
134
+ if (Object.is(value, prev)) {
135
+ return;
136
+ }
137
+ useChatInputStore.getState().setSnapshot({ selectedSkills: value });
138
+ };
139
+
140
+ selectModel = (value: string) => {
141
+ this.setSelectedModel(value);
142
+ this.sessionPreferenceSync.syncSelectedSessionPreferences();
143
+ };
144
+
145
+ selectThinkingLevel = (value: ThinkingLevel) => {
146
+ this.setSelectedThinkingLevel(value);
147
+ this.sessionPreferenceSync.syncSelectedSessionPreferences();
148
+ };
149
+
150
+ selectSkills = (next: string[]) => {
151
+ this.setSelectedSkills(next);
152
+ };
153
+
154
+ private resolveThinkingForModel(modelOption: ChatModelOption | undefined, current: ThinkingLevel | null): ThinkingLevel | null {
155
+ const capability = modelOption?.thinkingCapability;
156
+ if (!capability || capability.supported.length === 0) {
157
+ return null;
158
+ }
159
+ if (current === 'off') {
160
+ return 'off';
161
+ }
162
+ if (current && capability.supported.includes(current)) {
163
+ return current;
164
+ }
165
+ if (capability.default && capability.supported.includes(capability.default)) {
166
+ return capability.default;
167
+ }
168
+ return 'off';
169
+ }
170
+
171
+ private reconcileThinkingForModel(model: string): void {
172
+ const snapshot = useChatInputStore.getState().snapshot;
173
+ const modelOption = snapshot.modelOptions.find((option) => option.value === model);
174
+ const nextThinking = this.resolveThinkingForModel(modelOption, snapshot.selectedThinkingLevel);
175
+ if (nextThinking !== snapshot.selectedThinkingLevel) {
176
+ useChatInputStore.getState().setSnapshot({ selectedThinkingLevel: nextThinking });
177
+ }
178
+ }
179
+ }