@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,75 @@
1
+ import type { SessionPatchUpdate, ThinkingLevel } from '@/api/types';
2
+ import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
3
+ import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
4
+
5
+ type QueuedSessionPreferenceSync = {
6
+ sessionKey: string;
7
+ patch: SessionPatchUpdate;
8
+ };
9
+
10
+ function normalizeOptionalModel(value: string): string | null {
11
+ const normalized = value.trim();
12
+ return normalized.length > 0 ? normalized : null;
13
+ }
14
+
15
+ function normalizeOptionalThinking(value: ThinkingLevel | null): ThinkingLevel | null {
16
+ return value ?? null;
17
+ }
18
+
19
+ export class ChatSessionPreferenceSync {
20
+ private inFlight: Promise<void> | null = null;
21
+ private queued: QueuedSessionPreferenceSync | null = null;
22
+
23
+ constructor(
24
+ private readonly updateSession: (
25
+ sessionKey: string,
26
+ patch: SessionPatchUpdate
27
+ ) => Promise<unknown>
28
+ ) {}
29
+
30
+ syncSelectedSessionPreferences = (): void => {
31
+ const inputSnapshot = useChatInputStore.getState().snapshot;
32
+ const sessionSnapshot = useChatSessionListStore.getState().snapshot;
33
+ const sessionKey = sessionSnapshot.selectedSessionKey;
34
+ if (!sessionKey) {
35
+ return;
36
+ }
37
+
38
+ this.enqueue({
39
+ sessionKey,
40
+ patch: {
41
+ preferredModel: normalizeOptionalModel(inputSnapshot.selectedModel),
42
+ preferredThinking: normalizeOptionalThinking(inputSnapshot.selectedThinkingLevel)
43
+ }
44
+ });
45
+ };
46
+
47
+ private enqueue(next: QueuedSessionPreferenceSync): void {
48
+ this.queued = next;
49
+ if (this.inFlight) {
50
+ return;
51
+ }
52
+ this.startFlush();
53
+ }
54
+
55
+ private startFlush(): void {
56
+ this.inFlight = this.flush()
57
+ .catch((error) => {
58
+ console.error(`Failed to sync chat session preferences: ${String(error)}`);
59
+ })
60
+ .finally(() => {
61
+ this.inFlight = null;
62
+ if (this.queued) {
63
+ this.startFlush();
64
+ }
65
+ });
66
+ }
67
+
68
+ private async flush(): Promise<void> {
69
+ while (this.queued) {
70
+ const current = this.queued;
71
+ this.queued = null;
72
+ await this.updateSession(current.sessionKey, current.patch);
73
+ }
74
+ }
75
+ }
@@ -5,7 +5,6 @@ import {
5
5
  buildModelStateHint,
6
6
  buildModelToolbarSelect,
7
7
  buildSelectedSkillItems,
8
- buildSessionTypeToolbarSelect,
9
8
  buildSkillPickerModel,
10
9
  buildThinkingToolbarSelect,
11
10
  resolveSlashQuery,
@@ -136,16 +135,6 @@ export function ChatInputBarContainer() {
136
135
  canStopGeneration: snapshot.canStopGeneration
137
136
  });
138
137
 
139
- const selectedSessionTypeOption =
140
- snapshot.sessionTypeOptions.find((option) => option.value === snapshot.selectedSessionType) ??
141
- (snapshot.selectedSessionType
142
- ? { value: snapshot.selectedSessionType, label: snapshot.selectedSessionType }
143
- : null);
144
- const shouldShowSessionTypeSelector =
145
- snapshot.canEditSessionType &&
146
- (snapshot.sessionTypeOptions.length > 1 ||
147
- Boolean(snapshot.selectedSessionType && snapshot.selectedSessionType !== 'native'));
148
-
149
138
  const selectedModelOption = modelRecords.find((option) => option.value === snapshot.selectedModel);
150
139
  const selectedModelThinkingCapability = selectedModelOption?.thinkingCapability;
151
140
  const thinkingSupportedLevels = selectedModelThinkingCapability?.supported ?? [];
