@nextclaw/ui 0.14.1 → 0.14.2

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 (165) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/assets/{api-DPcvCLfa.js → api-a14tR_z1.js} +7 -7
  3. package/dist/assets/channels-list-page-CFGkm4DT.js +8 -0
  4. package/dist/assets/chat-page-CEdavJXc.js +105 -0
  5. package/dist/assets/{config-split-page-BCJhqdK9.js → config-split-page-B5b9B0uE.js} +1 -1
  6. package/dist/assets/{confirm-dialog-CWh5FfK2.js → confirm-dialog-ClH_Ec7n.js} +1 -1
  7. package/dist/assets/{desktop-update-config-DsRhBizi.js → desktop-update-config-DPNDnVpk.js} +1 -1
  8. package/dist/assets/{dist-DPrgcEC0.js → dist-BtCS2Ucq.js} +1 -1
  9. package/dist/assets/{dist-DsE4OsSS.js → dist-CLGGLm8b.js} +1 -1
  10. package/dist/assets/doc-browser-5nMWYKBD.js +1 -0
  11. package/dist/assets/doc-browser-BJRLhztv.js +1 -0
  12. package/dist/assets/doc-browser-context-D4JwMMxK.js +1 -0
  13. package/dist/assets/{ellipsis-B81FStLm.js → ellipsis-CtJXjIol.js} +1 -1
  14. package/dist/assets/{external-link-C_dmG-WJ.js → external-link-CdJiG13p.js} +1 -1
  15. package/dist/assets/index-BDN1zzMQ.js +105 -0
  16. package/dist/assets/index-g3uSZXoc.css +1 -0
  17. package/dist/assets/mcp-marketplace-page-BU0kIjhR.js +40 -0
  18. package/dist/assets/mcp-marketplace-page-sMNFSq_T.js +1 -0
  19. package/dist/assets/middleware-CkFT3R4v.js +1 -0
  20. package/dist/assets/model-config-page-D6hYXROd.js +1 -0
  21. package/dist/assets/plus-DPjb0piO.js +1 -0
  22. package/dist/assets/provider-scoped-model-input-DAX2HgmR.js +1 -0
  23. package/dist/assets/providers-config-page-BzAeaACA.js +1 -0
  24. package/dist/assets/{react-sZz5HSCm.js → react-C6cZgP44.js} +1 -1
  25. package/dist/assets/remote-B0G80uPr.js +1 -0
  26. package/dist/assets/rotate-cw-DnEB1Rju.js +1 -0
  27. package/dist/assets/runtime-config-page-C5xKaV3i.js +1 -0
  28. package/dist/assets/{save-MzjoiHri.js → save-DKA0x4zk.js} +1 -1
  29. package/dist/assets/{search-B29dE4GN.js → search-BNlcoj5c.js} +1 -1
  30. package/dist/assets/{search-config-page-Dl5sZzob.js → search-config-page-Cq9uJ-N8.js} +1 -1
  31. package/dist/assets/{secrets-config-page-C_l-IFCK.js → secrets-config-page-CxSbZLBo.js} +2 -2
  32. package/dist/assets/{select-BKmh05hd.js → select-CvTlDZTQ.js} +1 -1
  33. package/dist/assets/{tag-chip-BS_7Ueom.js → tag-chip-8ffF2y2C.js} +1 -1
  34. package/dist/assets/use-config-DmT0o1gv.js +1 -0
  35. package/dist/assets/x-BiSSpgqB.js +1 -0
  36. package/dist/index.html +15 -14
  37. package/package.json +9 -9
  38. package/src/app/components/app-presenter-provider.tsx +11 -2
  39. package/src/app/components/layout/__tests__/app-layout.test.tsx +11 -2
  40. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +110 -79
  41. package/src/app/components/layout/app-layout.tsx +18 -4
  42. package/src/app/components/layout/sidebar-items.tsx +194 -71
  43. package/src/app/components/layout/sidebar-rail.styles.ts +15 -0
  44. package/src/app/components/layout/sidebar.tsx +340 -83
  45. package/src/app/managers/viewport-layout.manager.ts +10 -0
  46. package/src/app/stores/viewport-layout.store.ts +39 -3
  47. package/src/features/agents/components/__tests__/agents-page.test.tsx +62 -39
  48. package/src/features/agents/components/agents-page.tsx +10 -6
  49. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +145 -0
  50. package/src/features/chat/components/conversation/__tests__/chat-conversation-panel.test.tsx +69 -762
  51. package/src/features/chat/components/conversation/__tests__/chat-conversation-workspace-section.test.tsx +132 -0
  52. package/src/features/chat/components/conversation/chat-conversation-content.tsx +22 -15
  53. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +14 -15
  54. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +18 -18
  55. package/src/features/chat/components/conversation/chat-conversation-workspace-section.tsx +10 -4
  56. package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +52 -11
  57. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +16 -14
  58. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +247 -0
  59. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +80 -7
  60. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +82 -25
  61. package/src/features/chat/components/layout/chat-sidebar.tsx +59 -92
  62. package/src/features/chat/components/providers/chat-presenter.provider.tsx +0 -6
  63. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +289 -0
  64. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +427 -0
  65. package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +204 -0
  66. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-attachments.ts +66 -0
  67. package/src/features/chat/features/{input/hooks/use-chat-input-bar-query-state.ts → conversation/hooks/use-session-conversation-input-query.ts} +32 -17
  68. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +281 -0
  69. package/src/features/chat/features/conversation/utils/session-conversation-input-toolbar.utils.ts +158 -0
  70. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +56 -0
  71. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +117 -0
  72. package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +77 -0
  73. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -0
  74. package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +10 -0
  75. package/src/features/chat/features/input/input-surface-plugins/input-surface-search.utils.ts +108 -0
  76. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +108 -0
  77. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +161 -0
  78. package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +22 -1
  79. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +26 -13
  80. package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +7 -28
  81. package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +6 -0
  82. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +26 -51
  83. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +2 -0
  84. package/src/features/chat/features/input/utils/ncp-chat-input-availability.utils.ts +5 -6
  85. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +47 -12
  86. package/src/features/chat/features/message/components/chat-message-list.container.tsx +93 -12
  87. package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +31 -12
  88. package/src/features/chat/features/message/utils/chat-message-markdown-part.utils.ts +38 -0
  89. package/src/features/chat/features/message/utils/chat-message-part.utils.ts +1 -1
  90. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +2 -2
  91. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +2 -2
  92. package/src/features/chat/features/session/hooks/__tests__/use-chat-session-project.test.tsx +3 -30
  93. package/src/features/chat/features/session/hooks/use-chat-session-project.ts +0 -5
  94. package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +8 -0
  95. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-menu.test.tsx +2 -2
  96. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-option-item.test.tsx +2 -0
  97. package/src/features/chat/features/session-type/components/chat-session-type-menu.tsx +2 -2
  98. package/src/features/chat/features/session-type/components/chat-session-type-option-item.tsx +2 -2
  99. package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +49 -31
  100. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +2 -2
  101. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +21 -10
  102. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +2 -2
  103. package/src/features/chat/features/welcome/components/chat-welcome.tsx +2 -2
  104. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +54 -0
  105. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +44 -76
  106. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +5 -1
  107. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +7 -1
  108. package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +40 -31
  109. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +59 -1
  110. package/src/features/chat/features/workspace/utils/chat-thread-workspace-session.utils.ts +109 -0
  111. package/src/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils.ts +44 -4
  112. package/src/features/chat/index.ts +0 -1
  113. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +51 -99
  114. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +135 -21
  115. package/src/features/chat/managers/chat-session-list.manager.ts +12 -43
  116. package/src/features/chat/managers/chat-thread.manager.ts +108 -34
  117. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +48 -1
  118. package/src/features/chat/pages/__tests__/ncp-chat-page.test.tsx +72 -0
  119. package/src/features/chat/pages/ncp-chat-page.tsx +0 -122
  120. package/src/features/chat/presenters/chat.presenter.ts +2 -19
  121. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +31 -0
  122. package/src/features/chat/stores/chat-thread.store.ts +42 -26
  123. package/src/features/panel-apps/hooks/use-panel-apps.ts +2 -1
  124. package/src/platforms/desktop/components/__tests__/desktop-app-shell.test.tsx +50 -10
  125. package/src/platforms/desktop/components/desktop-app-shell.tsx +34 -10
  126. package/src/platforms/desktop/components/desktop-window-chrome.tsx +41 -16
  127. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +24 -1
  128. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +8 -4
  129. package/src/shared/lib/i18n/locales/en-US/chat.json +20 -2
  130. package/src/shared/lib/i18n/locales/en-US/core.json +2 -0
  131. package/src/shared/lib/i18n/locales/zh-CN/chat.json +20 -2
  132. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -0
  133. package/src/shared/lib/ui-document-title/__tests__/ui-document-title.test.ts +49 -0
  134. package/src/shared/lib/ui-document-title/index.ts +50 -21
  135. package/dist/assets/channels-list-page-D6jtziu4.js +0 -8
  136. package/dist/assets/chat-page-BBnNJoKG.js +0 -105
  137. package/dist/assets/doc-browser-C8xOF_9F.js +0 -1
  138. package/dist/assets/doc-browser-DgQedeAn.js +0 -1
  139. package/dist/assets/doc-browser-context-B46tQGmO.js +0 -1
  140. package/dist/assets/index-7o1WdUbU.js +0 -103
  141. package/dist/assets/index-DQWmEWzk.css +0 -1
  142. package/dist/assets/mcp-marketplace-page-CWZN7ohK.js +0 -1
  143. package/dist/assets/mcp-marketplace-page-Dzull_4-.js +0 -40
  144. package/dist/assets/model-config-page-Cv0O3BIj.js +0 -1
  145. package/dist/assets/plus-Bnt56oqG.js +0 -1
  146. package/dist/assets/provider-scoped-model-input-Duuh7i3h.js +0 -1
  147. package/dist/assets/providers-config-page-CPDz0Lxc.js +0 -1
  148. package/dist/assets/remote-B0LMs8u7.js +0 -1
  149. package/dist/assets/rotate-cw-BfyG_KEF.js +0 -1
  150. package/dist/assets/runtime-config-page-q0fvv8ME.js +0 -1
  151. package/dist/assets/use-config-DZkBWIdL.js +0 -1
  152. package/dist/assets/x-CW2KO-Hz.js +0 -1
  153. package/src/features/chat/components/conversation/chat-conversation-alerts.tsx +0 -37
  154. package/src/features/chat/features/input/components/__tests__/chat-attachment-upload-limit.test.ts +0 -38
  155. package/src/features/chat/features/input/components/chat-input-bar.container.tsx +0 -496
  156. package/src/features/chat/features/message/utils/chat-message-inline-content.utils.ts +0 -95
  157. package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +0 -448
  158. package/src/features/chat/managers/__tests__/chat-run-snapshot.manager.test.ts +0 -129
  159. package/src/features/chat/managers/__tests__/chat-run.manager.test.ts +0 -236
  160. package/src/features/chat/managers/__tests__/chat-session-preference-sync.manager.test.ts +0 -219
  161. package/src/features/chat/managers/chat-input.manager.ts +0 -430
  162. package/src/features/chat/managers/chat-run.manager.ts +0 -110
  163. package/src/features/chat/managers/chat-session-preference-sync.manager.ts +0 -142
  164. package/src/features/chat/stores/chat-input.store.ts +0 -104
  165. /package/dist/assets/{config-hints-CTqBnCDp.js → config-hints-BkYBc7z0.js} +0 -0
