@nextclaw/ui 0.10.5 → 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.
Files changed (89) hide show
  1. package/CHANGELOG.md +23 -2
  2. package/dist/assets/{ChannelsList-Nu7Ig6_-.js → ChannelsList-CVPqrxns.js} +4 -4
  3. package/dist/assets/ChatPage-BO1VUrAY.js +37 -0
  4. package/dist/assets/{DocBrowser-3CfKmJA6.js → DocBrowser-FBwg8iji.js} +1 -1
  5. package/dist/assets/{LogoBadge-DdthDJOp.js → LogoBadge-BCmJfRT8.js} +1 -1
  6. package/dist/assets/MarketplacePage-DWxXUOCx.js +49 -0
  7. package/dist/assets/{McpMarketplacePage-Dg8GSZh6.js → McpMarketplacePage-Bth9X_hu.js} +2 -2
  8. package/dist/assets/{ModelConfig-DyQ6cC92.js → ModelConfig-PkSp_ioc.js} +1 -1
  9. package/dist/assets/ProvidersList-DVDge8wa.js +1 -0
  10. package/dist/assets/RemoteAccessPage-BVkzfEaL.js +1 -0
  11. package/dist/assets/RuntimeConfig-ByJs3khh.js +1 -0
  12. package/dist/assets/{SearchConfig-R1BcCLWO.js → SearchConfig-KZUAqYJN.js} +1 -1
  13. package/dist/assets/{SecretsConfig-D-jZMHeY.js → SecretsConfig-qwB_Y_Ka.js} +2 -2
  14. package/dist/assets/SessionsConfig-CGCl4UTr.js +2 -0
  15. package/dist/assets/index-CrilScMo.css +1 -0
  16. package/dist/assets/{index-BulnQWr6.js → index-D41ntvb7.js} +6 -6
  17. package/dist/assets/{label-C7yzBvzK.js → label-7JEFhkur.js} +1 -1
  18. package/dist/assets/ncp-session-adapter-BOqhkrc-.js +1 -0
  19. package/dist/assets/{page-layout-DF0xpax2.js → page-layout-B7q511TE.js} +1 -1
  20. package/dist/assets/popover-CywJGmPr.js +1 -0
  21. package/dist/assets/security-config-zi2UxN5r.js +1 -0
  22. package/dist/assets/skeleton-qUJZQ03S.js +1 -0
  23. package/dist/assets/{status-dot-B9opOZ22.js → status-dot-BilwNdTT.js} +1 -1
  24. package/dist/assets/{switch-l1P0ev4D.js → switch-BLp2Pno1.js} +1 -1
  25. package/dist/assets/tabs-custom-CgIdQMGC.js +1 -0
  26. package/dist/assets/useConfirmDialog-BitswAkv.js +1 -0
  27. package/dist/assets/{vendor-CNhxtHCf.js → vendor-D_JxmsLV.js} +87 -87
  28. package/dist/index.html +3 -3
  29. package/package.json +4 -4
  30. package/src/App.test.tsx +42 -10
  31. package/src/App.tsx +5 -40
  32. package/src/api/api-base.test.ts +37 -0
  33. package/src/api/api-base.ts +0 -4
  34. package/src/api/config.ts +2 -270
  35. package/src/api/ncp-attachments.ts +12 -12
  36. package/src/api/types.ts +4 -121
  37. package/src/components/chat/ChatPage.tsx +1 -11
  38. package/src/components/chat/ChatSidebar.test.tsx +1 -50
  39. package/src/components/chat/ChatSidebar.tsx +0 -5
  40. package/src/components/chat/README.md +2 -0
  41. package/src/components/chat/adapters/chat-message.adapter.test.ts +39 -0
  42. package/src/components/chat/adapters/chat-message.adapter.ts +56 -0
  43. package/src/components/chat/chat-attachment-upload-limit.test.ts +41 -0
  44. package/src/components/chat/chat-composer-state.test.ts +4 -4
  45. package/src/components/chat/chat-composer-state.ts +1 -1
  46. package/src/components/chat/chat-session-display.ts +9 -0
  47. package/src/components/chat/chat-session-label.service.ts +3 -12
  48. package/src/components/chat/chat-session-preference-sync.test.ts +10 -13
  49. package/src/components/chat/chat-stream/types.ts +4 -57
  50. package/src/components/chat/containers/chat-input-bar.container.tsx +2 -2
  51. package/src/components/chat/ncp/NcpChatPage.tsx +3 -3
  52. package/src/components/chat/ncp/ncp-chat-input.manager.ts +1 -1
  53. package/src/components/chat/useHydratedNcpAgent.test.tsx +77 -0
  54. package/src/components/config/README.md +2 -0
  55. package/src/components/config/SessionsConfig.tsx +152 -132
  56. package/src/hooks/use-auth.test.ts +3 -3
  57. package/src/hooks/use-auth.ts +16 -4
  58. package/src/hooks/use-realtime-query-bridge.ts +0 -24
  59. package/src/hooks/useConfig.ts +10 -137
  60. package/src/lib/session-run-status.ts +1 -63
  61. package/src/vite-env.d.ts +1 -0
  62. package/vite.config.ts +4 -4
  63. package/dist/assets/ChatPage-CBCFSk4e.js +0 -38
  64. package/dist/assets/MarketplacePage-inGGiv1T.js +0 -49
  65. package/dist/assets/ProvidersList-B2T8Lc_i.js +0 -1
  66. package/dist/assets/RemoteAccessPage-C9LxgK-C.js +0 -1
  67. package/dist/assets/RuntimeConfig-Ey4VIqTW.js +0 -1
  68. package/dist/assets/SessionsConfig-Cawoh4_2.js +0 -2
  69. package/dist/assets/chat-message-BbuIK4dQ.js +0 -3
  70. package/dist/assets/index-kaPUhd-8.css +0 -1
  71. package/dist/assets/popover-DjaScZDJ.js +0 -1
  72. package/dist/assets/security-config-Bg2eriNx.js +0 -1
  73. package/dist/assets/skeleton-DycBJAJF.js +0 -1
  74. package/dist/assets/tabs-custom-BG9y2JhC.js +0 -1
  75. package/dist/assets/useConfirmDialog-DTducNfn.js +0 -1
  76. package/src/api/config.stream.test.ts +0 -115
  77. package/src/components/chat/chat-chain.test.ts +0 -22
  78. package/src/components/chat/chat-chain.ts +0 -23
  79. package/src/components/chat/chat-page-data.ts +0 -171
  80. package/src/components/chat/chat-page-runtime.ts +0 -190
  81. package/src/components/chat/chat-stream/nextbot-parsers.ts +0 -52
  82. package/src/components/chat/chat-stream/nextbot-runtime-agent.ts +0 -413
  83. package/src/components/chat/chat-stream/stream-event-adapter.ts +0 -98
  84. package/src/components/chat/chat-stream/transport.ts +0 -253
  85. package/src/components/chat/legacy/LegacyChatPage.tsx +0 -223
  86. package/src/components/chat/managers/chat-input.manager.ts +0 -228
  87. package/src/components/chat/managers/chat-thread.manager.ts +0 -87
  88. package/src/components/chat/presenter/chat.presenter.ts +0 -32
  89. 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
- }