@@ -156,17 +145,6 @@ export function ChatInputBarContainer() {
156
145
  : snapshot.stopDisabledReason?.trim() || t('chatStopUnavailable');
157
146
 
158
147
  const toolbarSelects = [
159
- buildSessionTypeToolbarSelect({
160
- selectedSessionType: snapshot.selectedSessionType,
161
- selectedSessionTypeOption,
162
- sessionTypeOptions: snapshot.sessionTypeOptions,
163
- onValueChange: presenter.chatInputManager.selectSessionType,
164
- canEditSessionType: snapshot.canEditSessionType,
165
- shouldShow: shouldShowSessionTypeSelector,
166
- texts: {
167
- sessionTypePlaceholder: t('chatSessionTypeLabel')
168
- }
169
- }),
170
148
  buildModelToolbarSelect({
171
149
  modelOptions: modelRecords,
172
150
  selectedModel: snapshot.selectedModel,
@@ -1,9 +1,12 @@
1
- import { useMemo } from 'react';
2
- import { type UiMessage } from '@nextclaw/agent-chat';
3
- import { ChatMessageList } from '@nextclaw/agent-chat-ui';
4
- import { adaptChatMessages, type ChatMessageSource } from '@/components/chat/adapters/chat-message.adapter';
5
- import { useI18n } from '@/components/providers/I18nProvider';
6
- import { formatDateTime, t } from '@/lib/i18n';
1
+ import { useMemo } from "react";
2
+ import { type UiMessage } from "@nextclaw/agent-chat";
3
+ import { ChatMessageList } from "@nextclaw/agent-chat-ui";
4
+ import {
5
+ adaptChatMessages,
6
+ type ChatMessageSource,
7
+ } from "@/components/chat/adapters/chat-message.adapter";
8
+ import { useI18n } from "@/components/providers/I18nProvider";
9
+ import { formatDateTime, t } from "@/lib/i18n";
7
10
 
8
11
  type ChatMessageListContainerProps = {
9
12
  uiMessages: UiMessage[];
@@ -20,11 +23,11 @@ export function ChatMessageListContainer(props: ChatMessageListContainerProps) {
20
23
  role: message.role,
21
24
  meta: {
22
25
  timestamp: message.meta?.timestamp,
23
- status: message.meta?.status
26
+ status: message.meta?.status,
24
27
  },
25
- parts: message.parts as unknown as ChatMessageSource['parts']
28
+ parts: message.parts as unknown as ChatMessageSource["parts"],
26
29
  })),
27
- [props.uiMessages]
30
+ [props.uiMessages],
28
31
  );
29
32
 
30
33
  const messages = useMemo(
@@ -34,33 +37,38 @@ export function ChatMessageListContainer(props: ChatMessageListContainerProps) {
34
37
  formatTimestamp: (value) => formatDateTime(value, language),
35
38
  texts: {
36
39
  roleLabels: {
37
- user: t('chatRoleUser'),
38
- assistant: t('chatRoleAssistant'),
39
- tool: t('chatRoleTool'),
40
- system: t('chatRoleSystem'),
41
- fallback: t('chatRoleMessage')
40
+ user: t("chatRoleUser"),
41
+ assistant: t("chatRoleAssistant"),
42
+ tool: t("chatRoleTool"),
43
+ system: t("chatRoleSystem"),
44
+ fallback: t("chatRoleMessage"),
42
45
  },
43
- reasoningLabel: t('chatReasoning'),
44
- toolCallLabel: t('chatToolCall'),
45
- toolResultLabel: t('chatToolResult'),
46
- toolNoOutputLabel: t('chatToolNoOutput'),
47
- toolOutputLabel: t('chatToolOutput'),
48
- unknownPartLabel: t('chatUnknownPart')
49
- }
46
+ reasoningLabel: t("chatReasoning"),
47
+ toolCallLabel: t("chatToolCall"),
48
+ toolResultLabel: t("chatToolResult"),
49
+ toolNoOutputLabel: t("chatToolNoOutput"),
50
+ toolOutputLabel: t("chatToolOutput"),
51
+ unknownPartLabel: t("chatUnknownPart"),
52
+ },
50
53
  }),
51
- [language, sourceMessages]
54
+ [language, sourceMessages],
52
55
  );