@@ -1,236 +0,0 @@
1
- import type { NcpMessage } from '@nextclaw/ncp';
2
- import { RUNTIME_DEFAULT_MODEL_VALUE } from '@nextclaw/shared';
3
- import { beforeEach, describe, expect, it, vi } from 'vitest';
4
- import { ChatRunManager } from '@/features/chat/managers/chat-run.manager';
5
- import { useChatInputStore } from '@/features/chat/stores/chat-input.store';
6
- import { useChatThreadStore } from '@/features/chat/stores/chat-thread.store';
7
-
8
- function createChatRunManager() {
9
- const uiManager = {
10
- isAtChatRoot: vi.fn(() => true),
11
- goToSession: vi.fn(),
12
- };
13
- return {
14
- manager: new ChatRunManager(
15
- uiManager as unknown as ConstructorParameters<typeof ChatRunManager>[0],
16
- ),
17
- uiManager,
18
- };
19
- }
20
-
21
- describe('ChatRunManager', () => {
22
- beforeEach(() => {
23
- useChatInputStore.setState({
24
- snapshot: {
25
- ...useChatInputStore.getState().snapshot,
26
- canStopGeneration: false,
27
- sendError: null,
28
- isSending: false,
29
- },
30
- });
31
- useChatThreadStore.setState({
32
- snapshot: {
33
- ...useChatThreadStore.getState().snapshot,
34
- isHistoryLoading: false,
35
- messages: [],
36
- isSending: false,
37
- isAwaitingAssistantOutput: false,
38
- contextWindow: null,
39
- },
40
- });
41
- });
42
-
43
- it('sends an envelope through the active runtime and materializes a root session', async () => {
44
- const { manager, uiManager } = createChatRunManager();
45
- const sendEnvelope = vi.fn(async () => ({
46
- sessionId: 'materialized-session',
47
- userMessageId: 'user-message-1',
48
- assistantMessageId: null,
49
- runId: 'run-1',
50
- }));
51
- manager.setActiveRuntime({
52
- sessionKey: null,
53
- sendEnvelope,
54
- abortCurrentRun: vi.fn(),
55
- resumeCurrentSessionRun: vi.fn(),
56
- });
57
-
58
- await manager.sendMessage({
59
- message: 'hello',
60
- agentId: 'engineer',
61
- sessionType: 'codex',
62
- projectRoot: '/tmp/project-alpha',
63
- requestedSkills: ['project:/tmp/project-alpha/.agents/skills/review'],
64
- });
65
-
66
- expect(sendEnvelope).toHaveBeenCalledWith(
67
- expect.objectContaining({
68
- metadata: expect.objectContaining({
69
- agentId: 'engineer',
70
- session_type: 'codex',
71
- projectRoot: '/tmp/project-alpha',
72
- requested_skill_refs: ['project:/tmp/project-alpha/.agents/skills/review'],
73
- }),
74
- }),
75
- );
76
- expect(uiManager.goToSession).toHaveBeenCalledWith('materialized-session', { replace: true });
77
- });
78
-
79
- it('keeps the runtime-default sentinel in request metadata to override stale session model metadata', async () => {
80
- const { manager } = createChatRunManager();
81
- const sendEnvelope = vi.fn(async () => ({
82
- sessionId: 'session-1',
83
- userMessageId: 'user-message-1',
84
- assistantMessageId: null,
85
- runId: 'run-1',
86
- }));
87
- manager.setActiveRuntime({
88
- sessionKey: 'session-1',
89
- sendEnvelope,
90
- abortCurrentRun: vi.fn(),
91
- resumeCurrentSessionRun: vi.fn(),
92
- });
93
-
94
- await manager.sendMessage({
95
- message: 'hello',
96
- sessionKey: 'session-1',
97
- agentId: 'main',
98
- sessionType: 'codex',
99
- model: RUNTIME_DEFAULT_MODEL_VALUE,
100
- });
101
-
102
- expect(sendEnvelope).toHaveBeenCalledWith(
103
- expect.objectContaining({
104
- metadata: expect.objectContaining({
105
- model: RUNTIME_DEFAULT_MODEL_VALUE,
106
- preferred_model: RUNTIME_DEFAULT_MODEL_VALUE,
107
- }),
108
- }),
109
- );
110
- });
111
-
112
- it('does not send through a runtime for another session', async () => {
113
- const { manager } = createChatRunManager();
114
- const sendEnvelope = vi.fn();
115
- manager.setActiveRuntime({
116
- sessionKey: 'session-1',
117
- sendEnvelope,
118
- abortCurrentRun: vi.fn(),
119
- resumeCurrentSessionRun: vi.fn(),
120
- });
121
-
122
- await manager.sendMessage({
123
- message: 'hello',
124
- sessionKey: 'session-2',
125
- agentId: 'main',
126
- });
127
-
128
- expect(sendEnvelope).not.toHaveBeenCalled();
129
- });
130
-
131
- it('routes stop and resume commands through the active runtime', async () => {
132
- const { manager } = createChatRunManager();
133
- const abortCurrentRun = vi.fn(async () => undefined);
134
- const resumeCurrentSessionRun = vi.fn(async () => undefined);
135
- manager.setActiveRuntime({
136
- sessionKey: 'session-1',
137
- sendEnvelope: vi.fn(),
138
- abortCurrentRun,
139
- resumeCurrentSessionRun,
140
- });
141
-
142
- await manager.stopCurrentRun();
143
- await manager.resumeRun({ sessionKey: 'session-1' });
144
- await manager.resumeRun({ sessionKey: 'session-2' });
145
-
146
- expect(abortCurrentRun).toHaveBeenCalledTimes(1);
147
- expect(resumeCurrentSessionRun).toHaveBeenCalledTimes(1);
148
- });
149
-
150
- it('syncs the run snapshot into chat input and thread state', () => {
151
- const { manager } = createChatRunManager();
152
- const message: NcpMessage = {
153
- id: 'message-1',
154
- sessionId: 'session-1',
155
- role: 'assistant',
156
- status: 'final',
157
- parts: [{ type: 'text', text: 'done' }],
158
- timestamp: '2026-06-10T00:00:00.000Z',
159
- };
160
-
161
- manager.applyRunSnapshot({
162
- routeSessionKey: 'session-1',
163
- isHydrating: true,
164
- isSending: false,
165
- isRunning: true,
166
- visibleMessages: [message],
167
- contextWindow: {
168
- usedContextTokens: 10,
169
- totalContextTokens: 100,
170
- availableContextTokens: 90,
171
- prunedUsedContextTokens: 10,
172
- droppedHistoryCount: 0,
173
- truncatedToolResultCount: 0,
174
- truncatedSystemPrompt: false,
175
- truncatedUserMessage: false,
176
- compacted: false,
177
- compactedUsedContextTokens: 10,
178
- compactedMessageCount: 0,
179
- updatedAt: '2026-06-10T00:00:00.000Z',
180
- },
181
- sendErrorMessage: 'failed',
182
- materializedSessionKey: null,
183
- });
184
-
185
- expect(useChatInputStore.getState().snapshot).toMatchObject({
186
- canStopGeneration: true,
187
- sendError: 'failed',
188
- isSending: true,
189
- });
190
- expect(useChatThreadStore.getState().snapshot).toMatchObject({
191
- isHistoryLoading: true,
192
- messages: [message],
193
- isSending: true,
194
- isAwaitingAssistantOutput: true,
195
- });
196
- });
197
-
198
- it('clears run-owned input and thread state', () => {
199
- const { manager } = createChatRunManager();
200
- useChatInputStore.getState().setSnapshot({
201
- canStopGeneration: true,
202
- sendError: 'failed',
203
- isSending: true,
204
- });
205
- useChatThreadStore.getState().setSnapshot({
206
- isHistoryLoading: true,
207
- messages: [
208
- {
209
- id: 'message-1',
210
- sessionId: 'session-1',
211
- role: 'assistant',
212
- status: 'final',
213
- parts: [{ type: 'text', text: 'done' }],
214
- timestamp: '2026-06-10T00:00:00.000Z',
215
- },
216
- ],
217
- isSending: true,
218
- isAwaitingAssistantOutput: true,
219
- });
220
-
221
- manager.clearRunState();
222
-
223
- expect(useChatInputStore.getState().snapshot).toMatchObject({
224
- canStopGeneration: false,
225
- sendError: null,
226
- isSending: false,
227
- });
228
- expect(useChatThreadStore.getState().snapshot).toMatchObject({
229
- isHistoryLoading: false,
230
- messages: [],
231
- isSending: false,
232
- isAwaitingAssistantOutput: false,
233
- contextWindow: null,
234
- });
235
- });
236
- });
@@ -1,219 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest';
2
- import { RUNTIME_DEFAULT_MODEL_VALUE } from '@nextclaw/shared';
3
- import { updateNcpSession } from '@/shared/lib/api';
4
- import { ChatSessionPreferenceSync } from '@/features/chat/managers/chat-session-preference-sync.manager';
5
- import { useChatInputStore } from '@/features/chat/stores/chat-input.store';
6
- import { useChatSessionListStore } from '@/features/chat/stores/chat-session-list.store';
7
- import { useChatThreadStore } from '@/features/chat/stores/chat-thread.store';
8
- import type * as SharedApiModule from '@/shared/lib/api';
9
-
10
- vi.mock('@/shared/lib/api', async (importOriginal) => {
11
- const actual = await importOriginal<typeof SharedApiModule>();
12
- return {
13
- ...actual,
14
- updateNcpSession: vi.fn(async () => ({
15
- sessionId: 'session-1',
16
- messageCount: 0,
17
- updatedAt: new Date().toISOString(),
18
- status: 'idle',
19
- metadata: {}
20
- }))
21
- };
22
- });
23
-
24
- describe('ChatSessionPreferenceSync', () => {
25
- afterEach(() => {
26
- useChatInputStore.setState((state) => ({
27
- snapshot: {
28
- ...state.snapshot,
29
- modelOptions: [],
30
- selectedModel: '',
31
- selectedThinkingLevel: null
32
- }
33
- }));
34
- useChatSessionListStore.setState((state) => ({
35
- snapshot: {
36
- ...state.snapshot,
37
- selectedSessionKey: null
38
- }
39
- }));
40
- useChatThreadStore.setState((state) => ({
41
- snapshot: {
42
- ...state.snapshot,
43
- canDeleteSession: false
44
- }
45
- }));
46
- vi.clearAllMocks();
47
- });
48
-
49
- it('persists the selected model and thinking to the current session metadata', async () => {
50
- useChatInputStore.setState((state) => ({
51
- snapshot: {
52
- ...state.snapshot,
53
- selectedModel: 'openai/gpt-5',
54
- selectedThinkingLevel: 'high'
55
- }
56
- }));
57
- useChatSessionListStore.setState((state) => ({
58
- snapshot: {
59
- ...state.snapshot,
60
- selectedSessionKey: 'session-1'
61
- }
62
- }));
63
- useChatThreadStore.setState((state) => ({
64
- snapshot: {
65
- ...state.snapshot,
66
- canDeleteSession: true
67
- }
68
- }));
69
-
70
- const sync = new ChatSessionPreferenceSync(updateNcpSession);
71
- sync.syncSelectedSessionPreferences();
72
- await vi.waitFor(() => {
73
- expect(updateNcpSession).toHaveBeenCalledWith('session-1', {
74
- preferredModel: 'openai/gpt-5',
75
- preferredThinking: 'high'
76
- });
77
- });
78
- });
79
-
80
- it('clears the persisted model preference when runtime default is selected', async () => {
81
- useChatInputStore.setState((state) => ({
82
- snapshot: {
83
- ...state.snapshot,
84
- selectedModel: RUNTIME_DEFAULT_MODEL_VALUE,
85
- selectedThinkingLevel: null
86
- }
87
- }));
88
- useChatSessionListStore.setState((state) => ({
89
- snapshot: {
90
- ...state.snapshot,
91
- selectedSessionKey: 'session-1'
92
- }
93
- }));
94
- useChatThreadStore.setState((state) => ({
95
- snapshot: {
96
- ...state.snapshot,
97
- canDeleteSession: true
98
- }
99
- }));
100
-
101
- const sync = new ChatSessionPreferenceSync(updateNcpSession);
102
- sync.syncSelectedSessionPreferences();
103
- await vi.waitFor(() => {
104
- expect(updateNcpSession).toHaveBeenCalledWith('session-1', {
105
- preferredModel: null,
106
- preferredThinking: null
107
- });
108
- });
109
- });
110
-
111
- it('uses the runtime-default thinking capability default when no session thinking is persisted', () => {
112
- useChatInputStore.setState((state) => ({
113
- snapshot: {
114
- ...state.snapshot,
115
- modelOptions: [
116
- {
117
- value: RUNTIME_DEFAULT_MODEL_VALUE,
118
- modelLabel: 'Runtime default',
119
- providerLabel: '',
120
- isRuntimeDefault: true,
121
- thinkingCapability: {
122
- supported: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh'],
123
- default: 'high',
124
- },
125
- },
126
- ],
127
- selectedModel: '',
128
- selectedThinkingLevel: null,
129
- }
130
- }));
131
-
132
- const sync = new ChatSessionPreferenceSync(updateNcpSession);
133
- sync.syncInputSelection({
134
- selectedSessionExists: true,
135
- defaultModel: RUNTIME_DEFAULT_MODEL_VALUE,
136
- selectedSessionPreferredThinking: null,
137
- fallbackPreferredThinking: null,
138
- });
139
-
140
- expect(useChatInputStore.getState().snapshot).toMatchObject({
141
- selectedModel: RUNTIME_DEFAULT_MODEL_VALUE,
142
- selectedThinkingLevel: 'high',
143
- });
144
- });
145
-
146
- it('does not rewrite the input snapshot when the resolved selection is unchanged', () => {
147
- useChatInputStore.setState((state) => ({
148
- snapshot: {
149
- ...state.snapshot,
150
- modelOptions: [
151
- {
152
- value: 'openai/gpt-5',
153
- modelLabel: 'GPT-5',
154
- providerLabel: 'OpenAI',
155
- thinkingCapability: null,
156
- },
157
- ],
158
- selectedModel: 'openai/gpt-5',
159
- selectedThinkingLevel: null,
160
- }
161
- }));
162
- const sync = new ChatSessionPreferenceSync(updateNcpSession);
163
- const listener = vi.fn();
164
- const unsubscribe = useChatInputStore.subscribe(listener);
165
-
166
- sync.syncInputSelection({
167
- selectedSessionExists: true,
168
- selectedSessionPreferredModel: 'openai/gpt-5',
169
- selectedSessionPreferredThinking: null,
170
- });
171
-
172
- unsubscribe();
173
- expect(listener).not.toHaveBeenCalled();
174
- });
175
-
176
- it('applies a historical session preferred model after its summary arrives', () => {
177
- useChatInputStore.setState((state) => ({
178
- snapshot: {
179
- ...state.snapshot,
180
- modelOptions: [
181
- {
182
- value: 'anthropic/claude-sonnet-4',
183
- modelLabel: 'Claude Sonnet 4',
184
- providerLabel: 'Anthropic',
185
- thinkingCapability: null,
186
- },
187
- {
188
- value: 'openai/gpt-5',
189
- modelLabel: 'GPT-5',
190
- providerLabel: 'OpenAI',
191
- thinkingCapability: null,
192
- },
193
- ],
194
- selectedModel: 'anthropic/claude-sonnet-4',
195
- selectedThinkingLevel: null,
196
- }
197
- }));
198
- const sync = new ChatSessionPreferenceSync(updateNcpSession);
199
-
200
- sync.syncInputSelection({
201
- selectedSessionKey: 'session-2',
202
- selectedSessionExists: false,
203
- fallbackPreferredModel: 'anthropic/claude-sonnet-4',
204
- });
205
- expect(useChatInputStore.getState().snapshot.selectedModel).toBe(
206
- 'anthropic/claude-sonnet-4',
207
- );
208
-
209
- sync.syncInputSelection({
210
- selectedSessionKey: 'session-2',
211
- selectedSessionExists: true,
212
- selectedSessionPreferredModel: 'openai/gpt-5',
213
- selectedSessionPreferredThinking: null,
214
- fallbackPreferredModel: 'anthropic/claude-sonnet-4',
215
- });
216
-
217
- expect(useChatInputStore.getState().snapshot.selectedModel).toBe('openai/gpt-5');
218
- });
219
- });