@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
@@ -8,8 +8,13 @@ import type {
8
8
  } from '@/shared/lib/api';
9
9
  import {
10
10
  normalizePersistedWorkspaceFileTab,
11
+ retainWorkspaceFileTabs,
11
12
  toPersistedWorkspaceFileTab,
12
13
  } from '@/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils';
14
+ import {
15
+ CHAT_WORKSPACE_PANEL_DEFAULT_WIDTH,
16
+ normalizeChatWorkspacePanelWidth,
17
+ } from '@/features/chat/features/workspace/utils/chat-workspace-panel-layout.utils';
13
18
 
14
19
  export type ChatChildSessionTab = {
15
20
  sessionKey: string;
@@ -44,9 +49,21 @@ export type ChatWorkspaceFileTab = {
44
49
  fullLines?: ChatFileOperationLineViewModel[];
45
50
  };
46
51
 
52
+ export type ChatWorkspacePanelKind =
53
+ | 'overview'
54
+ | 'child-sessions'
55
+ | 'child-session'
56
+ | 'side-chat-draft'
57
+ | 'project-files'
58
+ | 'file'
59
+ | 'cron';
60
+
47
61
  export type ChatWorkspaceNavigationEntry =
62
+ | { kind: 'overview' }
63
+ | { kind: 'child-sessions' }
48
64
  | { kind: 'child-session'; key: string }
49
65
  | { kind: 'side-chat-draft'; key: string }
66
+ | { kind: 'project-files' }
50
67
  | { kind: 'file'; key: string }
51
68
  | { kind: 'cron' };
52
69
 
@@ -69,7 +86,7 @@ export type ChatThreadSnapshot = {
69
86
  parentSessionKey?: string | null;
70
87
  parentSessionLabel?: string | null;
71
88
  workspacePanelParentKey?: string | null;
72
- activeWorkspacePanelKind?: "child-session" | "side-chat-draft" | "file" | "cron" | null;
89
+ activeWorkspacePanelKind?: ChatWorkspacePanelKind | null;
73
90
  childSessionTabs: ChatChildSessionTab[];
74
91
  activeChildSessionKey?: string | null;
75
92
  activeSideChatDraft?: ChatWorkspaceSideChatDraft | null;
@@ -77,6 +94,7 @@ export type ChatThreadSnapshot = {
77
94
  activeWorkspaceFileKey?: string | null;
78
95
  workspaceNavigationHistory: ChatWorkspaceNavigationEntry[];
79
96
  workspaceNavigationHistoryIndex: number;
97
+ workspacePanelWidth: number;
80
98
  contextWindow?: SessionContextWindowView | null;
81
99
  };
82
100
 
@@ -98,6 +116,7 @@ type PersistedChatThreadStore = {
98
116
  activeWorkspaceFileKey?: unknown;
99
117
  workspaceNavigationHistory?: unknown;
100
118
  workspaceNavigationHistoryIndex?: unknown;
119
+ workspacePanelWidth?: unknown;
101
120
  };
102
121
  };
103
122
 
@@ -110,9 +129,10 @@ type PersistedChatWorkspaceSnapshot = Pick<
110
129
  | 'activeWorkspaceFileKey'
111
130
  | 'workspaceNavigationHistory'
112
131
  | 'workspaceNavigationHistoryIndex'
132
+ | 'workspacePanelWidth'
113
133
  >;
114
134
 
115
- type PersistedWorkspacePanelKind = 'child-session' | 'file' | 'cron';
135
+ type PersistedWorkspacePanelKind = Exclude<ChatWorkspacePanelKind, 'side-chat-draft'>;
116
136
 
117
137
  const initialSnapshot: ChatThreadSnapshot = {
118
138
  sessionTypeLabel: null,
@@ -141,6 +161,7 @@ const initialSnapshot: ChatThreadSnapshot = {
141
161
  activeWorkspaceFileKey: null,
142
162
  workspaceNavigationHistory: [],
143
163
  workspaceNavigationHistoryIndex: 0,
164
+ workspacePanelWidth: CHAT_WORKSPACE_PANEL_DEFAULT_WIDTH,
144
165
  contextWindow: null
145
166
  };
146
167
 
@@ -163,7 +184,14 @@ function normalizeHistoryIndex(value: unknown, maxIndex: number): number {
163
184
  function isWorkspacePanelKind(
164
185
  value: unknown,
165
186
  ): value is PersistedWorkspacePanelKind {
166
- return value === 'child-session' || value === 'file' || value === 'cron';
187
+ return (
188
+ value === 'overview' ||
189
+ value === 'child-sessions' ||
190
+ value === 'child-session' ||
191
+ value === 'project-files' ||
192
+ value === 'file' ||
193
+ value === 'cron'
194
+ );
167
195
  }
168
196
 
169
197
  function normalizePersistedWorkspaceNavigationEntry(
@@ -172,8 +200,13 @@ function normalizePersistedWorkspaceNavigationEntry(
172
200
  if (!isRecord(value)) {
173
201
  return null;
174
202
  }
175
- if (value.kind === 'cron') {
176
- return { kind: 'cron' };
203
+ if (
204
+ value.kind === 'overview' ||
205
+ value.kind === 'child-sessions' ||
206
+ value.kind === 'project-files' ||
207
+ value.kind === 'cron'
208
+ ) {
209
+ return { kind: value.kind };
177
210
  }
178
211
  const key = normalizeOptionalString(value.key);
179
212
  if (!key || (value.kind !== 'child-session' && value.kind !== 'file')) {
@@ -194,6 +227,15 @@ function createWorkspaceNavigationEntryFromSnapshot({
194
227
  activeChildSessionKey: string | null;
195
228
  activeWorkspaceFileKey: string | null;
196
229
  }): ChatWorkspaceNavigationEntry | null {
230
+ if (activeWorkspacePanelKind === 'overview') {
231
+ return { kind: 'overview' };
232
+ }
233
+ if (activeWorkspacePanelKind === 'child-sessions') {
234
+ return { kind: 'child-sessions' };
235
+ }
236
+ if (activeWorkspacePanelKind === 'project-files') {
237
+ return { kind: 'project-files' };
238
+ }
197
239
  if (activeWorkspacePanelKind === 'cron') {
198
240
  return { kind: 'cron' };
199
241
  }
@@ -218,17 +260,25 @@ function normalizePersistedWorkspaceSnapshot(
218
260
  if (!isRecord(value)) {
219
261
  return null;
220
262
  }
221
- const workspaceFileTabs = Array.isArray(value.workspaceFileTabs)
263
+ const workspacePanelParentKey = normalizeOptionalString(value.workspacePanelParentKey);
264
+ const activeWorkspaceFileKey = normalizeOptionalString(value.activeWorkspaceFileKey);
265
+ const normalizedWorkspaceFileTabs = Array.isArray(value.workspaceFileTabs)
222
266
  ? value.workspaceFileTabs
223
267
  .map(normalizePersistedWorkspaceFileTab)
224
268
  .filter((tab): tab is ChatWorkspaceFileTab => tab !== null)
225
- .slice(-CHAT_THREAD_MAX_PERSISTED_WORKSPACE_FILE_TABS)
226
269
  : [];
227
- const activeWorkspaceFileKey = normalizeOptionalString(value.activeWorkspaceFileKey);
270
+ const workspaceFileTabs = retainWorkspaceFileTabs(
271
+ normalizedWorkspaceFileTabs,
272
+ activeWorkspaceFileKey,
273
+ CHAT_THREAD_MAX_PERSISTED_WORKSPACE_FILE_TABS,
274
+ );
275
+ const sessionWorkspaceFileTabs = workspaceFileTabs.filter(
276
+ (tab) => tab.parentSessionKey === workspacePanelParentKey,
277
+ );
228
278
  const resolvedActiveWorkspaceFileKey =
229
- activeWorkspaceFileKey && workspaceFileTabs.some((tab) => tab.key === activeWorkspaceFileKey)
279
+ activeWorkspaceFileKey && sessionWorkspaceFileTabs.some((tab) => tab.key === activeWorkspaceFileKey)
230
280
  ? activeWorkspaceFileKey
231
- : workspaceFileTabs[0]?.key ?? null;
281
+ : sessionWorkspaceFileTabs[0]?.key ?? null;
232
282
  const activeWorkspacePanelKind = isWorkspacePanelKind(value.activeWorkspacePanelKind)
233
283
  ? value.activeWorkspacePanelKind
234
284
  : null;
@@ -265,13 +315,16 @@ function normalizePersistedWorkspaceSnapshot(
265
315
  : 0;
266
316
 
267
317
  return {
268
- workspacePanelParentKey: normalizeOptionalString(value.workspacePanelParentKey),
318
+ workspacePanelParentKey,
269
319
  activeWorkspacePanelKind: resolvedActiveWorkspacePanelKind,
270
320
  activeChildSessionKey,
271
321
  workspaceFileTabs,
272
322
  activeWorkspaceFileKey: resolvedActiveWorkspaceFileKey,
273
323
  workspaceNavigationHistory,
274
324
  workspaceNavigationHistoryIndex,
325
+ workspacePanelWidth: normalizeChatWorkspacePanelWidth(
326
+ value.workspacePanelWidth,
327
+ ),
275
328
  };
276
329
  }
277
330
 
@@ -309,12 +362,16 @@ export const useChatThreadStore = create<ChatThreadStore>()(
309
362
  ? null
310
363
  : state.snapshot.activeWorkspacePanelKind,
311
364
  activeChildSessionKey: state.snapshot.activeChildSessionKey,
312
- workspaceFileTabs: state.snapshot.workspaceFileTabs
313
- .slice(-CHAT_THREAD_MAX_PERSISTED_WORKSPACE_FILE_TABS)
365
+ workspaceFileTabs: retainWorkspaceFileTabs(
366
+ state.snapshot.workspaceFileTabs,
367
+ state.snapshot.activeWorkspaceFileKey,
368
+ CHAT_THREAD_MAX_PERSISTED_WORKSPACE_FILE_TABS,
369
+ )
314
370
  .map(toPersistedWorkspaceFileTab),
315
371
  activeWorkspaceFileKey: state.snapshot.activeWorkspaceFileKey,
316
372
  workspaceNavigationHistory,
317
373
  workspaceNavigationHistoryIndex,
374
+ workspacePanelWidth: state.snapshot.workspacePanelWidth,
318
375
  }
319
376
  };
320
377
  },
@@ -1,25 +1,42 @@
1
- import { fireEvent, render, screen } from '@testing-library/react';
2
- import { beforeEach, describe, expect, it } from 'vitest';
3
- import { AppearanceSettingsPage } from '@/features/settings/pages/appearance-settings-page';
4
- import { useSideDockStore } from '@/features/side-dock';
1
+ import { fireEvent, render, screen } from "@testing-library/react";
2
+ import { beforeEach, describe, expect, it } from "vitest";
3
+ import { AppearanceSettingsPage } from "@/features/settings/pages/appearance-settings-page";
4
+ import { useChatMessageLayoutStore } from "@/features/chat";
5
+ import { useSideDockStore } from "@/features/side-dock";
5
6
 
6
- describe('AppearanceSettingsPage', () => {
7
+ describe("AppearanceSettingsPage", () => {
7
8
  beforeEach(() => {
8
9
  window.localStorage.clear();
10
+ useChatMessageLayoutStore.getState().setLayout("card");
9
11
  useSideDockStore.getState().setVisible(true);
10
12
  useSideDockStore.getState().setPinnedItems([]);
11
13
  });
12
14
 
13
- it('reopens the SideDock from the appearance settings switch', () => {
15
+ it("reopens the SideDock from the appearance settings switch", () => {
14
16
  useSideDockStore.getState().setVisible(false);
15
17
 
16
18
  render(<AppearanceSettingsPage />);
17
19
 
18
- const visibilitySwitch = screen.getByRole('switch', { name: 'Show SideDock' });
19
- expect(visibilitySwitch.getAttribute('aria-checked')).toBe('false');
20
+ const visibilitySwitch = screen.getByRole("switch", {
21
+ name: "Show SideDock",
22
+ });
23
+ expect(visibilitySwitch.getAttribute("aria-checked")).toBe("false");
20
24
 
21
25
  fireEvent.click(visibilitySwitch);
22
26
 
23
27
  expect(useSideDockStore.getState().isVisible).toBe(true);
24
28
  });
29
+
30
+ it("switches the chat message layout from appearance settings", () => {
31
+ render(<AppearanceSettingsPage />);
32
+
33
+ const cardOption = screen.getByRole("radio", { name: /^Cards/ });
34
+ const flatOption = screen.getByRole("radio", { name: /^Flat/ });
35
+ expect(cardOption.getAttribute("aria-checked")).toBe("true");
36
+
37
+ fireEvent.click(flatOption);
38
+
39
+ expect(flatOption.getAttribute("aria-checked")).toBe("true");
40
+ expect(useChatMessageLayoutStore.getState().layout).toBe("flat");
41
+ });
25
42
  });
@@ -1,33 +1,112 @@
1
- import { Palette } from 'lucide-react';
2
- import { PageHeader, PageLayout } from '@/app/components/layout/page-layout';
3
- import { SettingRow } from '@/shared/components/settings/setting-row';
4
- import { Card, CardContent, CardHeader, CardTitle } from '@/shared/components/ui/card';
5
- import { Switch } from '@/shared/components/ui/switch';
6
- import { useSideDockStore } from '@/features/side-dock';
7
- import { t } from '@/shared/lib/i18n';
1
+ import type { ChatMessageLayout } from "@nextclaw/agent-chat-ui";
2
+ import { Check, Palette } from "lucide-react";
3
+ import { PageHeader, PageLayout } from "@/app/components/layout/page-layout";
4
+ import { useChatMessageLayoutStore } from "@/features/chat";
5
+ import { SettingRow } from "@/shared/components/settings/setting-row";
6
+ import {
7
+ Card,
8
+ CardContent,
9
+ CardHeader,
10
+ CardTitle,
11
+ } from "@/shared/components/ui/card";
12
+ import { Switch } from "@/shared/components/ui/switch";
13
+ import { useSideDockStore } from "@/features/side-dock";
14
+ import { t } from "@/shared/lib/i18n";
15
+ import { cn } from "@/shared/lib/utils";
16
+
17
+ const CHAT_MESSAGE_LAYOUT_OPTIONS: Array<{
18
+ value: ChatMessageLayout;
19
+ labelKey: string;
20
+ descriptionKey: string;
21
+ }> = [
22
+ {
23
+ value: "card",
24
+ labelKey: "chatMessageLayoutCard",
25
+ descriptionKey: "chatMessageLayoutCardDescription",
26
+ },
27
+ {
28
+ value: "flat",
29
+ labelKey: "chatMessageLayoutFlat",
30
+ descriptionKey: "chatMessageLayoutFlatDescription",
31
+ },
32
+ ];
8
33
 
9
34
  export function AppearanceSettingsPage() {
35
+ const messageLayout = useChatMessageLayoutStore((state) => state.layout);
36
+ const setMessageLayout = useChatMessageLayoutStore(
37
+ (state) => state.setLayout,
38
+ );
10
39
  const isSideDockVisible = useSideDockStore((state) => state.isVisible);
11
40
  const setSideDockVisible = useSideDockStore((state) => state.setVisible);
12
41
 
13
42
  return (
14
43
  <PageLayout>
15
- <PageHeader title={t('appearance')} />
44
+ <PageHeader title={t("appearance")} />
16
45
  <Card>
17
46
  <CardHeader>
18
47
  <CardTitle className="flex items-center gap-2">
19
48
  <Palette className="h-4 w-4" />
20
- {t('appearance')}
49
+ {t("appearance")}
21
50
  </CardTitle>
22
51
  </CardHeader>
23
- <CardContent>
52
+ <CardContent className="space-y-4">
53
+ <SettingRow
54
+ title={t("chatMessageLayoutTitle")}
55
+ description={t("chatMessageLayoutDescription")}
56
+ >
57
+ <div
58
+ role="radiogroup"
59
+ aria-label={t("chatMessageLayoutTitle")}
60
+ className="grid gap-2 sm:grid-cols-2"
61
+ >
62
+ {CHAT_MESSAGE_LAYOUT_OPTIONS.map((option) => {
63
+ const selected = option.value === messageLayout;
64
+ return (
65
+ <button
66
+ key={option.value}
67
+ type="button"
68
+ role="radio"
69
+ aria-checked={selected}
70
+ onClick={() => setMessageLayout(option.value)}
71
+ className={cn(
72
+ "flex min-w-0 items-start gap-3 rounded-xl border px-3 py-3 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35",
73
+ selected
74
+ ? "border-primary/35 bg-primary/5 text-foreground"
75
+ : "border-border bg-card text-muted-foreground hover:border-border/80 hover:bg-muted/45 hover:text-foreground",
76
+ )}
77
+ >
78
+ <span
79
+ className={cn(
80
+ "mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full border",
81
+ selected
82
+ ? "border-primary bg-primary text-primary-foreground"
83
+ : "border-border bg-background",
84
+ )}
85
+ >
86
+ {selected ? (
87
+ <Check className="h-2.5 w-2.5" strokeWidth={3} />
88
+ ) : null}
89
+ </span>
90
+ <span className="min-w-0">
91
+ <span className="block text-sm font-medium">
92
+ {t(option.labelKey)}
93
+ </span>
94
+ <span className="mt-1 block text-xs leading-5 text-muted-foreground">
95
+ {t(option.descriptionKey)}
96
+ </span>
97
+ </span>
98
+ </button>
99
+ );
100
+ })}
101
+ </div>
102
+ </SettingRow>
24
103
  <SettingRow
25
- title={t('sideDockVisibilityTitle')}
26
- description={t('sideDockVisibilityDescription')}
104
+ title={t("sideDockVisibilityTitle")}
105
+ description={t("sideDockVisibilityDescription")}
27
106
  control={
28
107
  <Switch
29
108
  id="appearance-side-dock-visible"
30
- aria-label={t('sideDockVisibilityTitle')}
109
+ aria-label={t("sideDockVisibilityTitle")}
31
110
  checked={isSideDockVisible}
32
111
  onCheckedChange={setSideDockVisible}
33
112
  />
package/src/index.css CHANGED
@@ -736,11 +736,16 @@
736
736
  .chat-markdown h5,
737
737
  .chat-markdown h6 {
738
738
  margin: 0.2rem 0 0;
739
+ padding-bottom: 0.5em;
739
740
  font-weight: 700;
740
741
  line-height: 1.35;
741
742
  letter-spacing: -0.01em;
742
743
  }
743
744
 
745
+ .chat-markdown :is(h1, h2, h3, h4, h5, h6) + * {
746
+ margin-top: 0;
747
+ }
748
+
744
749
  .chat-markdown h1 {
745
750
  font-size: 1.22rem;
746
751
  }
@@ -1338,9 +1343,38 @@
1338
1343
  }
1339
1344
 
1340
1345
  .chat-markdown img {
1346
+ display: block;
1347
+ width: auto;
1348
+ max-width: min(100%, 32rem);
1349
+ height: auto;
1350
+ }
1351
+
1352
+ .chat-markdown [data-chat-message-image-preview] {
1353
+ margin-block: 0.72rem;
1354
+ }
1355
+
1356
+ .chat-markdown [data-chat-message-image-preview]:first-child,
1357
+ .chat-markdown [data-chat-message-image-preview]
1358
+ + [data-chat-message-image-preview] {
1359
+ margin-top: 0;
1360
+ }
1361
+
1362
+ .chat-markdown [data-chat-message-image-preview]:last-child {
1363
+ margin-bottom: 0;
1364
+ }
1365
+
1366
+ .chat-markdown [data-chat-image-row="three-column"] {
1367
+ display: grid;
1368
+ grid-template-columns: repeat(3, minmax(0, 1fr));
1369
+ align-items: start;
1370
+ gap: 0.72rem;
1371
+ }
1372
+
1373
+ .chat-markdown
1374
+ [data-chat-image-row="three-column"]
1375
+ > [data-chat-message-image-preview] {
1341
1376
  max-width: 100%;
1342
- border-radius: 0.75rem;
1343
- border: 1px solid var(--md-border);
1377
+ margin: 0;
1344
1378
  }
1345
1379
 
1346
1380
  .chat-markdown blockquote {
@@ -0,0 +1,61 @@
1
+ import { render } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+ import { FileTypeIcon } from "@/shared/components/file-type-icon";
4
+
5
+ function readIcon(container: HTMLElement, name: string): SVGElement {
6
+ const icon = container.querySelector<SVGElement>(
7
+ `[data-file-type-icon="${name}"]`,
8
+ );
9
+ expect(icon).not.toBeNull();
10
+ expect(icon?.innerHTML).not.toContain("undefined");
11
+ expect(icon?.childElementCount).toBeGreaterThan(0);
12
+ return icon!;
13
+ }
14
+
15
+ describe("FileTypeIcon", () => {
16
+ it("renders recognizable SVG icons for common file types", () => {
17
+ const { container } = render(
18
+ <>
19
+ <FileTypeIcon fileName="src/app.tsx" />
20
+ <FileTypeIcon fileName="scripts/setup.js" />
21
+ <FileTypeIcon fileName="docs/guide.md" />
22
+ <FileTypeIcon fileName="assets/hero.png" />
23
+ <FileTypeIcon fileName="reports/data.xlsx" />
24
+ </>,
25
+ );
26
+
27
+ expect(readIcon(container, "react-typescript").tagName).toBe("svg");
28
+ expect(readIcon(container, "javascript").tagName).toBe("svg");
29
+ expect(readIcon(container, "markdown").tagName).toBe("svg");
30
+ expect(readIcon(container, "image").tagName).toBe("svg");
31
+ expect(readIcon(container, "excel").tagName).toBe("svg");
32
+ });
33
+
34
+ it("recognizes editor-style special file names", () => {
35
+ const { container } = render(
36
+ <>
37
+ <FileTypeIcon fileName="package.json" />
38
+ <FileTypeIcon fileName="Dockerfile" />
39
+ <FileTypeIcon fileName=".env.local" />
40
+ <FileTypeIcon fileName="vitest.unit.config.ts" />
41
+ <FileTypeIcon fileName="PHOTO.JPEG" />
42
+ </>,
43
+ );
44
+
45
+ expect(readIcon(container, "npm")).toBeTruthy();
46
+ expect(readIcon(container, "docker")).toBeTruthy();
47
+ expect(readIcon(container, "dotenv")).toBeTruthy();
48
+ expect(readIcon(container, "vitest")).toBeTruthy();
49
+ expect(readIcon(container, "image")).toBeTruthy();
50
+ });
51
+
52
+ it("uses the neutral SVG fallback and compact size predictably", () => {
53
+ const { container } = render(
54
+ <FileTypeIcon fileName="artifact.customtype" size="compact" />,
55
+ );
56
+
57
+ const fallback = readIcon(container, "default");
58
+ expect(fallback.classList.contains("h-3.5")).toBe(true);
59
+ expect(fallback.getAttribute("aria-hidden")).toBe("true");
60
+ });
61
+ });
@@ -80,6 +80,25 @@ describe('DocBrowserManager layout notifications', () => {
80
80
 
81
81
  expect(onRightPanelOpened).toHaveBeenCalledTimes(1);
82
82
  });
83
+
84
+ it('persists the docked global right panel width across rehydration', async () => {
85
+ const { result } = renderHook(() => useDocBrowser(), { wrapper });
86
+
87
+ act(() => {
88
+ result.current.setDockedWidth(610);
89
+ });
90
+
91
+ await waitFor(() => {
92
+ expect(readPersistedDocBrowserSnapshot()).toMatchObject({
93
+ state: { snapshot: { dockedWidth: 610 } },
94
+ });
95
+ });
96
+
97
+ const savedState = window.localStorage.getItem(docBrowserStorageKey);
98
+ await resetMemoryThenRehydrate(savedState);
99
+
100
+ expect(useDocBrowserStore.getState().snapshot.dockedWidth).toBe(610);
101
+ });
83
102
  });
84
103
 
85
104
  describe('DocBrowserProvider dedupe keys', () => {
@@ -24,6 +24,7 @@ const { docBrowserState } = vi.hoisted<{
24
24
  docBrowserState: {
25
25
  isOpen: true,
26
26
  mode: "docked" as "docked" | "floating",
27
+ dockedWidth: 420,
27
28
  tabs: [
28
29
  {
29
30
  id: "docs",
@@ -58,6 +59,7 @@ const { docBrowserState } = vi.hoisted<{
58
59
  openNewTab: vi.fn(),
59
60
  close: vi.fn(),
60
61
  toggleMode: vi.fn(),
62
+ setDockedWidth: vi.fn(),
61
63
  goBack: vi.fn(),
62
64
  goForward: vi.fn(),
63
65
  navigate: vi.fn(),
@@ -156,6 +158,20 @@ describe("DocBrowser", () => {
156
158
  expect(screen.getByTestId("resizable-right-panel-handle")).toBeTruthy();
157
159
  });
158
160
 
161
+ it("commits the resized docked width to the global browser state owner", () => {
162
+ render(<DocBrowser />);
163
+
164
+ firePointerEvent(
165
+ screen.getByTestId("resizable-right-panel-handle"),
166
+ "pointerdown",
167
+ { clientX: 800, pointerId: 1 },
168
+ );
169
+ firePointerEvent(window, "pointermove", { clientX: 620, pointerId: 1 });
170
+ firePointerEvent(window, "pointerup", { clientX: 620, pointerId: 1 });
171
+
172
+ expect(docBrowserState.setDockedWidth).toHaveBeenCalledWith(600);
173
+ });
174
+
159
175
  it("uses a full viewport panel without desktop resize controls in fullscreen mode", () => {
160
176
  render(<DocBrowser displayMode="fullscreen" />);
161
177
 
@@ -57,6 +57,7 @@ export function DocBrowserProvider({
57
57
  openNewTab: manager.openNewTab,
58
58
  close: manager.close,
59
59
  toggleMode: manager.toggleMode,
60
+ setDockedWidth: manager.setDockedWidth,
60
61
  navigate: manager.navigate,
61
62
  syncUrl: manager.syncUrl,
62
63
  goBack: manager.goBack,
@@ -28,6 +28,10 @@ import { ResizableRightPanel } from '@/shared/components/resizable-right-panel/r
28
28
  import { cn } from '@/shared/lib/utils';
29
29
  import { t } from '@/shared/lib/i18n';
30
30
  import { GripVertical } from 'lucide-react';
31
+ import {
32
+ DOC_BROWSER_DOCKED_MAX_WIDTH,
33
+ DOC_BROWSER_DOCKED_MIN_WIDTH,
34
+ } from '@/shared/components/doc-browser/utils/doc-browser-state.utils';
31
35
 
32
36
  type DocBrowserProps = {
33
37
  customTabRenderers?: DocBrowserCustomTabRenderers;
@@ -159,6 +163,7 @@ export function DocBrowser({ customTabRenderers = {}, displayMode = 'desktop', d
159
163
  const {
160
164
  isOpen,
161
165
  mode,
166
+ dockedWidth,
162
167
  tabs,
163
168
  activeTabId,
164
169
  activeHistory,
@@ -169,6 +174,7 @@ export function DocBrowser({ customTabRenderers = {}, displayMode = 'desktop', d
169
174
  openNewTab,
170
175
  close,
171
176
  toggleMode,
177
+ setDockedWidth,
172
178
  goBack,
173
179
  goForward,
174
180
  navigate,
@@ -386,9 +392,11 @@ export function DocBrowser({ customTabRenderers = {}, displayMode = 'desktop', d
386
392
  return (
387
393
  <ResizableRightPanel
388
394
  data-testid="doc-browser-panel"
389
- defaultWidth={420}
390
- minWidth={320}
391
- maxWidth={860}
395
+ defaultWidth={dockedWidth}
396
+ width={dockedWidth}
397
+ minWidth={DOC_BROWSER_DOCKED_MIN_WIDTH}
398
+ maxWidth={DOC_BROWSER_DOCKED_MAX_WIDTH}
399
+ onWidthCommit={setDockedWidth}
392
400
  >
393
401
  {panelContent}
394
402
  </ResizableRightPanel>
@@ -11,6 +11,7 @@ import {
11
11
  createDocBrowserActiveHistoryEntry,
12
12
  createDefaultDocBrowserState,
13
13
  createDocBrowserTab,
14
+ normalizeDocBrowserDockedWidth,
14
15
  } from '@/shared/components/doc-browser/utils/doc-browser-state.utils';
15
16
  import {
16
17
  DOC_BROWSER_HOME_TAB_KIND,
@@ -297,6 +298,10 @@ export class DocBrowserManager {
297
298
  this.onRightPanelOpened?.();
298
299
  };
299
300
 
301
+ readonly setDockedWidth = (width: number): void => {
302
+ this.setSnapshot((prev) => ({ ...prev, dockedWidth: normalizeDocBrowserDockedWidth(width) }));
303
+ };
304
+
300
305
  readonly navigate = (url: string): void => {
301
306
  this.setSnapshot((prev) => {
302
307
  if (!prev.tabs.length) {
@@ -8,7 +8,10 @@ import type {
8
8
  DocBrowserStateUpdate,
9
9
  DocBrowserTab,
10
10
  } from '@/shared/components/doc-browser/types/doc-browser.types';
11
- import { createDefaultDocBrowserState } from '@/shared/components/doc-browser/utils/doc-browser-state.utils';
11
+ import {
12
+ createDefaultDocBrowserState,
13
+ normalizeDocBrowserDockedWidth,
14
+ } from '@/shared/components/doc-browser/utils/doc-browser-state.utils';
12
15
 
13
16
  const DOC_BROWSER_STORAGE_KEY = 'nextclaw.doc-browser.state';
14
17
  const DOC_BROWSER_STORAGE_VERSION = 1;
@@ -168,6 +171,7 @@ function normalizePersistedDocBrowserState(value: unknown): DocBrowserState | nu
168
171
  return {
169
172
  isOpen: value.isOpen === true,
170
173
  mode: isDocBrowserMode(value.mode) ? value.mode : 'docked',
174
+ dockedWidth: normalizeDocBrowserDockedWidth(value.dockedWidth),
171
175
  tabs,
172
176
  activeTabId: resolvedActiveTabId,
173
177
  activeHistory: resolvedActiveHistory,
@@ -198,6 +202,7 @@ export const useDocBrowserStore = create<DocBrowserStore>()(
198
202
  snapshot: {
199
203
  isOpen: state.snapshot.isOpen,
200
204
  mode: state.snapshot.mode,
205
+ dockedWidth: state.snapshot.dockedWidth,
201
206
  tabs: state.snapshot.tabs.slice(-DOC_BROWSER_MAX_PERSISTED_TABS),
202
207
  activeTabId: state.snapshot.activeTabId,
203
208
  activeHistory: state.snapshot.activeHistory,
@@ -76,6 +76,7 @@ export type DocBrowserOpenOptions = {
76
76
  export type DocBrowserState = {
77
77
  isOpen: boolean;
78
78
  mode: DocBrowserMode;
79
+ dockedWidth: number;
79
80
  tabs: DocBrowserTab[];
80
81
  activeTabId: string;
81
82
  activeHistory: DocBrowserActiveHistoryEntry[];
@@ -90,6 +91,7 @@ export type DocBrowserActions = {
90
91
  openNewTab: () => void;
91
92
  close: () => void;
92
93
  toggleMode: () => void;
94
+ setDockedWidth: (width: number) => void;
93
95
  navigate: (url: string) => void;
94
96
  syncUrl: (url: string) => void;
95
97
  goBack: () => void;