53
56
 
54
57
  return (
55
58
  <ChatMessageList
56
59
  messages={messages}
57
60
  isSending={props.isSending}
58
- hasStreamingDraft={props.uiMessages.some((message) => message.meta?.status === 'streaming')}
61
+ hasAssistantDraft={props.uiMessages.some(
62
+ (message) =>
63
+ message.role === "assistant" &&
64
+ (message.meta?.status === "streaming" ||
65
+ message.meta?.status === "pending"),
66
+ )}
59
67
  className={props.className}
60
68
  texts={{
61
- copyCodeLabel: t('chatCodeCopy'),
62
- copiedCodeLabel: t('chatCodeCopied'),
63
- typingLabel: t('chatTyping')
69
+ copyCodeLabel: t("chatCodeCopy"),
70
+ copiedCodeLabel: t("chatCodeCopied"),
71
+ typingLabel: t("chatTyping"),
64
72
  }}
65
73
  />
66
74
  );
@@ -0,0 +1,252 @@
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 { confirm, ConfirmDialog } = useConfirmDialog();
22
+ const location = useLocation();
23
+ const navigate = useNavigate();
24
+ const { sessionId: routeSessionIdParam } = useParams<{ sessionId?: string }>();
25
+ const threadRef = useRef<HTMLDivElement | null>(null);
26
+ const selectedSessionKeyRef = useRef<string | null>(selectedSessionKey);
27
+ const modelHydratedSessionKeyRef = useRef<string | null>(null);
28
+ const thinkingHydratedSessionKeyRef = useRef<string | null>(null);
29
+ const routeSessionKey = useMemo(
30
+ () => parseSessionKeyFromRoute(routeSessionIdParam),
31
+ [routeSessionIdParam]
32
+ );
33
+ const {
34
+ sessionsQuery,
35
+ installedSkillsQuery,
36
+ chatCapabilitiesQuery,
37
+ historyQuery,
38
+ isProviderStateResolved,
39
+ modelOptions,
40
+ sessions,
41
+ skillRecords,
42
+ selectedSession,
43
+ hydratedSessionModel,
44
+ historyMessages,
45
+ selectedSessionThinkingLevel,
46
+ sessionTypeOptions,
47
+ defaultSessionType,
48
+ selectedSessionType,
49
+ canEditSessionType,
50
+ sessionTypeUnavailable,
51
+ sessionTypeUnavailableMessage
52
+ } = useChatPageData({
53
+ query,
54
+ selectedSessionKey,
55
+ selectedAgentId,
56
+ pendingSessionType,
57
+ setPendingSessionType: presenter.chatInputManager.setPendingSessionType,
58
+ setSelectedModel: presenter.chatInputManager.setSelectedModel
59
+ });
60
+ const {
61
+ uiMessages,
62
+ isSending,
63
+ isAwaitingAssistantOutput,
64
+ canStopCurrentRun,
65
+ stopDisabledReason,
66
+ lastSendError,
67
+ activeBackendRunId,
68
+ sendMessage,
69
+ stopCurrentRun,
70
+ resumeRun,
71
+ resetStreamState,
72
+ applyHistoryMessages
73
+ } = useChatRuntimeController(
74
+ {
75
+ selectedSessionKeyRef,
76
+ setSelectedSessionKey: presenter.chatSessionListManager.setSelectedSessionKey,
77
+ setDraft: presenter.chatInputManager.setDraft,
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
+ const shouldHydrateModelFromSession =
143
+ !isSending &&
144
+ !isAwaitingAssistantOutput &&
145
+ !sessionsQuery.isLoading &&
146
+ isProviderStateResolved &&
147
+ modelOptions.length > 0 &&
148
+ selectedSessionKey !== modelHydratedSessionKeyRef.current;
149
+ const shouldHydrateThinkingFromHistory =
150
+ !isSending &&
151
+ !isAwaitingAssistantOutput &&
152
+ !historyQuery.isLoading &&
153
+ isProviderStateResolved &&
154
+ modelOptions.length > 0 &&
155
+ selectedSessionKey !== thinkingHydratedSessionKeyRef.current;
156
+
157
+ presenter.chatInputManager.syncSnapshot({
158
+ isProviderStateResolved,
159
+ defaultSessionType,
160
+ canStopGeneration: canStopCurrentRun,
161
+ stopDisabledReason,
162
+ stopSupported: chatCapabilitiesQuery.data?.stopSupported ?? false,
163
+ stopReason: chatCapabilitiesQuery.data?.stopReason,
164
+ sendError: lastSendError,
165
+ isSending,
166
+ modelOptions,
167
+ ...(shouldHydrateModelFromSession ? { selectedModel: hydratedSessionModel } : {}),
168
+ sessionTypeOptions,
169
+ selectedSessionType,
170
+ ...(shouldHydrateThinkingFromHistory ? { selectedThinkingLevel: selectedSessionThinkingLevel } : {}),
171
+ canEditSessionType,
172
+ sessionTypeUnavailable,
173
+ skillRecords,
174
+ isSkillsLoading: installedSkillsQuery.isLoading
175
+ });
176
+ if (shouldHydrateModelFromSession) {
177
+ modelHydratedSessionKeyRef.current = selectedSessionKey;
178
+ }
179
+ if (shouldHydrateThinkingFromHistory) {
180
+ thinkingHydratedSessionKeyRef.current = selectedSessionKey;
181
+ }
182
+ if (!selectedSessionKey) {
183
+ modelHydratedSessionKeyRef.current = null;
184
+ thinkingHydratedSessionKeyRef.current = null;
185
+ }
186
+ presenter.chatSessionListManager.syncSnapshot({
187
+ sessions,
188
+ query,
189
+ isLoading: sessionsQuery.isLoading
190
+ });
191
+ presenter.chatRunStatusManager.syncSnapshot({
192
+ sessionRunStatusByKey,
193
+ isLocallyRunning: isSending || Boolean(activeBackendRunId),
194
+ activeBackendRunId
195
+ });
196
+ presenter.chatThreadManager.syncSnapshot({
197
+ isProviderStateResolved,
198
+ modelOptions,
199
+ sessionTypeUnavailable,
200
+ sessionTypeUnavailableMessage,
201
+ sessionTypeLabel: currentSessionTypeLabel,
202
+ selectedSessionKey,
203
+ sessionDisplayName: currentSessionDisplayName,
204
+ canDeleteSession: Boolean(selectedSession),
205
+ threadRef,
206
+ isHistoryLoading: historyQuery.isLoading,
207
+ uiMessages,
208
+ isSending,
209
+ isAwaitingAssistantOutput
210
+ });
211
+ }, [
212
+ activeBackendRunId,
213
+ canEditSessionType,
214
+ canStopCurrentRun,
215
+ currentSessionDisplayName,
216
+ currentSessionTypeLabel,
217
+ chatCapabilitiesQuery.data?.stopReason,
218
+ chatCapabilitiesQuery.data?.stopSupported,
219
+ defaultSessionType,
220
+ historyQuery.isLoading,
221
+ installedSkillsQuery.isLoading,
222
+ isAwaitingAssistantOutput,
223
+ hydratedSessionModel,
224
+ isProviderStateResolved,
225
+ isSending,
226
+ lastSendError,
227
+ modelOptions.length,
228
+ modelOptions,
229
+ presenter,
230
+ query,
231
+ selectedSession,
232
+ selectedSessionKey,
233
+ selectedSessionThinkingLevel,
234
+ selectedSessionType,
235
+ sessionRunStatusByKey,
236
+ sessionTypeOptions,
237
+ sessionTypeUnavailable,
238
+ sessionTypeUnavailableMessage,
239
+ sessions,
240
+ sessionsQuery.isLoading,
241
+ skillRecords,
242
+ stopDisabledReason,
243
+ threadRef,
244
+ uiMessages
245
+ ]);
246
+
247
+ return (
248
+ <ChatPresenterProvider presenter={presenter}>
249
+ <ChatPageLayout view={view} confirmDialog={<ConfirmDialog />} />
250
+ </ChatPresenterProvider>
251
+ );
252
+ }
@@ -5,12 +5,15 @@ import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
5
5
  import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
