@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/assets/{ChannelsList-DF2U-LY1.js → ChannelsList-DBcoVJRW.js} +1 -1
  3. package/dist/assets/ChatPage-CD3cxyyM.js +37 -0
  4. package/dist/assets/{DocBrowser-B9ws5JL7.js → DocBrowser-DDX2HMXW.js} +1 -1
  5. package/dist/assets/{LogoBadge-DvGAzkZ3.js → LogoBadge-J53F_3JA.js} +1 -1
  6. package/dist/assets/{MarketplacePage-DG5mHWJ8.js → MarketplacePage-0BZ4bza0.js} +2 -2
  7. package/dist/assets/{ModelConfig-BL_HsOsm.js → ModelConfig-Wzq9wGHV.js} +1 -1
  8. package/dist/assets/{ProvidersList-CH5z00YT.js → ProvidersList-kwzRS8_M.js} +1 -1
  9. package/dist/assets/RuntimeConfig-N771_AM6.js +1 -0
  10. package/dist/assets/{SearchConfig-BhaI0fUf.js → SearchConfig-DVt5QVa_.js} +1 -1
  11. package/dist/assets/{SecretsConfig-CFoimOh9.js → SecretsConfig-CkwauPa8.js} +2 -2
  12. package/dist/assets/SessionsConfig-C3mnHzkZ.js +2 -0
  13. package/dist/assets/{session-run-status-TkIuGbVw.js → chat-message-pxr79GDs.js} +3 -3
  14. package/dist/assets/{index-X5J6Mm--.js → index-BIvFMkN4.js} +1 -1
  15. package/dist/assets/index-CzkY1reu.js +8 -0
  16. package/dist/assets/{index-uMsNsQX6.js → index-GdpEEKnz.js} +1 -1
  17. package/dist/assets/index-RZ0kHHRI.css +1 -0
  18. package/dist/assets/{label-D8ly4a2P.js → label-CmksBHgc.js} +1 -1
  19. package/dist/assets/{page-layout-BSYfvwbp.js → page-layout-Db0GbnhS.js} +1 -1
  20. package/dist/assets/security-config-CjLFME5Q.js +1 -0
  21. package/dist/assets/skeleton-CkpQeVWN.js +1 -0
  22. package/dist/assets/{switch-Ce_g9lpN.js → switch-C24d-UJU.js} +1 -1
  23. package/dist/assets/tabs-custom-D89bh-fc.js +1 -0
  24. package/dist/assets/{useConfirmDialog-A8Ek8Wu7.js → useConfirmDialog-BeP35LcG.js} +2 -2
  25. package/dist/assets/{vendor-B7ozqnFC.js → vendor-psXJBy9u.js} +65 -70
  26. package/dist/index.html +3 -3
  27. package/package.json +5 -2
  28. package/src/api/config.ts +38 -0
  29. package/src/api/types.ts +19 -0
  30. package/src/components/chat/ChatPage.tsx +10 -324
  31. package/src/components/chat/adapters/chat-message.adapter.test.ts +1 -0
  32. package/src/components/chat/chat-chain.test.ts +22 -0
  33. package/src/components/chat/chat-chain.ts +23 -0
  34. package/src/components/chat/chat-page-shell.tsx +103 -0
  35. package/src/components/chat/containers/chat-message-list.container.tsx +5 -1
  36. package/src/components/chat/legacy/LegacyChatPage.tsx +228 -0
  37. package/src/components/chat/ncp/NcpChatPage.tsx +349 -0
  38. package/src/components/chat/ncp/ncp-chat-input.manager.ts +173 -0
  39. package/src/components/chat/ncp/ncp-chat-page-data.ts +134 -0
  40. package/src/components/chat/ncp/ncp-chat-thread.manager.ts +89 -0
  41. package/src/components/chat/ncp/ncp-chat.presenter.ts +33 -0
  42. package/src/components/chat/ncp/ncp-session-adapter.test.ts +49 -0
  43. package/src/components/chat/ncp/ncp-session-adapter.ts +194 -0
  44. package/src/components/chat/presenter/chat-presenter-context.tsx +43 -4
  45. package/src/hooks/useConfig.ts +42 -0
  46. package/src/lib/i18n.ts +1 -1
  47. package/tailwind.config.js +8 -3
  48. package/tsconfig.json +4 -1
  49. package/dist/assets/ChatPage-BX39y0U5.js +0 -36
  50. package/dist/assets/RuntimeConfig-BplBgkwo.js +0 -1
  51. package/dist/assets/SessionsConfig-BHTAYn9T.js +0 -2
  52. package/dist/assets/index-BLeJkJ0o.css +0 -1
  53. package/dist/assets/index-DK4TS5ev.js +0 -8
  54. package/dist/assets/security-config-DlKEYHNN.js +0 -1
  55. package/dist/assets/skeleton-CWbsNx2h.js +0 -1
  56. package/dist/assets/tabs-custom-Cf5azvT5.js +0 -1
