@nextclaw/ui 0.15.1 → 0.15.3

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 (192) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/assets/{api-b1R9VdeH.js → api-iNBpppzj.js} +3 -3
  3. package/dist/assets/{appearance-settings-page-D2O9pch9.js → appearance-settings-page-DMffQFR3.js} +1 -1
  4. package/dist/assets/{book-open-B77lQ2Tg.js → book-open-C3mJGwiy.js} +1 -1
  5. package/dist/assets/channels-list-page-CCtp0BRS.js +8 -0
  6. package/dist/assets/chat-page-H5a0dWvO.js +1 -0
  7. package/dist/assets/config-split-page-CrOJgB-Y.js +1 -0
  8. package/dist/assets/{confirm-dialog-C-NXSioX.js → confirm-dialog-Chv50USy.js} +2 -2
  9. package/dist/assets/{desktop-update-config-usHpXugP.js → desktop-update-config-KzTuXRf_.js} +1 -1
  10. package/dist/assets/{dist-jdGmbb6w.js → dist-BunzmC4B.js} +1 -1
  11. package/dist/assets/{dist-YtoqU-AT.js → dist-ChmGwvtK.js} +1 -1
  12. package/dist/assets/doc-browser-DNRL4Zvd.js +1 -0
  13. package/dist/assets/doc-browser-Dg0pmkOg.js +1 -0
  14. package/dist/assets/doc-browser-context-BHY-0FSD.js +1 -0
  15. package/dist/assets/{es2015-bNpL2LKy.js → es2015-DQZ5JWwK.js} +1 -1
  16. package/dist/assets/{external-link-BM6fk8q-.js → external-link-CU0kzzIP.js} +1 -1
  17. package/dist/assets/form-actions-BvCp_0lS.js +1 -0
  18. package/dist/assets/{i18n-provider-CntWolRA.js → i18n-provider-Cu8XyZb_.js} +1 -1
  19. package/dist/assets/index-CzfBdvK1.css +1 -0
  20. package/dist/assets/index-DGqHne1Q.js +104 -0
  21. package/dist/assets/loader-circle-CDSFuzmk.js +1 -0
  22. package/dist/assets/mcp-marketplace-page-BeEQK0DL.js +9 -0
  23. package/dist/assets/mcp-marketplace-page-D6idOTLp.js +1 -0
  24. package/dist/assets/model-config-page-bNNo-XyG.js +1 -0
  25. package/dist/assets/plus-D4DsdLN-.js +1 -0
  26. package/dist/assets/{provider-scoped-model-input-Cm_a4GHf.js → provider-scoped-model-input-IlgQCsoT.js} +1 -1
  27. package/dist/assets/providers-config-page-wLo6CAJw.js +1 -0
  28. package/dist/assets/{react-BN8uzPnp.js → react-BPIsQIlO.js} +1 -1
  29. package/dist/assets/{refresh-cw-BpVcFPmJ.js → refresh-cw-CILd1vAP.js} +1 -1
  30. package/dist/assets/remote-uKEYp7AR.js +1 -0
  31. package/dist/assets/rotate-cw-wPFPg1kH.js +1 -0
  32. package/dist/assets/runtime-config-page-DDn1sP4G.js +1 -0
  33. package/dist/assets/{save-BpFW5fpk.js → save-ByJqcPTP.js} +1 -1
  34. package/dist/assets/{search-D0uO7enf.js → search-DVIV6pfD.js} +1 -1
  35. package/dist/assets/search-config-page-B-Cf4iiB.js +1 -0
  36. package/dist/assets/secrets-config-page-DPKsY8Iw.js +3 -0
  37. package/dist/assets/security-config-e9SSBlKr.js +1 -0
  38. package/dist/assets/{select-Bh-CrA4p.js → select-BdgczSNw.js} +1 -1
  39. package/dist/assets/{setting-row-BJfCHRcb.js → setting-row-BkTVUTKx.js} +1 -1
  40. package/dist/assets/{settings-2-Blzi1vr6.js → settings-2-CH5shGg_.js} +1 -1
  41. package/dist/assets/{side-dock-ZdaSrQVB.js → side-dock-BsBvcJ9i.js} +2 -2
  42. package/dist/assets/{skeleton-BLlEzebZ.js → skeleton-DeX1FKw7.js} +1 -1
  43. package/dist/assets/switch-Cv8Ekb3x.js +1 -0
  44. package/dist/assets/{tag-chip-CzGihiQx.js → tag-chip-RB9uKcoC.js} +1 -1
  45. package/dist/assets/{tooltip-CdcrBjrC.js → tooltip-oQlEN-l6.js} +1 -1
  46. package/dist/assets/{use-config-CVPgsBmU.js → use-config-l3W7y8Qz.js} +1 -1
  47. package/dist/assets/{use-confirm-dialog-CSeBk9SF.js → use-confirm-dialog-BAxaVOJI.js} +1 -1
  48. package/dist/assets/x-BBMffEN0.js +1 -0
  49. package/dist/index.html +38 -30
  50. package/index.html +12 -4
  51. package/package.json +9 -9
  52. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +29 -1
  53. package/src/app/components/layout/sidebar-items.tsx +46 -7
  54. package/src/app/components/layout/sidebar-rail.styles.ts +2 -2
  55. package/src/app/components/layout/sidebar.tsx +84 -62
  56. package/src/app/configs/app-navigation.config.ts +49 -0
  57. package/src/app/styles/design-system.css +99 -4
  58. package/src/features/channels/components/config/channel-form.tsx +14 -11
  59. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +40 -0
  60. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +3 -1
  61. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +1 -1
  62. package/src/features/chat/components/layout/__tests__/chat-sidebar-toolbar.test.tsx +48 -0
  63. package/src/features/chat/components/layout/__tests__/chat-sidebar-utility-menu.test.tsx +55 -0
  64. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +0 -3
  65. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +3 -4
  66. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +30 -31
  67. package/src/features/chat/components/layout/chat-sidebar.tsx +21 -7
  68. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +2 -1
  69. package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +1 -1
  70. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +22 -1
  71. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +62 -1
  72. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +10 -5
  73. package/src/features/chat/features/message/components/chat-message-list.container.tsx +47 -1
  74. package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +11 -1
  75. package/src/features/chat/features/message/utils/__tests__/chat-message-texts.utils.test.ts +23 -0
  76. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +0 -11
  77. package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +5 -0
  78. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +138 -0
  79. package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +116 -0
  80. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +108 -46
  81. package/src/features/chat/features/session/components/chat-sidebar-session-entry.tsx +6 -0
  82. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +48 -24
  83. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +0 -3
  84. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +0 -4
  85. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +2 -2
  86. package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +25 -3
  87. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +9 -4
  88. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +1 -1
  89. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +1 -1
  90. package/src/features/chat/features/welcome/components/chat-welcome.tsx +0 -5
  91. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +226 -56
  92. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +1 -0
  93. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +144 -0
  94. package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +99 -0
  95. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +2 -2
  96. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +4 -2
  97. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +232 -21
  98. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +3 -0
  99. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +44 -1
  100. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +45 -3
  101. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +4 -0
  102. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +33 -2
  103. package/src/features/chat/managers/chat-session-list.manager.ts +32 -0
  104. package/src/features/chat/managers/chat-thread.manager.ts +2 -0
  105. package/src/features/chat/stores/chat-session-list.store.ts +28 -5
  106. package/src/features/chat/stores/chat-thread.store.ts +3 -0
  107. package/src/features/cron/components/cron-config.tsx +63 -80
  108. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.tsx +44 -6
  109. package/src/features/marketplace/components/marketplace-detail-doc.tsx +8 -4
  110. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  111. package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +5 -12
  112. package/src/features/marketplace/hooks/use-marketplace-detail-doc-entry.ts +84 -0
  113. package/src/features/panel-apps/components/panel-app-list-item.tsx +6 -6
  114. package/src/features/panel-apps/components/panel-apps-list.tsx +2 -2
  115. package/src/features/panel-apps/index.ts +1 -0
  116. package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +2 -10
  117. package/src/features/panel-apps/utils/panel-app-iframe.utils.ts +6 -0
  118. package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +11 -3
  119. package/src/features/pwa/managers/pwa-shell-theme.manager.ts +3 -1
  120. package/src/features/right-panel-resources/components/right-panel-resource-home-page.tsx +1 -1
  121. package/src/features/service-apps/components/service-apps-panel.tsx +145 -35
  122. package/src/features/settings/components/config/provider-auth-section.tsx +1 -1
  123. package/src/features/settings/components/config/provider-form.tsx +11 -8
  124. package/src/features/settings/components/config/provider-models-section.tsx +7 -7
  125. package/src/features/settings/components/config/provider-pill-selector.tsx +1 -1
  126. package/src/features/settings/components/config/secrets-config-form.tsx +5 -4
  127. package/src/features/settings/pages/model-config-page.tsx +26 -25
  128. package/src/features/settings/pages/providers-config-page.tsx +10 -13
  129. package/src/features/settings/pages/search-config-page.tsx +8 -5
  130. package/src/features/side-dock/components/side-dock.tsx +3 -3
  131. package/src/features/system-status/components/config/runtime-config-editor.tsx +5 -4
  132. package/src/index.css +257 -13
  133. package/src/shared/components/actions/action-link.tsx +1 -1
  134. package/src/shared/components/config-split-page.tsx +6 -6
  135. package/src/shared/components/doc-browser/__tests__/doc-browser-url.utils.test.ts +73 -0
  136. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +48 -0
  137. package/src/shared/components/doc-browser/doc-browser-context.tsx +4 -0
  138. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +6 -2
  139. package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +2 -1
  140. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +10 -10
  141. package/src/shared/components/doc-browser/doc-browser.tsx +6 -4
  142. package/src/shared/components/doc-browser/utils/doc-browser-url.utils.ts +62 -4
  143. package/src/shared/components/resizable-right-panel/__tests__/resizable-right-panel.test.tsx +18 -0
  144. package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +28 -3
  145. package/src/shared/components/ui/actions/__tests__/icon-action-button.test.tsx +36 -0
  146. package/src/shared/components/ui/actions/form-actions.tsx +25 -0
  147. package/src/shared/components/ui/actions/icon-action-button.tsx +25 -2
  148. package/src/shared/components/ui/button.tsx +1 -1
  149. package/src/shared/components/ui/card.tsx +12 -4
  150. package/src/shared/components/ui/dialog.tsx +1 -1
  151. package/src/shared/components/ui/input.tsx +1 -1
  152. package/src/shared/components/ui/select.tsx +1 -1
  153. package/src/shared/components/ui/switch.tsx +1 -1
  154. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +2 -1
  155. package/src/shared/components/ui/tabs.tsx +1 -1
  156. package/src/shared/components/ui/textarea.tsx +1 -1
  157. package/src/shared/hooks/use-server-path-browse.ts +26 -4
  158. package/src/shared/hooks/use-server-path-read.ts +11 -1
  159. package/src/shared/lib/api/utils/server-path.utils.ts +1 -0
  160. package/src/shared/lib/i18n/locales/en-US/chat.json +99 -0
  161. package/src/shared/lib/i18n/locales/en-US/core.json +3 -0
  162. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +5 -0
  163. package/src/shared/lib/i18n/locales/zh-CN/chat.json +99 -0
  164. package/src/shared/lib/i18n/locales/zh-CN/core.json +3 -0
  165. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +5 -0
  166. package/src/shared/lib/session-project/workspace-file-breadcrumb.utils.ts +9 -5
  167. package/src/shared/lib/theme/index.test.ts +21 -0
  168. package/src/shared/lib/theme/index.ts +6 -4
  169. package/src/vite-env.d.ts +2 -0
  170. package/dist/assets/channels-list-page-DcsIQc6A.js +0 -8
  171. package/dist/assets/chat-page-CZ7WxPir.js +0 -1
  172. package/dist/assets/config-split-page-BWQ6_Raq.js +0 -1
  173. package/dist/assets/doc-browser-B_Tf9n70.js +0 -1
  174. package/dist/assets/doc-browser-BdZPFOVa.js +0 -1
  175. package/dist/assets/doc-browser-context-DwokemUn.js +0 -1
  176. package/dist/assets/index-BIcEw90u.js +0 -104
  177. package/dist/assets/index-BiNSTKTU.css +0 -1
  178. package/dist/assets/loader-circle-MBgNSVIH.js +0 -1
  179. package/dist/assets/mcp-marketplace-page-CLmUstLN.js +0 -9
  180. package/dist/assets/mcp-marketplace-page-rZkmWPMy.js +0 -1
  181. package/dist/assets/model-config-page-Di0ySJD2.js +0 -1
  182. package/dist/assets/plus-vDqW5Kxp.js +0 -1
  183. package/dist/assets/providers-config-page-BAf2W0MV.js +0 -1
  184. package/dist/assets/remote-CLQ0IBM8.js +0 -1
  185. package/dist/assets/rotate-cw-CvNBv7_w.js +0 -1
  186. package/dist/assets/runtime-config-page-CflvvoDc.js +0 -1
  187. package/dist/assets/search-config-page-D0klFUXd.js +0 -1
  188. package/dist/assets/secrets-config-page-dVZ631Wp.js +0 -3
  189. package/dist/assets/security-config-C5gnlkuj.js +0 -1
  190. package/dist/assets/switch-CFLJFNSd.js +0 -1
  191. package/dist/assets/x-BF2KQ--V.js +0 -1
  192. /package/dist/assets/{config-hints-fGnUjDe9.js → config-hints-C_iP1zl_.js} +0 -0