6
6
  import { normalizeSessionType } from '@/components/chat/useChatSessionTypeState';
7
7
  import type { ChatInputSnapshot } from '@/components/chat/stores/chat-input.store';
8
+ import { ChatSessionPreferenceSync } from '@/components/chat/chat-session-preference-sync';
8
9
  import type { SetStateAction } from 'react';
9
10
  import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
10
11
  import type { ThinkingLevel } from '@/api/types';
11
12
  import type { ChatModelOption } from '@/components/chat/chat-input.types';
12
13
 
13
14
  export class ChatInputManager {
15
+ private readonly sessionPreferenceSync = new ChatSessionPreferenceSync(updateSession);
16
+
14
17
  constructor(
15
18
  private uiManager: ChatUiManager,
16
19
  private streamActionsManager: ChatStreamActionsManager
@@ -139,10 +142,12 @@ export class ChatInputManager {
139
142
 
140
143
  selectModel = (value: string) => {
141
144
  this.setSelectedModel(value);
145
+ this.sessionPreferenceSync.syncSelectedSessionPreferences();
142
146
  };
143
147
 
144
148
  selectThinkingLevel = (value: ThinkingLevel) => {
145
149
  this.setSelectedThinkingLevel(value);
150
+ this.sessionPreferenceSync.syncSelectedSessionPreferences();
146
151
  };
147
152
 
148
153
  selectSkills = (next: string[]) => {
@@ -0,0 +1,39 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { ChatSessionListManager } from '@/components/chat/managers/chat-session-list.manager';
3
+ import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
4
+ import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
5
+
6
+ describe('ChatSessionListManager', () => {
7
+ beforeEach(() => {
8
+ useChatInputStore.setState({
9
+ snapshot: {
10
+ ...useChatInputStore.getState().snapshot,
11
+ defaultSessionType: 'native',
12
+ pendingSessionType: 'native'
13
+ }
14
+ });
15
+ useChatSessionListStore.setState({
16
+ snapshot: {
17
+ ...useChatSessionListStore.getState().snapshot,
18
+ selectedSessionKey: 'session-1'
19
+ }
20
+ });
21
+ });
22
+
23
+ it('applies the requested session type when creating a session', () => {
24
+ const uiManager = {
25
+ goToChatRoot: vi.fn()
26
+ } as unknown as ConstructorParameters<typeof ChatSessionListManager>[0];
27
+ const streamActionsManager = {
28
+ resetStreamState: vi.fn()
29
+ } as unknown as ConstructorParameters<typeof ChatSessionListManager>[1];
30
+
31
+ const manager = new ChatSessionListManager(uiManager, streamActionsManager);
32
+ manager.createSession('codex');
33
+
34
+ expect(streamActionsManager.resetStreamState).toHaveBeenCalledTimes(1);
35
+ expect(uiManager.goToChatRoot).toHaveBeenCalledTimes(1);
36
+ expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
37
+ expect(useChatInputStore.getState().snapshot.pendingSessionType).toBe('codex');
38
+ });
39
+ });
@@ -53,11 +53,17 @@ export class ChatSessionListManager {
53
53
  useChatSessionListStore.getState().setSnapshot({ selectedSessionKey: value });
54
54
  };
55
55
 
56
- createSession = () => {
57
- const defaultSessionType = useChatInputStore.getState().snapshot.defaultSessionType || 'native';
56
+ createSession = (sessionType?: string) => {
57
+ const { snapshot } = useChatInputStore.getState();
58
+ const { defaultSessionType: configuredDefaultSessionType } = snapshot;
59
+ const defaultSessionType = configuredDefaultSessionType || 'native';
60
+ const nextSessionType =
61
+ typeof sessionType === 'string' && sessionType.trim().length > 0
62
+ ? sessionType.trim()
63
+ : defaultSessionType;
58
64
  this.streamActionsManager.resetStreamState();
59
65
  this.setSelectedSessionKey(null);
60
- useChatInputStore.getState().setSnapshot({ pendingSessionType: defaultSessionType });
66
+ useChatInputStore.getState().setSnapshot({ pendingSessionType: nextSessionType });
61
67
  this.uiManager.goToChatRoot();
62
68
  };
63
69