@@ -0,0 +1,228 @@
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 { useChatInputStore } from '@/components/chat/stores/chat-input.store';
12
+ import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
13
+
14
+ export function LegacyChatPage({ view }: ChatPageProps) {
15
+ const [presenter] = useState(() => new ChatPresenter());
16
+ const query = useChatSessionListStore((state) => state.snapshot.query);
17
+ const selectedSessionKey = useChatSessionListStore((state) => state.snapshot.selectedSessionKey);
18
+ const selectedAgentId = useChatSessionListStore((state) => state.snapshot.selectedAgentId);
19
+ const pendingSessionType = useChatInputStore((state) => state.snapshot.pendingSessionType);
20
+ const { confirm, ConfirmDialog } = useConfirmDialog();
21
+ const location = useLocation();
22
+ const navigate = useNavigate();
23
+ const { sessionId: routeSessionIdParam } = useParams<{ sessionId?: string }>();
24
+ const threadRef = useRef<HTMLDivElement | null>(null);
25
+ const selectedSessionKeyRef = useRef<string | null>(selectedSessionKey);
26
+ const thinkingHydratedSessionKeyRef = useRef<string | null>(null);
27
+ const routeSessionKey = useMemo(
28
+ () => parseSessionKeyFromRoute(routeSessionIdParam),
29
+ [routeSessionIdParam]
30
+ );
31
+ const {
32
+ sessionsQuery,
33
+ installedSkillsQuery,
34
+ chatCapabilitiesQuery,
35
+ historyQuery,
36
+ isProviderStateResolved,
37
+ modelOptions,
38
+ sessions,
39
+ skillRecords,
40
+ selectedSession,
41
+ historyMessages,
42
+ selectedSessionThinkingLevel,
43
+ sessionTypeOptions,
44
+ defaultSessionType,
45
+ selectedSessionType,
46
+ canEditSessionType,
47
+ sessionTypeUnavailable,
48
+ sessionTypeUnavailableMessage
49
+ } = useChatPageData({
50
+ query,
51
+ selectedSessionKey,
52
+ selectedAgentId,
53
+ pendingSessionType,
54
+ setPendingSessionType: presenter.chatInputManager.setPendingSessionType,
55
+ setSelectedModel: presenter.chatInputManager.setSelectedModel
56
+ });
57
+ const {
58
+ uiMessages,
59
+ isSending,
60
+ isAwaitingAssistantOutput,
61
+ canStopCurrentRun,
62
+ stopDisabledReason,
63
+ lastSendError,
64
+ activeBackendRunId,
65
+ sendMessage,
66
+ stopCurrentRun,
67
+ resumeRun,
68
+ resetStreamState,
69
+ applyHistoryMessages
70
+ } = useChatRuntimeController(
71
+ {
72
+ selectedSessionKeyRef,
73
+ setSelectedSessionKey: presenter.chatSessionListManager.setSelectedSessionKey,
74
+ setDraft: presenter.chatInputManager.setDraft,
75
+ refetchSessions: sessionsQuery.refetch,
76
+ refetchHistory: historyQuery.refetch
77
+ },
78
+ presenter.chatController
79
+ );
80
+
81
+ useEffect(() => {
82
+ presenter.chatStreamActionsManager.bind({
83
+ sendMessage,
84
+ stopCurrentRun,
85
+ resumeRun,
86
+ resetStreamState,
87
+ applyHistoryMessages
88
+ });
89
+ }, [applyHistoryMessages, presenter, resetStreamState, resumeRun, sendMessage, stopCurrentRun]);
90
+
91
+ const { sessionRunStatusByKey } = useSessionRunStatus({
92
+ view,
93
+ selectedSessionKey,
94
+ activeBackendRunId,
95
+ isLocallyRunning: isSending || Boolean(activeBackendRunId),
96
+ resumeRun: presenter.chatStreamActionsManager.resumeRun
97
+ });
98
+
99
+ useChatSessionSync({
100
+ view,
101
+ routeSessionKey,
102
+ selectedSessionKey,
103
+ selectedAgentId,
104
+ setSelectedSessionKey: presenter.chatSessionListManager.setSelectedSessionKey,
105
+ setSelectedAgentId: presenter.chatSessionListManager.setSelectedAgentId,
106
+ selectedSessionKeyRef,
107
+ resetStreamState: presenter.chatStreamActionsManager.resetStreamState,
108
+ resolveAgentIdFromSessionKey
109
+ });
110
+
111
+ useEffect(() => {
112
+ presenter.chatStreamActionsManager.applyHistoryMessages(historyMessages, {
113
+ isLoading: historyQuery.isLoading
114
+ });
115
+ }, [historyMessages, historyQuery.isLoading, presenter]);
116
+
117
+ useEffect(() => {
118
+ presenter.chatUiManager.syncState({
119
+ pathname: location.pathname
120
+ });
121
+ presenter.chatUiManager.bindActions({
122
+ navigate,
123
+ confirm
124
+ });
125
+ }, [confirm, location.pathname, navigate, presenter]);
126
+
127
+ const currentSessionDisplayName = selectedSession ? sessionDisplayName(selectedSession) : undefined;
128
+
129
+ useEffect(() => {
130
+ presenter.chatThreadManager.bindActions({
131
+ refetchSessions: sessionsQuery.refetch
132
+ });
133
+ }, [presenter, sessionsQuery.refetch]);
134
+
135
+ useEffect(() => {
136
+ const shouldHydrateThinkingFromHistory =
137
+ !isSending &&
138
+ !isAwaitingAssistantOutput &&
139
+ !historyQuery.isLoading &&
140
+ selectedSessionKey !== thinkingHydratedSessionKeyRef.current;
141
+
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
+ ...(shouldHydrateThinkingFromHistory ? { selectedThinkingLevel: selectedSessionThinkingLevel } : {}),
155
+ canEditSessionType,
156
+ sessionTypeUnavailable,
157
+ skillRecords,
158
+ isSkillsLoading: installedSkillsQuery.isLoading
159
+ });
160
+ if (shouldHydrateThinkingFromHistory) {
161
+ thinkingHydratedSessionKeyRef.current = selectedSessionKey;
162
+ }
163
+ if (!selectedSessionKey) {
164
+ thinkingHydratedSessionKeyRef.current = null;
165
+ }
166
+ presenter.chatSessionListManager.syncSnapshot({
167
+ sessions,
168
+ query,
169
+ isLoading: sessionsQuery.isLoading
170
+ });
171
+ presenter.chatRunStatusManager.syncSnapshot({
172
+ sessionRunStatusByKey,
173
+ isLocallyRunning: isSending || Boolean(activeBackendRunId),
174
+ activeBackendRunId
175
+ });
176
+ presenter.chatThreadManager.syncSnapshot({
177
+ isProviderStateResolved,
178
+ modelOptions,
179
+ sessionTypeUnavailable,
180
+ sessionTypeUnavailableMessage,
181
+ selectedSessionKey,
182
+ sessionDisplayName: currentSessionDisplayName,
183
+ canDeleteSession: Boolean(selectedSession),
184
+ threadRef,
185
+ isHistoryLoading: historyQuery.isLoading,
186
+ uiMessages,
187
+ isSending,
188
+ isAwaitingAssistantOutput
189
+ });
190
+ }, [
191
+ activeBackendRunId,
192
+ canEditSessionType,
193
+ canStopCurrentRun,
194
+ currentSessionDisplayName,
195
+ chatCapabilitiesQuery.data?.stopReason,
196
+ chatCapabilitiesQuery.data?.stopSupported,
197
+ defaultSessionType,
198
+ historyQuery.isLoading,
199
+ installedSkillsQuery.isLoading,
200
+ isAwaitingAssistantOutput,
201
+ isProviderStateResolved,
202
+ isSending,
203
+ lastSendError,
204
+ modelOptions,
205
+ presenter,
206
+ query,
207
+ selectedSession,
208
+ selectedSessionKey,
209
+ selectedSessionThinkingLevel,
210
+ selectedSessionType,
211
+ sessionRunStatusByKey,
212
+ sessionTypeOptions,
213
+ sessionTypeUnavailable,
214
+ sessionTypeUnavailableMessage,
215
+ sessions,
216
+ sessionsQuery.isLoading,
217
+ skillRecords,
218
+ stopDisabledReason,
219
+ threadRef,
220
+ uiMessages
221
+ ]);
222
+
223
+ return (
224
+ <ChatPresenterProvider presenter={presenter}>
225
+ <ChatPageLayout view={view} confirmDialog={<ConfirmDialog />} />
226
+ </ChatPresenterProvider>
227
+ );
228
+ }
@@ -0,0 +1,349 @@
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/config';
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, 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 { useConfirmDialog } from '@/hooks/useConfirmDialog';
18
+ import { normalizeRequestedSkills } from '@/lib/chat-runtime-utils';
19
+
20
+ function createFetchWithCredentials(): typeof fetch {
21
+ return (input, init) =>
22
+ fetch(input, {
23
+ credentials: 'include',
24
+ ...init
25
+ });
26
+ }
27
+
28
+ function buildNcpSendMetadata(payload: {
29
+ model?: string;
30
+ thinkingLevel?: string;
31
+ sessionType?: string;
32
+ requestedSkills?: string[];
33
+ }): Record<string, unknown> {
34
+ const metadata: Record<string, unknown> = {};
35
+ if (payload.model?.trim()) {
36
+ metadata.model = payload.model.trim();
37
+ metadata.preferred_model = payload.model.trim();
38
+ }
39
+ if (payload.thinkingLevel?.trim()) {
40
+ metadata.thinking = payload.thinkingLevel.trim();
41
+ metadata.preferred_thinking = payload.thinkingLevel.trim();
42
+ }
43
+ if (payload.sessionType?.trim()) {
44
+ metadata.session_type = payload.sessionType.trim();
45
+ }
46
+ const requestedSkills = normalizeRequestedSkills(payload.requestedSkills);
47
+ if (requestedSkills.length > 0) {
48
+ metadata.requested_skills = requestedSkills;
49
+ }
50
+ return metadata;
51
+ }
52
+
53
+ function isMissingNcpSessionError(error: unknown): boolean {
54
+ if (!(error instanceof Error)) {
55
+ return false;
56
+ }
57
+ return error.message.includes('ncp session not found:');
58
+ }
59
+
60
+ export function NcpChatPage({ view }: ChatPageProps) {
61
+ const [presenter] = useState(() => new NcpChatPresenter());
62
+ const [draftSessionId, setDraftSessionId] = useState(() => createNcpSessionId());
63
+ const query = useChatSessionListStore((state) => state.snapshot.query);
64
+ const selectedSessionKey = useChatSessionListStore((state) => state.snapshot.selectedSessionKey);
65
+ const selectedAgentId = useChatSessionListStore((state) => state.snapshot.selectedAgentId);
66
+ const pendingSessionType = useChatInputStore((state) => state.snapshot.pendingSessionType);
67
+ const { confirm, ConfirmDialog } = useConfirmDialog();
68
+ const location = useLocation();
69
+ const navigate = useNavigate();
70
+ const { sessionId: routeSessionIdParam } = useParams<{ sessionId?: string }>();
71
+ const threadRef = useRef<HTMLDivElement | null>(null);
72
+ const selectedSessionKeyRef = useRef<string | null>(selectedSessionKey);
73
+ const thinkingHydratedSessionKeyRef = useRef<string | null>(null);
74
+ const routeSessionKey = useMemo(
75
+ () => parseSessionKeyFromRoute(routeSessionIdParam),
76
+ [routeSessionIdParam]
77
+ );
78
+ const {
79
+ sessionsQuery,
80
+ installedSkillsQuery,
81
+ isProviderStateResolved,
82
+ modelOptions,
83
+ sessionSummaries,
84
+ sessions,
85
+ skillRecords,
86
+ selectedSession,
87
+ selectedSessionThinkingLevel,
88
+ sessionTypeOptions,
89
+ defaultSessionType,
90
+ selectedSessionType,
91
+ canEditSessionType,
92
+ sessionTypeUnavailable,
93
+ sessionTypeUnavailableMessage
94
+ } = useNcpChatPageData({
95
+ query,
96
+ selectedSessionKey,
97
+ pendingSessionType,
98
+ setPendingSessionType: presenter.chatInputManager.setPendingSessionType,
99
+ setSelectedModel: presenter.chatInputManager.setSelectedModel
100
+ });
101
+
102
+ const activeSessionId = selectedSessionKey ?? draftSessionId;
103
+ const sessionSummariesRef = useRef(sessionSummaries);
104
+ useEffect(() => {
105
+ sessionSummariesRef.current = sessionSummaries;
106
+ }, [sessionSummaries]);
107
+
108
+ const [ncpClient] = useState(
109
+ () =>
110
+ new NcpHttpAgentClientEndpoint({
111
+ baseUrl: API_BASE,
112
+ basePath: '/api/ncp/agent',
113
+ fetchImpl: createFetchWithCredentials()
114
+ })
115
+ );
116
+
117
+ const loadSeed = useCallback(async (sessionId: string, signal: AbortSignal): Promise<NcpConversationSeed> => {
118
+ signal.throwIfAborted();
119
+ let history: Awaited<ReturnType<typeof fetchNcpSessionMessages>> | null = null;
120
+ try {
121
+ history = await fetchNcpSessionMessages(sessionId, 300);
122
+ } catch (error) {
123
+ if (!isMissingNcpSessionError(error)) {
124
+ throw error;
125
+ }
126
+ }
127
+ signal.throwIfAborted();
128
+
129
+ const sessionSummary = sessionSummariesRef.current.find((item) => item.sessionId === sessionId) ?? null;
130
+ return {
131
+ messages: history?.messages ?? [],
132
+ status: sessionSummary?.status === 'running' ? 'running' : 'idle'
133
+ };
134
+ }, []);
135
+
136
+ const agent = useHydratedNcpAgent({
137
+ sessionId: activeSessionId,
138
+ client: ncpClient,
139
+ loadSeed
140
+ });
141
+
142
+ useEffect(() => {
143
+ presenter.setDraftSessionId(draftSessionId);
144
+ }, [draftSessionId, presenter]);
145
+
146
+ useEffect(() => {
147
+ if (selectedSessionKey === null) {
148
+ const nextDraftSessionId = createNcpSessionId();
149
+ setDraftSessionId(nextDraftSessionId);
150
+ presenter.setDraftSessionId(nextDraftSessionId);
151
+ }
152
+ }, [presenter, selectedSessionKey]);
153
+
154
+ const uiMessages = useMemo(
155
+ () => adaptNcpMessagesToUiMessages(agent.visibleMessages),
156
+ [agent.visibleMessages]
157
+ );
158
+ const isSending = agent.isSending || agent.isRunning;
159
+ const isAwaitingAssistantOutput = agent.isRunning;
160
+ const canStopCurrentRun = agent.isRunning;
161
+ const stopDisabledReason = agent.isRunning ? null : '__preparing__';
162
+ const lastSendError = agent.hydrateError?.message ?? agent.snapshot.error?.message ?? null;
163
+ const activeBackendRunId = agent.activeRunId;
164
+ const sessionRunStatusByKey = useMemo(() => {
165
+ const map = new Map<string, 'running'>();
166
+ for (const sessionSummary of sessionSummaries) {
167
+ if (sessionSummary.status === 'running') {
168
+ map.set(sessionSummary.sessionId, 'running');
169
+ }
170
+ }
171
+ return map;
172
+ }, [sessionSummaries]);
173
+
174
+ useEffect(() => {
175
+ presenter.chatStreamActionsManager.bind({
176
+ sendMessage: async (payload) => {
177
+ if (payload.sessionKey !== activeSessionId) {
178
+ return;
179
+ }
180
+ const metadata = buildNcpSendMetadata({
181
+ model: payload.model,
182
+ thinkingLevel: payload.thinkingLevel,
183
+ sessionType: payload.sessionType,
184
+ requestedSkills: payload.requestedSkills
185
+ });
186
+ try {
187
+ void sessionsQuery.refetch();
188
+ await agent.send({
189
+ sessionId: payload.sessionKey,
190
+ message: {
191
+ id: `user-${Date.now().toString(36)}`,
192
+ sessionId: payload.sessionKey,
193
+ role: 'user',
194
+ status: 'final',
195
+ parts: [{ type: 'text', text: payload.message }],
196
+ timestamp: new Date().toISOString(),
197
+ ...(Object.keys(metadata).length > 0 ? { metadata } : {})
198
+ },
199
+ ...(Object.keys(metadata).length > 0 ? { metadata } : {})
200
+ });
201
+ await sessionsQuery.refetch();
202
+ } catch (error) {
203
+ if (payload.restoreDraftOnError) {
204
+ presenter.chatInputManager.setDraft((currentDraft) =>
205
+ currentDraft.trim().length === 0 ? payload.message : currentDraft
206
+ );
207
+ }
208
+ throw error;
209
+ }
210
+ },
211
+ stopCurrentRun: async () => {
212
+ await agent.abort();
213
+ await sessionsQuery.refetch();
214
+ },
215
+ resumeRun: async (run: ChatRunView) => {
216
+ if (run.sessionKey !== activeSessionId) {
217
+ return;
218
+ }
219
+ await agent.streamRun();
220
+ },
221
+ resetStreamState: () => {
222
+ selectedSessionKeyRef.current = null;
223
+ },
224
+ applyHistoryMessages: () => {}
225
+ });
226
+ }, [activeSessionId, agent, presenter, sessionsQuery]);
227
+
228
+ useChatSessionSync({
229
+ view,
230
+ routeSessionKey,
231
+ selectedSessionKey,
232
+ selectedAgentId,
233
+ setSelectedSessionKey: presenter.chatSessionListManager.setSelectedSessionKey,
234
+ setSelectedAgentId: presenter.chatSessionListManager.setSelectedAgentId,
235
+ selectedSessionKeyRef,
236
+ resetStreamState: presenter.chatStreamActionsManager.resetStreamState,
237
+ resolveAgentIdFromSessionKey
238
+ });
239
+
240
+ useEffect(() => {
241
+ presenter.chatUiManager.syncState({
242
+ pathname: location.pathname
243
+ });
244
+ presenter.chatUiManager.bindActions({
245
+ navigate,
246
+ confirm
247
+ });
248
+ }, [confirm, location.pathname, navigate, presenter]);
249
+
250
+ const currentSessionDisplayName = selectedSession ? sessionDisplayName(selectedSession) : undefined;
251
+
252
+ useEffect(() => {
253
+ presenter.chatThreadManager.bindActions({
254
+ refetchSessions: sessionsQuery.refetch
255
+ });
256
+ }, [presenter, sessionsQuery.refetch]);
257
+
258
+ useEffect(() => {
259
+ const shouldHydrateThinkingFromSession =
260
+ !isSending &&
261
+ !isAwaitingAssistantOutput &&
262
+ !agent.isHydrating &&
263
+ selectedSessionKey !== thinkingHydratedSessionKeyRef.current;
264
+
265
+ presenter.chatInputManager.syncSnapshot({
266
+ isProviderStateResolved,
267
+ defaultSessionType,
268
+ canStopGeneration: canStopCurrentRun,
269
+ stopDisabledReason,
270
+ stopSupported: true,
271
+ stopReason: undefined,
272
+ sendError: lastSendError,
273
+ isSending,
274
+ modelOptions,
275
+ sessionTypeOptions,
276
+ selectedSessionType,
277
+ ...(shouldHydrateThinkingFromSession ? { selectedThinkingLevel: selectedSessionThinkingLevel } : {}),
278
+ canEditSessionType,
279
+ sessionTypeUnavailable,
280
+ skillRecords,
281
+ isSkillsLoading: installedSkillsQuery.isLoading
282
+ });
283
+ if (shouldHydrateThinkingFromSession) {
284
+ thinkingHydratedSessionKeyRef.current = selectedSessionKey;
285
+ }
286
+ if (!selectedSessionKey) {
287
+ thinkingHydratedSessionKeyRef.current = null;
288
+ }
289
+ presenter.chatSessionListManager.syncSnapshot({
290
+ sessions,
291
+ query,
292
+ isLoading: sessionsQuery.isLoading
293
+ });
294
+ presenter.chatRunStatusManager.syncSnapshot({
295
+ sessionRunStatusByKey,
296
+ isLocallyRunning: isSending || Boolean(activeBackendRunId),
297
+ activeBackendRunId
298
+ });
299
+ presenter.chatThreadManager.syncSnapshot({
300
+ isProviderStateResolved,
301
+ modelOptions,
302
+ sessionTypeUnavailable,
303
+ sessionTypeUnavailableMessage,
304
+ selectedSessionKey,
305
+ sessionDisplayName: currentSessionDisplayName,
306
+ canDeleteSession: Boolean(selectedSession),
307
+ threadRef,
308
+ isHistoryLoading: agent.isHydrating,
309
+ uiMessages,
310
+ isSending,
311
+ isAwaitingAssistantOutput
312
+ });
313
+ }, [
314
+ activeBackendRunId,
315
+ agent.isHydrating,
316
+ canEditSessionType,
317
+ canStopCurrentRun,
318
+ currentSessionDisplayName,
319
+ defaultSessionType,
320
+ installedSkillsQuery.isLoading,
321
+ isAwaitingAssistantOutput,
322
+ isProviderStateResolved,
323
+ isSending,
324
+ lastSendError,
325
+ modelOptions,
326
+ presenter,
327
+ query,
328
+ selectedSession,
329
+ selectedSessionKey,
330
+ selectedSessionThinkingLevel,
331
+ selectedSessionType,
332
+ sessionRunStatusByKey,
333
+ sessionTypeOptions,
334
+ sessionTypeUnavailable,
335
+ sessionTypeUnavailableMessage,
336
+ sessions,
337
+ sessionsQuery.isLoading,
338
+ skillRecords,
339
+ stopDisabledReason,
340
+ threadRef,
341
+ uiMessages
342
+ ]);
343
+
344
+ return (
345
+ <ChatPresenterProvider presenter={presenter}>
346
+ <ChatPageLayout view={view} confirmDialog={<ConfirmDialog />} />
347
+ </ChatPresenterProvider>
348
+ );
349
+ }