@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,17 +1,21 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
2
  import type * as SharedApi from '@/shared/lib/api';
3
3
  import { ChatSessionListManager } from '@/features/chat/managers/chat-session-list.manager';
4
- import { useChatInputStore } from '@/features/chat/stores/chat-input.store';
5
4
  import { useChatSessionListStore } from '@/features/chat/stores/chat-session-list.store';
6
5
  import { useChatThreadStore } from '@/features/chat/stores/chat-thread.store';
7
6
 
8
7
  const chatSessionListModeStorageKey = 'nextclaw.chat.session-list.mode';
8
+ const persistStorage = new Map<string, unknown>();
9
9
 
10
10
  function createLocalStoragePersistStorage() {
11
11
  return {
12
- getItem: (name: string) => JSON.parse(window.localStorage.getItem(name) ?? 'null'),
13
- setItem: (name: string, value: unknown) => window.localStorage.setItem(name, JSON.stringify(value)),
14
- removeItem: (name: string) => window.localStorage.removeItem(name)
12
+ getItem: (name: string) => persistStorage.get(name) ?? null,
13
+ setItem: (name: string, value: unknown) => {
14
+ persistStorage.set(name, value);
15
+ },
16
+ removeItem: (name: string) => {
17
+ persistStorage.delete(name);
18
+ },
15
19
  };
16
20
  }
17
21
 
@@ -28,19 +32,11 @@ vi.mock('@/shared/lib/api', async (importOriginal) => {
28
32
  });
29
33
 