@@ -14,6 +14,7 @@ const serverPathBrowseMock = vi.fn();
14
14
  type RenderWorkspaceFilePreviewOptions = {
15
15
  file?: Partial<ChatWorkspaceFileTab>;
16
16
  onFileOpen?: (action: ChatFileOpenActionViewModel) => void;
17
+ refreshVersion?: number;
17
18
  sessionProjectRoot?: string | null;
18
19
  sessionWorkingDir?: string | null;
19
20
  };
@@ -82,6 +83,7 @@ function mockTextRead(overrides: TextReadDataOverrides = {}) {
82
83
  function renderWorkspaceFilePreview({
83
84
  file,
84
85
  onFileOpen = vi.fn(),
86
+ refreshVersion,
85
87
  sessionProjectRoot = "/tmp",
86
88
  sessionWorkingDir = "/tmp",
87
89
  }: RenderWorkspaceFilePreviewOptions = {}) {
@@ -90,6 +92,7 @@ function renderWorkspaceFilePreview({
90
92
  file={buildWorkspaceFile(file ?? {})}
91
93
  sessionProjectRoot={sessionProjectRoot}
92
94
  sessionWorkingDir={sessionWorkingDir}
95
+ refreshVersion={refreshVersion}
93
96
  onFileOpen={onFileOpen}
94
97
  />,
95
98
  );
@@ -97,74 +100,148 @@ function renderWorkspaceFilePreview({
97
100
  }
98
101
 
99
102
  function mockWorkspaceBreadcrumbBrowseTree() {
100
- serverPathBrowseMock.mockImplementation(
101
- ({ path }: { path?: string | null }) => ({
102
- isLoading: false,
103
- error: null,
104
- data:
105
- path === "/tmp/workspace/src/components"
106
- ? {
107
- currentPath: "/tmp/workspace/src/components",
108
- parentPath: "/tmp/workspace/src",
109
- homePath: "/Users/demo",
110
- breadcrumbs: [
111
- { label: "workspace", path: "/tmp/workspace" },
112
- { label: "src", path: "/tmp/workspace/src" },
113
- { label: "components", path: "/tmp/workspace/src/components" },
114
- ],
115
- entries: [
116
- {
117
- name: "button.tsx",
118
- path: "/tmp/workspace/src/components/button.tsx",
119
- kind: "file",
120
- hidden: false,
121
- },
122
- ],
123
- }
124
- : {
125
- currentPath: "/tmp/workspace/src",
126
- parentPath: "/tmp/workspace",
127
- homePath: "/Users/demo",
128
- breadcrumbs: [
129
- { label: "workspace", path: "/tmp/workspace" },
130
- { label: "src", path: "/tmp/workspace/src" },
131
- ],
132
- entries: [
133
- {
134
- name: "components",
135
- path: "/tmp/workspace/src/components",
136
- kind: "directory",
137
- hidden: false,
138
- },
139
- {
140
- name: "index.ts",
141
- path: "/tmp/workspace/src/index.ts",
142
- kind: "file",
143
- hidden: false,
144
- },
145
- ],
103
+ serverPathBrowseMock.mockImplementation(({ path }: { path?: string | null }) => {
104
+ if (path === "/tmp/workspace/src/components") {
105
+ return {
106
+ isLoading: false,
107
+ error: null,
108
+ data: {
109
+ currentPath: "/tmp/workspace/src/components",
110
+ parentPath: "/tmp/workspace/src",
111
+ homePath: "/Users/demo",
112
+ breadcrumbs: [
113
+ { label: "workspace", path: "/tmp/workspace" },
114
+ { label: "src", path: "/tmp/workspace/src" },
115
+ { label: "components", path: "/tmp/workspace/src/components" },
116
+ ],
117
+ entries: [
118
+ {
119
+ name: "button.tsx",
120
+ path: "/tmp/workspace/src/components/button.tsx",
121
+ kind: "file",
122
+ hidden: false,
146
123
  },
147
- }),
148
- );
124
+ ],
125
+ },
126
+ };
127
+ }
128
+
129
+ if (path === "/tmp/workspace/src") {
130
+ return {
131
+ isLoading: false,
132
+ error: null,
133
+ data: {
134
+ currentPath: "/tmp/workspace/src",
135
+ parentPath: "/tmp/workspace",
136
+ homePath: "/Users/demo",
137
+ breadcrumbs: [
138
+ { label: "workspace", path: "/tmp/workspace" },
139
+ { label: "src", path: "/tmp/workspace/src" },
140
+ ],
141
+ entries: [
142
+ {
143
+ name: "components",
144
+ path: "/tmp/workspace/src/components",
145
+ kind: "directory",
146
+ hidden: false,
147
+ },
148
+ {
149
+ name: "index.ts",
150
+ path: "/tmp/workspace/src/index.ts",
151
+ kind: "file",
152
+ hidden: false,
153
+ },
154
+ ],
155
+ },
156
+ };
157
+ }
158
+
159
+ return {
160
+ isLoading: false,
161
+ error: new Error("server path must point to a directory"),
162
+ data: null,
163
+ };
164
+ });
149
165
  }
150
166
 
151
167
  beforeEach(() => {
152
168
  serverPathReadMock.mockReset();
153
169
  serverPathBrowseMock.mockReset();
154
- serverPathBrowseMock.mockReturnValue({
170
+ serverPathReadMock.mockReturnValue({
155
171
  isLoading: false,
156
172
  error: null,
157
- data: {
158
- currentPath: "/tmp",
159
- parentPath: null,
160
- homePath: "/Users/demo",
161
- breadcrumbs: [],
162
- entries: [],
163
- },
173
+ data: null,
174
+ });
175
+ serverPathBrowseMock.mockReturnValue({
176
+ isLoading: false,
177
+ error: new Error("server path must point to a directory"),
178
+ data: null,
164
179
  });
165
180
  });
166
181
 
167
182
  describe("ChatSessionWorkspaceFilePreview rendering", () => {
183
+ it("renders attachment content URLs as workspace-native media previews", () => {
184
+ renderWorkspaceFilePreview({
185
+ file: {
186
+ key: "attachment-image",
187
+ path: "photo.png",
188
+ label: "photo.png",
189
+ viewMode: "preview",
190
+ contentUrl: "/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2Fphoto",
191
+ mimeType: "image/png",
192
+ },
193
+ });
194
+
195
+ expect(serverPathReadMock).toHaveBeenCalledWith(
196
+ expect.objectContaining({ enabled: false }),
197
+ );
198
+ expect(serverPathBrowseMock).toHaveBeenCalledWith(
199
+ expect.objectContaining({ enabled: false }),
200
+ );
201
+ const image = screen.getByTestId("workspace-content-image");
202
+ expect(image.getAttribute("src")).toBe(
203
+ "/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2Fphoto",
204
+ );
205
+ expect(image.getAttribute("alt")).toBe("photo.png");
206
+ });
207
+
208
+ it("offers download and system open actions for unsupported office attachments", () => {
209
+ renderWorkspaceFilePreview({
210
+ file: {
211
+ key: "attachment-docx",
212
+ path: "overview.zh-CN.docx",
213
+ label: "overview.zh-CN.docx",
214
+ viewMode: "preview",
215
+ contentUrl: "/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2Foverview",
216
+ mimeType:
217
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
218
+ },
219
+ });
220
+
221
+ expect(screen.getByTestId("workspace-content-unsupported")).toBeTruthy();
222
+ expect(
223
+ screen.getByTestId("workspace-content-unsupported").textContent,
224
+ ).toContain("overview.zh-CN.docx");
225
+ expect(screen.getByText(t("chatWorkspacePreviewUnsupported"))).toBeTruthy();
226
+ expect(screen.getByText(t("chatWorkspacePreviewUnsupportedHint"))).toBeTruthy();
227
+
228
+ const download = screen.getByRole("link", {
229
+ name: t("chatWorkspacePreviewDownload"),
230
+ });
231
+ expect(download.getAttribute("href")).toBe(
232
+ "/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2Foverview",
233
+ );
234
+ expect(download.getAttribute("download")).toBe("overview.zh-CN.docx");
235
+
236
+ const openExternally = screen.getByRole("link", {
237
+ name: t("chatWorkspacePreviewOpenExternally"),
238
+ });
239
+ expect(openExternally.getAttribute("href")).toBe(
240
+ "/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2Foverview",
241
+ );
242
+ expect(openExternally.getAttribute("target")).toBe("_blank");
243
+ });
244
+
168
245
  it("renders preview files inside a full-height workspace code surface", () => {
169
246
  mockTextRead();
170
247
  renderWorkspaceFilePreview();
@@ -229,6 +306,26 @@ describe("ChatSessionWorkspaceFilePreview rendering", () => {
229
306
  expect(screen.queryByTestId("file-code-surface")).toBeNull();
230
307
  });
231
308
 
309
+ it("adds the refresh version to rendered HTML iframe URLs", () => {
310
+ mockTextRead({
311
+ resolvedPath: "/tmp/example.html",
312
+ text: "<!doctype html><h1>Hello</h1>",
313
+ });
314
+ renderWorkspaceFilePreview({
315
+ file: {
316
+ label: "example.html",
317
+ path: "/tmp/example.html",
318
+ previewViewer: "rendered",
319
+ viewMode: "preview",
320
+ },
321
+ refreshVersion: 3,
322
+ });
323
+
324
+ expect(screen.getByTestId("workspace-html-preview").getAttribute("src")).toContain(
325
+ "refresh=3",
326
+ );
327
+ });
328
+
232
329
  it("keeps HTML files in the source preview when source is requested", () => {
233
330
  mockTextRead({
234
331
  resolvedPath: "/tmp/example.html",
@@ -291,6 +388,79 @@ describe("ChatSessionWorkspaceFilePreview rendering", () => {
291
388
  expect(screen.getByText("tmp")).toBeTruthy();
292
389
  expect(screen.getByText("example.ts")).toBeTruthy();
293
390
  });
391
+
392
+ it("renders directory entries and opens child directories or files", () => {
393
+ serverPathReadMock.mockReturnValue({
394
+ isLoading: false,
395
+ error: new Error("server path must point to a file"),
396
+ data: null,
397
+ });
398
+ serverPathBrowseMock.mockReturnValue({
399
+ isLoading: false,
400
+ error: null,
401
+ data: {
402
+ currentPath: "/tmp/workspace/src",
403
+ parentPath: "/tmp/workspace",
404
+ homePath: "/Users/demo",
405
+ breadcrumbs: [
406
+ { label: "workspace", path: "/tmp/workspace" },
407
+ { label: "src", path: "/tmp/workspace/src" },
408
+ ],
409
+ entries: [
410
+ {
411
+ name: "components",
412
+ path: "/tmp/workspace/src/components",
413
+ kind: "directory",
414
+ hidden: false,
415
+ },
416
+ {
417
+ name: "index.ts",
418
+ path: "/tmp/workspace/src/index.ts",
419
+ kind: "file",
420
+ hidden: false,
421
+ },
422
+ ],
423
+ },
424
+ });
425
+ const onFileOpen = vi.fn();
426
+
427
+ renderWorkspaceFilePreview({
428
+ file: {
429
+ path: "src",
430
+ label: "src",
431
+ viewMode: "preview",
432
+ },
433
+ onFileOpen,
434
+ sessionProjectRoot: "/tmp/workspace",
435
+ sessionWorkingDir: "/tmp/workspace",
436
+ });
437
+
438
+ expect(serverPathBrowseMock).toHaveBeenCalledWith({
439
+ path: "src",
440
+ basePath: "/tmp/workspace",
441
+ includeFiles: true,
442
+ enabled: true,
443
+ });
444
+ expect(screen.getByTestId("workspace-directory-browser")).toBeTruthy();
445
+
446
+ fireEvent.click(
447
+ screen.getByRole("button", { name: "Open directory: components" }),
448
+ );
449
+ expect(onFileOpen).toHaveBeenCalledWith({
450
+ path: "/tmp/workspace/src/components",
451
+ label: "components",
452
+ viewMode: "preview",
453
+ });
454
+
455
+ fireEvent.click(
456
+ screen.getByRole("button", { name: "Open file: index.ts" }),
457
+ );
458
+ expect(onFileOpen).toHaveBeenCalledWith({
459
+ path: "/tmp/workspace/src/index.ts",
460
+ label: "index.ts",
461
+ viewMode: "preview",
462
+ });
463
+ });
294
464
  });
295
465
 
296
466
  describe("ChatSessionWorkspaceFilePreview breadcrumbs", () => {
@@ -40,6 +40,7 @@ it("shows compact child session metadata without repeating the tab title", () =>
40
40
  kind: "child-session",
41
41
  tab: createChildTab(),
42
42
  }}
43
+ filePreviewRefreshVersion={0}
43
44
  sessionCronJobs={[]}
44
45
  sessionProjectRoot={null}
45
46
  sessionWorkingDir={null}
@@ -0,0 +1,144 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+ import { ChatSessionWorkspacePanel } from "@/features/chat/features/workspace/components/chat-session-workspace-panel";
5
+ import type { ChatWorkspaceFileTab } from "@/features/chat/stores/chat-thread.store";
6
+ import type * as ReactQuery from "@tanstack/react-query";
7
+
8
+ const mocks = vi.hoisted(() => ({
9
+ syncVisibleWorkspaceSelection: vi.fn(),
10
+ selectChildSessionDetail: vi.fn(),
11
+ selectWorkspaceFile: vi.fn(),
12
+ closeWorkspaceFile: vi.fn(),
13
+ openSessionCronPanel: vi.fn(),
14
+ goBackWorkspacePanel: vi.fn(),
15
+ goForwardWorkspacePanel: vi.fn(),
16
+ closeWorkspacePanel: vi.fn(),
17
+ invalidateQueries: vi.fn(),
18
+ }));
19
+
20
+ vi.mock("@tanstack/react-query", async (importOriginal) => ({
21
+ ...(await importOriginal<typeof ReactQuery>()),
22
+ useQueryClient: () => ({
23
+ invalidateQueries: mocks.invalidateQueries,
24
+ }),
25
+ }));
26
+
27
+ vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
28
+ usePresenter: () => ({
29
+ chatThreadManager: mocks,
30
+ }),
31
+ }));
32
+
33
+ vi.mock("@/features/chat/features/ncp/hooks/use-ncp-child-session-tabs-view", () => ({
34
+ useNcpChildSessionTabsView: () => [],
35
+ }));
36
+
37
+ vi.mock("@/features/chat/features/workspace/components/chat-session-workspace-panel-content", () => ({
38
+ ChatSessionWorkspacePanelContent: ({
39
+ activeSelection,
40
+ filePreviewRefreshVersion,
41
+ }: {
42
+ activeSelection: { kind: string };
43
+ filePreviewRefreshVersion: number;
44
+ }) => (
45
+ <div
46
+ data-file-refresh-version={filePreviewRefreshVersion}
47
+ data-testid="workspace-panel-content"
48
+ >
49
+ {activeSelection.kind}
50
+ </div>
51
+ ),
52
+ }));
53
+
54
+ function createWorkspaceFileTab(): ChatWorkspaceFileTab {
55
+ return {
56
+ key: "file-tab",
57
+ parentSessionKey: "session-1",
58
+ path: "README.md",
59
+ viewMode: "preview",
60
+ };
61
+ }
62
+
63
+ function renderPanel(displayMode: "docked" | "overlay" = "docked") {
64
+ const fileTab = createWorkspaceFileTab();
65
+
66
+ return render(
67
+ <ChatSessionWorkspacePanel
68
+ sessionKey="session-1"
69
+ childSessionTabs={[]}
70
+ activeChildSessionKey={null}
71
+ activeSideChatDraft={null}
72
+ workspaceFileTabs={[fileTab]}
73
+ activeWorkspaceFileKey={fileTab.key}
74
+ workspaceNavigationHistory={[{ kind: "file", key: fileTab.key }]}
75
+ workspaceNavigationHistoryIndex={0}
76
+ activePanelKind="file"
77
+ sessionCronJobs={[]}
78
+ sessionProjectRoot={null}
79
+ sessionWorkingDir={null}
80
+ displayMode={displayMode}
81
+ />,
82
+ );
83
+ }
84
+
85
+ describe("ChatSessionWorkspacePanel", () => {
86
+ beforeEach(() => {
87
+ vi.clearAllMocks();
88
+ });
89
+
90
+ it("refreshes the active workspace file from the top action bar", async () => {
91
+ const user = userEvent.setup();
92
+ renderPanel();
93
+
94
+ await user.click(screen.getByRole("button", { name: "Refresh preview" }));
95
+
96
+ expect(mocks.invalidateQueries).toHaveBeenCalledWith({
97
+ queryKey: ["server-path-read", "README.md", null],
98
+ });
99
+ expect(mocks.invalidateQueries).toHaveBeenCalledWith({
100
+ queryKey: ["server-path-browse", "README.md", "", true],
101
+ });
102
+ expect(
103
+ screen
104
+ .getByTestId("workspace-panel-content")
105
+ .getAttribute("data-file-refresh-version"),
106
+ ).toBe("1");
107
+ });
108
+
109
+ it("maximizes and restores the docked workspace panel within its container", async () => {
110
+ const user = userEvent.setup();
111
+ renderPanel();
112
+
113
+ const panel = screen.getByTestId("chat-session-workspace-panel");
114
+ expect(panel.className).not.toContain("absolute");
115
+
116
+ await user.click(
117
+ screen.getByRole("button", { name: "Maximize workspace panel" }),
118
+ );
119
+
120
+ expect(panel.className).toContain("absolute");
121
+ expect(panel.className).not.toContain("fixed");
122
+ expect(
123
+ screen.queryByTestId("resizable-right-panel-handle"),
124
+ ).toBeNull();
125
+
126
+ await user.click(
127
+ screen.getByRole("button", { name: "Restore workspace panel" }),
128
+ );
129
+
130
+ expect(panel.className).not.toContain("absolute");
131
+ expect(screen.getByTestId("resizable-right-panel-handle")).toBeTruthy();
132
+ });
133
+
134
+ it("does not show a redundant maximize action in viewport overlay mode", () => {
135
+ renderPanel("overlay");
136
+
137
+ expect(
138
+ screen.queryByRole("button", { name: "Maximize workspace panel" }),
139
+ ).toBeNull();
140
+ expect(screen.getByTestId("chat-session-workspace-panel").className).toContain(
141
+ "fixed",
142
+ );
143
+ });
144
+ });
@@ -0,0 +1,99 @@
1
+ import type { ReactNode } from "react";
2
+ import type { ChatFileOpenActionViewModel } from "@nextclaw/agent-chat-ui";
3
+ import { ChevronRight, FileCode2, Folder } from "lucide-react";
4
+ import type { useServerPathBrowse } from "@/shared/hooks/use-server-path-browse";
5
+ import type { ServerPathEntryView } from "@/shared/lib/api";
6
+ import { t } from "@/shared/lib/i18n";
7
+ import { cn } from "@/shared/lib/utils";
8
+
9
+ type ChatSessionWorkspaceDirectoryBrowserProps = {
10
+ browseQuery: ReturnType<typeof useServerPathBrowse>;
11
+ onFileOpen: (action: ChatFileOpenActionViewModel) => void;
12
+ renderStatus: (params: {
13
+ text: string;
14
+ tone?: "muted" | "error";
15
+ }) => ReactNode;
16
+ };
17
+
18
+ function WorkspaceDirectoryEntryIcon({
19
+ kind,
20
+ }: {
21
+ kind: ServerPathEntryView["kind"];
22
+ }) {
23
+ return kind === "directory" ? (
24
+ <Folder className="h-4 w-4 shrink-0 text-emerald-600" />
25
+ ) : (
26
+ <FileCode2 className="h-4 w-4 shrink-0 text-sky-600" />
27
+ );
28
+ }
29
+
30
+ function buildDirectoryEntryLabel(entry: ServerPathEntryView): string {
31
+ const actionLabel =
32
+ entry.kind === "directory"
33
+ ? t("chatWorkspaceOpenDirectory")
34
+ : t("chatWorkspaceOpenFile");
35
+ return `${actionLabel}: ${entry.name}`;
36
+ }
37
+
38
+ export function ChatSessionWorkspaceDirectoryBrowser({
39
+ browseQuery,
40
+ onFileOpen,
41
+ renderStatus,
42
+ }: ChatSessionWorkspaceDirectoryBrowserProps) {
43
+ const entries = browseQuery.data?.entries ?? [];
44
+ const errorMessage = browseQuery.error
45
+ ? browseQuery.error instanceof Error
46
+ ? browseQuery.error.message
47
+ : String(browseQuery.error)
48
+ : null;
49
+ const openEntry = (entry: ServerPathEntryView) => {
50
+ onFileOpen({
51
+ path: entry.path,
52
+ label: entry.name,
53
+ viewMode: "preview",
54
+ });
55
+ };
56
+
57
+ if (browseQuery.isLoading && !browseQuery.data) {
58
+ return renderStatus({ text: t("chatWorkspaceLoadingDirectory") });
59
+ }
60
+
61
+ if (errorMessage && !browseQuery.data) {
62
+ return renderStatus({
63
+ tone: "error",
64
+ text: `${t("chatWorkspaceDirectoryLoadFailed")}: ${errorMessage}`,
65
+ });
66
+ }
67
+
68
+ if (entries.length === 0) {
69
+ return renderStatus({ text: t("chatWorkspaceDirectoryEmpty") });
70
+ }
71
+
72
+ return (
73
+ <div
74
+ data-testid="workspace-directory-browser"
75
+ className="h-full overflow-auto bg-white px-2 py-2 custom-scrollbar"
76
+ >
77
+ <div className="space-y-0.5">
78
+ {entries.map((entry) => (
79
+ <button
80
+ key={entry.path}
81
+ type="button"
82
+ aria-label={buildDirectoryEntryLabel(entry)}
83
+ className={cn(
84
+ "flex h-8 w-full min-w-0 items-center gap-2 rounded-sm px-2 text-left text-sm text-gray-700 transition-colors hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border",
85
+ entry.hidden ? "opacity-65" : null,
86
+ )}
87
+ onClick={() => openEntry(entry)}
88
+ >
89
+ <WorkspaceDirectoryEntryIcon kind={entry.kind} />
90
+ <span className="min-w-0 flex-1 truncate">{entry.name}</span>
91
+ {entry.kind === "directory" ? (
92
+ <ChevronRight className="h-3.5 w-3.5 shrink-0 text-gray-300" />
93
+ ) : null}
94
+ </button>
95
+ ))}
96
+ </div>
97
+ </div>
98
+ );
99
+ }
@@ -67,7 +67,7 @@ export function WorkspaceBreadcrumbBrowser({
67
67
  <Fragment key={breadcrumb.path}>
68
68
  <button
69
69
  type="button"
70
- className="h-5 rounded-sm px-1 text-left hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
70
+ className="h-5 rounded-sm px-1 text-left hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
71
71
  onClick={() => onBrowsePathChange(breadcrumb.path)}
72
72
  >
73
73
  {breadcrumb.label}
@@ -97,7 +97,7 @@ export function WorkspaceBreadcrumbBrowser({
97
97
  key={entry.path}
98
98
  type="button"
99
99
  className={cn(
100
- "flex h-6 w-full items-center gap-1.5 rounded-sm px-1.5 text-left text-[11px] text-gray-700 transition-colors hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40",
100
+ "flex h-6 w-full items-center gap-1.5 rounded-sm px-1.5 text-left text-[11px] text-gray-700 transition-colors hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border",
101
101
  entry.hidden ? "opacity-65" : null,
102
102
  )}
103
103
  onClick={() => openEntry(entry)}
@@ -1,6 +1,6 @@
1
1
  import { Fragment, useState } from "react";
2
2
  import type { ChatFileOpenActionViewModel } from "@nextclaw/agent-chat-ui";
3
- import { ChevronRight, FileCode2, FolderTree } from "lucide-react";
3
+ import { ChevronRight, FileCode2, Folder, FolderTree } from "lucide-react";
4
4
  import { Popover, PopoverContent, PopoverTrigger } from "@/shared/components/ui/popover";
5
5
  import { WorkspaceBreadcrumbBrowser } from "./chat-session-workspace-file-breadcrumb-browser";
6
6
  import type {
@@ -48,7 +48,7 @@ function WorkspaceBreadcrumbSegmentButton({
48
48
  type="button"
49
49
  aria-current={segment.isCurrent ? "page" : undefined}
50
50
  className={cn(
51
- "inline-flex h-5 items-center gap-1 rounded-sm px-1 text-[11px] leading-none transition-colors hover:bg-gray-200/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40",
51
+ "inline-flex h-5 items-center gap-1 rounded-sm px-1 text-[11px] leading-none transition-colors hover:bg-gray-200/80 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border",
52
52
  segment.kind === "workspace"
53
53
  ? "bg-primary/8 text-primary hover:bg-primary/12"
54
54
  : segment.isCurrent
@@ -59,6 +59,8 @@ function WorkspaceBreadcrumbSegmentButton({
59
59
  >
60
60
  {segment.kind === "workspace" ? (
61
61
  <FolderTree className="h-3 w-3 shrink-0" />
62
+ ) : segment.kind === "directory" && segment.isCurrent ? (
63
+ <Folder className="h-3 w-3 shrink-0" />
62
64
  ) : segment.isCurrent ? (
63
65
  <FileCode2 className="h-3 w-3 shrink-0" />
64
66
  ) : null}