@nextclaw/ui 0.15.3 → 0.15.4

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 (184) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/assets/aiden0z-pptx-renderer.es-Bd99vut2.js +94 -0
  3. package/dist/assets/api-BOXOrpcJ.js +13 -0
  4. package/dist/assets/app-presenter-provider-CkHLbvZQ.js +106 -0
  5. package/dist/assets/app-presenter-provider-D3BuuOyZ.css +1 -0
  6. package/dist/assets/appearance-settings-page-fLIJbx1C.js +1 -0
  7. package/dist/assets/{book-open-C3mJGwiy.js → book-open-DWUjS6aW.js} +1 -1
  8. package/dist/assets/channels-list-page-CvCTJraa.js +8 -0
  9. package/dist/assets/chat-page-B42UUIFR.js +1 -0
  10. package/dist/assets/chunk-DseTPa7n.js +1 -0
  11. package/dist/assets/config-split-page-DBgt9S8b.js +1 -0
  12. package/dist/assets/confirm-dialog-GSXIvoRU.js +5 -0
  13. package/dist/assets/desktop-update-config-7bmNeWIC.js +1 -0
  14. package/dist/assets/dist-DC7f4TJH.js +41 -0
  15. package/dist/assets/doc-browser-CgdoLUT92.js +1 -0
  16. package/dist/assets/doc-browser-D0monHjG.js +1 -0
  17. package/dist/assets/doc-browser-context-CGwEOhBy.js +1 -0
  18. package/dist/assets/docx-preview-eFH9DRsS.js +20 -0
  19. package/dist/assets/download-DiP9zx-J.js +1 -0
  20. package/dist/assets/{external-link-CU0kzzIP.js → external-link-C6ZeSPef.js} +1 -1
  21. package/dist/assets/eye-suvKmtwn.js +1 -0
  22. package/dist/assets/folder-CMELbQGG.js +1 -0
  23. package/dist/assets/{form-actions-BvCp_0lS.js → form-actions-CUAzpNe_.js} +1 -1
  24. package/dist/assets/index-BFeNDQ4R.css +1 -0
  25. package/dist/assets/index-D_6U-M94.js +2 -0
  26. package/dist/assets/jszip.min-Das2IAnb.js +2 -0
  27. package/dist/assets/loader-circle-tYHStefV.js +1 -0
  28. package/dist/assets/logo-badge-Cu5UGH_4.js +1 -0
  29. package/dist/assets/mcp-marketplace-page-BUzHDwW6.js +9 -0
  30. package/dist/assets/mcp-marketplace-page-ohUAdfqJ.js +1 -0
  31. package/dist/assets/model-config-page-CWP6pyBm.js +1 -0
  32. package/dist/assets/plus-tywizkeY.js +1 -0
  33. package/dist/assets/popover-DImguRok.js +1 -0
  34. package/dist/assets/provider-models-C_yOh6DE.js +1 -0
  35. package/dist/assets/provider-scoped-model-input-CE7p4SbJ.js +1 -0
  36. package/dist/assets/providers-config-page-BKPo8SjH.js +1 -0
  37. package/dist/assets/react-5sjAT_6d.js +8 -0
  38. package/dist/assets/{refresh-cw-CILd1vAP.js → refresh-cw-CHLBzWWj.js} +1 -1
  39. package/dist/assets/remote-DXJryUxB.js +1 -0
  40. package/dist/assets/rotate-cw-BefO5brU.js +1 -0
  41. package/dist/assets/runtime-config-page-1Axv4QGu.js +1 -0
  42. package/dist/assets/{save-ByJqcPTP.js → save-BABzyrjB.js} +1 -1
  43. package/dist/assets/{search-DVIV6pfD.js → search-BTqxG3ZG.js} +1 -1
  44. package/dist/assets/search-config-page-BkKKmxmS.js +1 -0
  45. package/dist/assets/secrets-config-page-CWJMyu-k.js +3 -0
  46. package/dist/assets/security-config-DeQUIXfi.js +1 -0
  47. package/dist/assets/select-TYZDBNmq.js +1 -0
  48. package/dist/assets/setting-row-DR23Z_5I.js +1 -0
  49. package/dist/assets/{settings-2-CH5shGg_.js → settings-2-D8WWcBnw.js} +1 -1
  50. package/dist/assets/{skeleton-DeX1FKw7.js → skeleton-CloHeg0S.js} +1 -1
  51. package/dist/assets/tabs-CX-ckWoo.js +1 -0
  52. package/dist/assets/tag-chip-mCc23Rhb.js +1 -0
  53. package/dist/assets/trash-2-SAW1Z4Po.js +1 -0
  54. package/dist/assets/use-config-BucdaK23.js +1 -0
  55. package/dist/assets/use-confirm-dialog-C95F6sT1.js +1 -0
  56. package/dist/assets/use-viewport-layout-C6m3Nte7.js +1 -0
  57. package/dist/assets/x-DtCPHwCu.js +1 -0
  58. package/dist/assets/xlsx-B6zVt1RK.js +64 -0
  59. package/dist/index.html +34 -28
  60. package/package.json +11 -6
  61. package/src/features/chat/components/__tests__/chat-popover-content.test.tsx +41 -0
  62. package/src/features/chat/components/chat-popover-content.tsx +26 -0
  63. package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +13 -0
  64. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +21 -11
  65. package/src/features/chat/components/conversation/__tests__/chat-conversation-header.test.tsx +2 -2
  66. package/src/features/chat/components/conversation/__tests__/chat-conversation-workspace-section.test.tsx +28 -1
  67. package/src/features/chat/components/conversation/chat-conversation-content.tsx +5 -4
  68. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +9 -18
  69. package/src/features/chat/components/conversation/chat-conversation-track.tsx +35 -0
  70. package/src/features/chat/components/conversation/chat-conversation-workspace-section.tsx +1 -0
  71. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +7 -7
  72. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +3 -14
  73. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +20 -1
  74. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +69 -54
  75. package/src/features/chat/features/message/components/chat-message-list.container.tsx +16 -1
  76. package/src/features/chat/features/message/utils/__tests__/chat-message-file-operation.utils.test.ts +30 -0
  77. package/src/features/chat/features/session/components/__tests__/chat-sidebar-session-item.test.tsx +37 -0
  78. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +4 -3
  79. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +3 -8
  80. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +65 -20
  81. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +26 -28
  82. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -3
  83. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +3 -3
  84. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +3 -3
  85. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +3 -3
  86. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +403 -106
  87. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +113 -2
  88. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +1 -0
  89. package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +197 -44
  90. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +9 -3
  91. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +7 -5
  92. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +57 -177
  93. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +239 -1
  94. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +56 -6
  95. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +29 -7
  96. package/src/features/chat/features/workspace/components/file-content-preview/use-workspace-file-buffer.ts +42 -0
  97. package/src/features/chat/features/workspace/components/file-content-preview/workspace-document-preview-state.tsx +18 -0
  98. package/src/features/chat/features/workspace/components/file-content-preview/workspace-docx-preview.css +89 -0
  99. package/src/features/chat/features/workspace/components/file-content-preview/workspace-docx-preview.tsx +87 -0
  100. package/src/features/chat/features/workspace/components/file-content-preview/workspace-presentation-preview.tsx +82 -0
  101. package/src/features/chat/features/workspace/components/file-content-preview/workspace-spreadsheet-preview.tsx +180 -0
  102. package/src/features/chat/features/workspace/components/workspace-file-content-preview.tsx +191 -0
  103. package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +1 -4
  104. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +53 -10
  105. package/src/features/chat/features/workspace/utils/chat-thread-workspace-session.utils.ts +68 -2
  106. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +17 -0
  107. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab.utils.ts +63 -0
  108. package/src/features/chat/features/workspace/utils/chat-workspace-file-viewer.utils.ts +49 -0
  109. package/src/features/chat/features/workspace/utils/chat-workspace-panel-layout.utils.ts +13 -0
  110. package/src/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils.ts +120 -38
  111. package/src/features/chat/index.ts +1 -0
  112. package/src/features/chat/managers/__tests__/chat-thread-workspace.manager.test.ts +76 -0
  113. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +44 -60
  114. package/src/features/chat/managers/chat-thread.manager.ts +123 -126
  115. package/src/features/chat/pages/ncp-chat-page.tsx +1 -2
  116. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +78 -1
  117. package/src/features/chat/stores/chat-message-layout.store.test.ts +37 -0
  118. package/src/features/chat/stores/chat-message-layout.store.ts +41 -0
  119. package/src/features/chat/stores/chat-thread.store.ts +70 -13
  120. package/src/features/settings/pages/__tests__/appearance-settings-page.test.tsx +25 -8
  121. package/src/features/settings/pages/appearance-settings-page.tsx +92 -13
  122. package/src/index.css +36 -2
  123. package/src/shared/components/__tests__/file-type-icon.test.tsx +61 -0
  124. package/src/shared/components/doc-browser/__tests__/doc-browser-context.test.tsx +19 -0
  125. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +16 -0
  126. package/src/shared/components/doc-browser/doc-browser-context.tsx +1 -0
  127. package/src/shared/components/doc-browser/doc-browser.tsx +11 -3
  128. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +5 -0
  129. package/src/shared/components/doc-browser/stores/doc-browser.store.ts +6 -1
  130. package/src/shared/components/doc-browser/types/doc-browser.types.ts +2 -0
  131. package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +11 -0
  132. package/src/shared/components/file-type-icon.tsx +272 -0
  133. package/src/shared/components/resizable-right-panel/__tests__/resizable-right-panel.test.tsx +22 -0
  134. package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +17 -2
  135. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +38 -0
  136. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +82 -2
  137. package/src/shared/lib/api/__tests__/server-path.utils.test.ts +28 -0
  138. package/src/shared/lib/api/utils/server-path.utils.ts +19 -20
  139. package/src/shared/lib/i18n/locales/en-US/chat.json +17 -1
  140. package/src/shared/lib/i18n/locales/en-US/core.json +6 -0
  141. package/src/shared/lib/i18n/locales/zh-CN/chat.json +17 -1
  142. package/src/shared/lib/i18n/locales/zh-CN/core.json +6 -0
  143. package/dist/assets/api-iNBpppzj.js +0 -13
  144. package/dist/assets/appearance-settings-page-DMffQFR3.js +0 -1
  145. package/dist/assets/channels-list-page-CCtp0BRS.js +0 -8
  146. package/dist/assets/chat-page-H5a0dWvO.js +0 -1
  147. package/dist/assets/config-split-page-CrOJgB-Y.js +0 -1
  148. package/dist/assets/confirm-dialog-Chv50USy.js +0 -5
  149. package/dist/assets/desktop-update-config-KzTuXRf_.js +0 -1
  150. package/dist/assets/dist-BunzmC4B.js +0 -1
  151. package/dist/assets/dist-ChmGwvtK.js +0 -1
  152. package/dist/assets/doc-browser-DNRL4Zvd.js +0 -1
  153. package/dist/assets/doc-browser-Dg0pmkOg.js +0 -1
  154. package/dist/assets/doc-browser-context-BHY-0FSD.js +0 -1
  155. package/dist/assets/es2015-DQZ5JWwK.js +0 -41
  156. package/dist/assets/i18n-provider-Cu8XyZb_.js +0 -1
  157. package/dist/assets/index-CzfBdvK1.css +0 -1
  158. package/dist/assets/index-DGqHne1Q.js +0 -104
  159. package/dist/assets/loader-circle-CDSFuzmk.js +0 -1
  160. package/dist/assets/mcp-marketplace-page-BeEQK0DL.js +0 -9
  161. package/dist/assets/mcp-marketplace-page-D6idOTLp.js +0 -1
  162. package/dist/assets/model-config-page-bNNo-XyG.js +0 -1
  163. package/dist/assets/plus-D4DsdLN-.js +0 -1
  164. package/dist/assets/provider-scoped-model-input-IlgQCsoT.js +0 -1
  165. package/dist/assets/providers-config-page-wLo6CAJw.js +0 -1
  166. package/dist/assets/react-BPIsQIlO.js +0 -8
  167. package/dist/assets/remote-uKEYp7AR.js +0 -1
  168. package/dist/assets/rotate-cw-wPFPg1kH.js +0 -1
  169. package/dist/assets/runtime-config-page-DDn1sP4G.js +0 -1
  170. package/dist/assets/search-config-page-B-Cf4iiB.js +0 -1
  171. package/dist/assets/secrets-config-page-DPKsY8Iw.js +0 -3
  172. package/dist/assets/security-config-e9SSBlKr.js +0 -1
  173. package/dist/assets/select-BdgczSNw.js +0 -1
  174. package/dist/assets/setting-row-BkTVUTKx.js +0 -1
  175. package/dist/assets/side-dock-BsBvcJ9i.js +0 -3
  176. package/dist/assets/switch-Cv8Ekb3x.js +0 -1
  177. package/dist/assets/tag-chip-RB9uKcoC.js +0 -1
  178. package/dist/assets/tooltip-oQlEN-l6.js +0 -1
  179. package/dist/assets/use-config-l3W7y8Qz.js +0 -1
  180. package/dist/assets/use-confirm-dialog-BAxaVOJI.js +0 -1
  181. package/dist/assets/x-BBMffEN0.js +0 -1
  182. /package/dist/assets/{config-hints-C_iP1zl_.js → config-hints-fDrYfl0l.js} +0 -0
  183. /package/dist/assets/{host-capabilities-9bB_998Z.js → host-capabilities-zZvY9U63.js} +0 -0
  184. /package/dist/assets/{middleware-BILWbJE4.js → middleware-BLMHv9YF.js} +0 -0