30
34
  function resetChatSessionListManagerState() {
31
- window.localStorage.clear();
32
- useChatSessionListStore.persist.setOptions({ storage: createLocalStoragePersistStorage() });
35
+ persistStorage.clear();
36
+ useChatSessionListStore.persist.setOptions({ storage: createLocalStoragePersistStorage() as never });
37
+ useChatThreadStore.persist.setOptions({ storage: createLocalStoragePersistStorage() as never });
33
38
  mocks.updateNcpSession.mockReset();
34
39
  mocks.updateNcpSession.mockResolvedValue({});
35
- useChatInputStore.setState({
36
- snapshot: {
37
- ...useChatInputStore.getState().snapshot,
38
- defaultSessionType: 'native',
39
- pendingSessionType: 'native',
40
- pendingProjectRoot: null,
41
- pendingProjectRootSessionKey: null
42
- }
43
- });
44
40
  useChatSessionListStore.setState({
45
41
  optimisticReadAtBySessionKey: {},
46
42
  snapshot: {
@@ -74,21 +70,22 @@ describe('ChatSessionListManager draft and selection flow', () => {
74
70
  goToSession: vi.fn(),
75
71
  isAtChatRoot: vi.fn(() => true),
76
72
  } as unknown as ConstructorParameters<typeof ChatSessionListManager>[0];
77
- const chatRunManager = {
78
- clearRunState: vi.fn()
79
- } as unknown as ConstructorParameters<typeof ChatSessionListManager>[1];
80
73
 
81
- const manager = new ChatSessionListManager(uiManager, chatRunManager);
74
+ const manager = new ChatSessionListManager(uiManager);
82
75
  manager.createSession('codex');
83
76
 
84
- expect(chatRunManager.clearRunState).toHaveBeenCalledTimes(1);
85
- expect(uiManager.navigateTo).toHaveBeenCalledWith('/chat/draft');
77
+ expect(uiManager.navigateTo).toHaveBeenCalledWith('/chat/draft', {
78
+ replace: true,
79
+ state: {
80
+ chatDraft: {
81
+ sessionType: 'codex',
82
+ projectRoot: null,
83
+ },
84
+ },
85
+ });
86
86
  expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
87
87
  expect(useChatThreadStore.getState().snapshot.sessionKey).toBeNull();
88
88
  expect(useChatThreadStore.getState().snapshot.hasSubmittedDraftMessage).toBe(false);
89
- expect(useChatInputStore.getState().snapshot.pendingSessionType).toBe('codex');
90
- expect(useChatInputStore.getState().snapshot.pendingProjectRoot).toBeNull();
91
- expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBeNull();
92
89
  });
93
90
 
94
91
  it('starts an agent draft chat through one owner state transition', () => {
@@ -98,22 +95,23 @@ describe('ChatSessionListManager draft and selection flow', () => {
98
95
  goToSession: vi.fn(),
99
96
  isAtChatRoot: vi.fn(() => true),
100
97
  } as unknown as ConstructorParameters<typeof ChatSessionListManager>[0];
101
- const chatRunManager = {
102
- clearRunState: vi.fn()
103
- } as unknown as ConstructorParameters<typeof ChatSessionListManager>[1];
104
98
 
105
- const manager = new ChatSessionListManager(uiManager, chatRunManager);
99
+ const manager = new ChatSessionListManager(uiManager);
106
100
  manager.startAgentDraftChat('researcher', 'codex');
107
101
 
108
- expect(chatRunManager.clearRunState).toHaveBeenCalledTimes(1);
109
- expect(uiManager.navigateTo).toHaveBeenCalledWith('/chat/draft');
102
+ expect(uiManager.navigateTo).toHaveBeenCalledWith('/chat/draft', {
103
+ replace: true,
104
+ state: {
105
+ chatDraft: {
106
+ sessionType: 'codex',
107
+ projectRoot: null,
108
+ },
109
+ },
110
+ });
110
111
  expect(useChatSessionListStore.getState().snapshot.selectedAgentId).toBe('researcher');
111
112
  expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
112
113
  expect(useChatThreadStore.getState().snapshot.sessionKey).toBeNull();
113
114
  expect(useChatThreadStore.getState().snapshot.hasSubmittedDraftMessage).toBe(false);
114
- expect(useChatInputStore.getState().snapshot.pendingSessionType).toBe('codex');
115
- expect(useChatInputStore.getState().snapshot.pendingProjectRoot).toBeNull();
116
- expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBeNull();
117
115
  });
118
116
 
119
117
  it('hydrates the draft project root when creating a session inside a project group', () => {
@@ -123,43 +121,19 @@ describe('ChatSessionListManager draft and selection flow', () => {
123
121
  goToSession: vi.fn(),
124
122
  isAtChatRoot: vi.fn(() => true),
125
123
  } as unknown as ConstructorParameters<typeof ChatSessionListManager>[0];
126
- const chatRunManager = {
127
- clearRunState: vi.fn()
128
- } as unknown as ConstructorParameters<typeof ChatSessionListManager>[1];
129
124
 
130
- const manager = new ChatSessionListManager(uiManager, chatRunManager);
125
+ const manager = new ChatSessionListManager(uiManager);
131
126
  manager.createSession('native', '/tmp/project-alpha');
132
127
 
133
- expect(useChatInputStore.getState().snapshot.pendingProjectRoot).toBe('/tmp/project-alpha');
134
- expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBeNull();
135
- });
136
-
137
- it('keeps the root draft key empty when send flow has no concrete session yet', () => {
138
- useChatSessionListStore.setState({
139
- snapshot: {
140
- ...useChatSessionListStore.getState().snapshot,
141
- selectedSessionKey: null
142
- }
128
+ expect(uiManager.navigateTo).toHaveBeenCalledWith('/chat/draft', {
129
+ replace: true,
130
+ state: {
131
+ chatDraft: {
132
+ sessionType: 'native',
133
+ projectRoot: '/tmp/project-alpha',
134
+ },
135
+ },
143
136
  });
144
- const uiManager = {
145
- goToChatRoot: vi.fn(),
146
- navigateTo: vi.fn(),
147
- goToSession: vi.fn(),
148
- isAtChatRoot: vi.fn(() => true),
149
- } as unknown as ConstructorParameters<typeof ChatSessionListManager>[0];
150
- const chatRunManager = {
151
- clearRunState: vi.fn()
152
- } as unknown as ConstructorParameters<typeof ChatSessionListManager>[1];
153
-
154
- const manager = new ChatSessionListManager(uiManager, chatRunManager);
155
- const sessionKey = manager.ensureDraftSession('native');
156
-
157
- expect(sessionKey).toBeNull();
158
- expect(uiManager.goToChatRoot).not.toHaveBeenCalled();
159
- expect(uiManager.navigateTo).not.toHaveBeenCalled();
160
- expect(uiManager.goToSession).not.toHaveBeenCalled();
161
- expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
162
- expect(useChatThreadStore.getState().snapshot.hasSubmittedDraftMessage).toBe(true);
163
137
  });
164
138
 
165
139
  it('does not eagerly replace the old selected session before the route finishes switching', () => {
@@ -169,15 +143,12 @@ describe('ChatSessionListManager draft and selection flow', () => {
169
143
  goToSession: vi.fn(),
170
144
  isAtChatRoot: vi.fn(() => true),
171
145
  } as unknown as ConstructorParameters<typeof ChatSessionListManager>[0];
172
- const chatRunManager = {
173
- clearRunState: vi.fn()
174
- } as unknown as ConstructorParameters<typeof ChatSessionListManager>[1];
175
146
 
176
- const manager = new ChatSessionListManager(uiManager, chatRunManager);
147
+ const manager = new ChatSessionListManager(uiManager);
177
148
  manager.createSession('native', '/tmp/project-alpha');
178
149
 
179
150
  expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
180
- expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBeNull();
151
+ expect(uiManager.navigateTo).toHaveBeenCalledWith('/chat/draft', expect.any(Object));
181
152
  });
182
153
 
183
154
  it('delegates existing-session selection to routing while preserving workspace panel state', () => {
@@ -187,11 +158,8 @@ describe('ChatSessionListManager draft and selection flow', () => {
187
158
  goToSession: vi.fn(),
188
159
  isAtChatRoot: vi.fn(() => true),
189
160
  } as unknown as ConstructorParameters<typeof ChatSessionListManager>[0];
190
- const chatRunManager = {
191
- clearRunState: vi.fn()
192
- } as unknown as ConstructorParameters<typeof ChatSessionListManager>[1];
193
161
 
194
- const manager = new ChatSessionListManager(uiManager, chatRunManager);
162
+ const manager = new ChatSessionListManager(uiManager);
195
163
  manager.selectSession('session-2');
196
164
 
197
165
  expect(uiManager.goToSession).toHaveBeenCalledWith('session-2');
@@ -208,14 +176,13 @@ describe('ChatSessionListManager list preference and read state', () => {
208
176
 
209
177
  it('updates the sidebar list mode without touching other session list state', () => {
210
178
  const uiManager = {} as ConstructorParameters<typeof ChatSessionListManager>[0];
211
- const chatRunManager = {} as ConstructorParameters<typeof ChatSessionListManager>[1];
212
179
 
213
- const manager = new ChatSessionListManager(uiManager, chatRunManager);
180
+ const manager = new ChatSessionListManager(uiManager);
214
181
  manager.setListMode('project-first');
215
182
 
216
183
  expect(useChatSessionListStore.getState().snapshot.listMode).toBe('project-first');
217
184
  expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBe('session-1');
218
- expect(JSON.parse(window.localStorage.getItem(chatSessionListModeStorageKey) ?? '{}')).toMatchObject({
185
+ expect(persistStorage.get(chatSessionListModeStorageKey)).toMatchObject({
219
186
  state: {
220
187
  snapshot: {
221
188
  listMode: 'project-first'
@@ -226,8 +193,7 @@ describe('ChatSessionListManager list preference and read state', () => {
226
193
 
227
194
  it('marks a session as read through the session list owner boundary', () => {
228
195
  const manager = new ChatSessionListManager(
229
- {} as ConstructorParameters<typeof ChatSessionListManager>[0],
230
- {} as ConstructorParameters<typeof ChatSessionListManager>[1]
196
+ {} as ConstructorParameters<typeof ChatSessionListManager>[0]
231
197
  );
232
198
 
233
199
  manager.markSessionRead('session-2', '2026-04-10T10:00:00.000Z');
@@ -242,8 +208,7 @@ describe('ChatSessionListManager list preference and read state', () => {
242
208
 
243
209
  it('skips persisting read state when the backend already has the same watermark', () => {
244
210
  const manager = new ChatSessionListManager(
245
- {} as ConstructorParameters<typeof ChatSessionListManager>[0],
246
- {} as ConstructorParameters<typeof ChatSessionListManager>[1]
211
+ {} as ConstructorParameters<typeof ChatSessionListManager>[0]
247
212
  );
248
213
 
249
214
  manager.markSessionRead(
@@ -258,8 +223,7 @@ describe('ChatSessionListManager list preference and read state', () => {
258
223
 
259
224
  it('marks a visible workspace child session as read through the session list owner', () => {
260
225
  const manager = new ChatSessionListManager(
261
- {} as ConstructorParameters<typeof ChatSessionListManager>[0],
262
- {} as ConstructorParameters<typeof ChatSessionListManager>[1]
226
+ {} as ConstructorParameters<typeof ChatSessionListManager>[0]
263
227
  );
264
228
 
265
229
  manager.markVisibleWorkspaceChildRead({
@@ -279,8 +243,7 @@ describe('ChatSessionListManager list preference and read state', () => {
279
243
 
280
244
  it('keeps running workspace child sessions unread until they settle', () => {
281
245
  const manager = new ChatSessionListManager(
282
- {} as ConstructorParameters<typeof ChatSessionListManager>[0],
283
- {} as ConstructorParameters<typeof ChatSessionListManager>[1]
246
+ {} as ConstructorParameters<typeof ChatSessionListManager>[0]
284
247
  );
285
248
 
286
249
  manager.markVisibleWorkspaceChildRead({
@@ -298,8 +261,8 @@ describe('ChatSessionListManager list preference and read state', () => {
298
261
 
299
262
  describe('ChatSessionListStore persistence', () => {
300
263
  beforeEach(() => {
301
- window.localStorage.clear();
302
- useChatSessionListStore.persist.setOptions({ storage: createLocalStoragePersistStorage() });
264
+ persistStorage.clear();
265
+ useChatSessionListStore.persist.setOptions({ storage: createLocalStoragePersistStorage() as never });
303
266
  useChatSessionListStore.setState({
304
267
  snapshot: {
305
268
  ...useChatSessionListStore.getState().snapshot,
@@ -322,15 +285,4 @@ describe('ChatSessionListStore persistence', () => {
322
285
  expect(useChatSessionListStore.getState().snapshot.listMode).toBe('time-first');
323
286
  });
324
287
 
325
- it('hydrates the sidebar list mode from persisted preference on store initialization', async () => {
326
- window.localStorage.setItem(
327
- chatSessionListModeStorageKey,
328
- JSON.stringify({ state: { snapshot: { listMode: 'project-first' } }, version: 0 })
329
- );
330
- vi.resetModules();
331
-
332
- const hydratedStoreModule = await import('@/features/chat/stores/chat-session-list.store');
333
-
334
- expect(hydratedStoreModule.useChatSessionListStore.getState().snapshot.listMode).toBe('project-first');
335
- });
336
288
  });
@@ -9,6 +9,19 @@ const { deleteNcpSessionMock, deleteSummaryMock } = vi.hoisted(() => ({
9
9
  deleteNcpSessionMock: vi.fn(async () => ({ deleted: true, sessionId: 'parent-session-1' })),
10
10
  deleteSummaryMock: vi.fn(),
11
11
  }));
12
+ const persistStorage = new Map<string, unknown>();
13
+
14
+ function createPersistStorage() {
15
+ return {
16
+ getItem: (name: string) => persistStorage.get(name) ?? null,
17
+ setItem: (name: string, value: unknown) => {
18
+ persistStorage.set(name, value);
19
+ },
20
+ removeItem: (name: string) => {
21
+ persistStorage.delete(name);
22
+ },
23
+ };
24
+ }
12
25
 
13
26
  vi.mock('@/shared/lib/api', async (importOriginal) => {
14
27
  const actual = await importOriginal<typeof SharedApi>();
@@ -20,6 +33,9 @@ vi.mock('@/shared/lib/api', async (importOriginal) => {
20
33
  });
21
34
 
22
35
  beforeEach(() => {
36
+ persistStorage.clear();
37
+ useChatSessionListStore.persist.setOptions({ storage: createPersistStorage() as never });
38
+ useChatThreadStore.persist.setOptions({ storage: createPersistStorage() as never });
23
39
  useChatSessionListStore.setState({
24
40
  optimisticReadAtBySessionKey: {},
25
41
  snapshot: {
@@ -41,6 +57,7 @@ beforeEach(() => {
41
57
  },
42
58
  ],
43
59
  activeChildSessionKey: null,
60
+ activeSideChatDraft: null,
44
61
  workspaceFileTabs: [],
45
62
  activeWorkspaceFileKey: null,
46
63
  workspaceNavigationHistory: [],
@@ -75,7 +92,6 @@ describe('ChatThreadManager', () => {
75
92
  const manager = new ChatThreadManager(
76
93
  uiManager,
77
94
  {} as ConstructorParameters<typeof ChatThreadManager>[1],
78
- {} as ConstructorParameters<typeof ChatThreadManager>[2],
79
95
  );
80
96
 
81
97
  manager.openChildSessionPanel({
@@ -105,7 +121,6 @@ describe('ChatThreadManager', () => {
105
121
  const manager = new ChatThreadManager(
106
122
  uiManager,
107
123
  {} as ConstructorParameters<typeof ChatThreadManager>[1],
108
- {} as ConstructorParameters<typeof ChatThreadManager>[2],
109
124
  );
110
125
 
111
126
  manager.openChildSessionPanel({
@@ -127,7 +142,6 @@ describe('ChatThreadManager', () => {
127
142
  const manager = new ChatThreadManager(
128
143
  uiManager,
129
144
  {} as ConstructorParameters<typeof ChatThreadManager>[1],
130
- {} as ConstructorParameters<typeof ChatThreadManager>[2],
131
145
  );
132
146
 
133
147
  manager.openSessionCronPanel('parent-session-1');
@@ -141,6 +155,79 @@ describe('ChatThreadManager', () => {
141
155
  expect(uiManager.goToSession).not.toHaveBeenCalled();
142
156
  });
143
157
 
158
+ it('opens a side chat draft beside the selected parent session', () => {
159
+ const uiManager = createUiManager({
160
+ goToSession: vi.fn(),
161
+ });
162
+ const manager = new ChatThreadManager(
163
+ uiManager,
164
+ {} as ConstructorParameters<typeof ChatThreadManager>[1],
165
+ );
166
+
167
+ manager.openSideChatDraft('parent-session-1');
168
+
169
+ const { snapshot } = useChatThreadStore.getState();
170
+ expect(snapshot).toMatchObject({
171
+ workspacePanelParentKey: 'parent-session-1',
172
+ activeWorkspacePanelKind: 'side-chat-draft',
173
+ activeChildSessionKey: null,
174
+ activeWorkspaceFileKey: null,
175
+ activeSideChatDraft: {
176
+ parentSessionKey: 'parent-session-1',
177
+ },
178
+ workspaceNavigationHistoryIndex: 0,
179
+ });
180
+ expect(snapshot.activeSideChatDraft?.draftKey).toEqual(expect.any(String));
181
+ expect(snapshot.workspaceNavigationHistory).toEqual([
182
+ {
183
+ kind: 'side-chat-draft',
184
+ key: snapshot.activeSideChatDraft?.draftKey,
185
+ },
186
+ ]);
187
+ expect(uiManager.goToSession).not.toHaveBeenCalled();
188
+ });
189
+
190
+ it('materializes an active side chat draft into a real child-session tab', () => {
191
+ const manager = new ChatThreadManager(
192
+ createUiManager(),
193
+ {} as ConstructorParameters<typeof ChatThreadManager>[1],
194
+ );
195
+
196
+ manager.openSideChatDraft('parent-session-1');
197
+ const draftKey = useChatThreadStore.getState().snapshot.activeSideChatDraft?.draftKey;
198
+ expect(draftKey).toEqual(expect.any(String));
199
+
200
+ manager.materializeSideChatDraft({
201
+ draftKey: draftKey ?? '',
202
+ sessionKey: 'child-session-side',
203
+ label: 'Side chat',
204
+ agentId: 'main',
205
+ });
206
+
207
+ expect(useChatThreadStore.getState().snapshot).toMatchObject({
208
+ workspacePanelParentKey: 'parent-session-1',
209
+ activeWorkspacePanelKind: 'child-session',
210
+ activeChildSessionKey: 'child-session-side',
211
+ activeSideChatDraft: null,
212
+ activeWorkspaceFileKey: null,
213
+ childSessionTabs: [
214
+ {
215
+ sessionKey: 'child-session-side',
216
+ parentSessionKey: 'parent-session-1',
217
+ label: 'Side chat',
218
+ agentId: 'main',
219
+ },
220
+ {
221
+ sessionKey: 'child-session-1',
222
+ },
223
+ ],
224
+ workspaceNavigationHistory: [
225
+ { kind: 'child-session', key: 'child-session-side' },
226
+ ],
227
+ workspaceNavigationHistoryIndex: 0,
228
+ });
229
+ });
230
+
144
231
  it('routes to the session before opening its cron panel when needed', () => {
145
232
  useChatSessionListStore.setState({
146
233
  snapshot: {
@@ -158,7 +245,6 @@ describe('ChatThreadManager', () => {
158
245
  const manager = new ChatThreadManager(
159
246
  uiManager,
160
247
  {} as ConstructorParameters<typeof ChatThreadManager>[1],
161
- {} as ConstructorParameters<typeof ChatThreadManager>[2],
162
248
  );
163
249
 
164
250
  manager.openSessionCronPanel('parent-session-1');
@@ -177,7 +263,6 @@ describe('ChatThreadManager', () => {
177
263
  const manager = new ChatThreadManager(
178
264
  uiManager,
179
265
  {} as ConstructorParameters<typeof ChatThreadManager>[1],
180
- {} as ConstructorParameters<typeof ChatThreadManager>[2],
181
266
  );
182
267
 
183
268
  manager.openFilePreview({
@@ -221,7 +306,6 @@ describe('ChatThreadManager workspace navigation', () => {
221
306
  const manager = new ChatThreadManager(
222
307
  uiManager,
223
308
  {} as ConstructorParameters<typeof ChatThreadManager>[1],
224
- {} as ConstructorParameters<typeof ChatThreadManager>[2],
225
309
  );
226
310
 
227
311
  manager.openChildSessionPanel({
@@ -278,7 +362,6 @@ describe('ChatThreadManager workspace navigation', () => {
278
362
  const manager = new ChatThreadManager(
279
363
  { goToSession: vi.fn() } as unknown as ConstructorParameters<typeof ChatThreadManager>[0],
280
364
  {} as ConstructorParameters<typeof ChatThreadManager>[1],
281
- {} as ConstructorParameters<typeof ChatThreadManager>[2],
282
365
  );
283
366
 
284
367
  manager.openChildSessionPanel({
@@ -304,7 +387,6 @@ describe('ChatThreadManager workspace navigation', () => {
304
387
  const manager = new ChatThreadManager(
305
388
  { goToSession: vi.fn() } as unknown as ConstructorParameters<typeof ChatThreadManager>[0],
306
389
  {} as ConstructorParameters<typeof ChatThreadManager>[1],
307
- {} as ConstructorParameters<typeof ChatThreadManager>[2],
308
390
  );
309
391
 
310
392
  manager.openChildSessionPanel({
@@ -330,6 +412,51 @@ describe('ChatThreadManager workspace navigation', () => {
330
412
  });
331
413
  });
332
414
 
415
+ describe('ChatThreadManager tool actions', () => {
416
+ it('opens child-session tool actions in the workspace panel before child tabs hydrate', async () => {
417
+ useChatThreadStore.getState().setSnapshot({
418
+ childSessionTabs: [],
419
+ activeChildSessionKey: null,
420
+ workspacePanelParentKey: null,
421
+ });
422
+ const uiManager = createUiManager({
423
+ goToSession: vi.fn(),
424
+ });
425
+ const manager = new ChatThreadManager(
426
+ uiManager,
427
+ {} as ConstructorParameters<typeof ChatThreadManager>[1],
428
+ );
429
+
430
+ await manager.handleToolAction({
431
+ kind: 'open-session',
432
+ sessionId: ' child-session-9 ',
433
+ sessionKind: 'child',
434
+ agentId: 'verifier-agent',
435
+ label: 'Verifier',
436
+ parentSessionId: ' parent-session-1 ',
437
+ });
438
+
439
+ expect(useChatThreadStore.getState().snapshot).toMatchObject({
440
+ workspacePanelParentKey: 'parent-session-1',
441
+ activeWorkspacePanelKind: 'child-session',
442
+ activeChildSessionKey: 'child-session-9',
443
+ activeWorkspaceFileKey: null,
444
+ childSessionTabs: [
445
+ {
446
+ sessionKey: 'child-session-9',
447
+ parentSessionKey: 'parent-session-1',
448
+ label: 'Verifier',
449
+ agentId: 'verifier-agent',
450
+ },
451
+ ],
452
+ workspaceNavigationHistory: [
453
+ { kind: 'child-session', key: 'child-session-9' },
454
+ ],
455
+ });
456
+ expect(uiManager.goToSession).not.toHaveBeenCalled();
457
+ });
458
+ });
459
+
333
460
  describe('ChatThreadManager showContent', () => {
334
461
  it('routes tool actions through the thread manager owner', async () => {
335
462
  const uiManager = createUiManager({
@@ -338,7 +465,6 @@ describe('ChatThreadManager showContent', () => {
338
465
  const manager = new ChatThreadManager(
339
466
  uiManager,
340
467
  {} as ConstructorParameters<typeof ChatThreadManager>[1],
341
- {} as ConstructorParameters<typeof ChatThreadManager>[2],
342
468
  );
343
469
 
344
470
  await manager.handleToolAction({
@@ -376,7 +502,6 @@ describe('ChatThreadManager showContent', () => {
376
502
  const manager = new ChatThreadManager(
377
503
  { goToSession: vi.fn() } as unknown as ConstructorParameters<typeof ChatThreadManager>[0],
378
504
  {} as ConstructorParameters<typeof ChatThreadManager>[1],
379
- {} as ConstructorParameters<typeof ChatThreadManager>[2],
380
505
  );
381
506
 
382
507
  await manager.handleToolAction({
@@ -414,7 +539,6 @@ describe('ChatThreadManager showContent', () => {
414
539
  const manager = new ChatThreadManager(
415
540
  uiManager,
416
541
  {} as ConstructorParameters<typeof ChatThreadManager>[1],
417
- {} as ConstructorParameters<typeof ChatThreadManager>[2],
418
542
  );
419
543
 
420
544
  await manager.handleToolAction({
@@ -469,7 +593,6 @@ describe('ChatThreadManager showContent', () => {
469
593
  const manager = new ChatThreadManager(
470
594
  uiManager,
471
595
  {} as ConstructorParameters<typeof ChatThreadManager>[1],
472
- {} as ConstructorParameters<typeof ChatThreadManager>[2],
473
596
  );
474
597
 
475
598
  await manager.handleUiShowContentEvent({
@@ -516,7 +639,6 @@ describe('ChatThreadManager showContent', () => {
516
639
  const manager = new ChatThreadManager(
517
640
  uiManager,
518
641
  {} as ConstructorParameters<typeof ChatThreadManager>[1],
519
- {} as ConstructorParameters<typeof ChatThreadManager>[2],
520
642
  );
521
643
 
522
644
  await manager.handleToolAction({
@@ -552,7 +674,6 @@ describe('ChatThreadManager inline showContent', () => {
552
674
  const manager = new ChatThreadManager(
553
675
  uiManager,
554
676
  {} as ConstructorParameters<typeof ChatThreadManager>[1],
555
- {} as ConstructorParameters<typeof ChatThreadManager>[2],
556
677
  );
557
678
 
558
679
  await manager.handleUiShowContentEvent({
@@ -581,7 +702,6 @@ describe('ChatThreadManager visible workspace selection', () => {
581
702
  const manager = new ChatThreadManager(
582
703
  { goToSession: vi.fn() } as unknown as ConstructorParameters<typeof ChatThreadManager>[0],
583
704
  sessionListManager,
584
- {} as ConstructorParameters<typeof ChatThreadManager>[2],
585
705
  );
586
706
  const tab = {
587
707
  sessionKey: 'child-session-1',
@@ -615,13 +735,9 @@ describe('ChatThreadManager deletion', () => {
615
735
  });
616
736
  }),
617
737
  } as unknown as ConstructorParameters<typeof ChatThreadManager>[1];
618
- const chatRunManager = {
619
- clearRunState: vi.fn(),
620
- } as unknown as ConstructorParameters<typeof ChatThreadManager>[2];
621
738
  const manager = new ChatThreadManager(
622
739
  uiManager,
623
740
  sessionListManager,
624
- chatRunManager,
625
741
  );
626
742
 
627
743
  await manager.deleteSession();
@@ -631,7 +747,6 @@ describe('ChatThreadManager deletion', () => {
631
747
  expect(useChatThreadStore.getState().snapshot).toMatchObject({
632
748
  sessionKey: null,
633
749
  canDeleteSession: false,
634
- messages: [],
635
750
  workspacePanelParentKey: null,
636
751
  childSessionTabs: [],
637
752
  activeChildSessionKey: null,
@@ -640,7 +755,6 @@ describe('ChatThreadManager deletion', () => {
640
755
  workspaceNavigationHistory: [],
641
756
  workspaceNavigationHistoryIndex: 0,
642
757
  });
643
- expect(chatRunManager.clearRunState).toHaveBeenCalledTimes(1);
644
758
  expect(deleteSummaryMock).toHaveBeenCalledWith(appQueryClient, 'parent-session-1');
645
759
  expect(removeQueries).toHaveBeenCalledWith({
646
760
  queryKey: ['ncp-session-messages', 'parent-session-1'],