@@ -16,12 +16,15 @@ import type {
16
16
  ChatChildSessionTab,
17
17
  ChatThreadSnapshot,
18
18
  ChatWorkspaceNavigationEntry,
19
+ ChatWorkspacePanelKind,
19
20
  ChatWorkspaceFileTab,
20
21
  } from '@/features/chat/stores/chat-thread.store';
21
22
  import { useChatThreadStore } from '@/features/chat/stores/chat-thread.store';
23
+ import { normalizeChatWorkspacePanelWidth } from '@/features/chat/features/workspace/utils/chat-workspace-panel-layout.utils';
22
24
  import { t } from '@/shared/lib/i18n';
23
25
  import {
24
26
  areWorkspaceNavigationEntriesEqual,
27
+ createWorkspaceSelectionPatch,
25
28
  createSideChatDraft,
26
29
  materializeSideChatDraftSnapshot,
27
30
  upsertChildSessionTab,
@@ -31,11 +34,19 @@ import {
31
34
  pushNavigationHistoryEntry,
32
35
  stepNavigationHistory,
33
36
  } from '@/shared/lib/navigation-history';
37
+ import {
38
+ resolveAlternateWorkspaceFileViewer,
39
+ type ChatWorkspaceFileViewer,
40
+ } from '@/features/chat/features/workspace/utils/chat-workspace-file-viewer.utils';
41
+ import {
42
+ createWorkspaceFileTab,
43
+ upsertWorkspaceFileTab,
44
+ } from '@/features/chat/features/workspace/utils/chat-workspace-file-tab.utils';
34
45
 
35
46
  type WorkspaceChildReadState = Parameters<ChatSessionListManager['markVisibleWorkspaceChildRead']>[0];
36
47
  export type ChatVisibleWorkspaceSelection =
37
48
  | { kind: 'child-session'; tab: WorkspaceChildReadState }
38
- | { kind: 'file' | 'side-chat-draft' | 'cron' }
49
+ | { kind: Exclude<ChatWorkspacePanelKind, 'child-session'> }
39
50
  | null;
40
51
 
41
52
  export class ChatThreadManager {
@@ -102,56 +113,29 @@ export class ChatThreadManager {
102
113
  return useChatSessionListStore.getState().snapshot.selectedSessionKey ?? null;
103
114
  };
104
115
 
105
- private buildWorkspaceFileTab = (
106
- action: ChatFileOpenActionViewModel,
107
- parentSessionKey: string | null,
108
- ): ChatWorkspaceFileTab | null => {
109
- const normalizedPath = action.path.trim();
110
- if (!normalizedPath) {
111
- return null;
112
- }
113
- const normalizedParentSessionKey = parentSessionKey?.trim() || null;
114
- const previewViewer =
115
- action.viewMode === 'preview' &&
116
- action.previewViewer !== 'rendered' &&
117
- (action.previewViewer === 'auto' || action.previewViewer === 'source' || /\.html?$/i.test(normalizedPath))
118
- ? 'source'
119
- : action.previewViewer ?? null;
120
- const tabViewIdentity = action.viewMode === 'preview' && previewViewer === 'rendered' ? 'preview:rendered' : action.viewMode;
121
- return {
122
- key: `${normalizedParentSessionKey ?? 'draft'}::${tabViewIdentity}::${normalizedPath}`,
123
- parentSessionKey: normalizedParentSessionKey,
124
- path: normalizedPath,
125
- label: action.label?.trim() || null,
126
- viewMode: action.viewMode,
127
- previewViewer,
128
- line: action.line ?? null,
129
- column: action.column ?? null,
130
- rawText: action.rawText ?? null,
131
- contentUrl: action.contentUrl?.trim() || null,
132
- mimeType: action.mimeType?.trim() || null,
133
- beforeText: action.beforeText ?? null,
134
- afterText: action.afterText ?? null,
135
- patchText: action.patchText ?? null,
136
- oldStartLine: action.oldStartLine ?? null,
137
- newStartLine: action.newStartLine ?? null,
138
- fullLines: action.fullLines,
139
- };
140
- };
141
-
142
- private upsertWorkspaceFileTab = (nextTab: ChatWorkspaceFileTab): ChatWorkspaceFileTab[] => {
116
+ private activateWorkspaceFileTab = (
117
+ nextTab: ChatWorkspaceFileTab,
118
+ adjacentToKey?: string,
119
+ ) => {
120
+ const { parentSessionKey } = nextTab;
143
121
  const { workspaceFileTabs } = useChatThreadStore.getState().snapshot;
144
- const existingIndex = workspaceFileTabs.findIndex((tab) => tab.key === nextTab.key);
145
- if (existingIndex === -1) {
146
- return [nextTab, ...workspaceFileTabs];
147
- }
148
- const nextTabs = [...workspaceFileTabs];
149
- nextTabs.splice(existingIndex, 1);
150
- nextTabs.unshift({
151
- ...workspaceFileTabs[existingIndex],
152
- ...nextTab,
122
+ this.setWorkspaceSelection({
123
+ workspacePanelParentKey: parentSessionKey,
124
+ activeWorkspacePanelKind: 'file',
125
+ workspaceFileTabs: upsertWorkspaceFileTab(
126
+ workspaceFileTabs,
127
+ nextTab,
128
+ adjacentToKey,
129
+ ),
130
+ activeWorkspaceFileKey: nextTab.key,
131
+ activeChildSessionKey: null,
132
+ activeSideChatDraft: null,
133
+ }, {
134
+ kind: 'file',
135
+ key: nextTab.key,
153
136
  });
154
- return nextTabs;
137
+ this.ensureWorkspaceParentRoute(parentSessionKey);
138
+ this.onWorkspacePanelOpened?.();
155
139
  };
156
140
 
157
141
  private ensureWorkspaceParentRoute = (parentSessionKey: string | null) => {
@@ -166,52 +150,6 @@ export class ChatThreadManager {
166
150
  }
167
151
  };
168
152
 
169
- private createWorkspaceSelectionPatch = (
170
- entry: ChatWorkspaceNavigationEntry,
171
- snapshot: ChatThreadSnapshot,
172
- ): Partial<ChatThreadSnapshot> | null => {
173
- if (entry.kind === 'cron') {
174
- return {
175
- activeWorkspacePanelKind: 'cron',
176
- activeChildSessionKey: null,
177
- activeSideChatDraft: null,
178
- activeWorkspaceFileKey: null,
179
- };
180
- }
181
- if (entry.kind === 'file') {
182
- if (!snapshot.workspaceFileTabs.some((tab) => tab.key === entry.key)) {
183
- return null;
184
- }
185
- return {
186
- activeWorkspacePanelKind: 'file',
187
- activeChildSessionKey: null,
188
- activeSideChatDraft: null,
189
- activeWorkspaceFileKey: entry.key,
190
- };
191
- }
192
- if (entry.kind === 'side-chat-draft') {
193
- const { activeSideChatDraft } = snapshot;
194
- if (activeSideChatDraft?.draftKey !== entry.key) {
195
- return null;
196
- }
197
- return {
198
- activeWorkspacePanelKind: 'side-chat-draft',
199
- activeChildSessionKey: null,
200
- activeSideChatDraft,
201
- activeWorkspaceFileKey: null,
202
- };
203
- }
204
- if (!entry.key) {
205
- return null;
206
- }
207
- return {
208
- activeWorkspacePanelKind: 'child-session',
209
- activeChildSessionKey: entry.key,
210
- activeSideChatDraft: null,
211
- activeWorkspaceFileKey: null,
212
- };
213
- };
214
-
215
153
  private setWorkspaceSelection = (
216
154
  patch: Partial<ChatThreadSnapshot>,
217
155
  entry: ChatWorkspaceNavigationEntry,
@@ -232,6 +170,59 @@ export class ChatThreadManager {
232
170
  });
233
171
  };
234
172
 
173
+ private openWorkspacePage = (
174
+ rawParentSessionKey: string,
175
+ kind: 'overview' | 'child-sessions' | 'project-files' | 'cron',
176
+ ) => {
177
+ const parentSessionKey = rawParentSessionKey.trim();
178
+ if (!parentSessionKey) {
179
+ return;
180
+ }
181
+ this.setWorkspaceSelection({
182
+ workspacePanelParentKey: parentSessionKey,
183
+ activeWorkspacePanelKind: kind,
184
+ activeChildSessionKey: null,
185
+ activeSideChatDraft: null,
186
+ activeWorkspaceFileKey: null,
187
+ }, { kind });
188
+ this.ensureWorkspaceParentRoute(parentSessionKey);
189
+ this.onWorkspacePanelOpened?.();
190
+ };
191
+
192
+ openWorkspaceOverview = (sessionKey: string) => {
193
+ this.openWorkspacePage(sessionKey, 'overview');
194
+ };
195
+
196
+ toggleWorkspacePanel = (sessionKey: string) => {
197
+ const normalizedSessionKey = sessionKey.trim();
198
+ if (!normalizedSessionKey) {
199
+ return;
200
+ }
201
+ const { snapshot } = useChatThreadStore.getState();
202
+ if (
203
+ snapshot.workspacePanelParentKey === normalizedSessionKey &&
204
+ snapshot.activeWorkspacePanelKind
205
+ ) {
206
+ this.closeWorkspacePanel();
207
+ return;
208
+ }
209
+ this.openWorkspaceOverview(normalizedSessionKey);
210
+ };
211
+
212
+ setWorkspacePanelWidth = (width: number) => {
213
+ this.syncSnapshot({
214
+ workspacePanelWidth: normalizeChatWorkspacePanelWidth(width),
215
+ });
216
+ };
217
+
218
+ openChildSessions = (sessionKey: string) => {
219
+ this.openWorkspacePage(sessionKey, 'child-sessions');
220
+ };
221
+
222
+ openProjectFiles = (sessionKey: string) => {
223
+ this.openWorkspacePage(sessionKey, 'project-files');
224
+ };
225
+
235
226
  openChildSessionPanel = (params: {
236
227
  parentSessionKey: string;
237
228
  activeChildSessionKey?: string | null;
@@ -317,23 +308,43 @@ export class ChatThreadManager {
317
308
 
318
309
  openFilePreview = (action: ChatFileOpenActionViewModel) => {
319
310
  const parentSessionKey = this.resolveWorkspaceParentSessionKey();
320
- const nextTab = this.buildWorkspaceFileTab(action, parentSessionKey);
311
+ const nextTab = createWorkspaceFileTab(action, parentSessionKey);
321
312
  if (!nextTab) {
322
313
  return;
323
314
  }
324
- this.setWorkspaceSelection({
325
- workspacePanelParentKey: parentSessionKey,
326
- activeWorkspacePanelKind: 'file',
327
- workspaceFileTabs: this.upsertWorkspaceFileTab(nextTab),
328
- activeWorkspaceFileKey: nextTab.key,
329
- activeChildSessionKey: null,
330
- activeSideChatDraft: null,
331
- }, {
332
- kind: 'file',
333
- key: nextTab.key,
334
- });
335
- this.ensureWorkspaceParentRoute(parentSessionKey);
336
- this.onWorkspacePanelOpened?.();
315
+ this.activateWorkspaceFileTab(nextTab);
316
+ };
317
+
318
+ openWorkspaceFileViewer = (
319
+ fileKey: string,
320
+ viewer?: ChatWorkspaceFileViewer,
321
+ ) => {
322
+ const sourceTab = useChatThreadStore.getState().snapshot.workspaceFileTabs
323
+ .find((tab) => tab.key === fileKey.trim());
324
+ if (!sourceTab || sourceTab.viewMode !== 'preview') {
325
+ return;
326
+ }
327
+ const nextViewer = viewer ?? resolveAlternateWorkspaceFileViewer(
328
+ sourceTab.path,
329
+ sourceTab.previewViewer,
330
+ );
331
+ if (!nextViewer) {
332
+ return;
333
+ }
334
+ const nextTab = createWorkspaceFileTab({
335
+ path: sourceTab.path,
336
+ label: sourceTab.label ?? undefined,
337
+ viewMode: 'preview',
338
+ previewViewer: nextViewer,
339
+ line: sourceTab.line ?? undefined,
340
+ column: sourceTab.column ?? undefined,
341
+ rawText: sourceTab.rawText ?? undefined,
342
+ contentUrl: sourceTab.contentUrl ?? undefined,
343
+ mimeType: sourceTab.mimeType ?? undefined,
344
+ }, sourceTab.parentSessionKey);
345
+ if (nextTab) {
346
+ this.activateWorkspaceFileTab(nextTab, sourceTab.key);
347
+ }
337
348
  };
338
349
 
339
350
  private openSessionFromToolAction = (action: ChatToolActionViewModel) => {
@@ -479,7 +490,7 @@ export class ChatThreadManager {
479
490
  };
480
491
  const restoreEntry = history.entries[history.index];
481
492
  const restorePatch = restoreEntry
482
- ? this.createWorkspaceSelectionPatch(restoreEntry, nextSnapshot)
493
+ ? createWorkspaceSelectionPatch(restoreEntry, nextSnapshot)
483
494
  : null;
484
495
  Object.assign(nextPatch, restorePatch ?? {
485
496
  activeWorkspacePanelKind: null,
@@ -503,21 +514,7 @@ export class ChatThreadManager {
503
514
  };
504
515
 
505
516
  openSessionCronPanel = (sessionKey: string) => {
506
- const parentSessionKey = sessionKey.trim();
507
- if (!parentSessionKey) {
508
- return;
509
- }
510
- this.setWorkspaceSelection({
511
- workspacePanelParentKey: parentSessionKey,
512
- activeWorkspacePanelKind: 'cron',
513
- activeChildSessionKey: null,
514
- activeSideChatDraft: null,
515
- activeWorkspaceFileKey: null,
516
- }, {
517
- kind: 'cron',
518
- });
519
- this.ensureWorkspaceParentRoute(parentSessionKey);
520
- this.onWorkspacePanelOpened?.();
517
+ this.openWorkspacePage(sessionKey, 'cron');
521
518
  };
522
519
 
523
520
  goBackWorkspacePanel = () => {
@@ -540,7 +537,7 @@ export class ChatThreadManager {
540
537
  if (!step) {
541
538
  return;
542
539
  }
543
- const selectionPatch = this.createWorkspaceSelectionPatch(step.entry, snapshot);
540
+ const selectionPatch = createWorkspaceSelectionPatch(step.entry, snapshot);
544
541
  if (!selectionPatch) {
545
542
  return;
546
543
  }
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  useEffect,
3
3
  useMemo,
4
- useState,
5
4
  } from "react";
6
5
  import { useLocation, useNavigate, useParams } from "react-router-dom";
7
6
  import {
@@ -51,7 +50,7 @@ function useNcpChatUiBindings() {
51
50
 
52
51
  export function NcpChatPage({ view }: ChatPageProps) {
53
52
  const appPresenter = useAppPresenter();
54
- const [presenter] = useState(() => new ChatPresenter(appPresenter));
53
+ const presenter = useMemo(() => new ChatPresenter(appPresenter), [appPresenter]);
55
54
  return (
56
55
  <ChatPresenterProvider presenter={presenter}>
57
56
  <NcpChatPageContent view={view} />
@@ -1,5 +1,8 @@
1
1
  import { beforeEach, describe, expect, it } from 'vitest';
2
- import { useChatThreadStore } from '@/features/chat/stores/chat-thread.store';
2
+ import {
3
+ useChatThreadStore,
4
+ } from '@/features/chat/stores/chat-thread.store';
5
+ import { CHAT_WORKSPACE_PANEL_DEFAULT_WIDTH } from '@/features/chat/features/workspace/utils/chat-workspace-panel-layout.utils';
3
6
 
4
7
  const chatThreadWorkspaceStorageKey = 'nextclaw.chat.workspace-panel.state';
5
8
 
@@ -27,6 +30,7 @@ function resetChatThreadStore() {
27
30
  activeWorkspaceFileKey: null,
28
31
  workspaceNavigationHistory: [],
29
32
  workspaceNavigationHistoryIndex: 0,
33
+ workspacePanelWidth: CHAT_WORKSPACE_PANEL_DEFAULT_WIDTH,
30
34
  },
31
35
  });
32
36
  }
@@ -118,6 +122,79 @@ describe('chat thread workspace panel persistence', () => {
118
122
  expect(persisted.state.snapshot.activeSideChatDraft).toBeUndefined();
119
123
  });
120
124
 
125
+ it('persists the stable workspace pages', async () => {
126
+ useChatThreadStore.getState().setSnapshot({
127
+ workspacePanelParentKey: 'session-1',
128
+ activeWorkspacePanelKind: 'project-files',
129
+ workspaceNavigationHistory: [
130
+ { kind: 'overview' },
131
+ { kind: 'child-sessions' },
132
+ { kind: 'project-files' },
133
+ ],
134
+ workspaceNavigationHistoryIndex: 2,
135
+ workspacePanelWidth: 620,
136
+ });
137
+
138
+ const persisted = window.localStorage.getItem(chatThreadWorkspaceStorageKey);
139
+ resetChatThreadStore();
140
+ if (persisted) {
141
+ window.localStorage.setItem(chatThreadWorkspaceStorageKey, persisted);
142
+ }
143
+ await useChatThreadStore.persist.rehydrate();
144
+
145
+ expect(useChatThreadStore.getState().snapshot).toMatchObject({
146
+ workspacePanelParentKey: 'session-1',
147
+ activeWorkspacePanelKind: 'project-files',
148
+ workspaceNavigationHistory: [
149
+ { kind: 'overview' },
150
+ { kind: 'child-sessions' },
151
+ { kind: 'project-files' },
152
+ ],
153
+ workspaceNavigationHistoryIndex: 2,
154
+ workspacePanelWidth: 620,
155
+ });
156
+ });
157
+
158
+ it('keeps the active file tab when persistence trims older workspace tabs', async () => {
159
+ const workspaceFileTabs = Array.from({ length: 9 }, (_, index) => ({
160
+ key: `session-1::preview::file-${index}.txt`,
161
+ parentSessionKey: 'session-1',
162
+ path: `file-${index}.txt`,
163
+ viewMode: 'preview' as const,
164
+ previewViewer: 'source' as const,
165
+ }));
166
+ const activeWorkspaceFileKey = workspaceFileTabs[8]!.key;
167
+ useChatThreadStore.getState().setSnapshot({
168
+ workspacePanelParentKey: 'session-1',
169
+ activeWorkspacePanelKind: 'file',
170
+ workspaceFileTabs,
171
+ activeWorkspaceFileKey,
172
+ workspaceNavigationHistory: [
173
+ { kind: 'file', key: activeWorkspaceFileKey },
174
+ ],
175
+ workspaceNavigationHistoryIndex: 0,
176
+ });
177
+
178
+ const persisted = window.localStorage.getItem(chatThreadWorkspaceStorageKey);
179
+ resetChatThreadStore();
180
+ if (persisted) {
181
+ window.localStorage.setItem(chatThreadWorkspaceStorageKey, persisted);
182
+ }
183
+ await useChatThreadStore.persist.rehydrate();
184
+
185
+ expect(useChatThreadStore.getState().snapshot).toMatchObject({
186
+ workspacePanelParentKey: 'session-1',
187
+ activeWorkspacePanelKind: 'file',
188
+ activeWorkspaceFileKey,
189
+ });
190
+ expect(useChatThreadStore.getState().snapshot.workspaceFileTabs).toHaveLength(8);
191
+ expect(
192
+ useChatThreadStore.getState().snapshot.workspaceFileTabs.some(
193
+ (tab) => tab.key === activeWorkspaceFileKey,
194
+ ),
195
+ ).toBe(true);
196
+ });
197
+
121
198
  it('hydrates the workspace panel state and repairs stale active file keys', async () => {
122
199
  window.localStorage.setItem(
123
200
  chatThreadWorkspaceStorageKey,
@@ -0,0 +1,37 @@
1
+ import { beforeEach, describe, expect, it } from "vitest";
2
+ import {
3
+ DEFAULT_CHAT_MESSAGE_LAYOUT,
4
+ useChatMessageLayoutStore,
5
+ } from "@/features/chat/stores/chat-message-layout.store";
6
+
7
+ const STORAGE_KEY = "nextclaw.chat.message-layout";
8
+
9
+ describe("chat message layout store", () => {
10
+ beforeEach(() => {
11
+ window.localStorage.clear();
12
+ useChatMessageLayoutStore.setState({
13
+ layout: DEFAULT_CHAT_MESSAGE_LAYOUT,
14
+ });
15
+ });
16
+
17
+ it("preserves an existing card layout preference", async () => {
18
+ window.localStorage.setItem(
19
+ STORAGE_KEY,
20
+ JSON.stringify({ state: { layout: "card" }, version: 1 }),
21
+ );
22
+
23
+ await useChatMessageLayoutStore.persist.rehydrate();
24
+ expect(useChatMessageLayoutStore.getState().layout).toBe("card");
25
+ });
26
+
27
+ it("falls back to the flat layout for an invalid persisted layout", async () => {
28
+ window.localStorage.setItem(
29
+ STORAGE_KEY,
30
+ JSON.stringify({ state: { layout: "unknown" }, version: 1 }),
31
+ );
32
+
33
+ await useChatMessageLayoutStore.persist.rehydrate();
34
+
35
+ expect(useChatMessageLayoutStore.getState().layout).toBe("flat");
36
+ });
37
+ });
@@ -0,0 +1,41 @@
1
+ import type { ChatMessageLayout } from "@nextclaw/agent-chat-ui";
2
+ import { create } from "zustand";
3
+ import { createJSONStorage, persist } from "zustand/middleware";
4
+
5
+ const CHAT_MESSAGE_LAYOUT_STORAGE_KEY = "nextclaw.chat.message-layout";
6
+ const CHAT_MESSAGE_LAYOUT_STORAGE_VERSION = 1;
7
+ export const DEFAULT_CHAT_MESSAGE_LAYOUT: ChatMessageLayout = "flat";
8
+
9
+ type ChatMessageLayoutStore = {
10
+ layout: ChatMessageLayout;
11
+ setLayout: (layout: ChatMessageLayout) => void;
12
+ };
13
+
14
+ function resolvePersistedLayout(persistedState: unknown): ChatMessageLayout {
15
+ if (!persistedState || typeof persistedState !== "object") {
16
+ return DEFAULT_CHAT_MESSAGE_LAYOUT;
17
+ }
18
+ const { layout } = persistedState as { layout?: unknown };
19
+ return layout === "flat" || layout === "card"
20
+ ? layout
21
+ : DEFAULT_CHAT_MESSAGE_LAYOUT;
22
+ }
23
+
24
+ export const useChatMessageLayoutStore = create<ChatMessageLayoutStore>()(
25
+ persist(
26
+ (set) => ({
27
+ layout: DEFAULT_CHAT_MESSAGE_LAYOUT,
28
+ setLayout: (layout) => set({ layout }),
29
+ }),
30
+ {
31
+ name: CHAT_MESSAGE_LAYOUT_STORAGE_KEY,
32
+ version: CHAT_MESSAGE_LAYOUT_STORAGE_VERSION,
33
+ storage: createJSONStorage(() => window.localStorage),
34
+ partialize: (state) => ({ layout: state.layout }),
35
+ merge: (persistedState, currentState) => ({
36
+ ...currentState,
37
+ layout: resolvePersistedLayout(persistedState),
38
+ }),
39
+ },
40
+ ),
41
+ );