@nextclaw/ui 0.15.2 → 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 (212) hide show
  1. package/CHANGELOG.md +41 -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-Ba2f7YiG.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-DxuPaDoK.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-BztL7P_T.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-eT8BF-8B.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-Cb2g0BOi.js → save-BABzyrjB.js} +1 -1
  43. package/dist/assets/{search-BcHjwdsb.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-2DWauOjY.js → settings-2-D8WWcBnw.js} +1 -1
  50. package/dist/assets/{skeleton-P5oM14E1.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 +14 -9
  61. package/src/app/components/layout/sidebar-items.tsx +3 -3
  62. package/src/app/components/layout/sidebar-rail.styles.ts +2 -2
  63. package/src/app/components/layout/sidebar.tsx +1 -1
  64. package/src/app/styles/design-system.css +17 -4
  65. package/src/features/chat/components/__tests__/chat-popover-content.test.tsx +41 -0
  66. package/src/features/chat/components/chat-popover-content.tsx +26 -0
  67. package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +13 -0
  68. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +21 -11
  69. package/src/features/chat/components/conversation/__tests__/chat-conversation-header.test.tsx +2 -2
  70. package/src/features/chat/components/conversation/__tests__/chat-conversation-workspace-section.test.tsx +28 -1
  71. package/src/features/chat/components/conversation/chat-conversation-content.tsx +5 -4
  72. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +9 -18
  73. package/src/features/chat/components/conversation/chat-conversation-track.tsx +35 -0
  74. package/src/features/chat/components/conversation/chat-conversation-workspace-section.tsx +1 -0
  75. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +7 -7
  76. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +4 -15
  77. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +20 -1
  78. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +69 -54
  79. package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +1 -1
  80. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +62 -1
  81. package/src/features/chat/features/message/components/chat-message-list.container.tsx +63 -2
  82. package/src/features/chat/features/message/utils/__tests__/chat-message-file-operation.utils.test.ts +30 -0
  83. package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +11 -1
  84. package/src/features/chat/features/message/utils/__tests__/chat-message-texts.utils.test.ts +23 -0
  85. package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +5 -0
  86. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +138 -0
  87. package/src/features/chat/features/session/components/__tests__/chat-sidebar-session-item.test.tsx +37 -0
  88. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +5 -4
  89. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +3 -8
  90. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +65 -20
  91. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +26 -28
  92. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -3
  93. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +5 -5
  94. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +4 -4
  95. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +4 -4
  96. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +463 -99
  97. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +113 -2
  98. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +1 -0
  99. package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +197 -44
  100. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +11 -5
  101. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +8 -6
  102. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +99 -67
  103. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +239 -1
  104. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +56 -6
  105. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +29 -7
  106. package/src/features/chat/features/workspace/components/file-content-preview/use-workspace-file-buffer.ts +42 -0
  107. package/src/features/chat/features/workspace/components/file-content-preview/workspace-document-preview-state.tsx +18 -0
  108. package/src/features/chat/features/workspace/components/file-content-preview/workspace-docx-preview.css +89 -0
  109. package/src/features/chat/features/workspace/components/file-content-preview/workspace-docx-preview.tsx +87 -0
  110. package/src/features/chat/features/workspace/components/file-content-preview/workspace-presentation-preview.tsx +82 -0
  111. package/src/features/chat/features/workspace/components/file-content-preview/workspace-spreadsheet-preview.tsx +180 -0
  112. package/src/features/chat/features/workspace/components/workspace-file-content-preview.tsx +191 -0
  113. package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +1 -4
  114. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +53 -10
  115. package/src/features/chat/features/workspace/utils/chat-thread-workspace-session.utils.ts +68 -2
  116. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +21 -0
  117. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab.utils.ts +63 -0
  118. package/src/features/chat/features/workspace/utils/chat-workspace-file-viewer.utils.ts +49 -0
  119. package/src/features/chat/features/workspace/utils/chat-workspace-panel-layout.utils.ts +13 -0
  120. package/src/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils.ts +120 -38
  121. package/src/features/chat/index.ts +1 -0
  122. package/src/features/chat/managers/__tests__/chat-thread-workspace.manager.test.ts +76 -0
  123. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +44 -60
  124. package/src/features/chat/managers/chat-thread.manager.ts +123 -124
  125. package/src/features/chat/pages/ncp-chat-page.tsx +1 -2
  126. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +78 -1
  127. package/src/features/chat/stores/chat-message-layout.store.test.ts +37 -0
  128. package/src/features/chat/stores/chat-message-layout.store.ts +41 -0
  129. package/src/features/chat/stores/chat-thread.store.ts +73 -13
  130. package/src/features/cron/components/cron-config.tsx +63 -80
  131. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.tsx +44 -6
  132. package/src/features/marketplace/components/marketplace-detail-doc.tsx +8 -4
  133. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  134. package/src/features/marketplace/hooks/use-marketplace-detail-doc-entry.ts +84 -0
  135. package/src/features/panel-apps/components/panel-apps-list.tsx +2 -2
  136. package/src/features/right-panel-resources/components/right-panel-resource-home-page.tsx +1 -1
  137. package/src/features/settings/pages/__tests__/appearance-settings-page.test.tsx +25 -8
  138. package/src/features/settings/pages/appearance-settings-page.tsx +92 -13
  139. package/src/features/side-dock/components/side-dock.tsx +3 -3
  140. package/src/index.css +275 -15
  141. package/src/shared/components/__tests__/file-type-icon.test.tsx +61 -0
  142. package/src/shared/components/actions/action-link.tsx +1 -1
  143. package/src/shared/components/doc-browser/__tests__/doc-browser-context.test.tsx +19 -0
  144. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +16 -0
  145. package/src/shared/components/doc-browser/doc-browser-context.tsx +1 -0
  146. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +1 -1
  147. package/src/shared/components/doc-browser/doc-browser.tsx +11 -3
  148. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +5 -0
  149. package/src/shared/components/doc-browser/stores/doc-browser.store.ts +6 -1
  150. package/src/shared/components/doc-browser/types/doc-browser.types.ts +2 -0
  151. package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +11 -0
  152. package/src/shared/components/file-type-icon.tsx +272 -0
  153. package/src/shared/components/resizable-right-panel/__tests__/resizable-right-panel.test.tsx +22 -0
  154. package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +17 -2
  155. package/src/shared/components/ui/actions/icon-action-button.tsx +1 -1
  156. package/src/shared/components/ui/button.tsx +1 -1
  157. package/src/shared/components/ui/dialog.tsx +1 -1
  158. package/src/shared/components/ui/input.tsx +1 -1
  159. package/src/shared/components/ui/select.tsx +1 -1
  160. package/src/shared/components/ui/switch.tsx +1 -1
  161. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +38 -0
  162. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +82 -2
  163. package/src/shared/components/ui/tabs.tsx +1 -1
  164. package/src/shared/components/ui/textarea.tsx +1 -1
  165. package/src/shared/lib/api/__tests__/server-path.utils.test.ts +28 -0
  166. package/src/shared/lib/api/utils/server-path.utils.ts +19 -20
  167. package/src/shared/lib/i18n/locales/en-US/chat.json +99 -0
  168. package/src/shared/lib/i18n/locales/en-US/core.json +6 -0
  169. package/src/shared/lib/i18n/locales/zh-CN/chat.json +99 -0
  170. package/src/shared/lib/i18n/locales/zh-CN/core.json +6 -0
  171. package/dist/assets/api-B7Qigvtm.js +0 -13
  172. package/dist/assets/appearance-settings-page-CuE0Iiqj.js +0 -1
  173. package/dist/assets/channels-list-page-CTsM6TcP.js +0 -8
  174. package/dist/assets/chat-page-C_5dcuE3.js +0 -1
  175. package/dist/assets/config-split-page-Fbb4PtTf.js +0 -1
  176. package/dist/assets/confirm-dialog-D2EG3nqE.js +0 -5
  177. package/dist/assets/desktop-update-config-BGR3HUi1.js +0 -1
  178. package/dist/assets/dist-7JTjycxE.js +0 -1
  179. package/dist/assets/dist-wXLhk4hl.js +0 -1
  180. package/dist/assets/doc-browser-bHyXnxlj.js +0 -1
  181. package/dist/assets/doc-browser-context-CYAGm5d5.js +0 -1
  182. package/dist/assets/doc-browser-gTrQ3jCF.js +0 -1
  183. package/dist/assets/es2015-j1SRCIX5.js +0 -41
  184. package/dist/assets/i18n-provider-B4oMxOFx.js +0 -1
  185. package/dist/assets/index-CvrvblgX.css +0 -1
  186. package/dist/assets/index-Obd4JYU2.js +0 -104
  187. package/dist/assets/loader-circle-CdzgK4eH.js +0 -1
  188. package/dist/assets/mcp-marketplace-page-Bt7CuOoq.js +0 -9
  189. package/dist/assets/mcp-marketplace-page-Ck3t78_n.js +0 -1
  190. package/dist/assets/model-config-page-DeKk9gvE.js +0 -1
  191. package/dist/assets/plus-Bx7wQbhG.js +0 -1
  192. package/dist/assets/provider-scoped-model-input-DsH2LF1q.js +0 -1
  193. package/dist/assets/providers-config-page-BFnGp68F.js +0 -1
  194. package/dist/assets/react-UirzUuST.js +0 -8
  195. package/dist/assets/remote-CDkHZtA8.js +0 -1
  196. package/dist/assets/rotate-cw-1JqxHTuw.js +0 -1
  197. package/dist/assets/runtime-config-page-BelAqiNG.js +0 -1
  198. package/dist/assets/search-config-page-zPm6--Ig.js +0 -1
  199. package/dist/assets/secrets-config-page-Cxyp-KuQ.js +0 -3
  200. package/dist/assets/security-config-B_gCq9fn.js +0 -1
  201. package/dist/assets/select-CdSEOwVM.js +0 -1
  202. package/dist/assets/setting-row-CTAtGexr.js +0 -1
  203. package/dist/assets/side-dock-DPvakJ0a.js +0 -3
  204. package/dist/assets/switch-BU_UEULb.js +0 -1
  205. package/dist/assets/tag-chip-D-WsKoT1.js +0 -1
  206. package/dist/assets/tooltip-CjkOaHW7.js +0 -1
  207. package/dist/assets/use-config--hx5oxQn.js +0 -1
  208. package/dist/assets/use-confirm-dialog-CD9a_SK1.js +0 -1
  209. package/dist/assets/x-DT5x45JU.js +0 -1
  210. /package/dist/assets/{config-hints-C_iP1zl_.js → config-hints-fDrYfl0l.js} +0 -0
  211. /package/dist/assets/{host-capabilities-9bB_998Z.js → host-capabilities-zZvY9U63.js} +0 -0
  212. /package/dist/assets/{middleware-BILWbJE4.js → middleware-BLMHv9YF.js} +0 -0
@@ -1,5 +1,5 @@
1
- import { fireEvent, render, screen } from "@testing-library/react";
2
- import { beforeEach, describe, expect, it, vi } from "vitest";
1
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import type {
4
4
  ChatFileOpenActionViewModel,
5
5
  ChatFileOperationBlockViewModel,
@@ -10,6 +10,11 @@ import { t } from "@/shared/lib/i18n";
10
10
 
11
11
  const serverPathReadMock = vi.fn();
12
12
  const serverPathBrowseMock = vi.fn();
13
+ const renderDocxMock = vi.fn();
14
+ const readSpreadsheetMock = vi.fn();
15
+ const sheetToJsonMock = vi.fn();
16
+ const openPresentationMock = vi.fn();
17
+ const destroyPresentationMock = vi.fn();
13
18
 
14
19
  type RenderWorkspaceFilePreviewOptions = {
15
20
  file?: Partial<ChatWorkspaceFileTab>;
@@ -20,6 +25,7 @@ type RenderWorkspaceFilePreviewOptions = {
20
25
  };
21
26
 
22
27
  type TextReadDataOverrides = {
28
+ kind?: "text" | "markdown" | "binary";
23
29
  languageHint?: string;
24
30
  resolvedPath?: string;
25
31
  text?: string;
@@ -53,6 +59,24 @@ vi.mock("@nextclaw/agent-chat-ui", () => ({
53
59
  ),
54
60
  }));
55
61
 
62
+ vi.mock("docx-preview", () => ({
63
+ renderAsync: (...args: unknown[]) => renderDocxMock(...args),
64
+ }));
65
+
66
+ vi.mock("xlsx", () => ({
67
+ read: (...args: unknown[]) => readSpreadsheetMock(...args),
68
+ utils: {
69
+ sheet_to_json: (...args: unknown[]) => sheetToJsonMock(...args),
70
+ },
71
+ }));
72
+
73
+ vi.mock("@aiden0z/pptx-renderer", () => ({
74
+ PptxViewer: {
75
+ open: (...args: unknown[]) => openPresentationMock(...args),
76
+ },
77
+ RECOMMENDED_ZIP_LIMITS: { maxEntries: 4_000 },
78
+ }));
79
+
56
80
  function buildWorkspaceFile(
57
81
  overrides: Partial<ChatWorkspaceFileTab>,
58
82
  ): ChatWorkspaceFileTab {
@@ -87,7 +111,7 @@ function renderWorkspaceFilePreview({
87
111
  sessionProjectRoot = "/tmp",
88
112
  sessionWorkingDir = "/tmp",
89
113
  }: RenderWorkspaceFilePreviewOptions = {}) {
90
- render(
114
+ const view = render(
91
115
  <ChatSessionWorkspaceFilePreview
92
116
  file={buildWorkspaceFile(file ?? {})}
93
117
  sessionProjectRoot={sessionProjectRoot}
@@ -96,77 +120,111 @@ function renderWorkspaceFilePreview({
96
120
  onFileOpen={onFileOpen}
97
121
  />,
98
122
  );
99
- return { onFileOpen };
123
+ return { ...view, onFileOpen };
100
124
  }
101
125
 
102
126
  function mockWorkspaceBreadcrumbBrowseTree() {
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,
123
- },
124
- ],
125
- },
126
- };
127
- }
127
+ serverPathBrowseMock.mockImplementation(
128
+ ({ path }: { path?: string | null }) => {
129
+ if (path === "/tmp/workspace/src/components") {
130
+ return {
131
+ isLoading: false,
132
+ error: null,
133
+ data: {
134
+ currentPath: "/tmp/workspace/src/components",
135
+ parentPath: "/tmp/workspace/src",
136
+ homePath: "/Users/demo",
137
+ breadcrumbs: [
138
+ { label: "workspace", path: "/tmp/workspace" },
139
+ { label: "src", path: "/tmp/workspace/src" },
140
+ { label: "components", path: "/tmp/workspace/src/components" },
141
+ ],
142
+ entries: [
143
+ {
144
+ name: "button.tsx",
145
+ path: "/tmp/workspace/src/components/button.tsx",
146
+ kind: "file",
147
+ hidden: false,
148
+ },
149
+ ],
150
+ },
151
+ };
152
+ }
153
+
154
+ if (path === "/tmp/workspace/src") {
155
+ return {
156
+ isLoading: false,
157
+ error: null,
158
+ data: {
159
+ currentPath: "/tmp/workspace/src",
160
+ parentPath: "/tmp/workspace",
161
+ homePath: "/Users/demo",
162
+ breadcrumbs: [
163
+ { label: "workspace", path: "/tmp/workspace" },
164
+ { label: "src", path: "/tmp/workspace/src" },
165
+ ],
166
+ entries: [
167
+ {
168
+ name: "components",
169
+ path: "/tmp/workspace/src/components",
170
+ kind: "directory",
171
+ hidden: false,
172
+ },
173
+ {
174
+ name: "index.ts",
175
+ path: "/tmp/workspace/src/index.ts",
176
+ kind: "file",
177
+ hidden: false,
178
+ },
179
+ ],
180
+ },
181
+ };
182
+ }
128
183
 
129
- if (path === "/tmp/workspace/src") {
130
184
  return {
131
185
  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
- },
186
+ error: new Error("server path must point to a directory"),
187
+ data: null,
156
188
  };
157
- }
158
-
159
- return {
160
- isLoading: false,
161
- error: new Error("server path must point to a directory"),
162
- data: null,
163
- };
164
- });
189
+ },
190
+ );
165
191
  }
166
192
 
167
193
  beforeEach(() => {
168
194
  serverPathReadMock.mockReset();
169
195
  serverPathBrowseMock.mockReset();
196
+ renderDocxMock.mockReset();
197
+ renderDocxMock.mockResolvedValue(undefined);
198
+ readSpreadsheetMock.mockReset();
199
+ readSpreadsheetMock.mockReturnValue({
200
+ SheetNames: ["Summary", "Details"],
201
+ Sheets: { Summary: { name: "Summary" }, Details: { name: "Details" } },
202
+ });
203
+ sheetToJsonMock.mockReset();
204
+ sheetToJsonMock.mockImplementation((sheet: { name: string }) =>
205
+ sheet.name === "Summary"
206
+ ? [
207
+ ["City", "Temperature"],
208
+ ["Hangzhou", "32 C"],
209
+ ]
210
+ : [["Status"], ["Sunny"]],
211
+ );
212
+ openPresentationMock.mockReset();
213
+ destroyPresentationMock.mockReset();
214
+ openPresentationMock.mockResolvedValue({ destroy: destroyPresentationMock });
215
+ vi.stubGlobal(
216
+ "fetch",
217
+ vi
218
+ .fn()
219
+ .mockResolvedValue(
220
+ new Response(new Uint8Array([1, 2, 3]), { status: 200 }),
221
+ ),
222
+ );
223
+ serverPathReadMock.mockReturnValue({
224
+ isLoading: false,
225
+ error: null,
226
+ data: null,
227
+ });
170
228
  serverPathBrowseMock.mockReturnValue({
171
229
  isLoading: false,
172
230
  error: new Error("server path must point to a directory"),
@@ -174,14 +232,297 @@ beforeEach(() => {
174
232
  });
175
233
  });
176
234
 
235
+ afterEach(() => {
236
+ vi.unstubAllGlobals();
237
+ });
238
+
177
239
  describe("ChatSessionWorkspaceFilePreview rendering", () => {
240
+ it("renders attachment content URLs as workspace-native media previews", () => {
241
+ renderWorkspaceFilePreview({
242
+ file: {
243
+ key: "attachment-image",
244
+ path: "photo.png",
245
+ label: "photo.png",
246
+ viewMode: "preview",
247
+ contentUrl: "/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2Fphoto",
248
+ mimeType: "image/png",
249
+ },
250
+ });
251
+
252
+ expect(serverPathReadMock).toHaveBeenCalledWith(
253
+ expect.objectContaining({ enabled: false }),
254
+ );
255
+ expect(serverPathBrowseMock).toHaveBeenCalledWith(
256
+ expect.objectContaining({ enabled: false }),
257
+ );
258
+ const image = screen.getByTestId("workspace-content-image");
259
+ expect(image.getAttribute("src")).toBe(
260
+ "/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2Fphoto",
261
+ );
262
+ expect(image.getAttribute("alt")).toBe("photo.png");
263
+ });
264
+
265
+ it("renders local SVG files through the automatic server-content viewer", () => {
266
+ mockTextRead({
267
+ resolvedPath: "/tmp/project/diagram.svg",
268
+ text: '<svg xmlns="http://www.w3.org/2000/svg" />',
269
+ });
270
+ renderWorkspaceFilePreview({
271
+ file: {
272
+ path: "/tmp/project/diagram.svg",
273
+ label: "diagram.svg",
274
+ previewViewer: "auto",
275
+ },
276
+ });
277
+
278
+ const image = screen.getByTestId("workspace-content-image");
279
+ expect(image.getAttribute("src")).toContain(
280
+ "/api/server-paths/content?path=%2Ftmp%2Fproject%2Fdiagram.svg",
281
+ );
282
+ expect(screen.queryByTestId("file-code-surface")).toBeNull();
283
+ });
284
+
285
+ it("renders SVG source when source viewer is explicitly requested", () => {
286
+ mockTextRead({
287
+ resolvedPath: "/tmp/project/diagram.svg",
288
+ text: '<svg xmlns="http://www.w3.org/2000/svg" />',
289
+ });
290
+ renderWorkspaceFilePreview({
291
+ file: {
292
+ path: "/tmp/project/diagram.svg",
293
+ label: "diagram.svg",
294
+ previewViewer: "source",
295
+ },
296
+ });
297
+
298
+ expect(screen.queryByTestId("workspace-content-image")).toBeNull();
299
+ expect(screen.getByTestId("file-code-surface")).toBeTruthy();
300
+ });
301
+
302
+ it.each([
303
+ ["image", "sample.png", "workspace-content-image"],
304
+ ["audio", "sample.mp3", "workspace-content-audio"],
305
+ ["video", "sample.mp4", "workspace-content-video"],
306
+ ])(
307
+ "does not report truncated metadata for local %s content",
308
+ (_kind, path, testId) => {
309
+ mockTextRead({
310
+ kind: "binary",
311
+ resolvedPath: `/tmp/${path}`,
312
+ text: undefined,
313
+ truncated: true,
314
+ });
315
+ renderWorkspaceFilePreview({
316
+ file: { path, label: path, previewViewer: "auto" },
317
+ });
318
+
319
+ expect(screen.getByTestId(testId)).toBeTruthy();
320
+ expect(
321
+ screen.queryByText(t("chatWorkspacePreviewTruncated")),
322
+ ).toBeNull();
323
+ },
324
+ );
325
+ });
326
+
327
+ describe("ChatSessionWorkspaceFilePreview Office rendering", () => {
328
+ it("renders DOCX attachments inside the workspace preview", async () => {
329
+ renderDocxMock.mockImplementationOnce(
330
+ async (_data: ArrayBuffer, bodyContainer: HTMLDivElement) => {
331
+ const wrapper = document.createElement("div");
332
+ wrapper.className = "docx-wrapper";
333
+ const page = document.createElement("section");
334
+ page.className = "docx";
335
+ const table = document.createElement("table");
336
+ const colgroup = document.createElement("colgroup");
337
+ for (let index = 0; index < 6; index += 1) {
338
+ colgroup.append(document.createElement("col"));
339
+ }
340
+ table.append(colgroup);
341
+ page.append(table);
342
+ wrapper.append(page);
343
+ bodyContainer.append(wrapper);
344
+ },
345
+ );
346
+ renderWorkspaceFilePreview({
347
+ file: {
348
+ key: "attachment-docx",
349
+ path: "overview.zh-CN.docx",
350
+ label: "overview.zh-CN.docx",
351
+ viewMode: "preview",
352
+ contentUrl:
353
+ "/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2Foverview",
354
+ mimeType:
355
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
356
+ },
357
+ });
358
+
359
+ expect(screen.getByTestId("workspace-content-docx")).toBeTruthy();
360
+ await waitFor(() => expect(renderDocxMock).toHaveBeenCalled());
361
+ await waitFor(() =>
362
+ expect(screen.getByTestId("workspace-content-docx").className).toContain(
363
+ "workspace-docx-preview--reflow",
364
+ ),
365
+ );
366
+ expect(
367
+ screen
368
+ .getByTestId("workspace-content-docx")
369
+ .querySelector(".workspace-docx-wide-table > table"),
370
+ ).toBeTruthy();
371
+ expect(fetch).toHaveBeenCalledWith(
372
+ "/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2Foverview",
373
+ expect.objectContaining({ signal: expect.any(AbortSignal) }),
374
+ );
375
+ expect(screen.queryByTestId("workspace-content-unsupported")).toBeNull();
376
+ });
377
+
378
+ it("renders a relative local DOCX before binary metadata finishes loading", async () => {
379
+ mockTextRead({
380
+ kind: "binary",
381
+ resolvedPath: "/tmp/report.docx",
382
+ text: undefined,
383
+ });
384
+ renderWorkspaceFilePreview({
385
+ file: {
386
+ path: "report.docx",
387
+ label: "report.docx",
388
+ previewViewer: "auto",
389
+ },
390
+ sessionWorkingDir: "/tmp",
391
+ });
392
+
393
+ expect(screen.getByTestId("workspace-content-docx")).toBeTruthy();
394
+ await waitFor(() => expect(renderDocxMock).toHaveBeenCalled());
395
+ expect(fetch).toHaveBeenCalledWith(
396
+ expect.stringContaining("path=report.docx&basePath=%2Ftmp"),
397
+ expect.objectContaining({ signal: expect.any(AbortSignal) }),
398
+ );
399
+ expect(screen.queryByText(t("chatWorkspacePreviewUnsupported"))).toBeNull();
400
+ });
401
+
402
+ it("preserves DOCX page geometry when the document defines it", async () => {
403
+ renderDocxMock.mockImplementationOnce(
404
+ async (_data: ArrayBuffer, bodyContainer: HTMLDivElement) => {
405
+ const wrapper = document.createElement("div");
406
+ wrapper.className = "docx-wrapper";
407
+ const page = document.createElement("section");
408
+ page.className = "docx";
409
+ page.style.width = "612pt";
410
+ page.style.padding = "72pt";
411
+ wrapper.append(page);
412
+ bodyContainer.append(wrapper);
413
+ },
414
+ );
415
+ renderWorkspaceFilePreview({
416
+ file: {
417
+ key: "attachment-docx-with-page-layout",
418
+ path: "page-layout.docx",
419
+ label: "page-layout.docx",
420
+ viewMode: "preview",
421
+ contentUrl:
422
+ "/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2Fpage-layout",
423
+ mimeType:
424
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
425
+ },
426
+ });
427
+
428
+ await waitFor(() => expect(renderDocxMock).toHaveBeenCalled());
429
+ expect(
430
+ screen.getByTestId("workspace-content-docx").className,
431
+ ).not.toContain("workspace-docx-preview--reflow");
432
+ });
433
+
434
+ it("renders Excel workbooks with sheet navigation", async () => {
435
+ renderWorkspaceFilePreview({
436
+ file: {
437
+ key: "attachment-xlsx",
438
+ path: "overview.xlsx",
439
+ label: "overview.xlsx",
440
+ viewMode: "preview",
441
+ contentUrl:
442
+ "/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2Foverview",
443
+ mimeType:
444
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
445
+ },
446
+ });
447
+
448
+ expect(screen.getByTestId("workspace-content-spreadsheet")).toBeTruthy();
449
+ await waitFor(() => expect(readSpreadsheetMock).toHaveBeenCalled());
450
+ expect(screen.getByText("Summary")).toBeTruthy();
451
+ expect(screen.getByText("Details")).toBeTruthy();
452
+ expect(screen.getByText("Hangzhou")).toBeTruthy();
453
+ fireEvent.click(screen.getByRole("button", { name: "Details" }));
454
+ expect(screen.getByText("Sunny")).toBeTruthy();
455
+ });
456
+
457
+ it("renders PowerPoint presentations in the workspace", async () => {
458
+ renderWorkspaceFilePreview({
459
+ file: {
460
+ key: "attachment-pptx",
461
+ path: "overview.pptx",
462
+ label: "overview.pptx",
463
+ viewMode: "preview",
464
+ contentUrl: "/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2Fslides",
465
+ mimeType:
466
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
467
+ },
468
+ });
469
+
470
+ expect(screen.getByTestId("workspace-content-presentation")).toBeTruthy();
471
+ await waitFor(() => expect(openPresentationMock).toHaveBeenCalled());
472
+ expect(openPresentationMock).toHaveBeenCalledWith(
473
+ expect.any(ArrayBuffer),
474
+ expect.any(HTMLElement),
475
+ expect.objectContaining({
476
+ lazySlides: true,
477
+ lazyMedia: true,
478
+ pdfjs: false,
479
+ zipLimits: { maxEntries: 4_000 },
480
+ }),
481
+ );
482
+ });
483
+
484
+ it("offers download and system open actions for legacy PowerPoint attachments", () => {
485
+ renderWorkspaceFilePreview({
486
+ file: {
487
+ key: "attachment-ppt",
488
+ path: "overview.ppt",
489
+ label: "overview.ppt",
490
+ viewMode: "preview",
491
+ contentUrl:
492
+ "/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2Foverview",
493
+ mimeType: "application/vnd.ms-powerpoint",
494
+ },
495
+ });
496
+
497
+ expect(screen.getByTestId("workspace-content-unsupported")).toBeTruthy();
498
+ expect(screen.getByText(t("chatWorkspacePreviewUnsupported"))).toBeTruthy();
499
+
500
+ const download = screen.getByRole("link", {
501
+ name: t("chatWorkspacePreviewDownload"),
502
+ });
503
+ expect(download.getAttribute("href")).toBe(
504
+ "/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2Foverview",
505
+ );
506
+ expect(download.getAttribute("download")).toBe("overview.ppt");
507
+
508
+ const openExternally = screen.getByRole("link", {
509
+ name: t("chatWorkspacePreviewOpenExternally"),
510
+ });
511
+ expect(openExternally.getAttribute("href")).toBe(
512
+ "/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2Foverview",
513
+ );
514
+ expect(openExternally.getAttribute("target")).toBe("_blank");
515
+ });
516
+ });
517
+
518
+ describe("ChatSessionWorkspaceFilePreview text rendering", () => {
178
519
  it("renders preview files inside a full-height workspace code surface", () => {
179
520
  mockTextRead();
180
521
  renderWorkspaceFilePreview();
181
522
 
182
- expect(screen.getByTestId("file-code-surface").getAttribute("data-layout")).toBe(
183
- "workspace",
184
- );
523
+ expect(
524
+ screen.getByTestId("file-code-surface").getAttribute("data-layout"),
525
+ ).toBe("workspace");
185
526
  });
186
527
 
187
528
  it("passes server language hints to the workspace code surface", () => {
@@ -195,10 +536,34 @@ describe("ChatSessionWorkspaceFilePreview rendering", () => {
195
536
  });
196
537
 
197
538
  expect(
198
- screen.getByTestId("file-code-surface").getAttribute("data-language-hint"),
539
+ screen
540
+ .getByTestId("file-code-surface")
541
+ .getAttribute("data-language-hint"),
199
542
  ).toBe("js");
200
543
  });
201
544
 
545
+ it("renders Markdown by default and keeps source view explicit", () => {
546
+ mockTextRead({
547
+ kind: "markdown",
548
+ resolvedPath: "/tmp/README.md",
549
+ text: "# Hello",
550
+ });
551
+ const { unmount } = renderWorkspaceFilePreview({
552
+ file: { path: "/tmp/README.md" },
553
+ });
554
+
555
+ expect(screen.getByTestId("markdown-preview")).toBeTruthy();
556
+ expect(screen.queryByTestId("file-code-surface")).toBeNull();
557
+
558
+ unmount();
559
+ renderWorkspaceFilePreview({
560
+ file: { path: "/tmp/README.md", previewViewer: "source" },
561
+ });
562
+
563
+ expect(screen.queryByTestId("markdown-preview")).toBeNull();
564
+ expect(screen.getByTestId("file-code-surface")).toBeTruthy();
565
+ });
566
+
202
567
  it("keeps HTML files in the source preview when preview viewer is automatic", () => {
203
568
  mockTextRead({
204
569
  resolvedPath: "/tmp/example.html",
@@ -234,7 +599,7 @@ describe("ChatSessionWorkspaceFilePreview rendering", () => {
234
599
  expect(frame.getAttribute("sandbox")).toBeNull();
235
600
  expect(frame.getAttribute("srcdoc")).toBeNull();
236
601
  expect(frame.getAttribute("src")).toContain(
237
- "/api/server-paths/content/__abs__/tmp/example.html",
602
+ "/api/server-paths/content?path=%2Ftmp%2Fexample.html",
238
603
  );
239
604
  expect(screen.queryByTestId("file-code-surface")).toBeNull();
240
605
  });
@@ -254,9 +619,9 @@ describe("ChatSessionWorkspaceFilePreview rendering", () => {
254
619
  refreshVersion: 3,
255
620
  });
256
621
 
257
- expect(screen.getByTestId("workspace-html-preview").getAttribute("src")).toContain(
258
- "refresh=3",
259
- );
622
+ expect(
623
+ screen.getByTestId("workspace-html-preview").getAttribute("src"),
624
+ ).toContain("refresh=3");
260
625
  });
261
626
 
262
627
  it("keeps HTML files in the source preview when source is requested", () => {
@@ -307,9 +672,9 @@ describe("ChatSessionWorkspaceFilePreview rendering", () => {
307
672
  },
308
673
  });
309
674
 
310
- expect(screen.getByTestId("file-code-surface").getAttribute("data-layout")).toBe(
311
- "workspace",
312
- );
675
+ expect(
676
+ screen.getByTestId("file-code-surface").getAttribute("data-layout"),
677
+ ).toBe("workspace");
313
678
  });
314
679
 
315
680
  it("does not repeat the preview badge inside the workspace header", () => {
@@ -322,7 +687,7 @@ describe("ChatSessionWorkspaceFilePreview rendering", () => {
322
687
  expect(screen.getByText("example.ts")).toBeTruthy();
323
688
  });
324
689
 
325
- it("renders directory entries and opens child directories or files", () => {
690
+ it("renders directory entries, expands child directories, and opens files", () => {
326
691
  serverPathReadMock.mockReturnValue({
327
692
  isLoading: false,
328
693
  error: new Error("server path must point to a file"),
@@ -376,18 +741,17 @@ describe("ChatSessionWorkspaceFilePreview rendering", () => {
376
741
  });
377
742
  expect(screen.getByTestId("workspace-directory-browser")).toBeTruthy();
378
743
 
379
- fireEvent.click(
380
- screen.getByRole("button", { name: "Open directory: components" }),
381
- );
382
- expect(onFileOpen).toHaveBeenCalledWith({
383
- path: "/tmp/workspace/src/components",
384
- label: "components",
385
- viewMode: "preview",
744
+ const componentsTreeItem = screen.getByRole("treeitem", {
745
+ name: "Open directory: components",
386
746
  });
747
+ const indexButton = screen.getByRole("button", { name: "index.ts" });
748
+ fireEvent.click(componentsTreeItem.querySelector("button")!);
749
+ expect(
750
+ componentsTreeItem.getAttribute("aria-expanded"),
751
+ ).toBe("true");
752
+ expect(onFileOpen).not.toHaveBeenCalled();
387
753
 
388
- fireEvent.click(
389
- screen.getByRole("button", { name: "Open file: index.ts" }),
390
- );
754
+ fireEvent.click(indexButton);
391
755
  expect(onFileOpen).toHaveBeenCalledWith({
392
756
  path: "/tmp/workspace/src/index.ts",
393
757
  label: "index.ts",
@@ -410,9 +774,9 @@ describe("ChatSessionWorkspaceFilePreview breadcrumbs", () => {
410
774
  expect(
411
775
  screen.getByTestId("workspace-file-breadcrumb-scroll").className,
412
776
  ).toContain("py-1.5");
413
- expect(screen.getByTestId("workspace-file-breadcrumbs").className).toContain(
414
- "workspace-horizontal-scrollbar",
415
- );
777
+ expect(
778
+ screen.getByTestId("workspace-file-breadcrumbs").className,
779
+ ).toContain("workspace-horizontal-scrollbar");
416
780
  });
417
781
 
418
782
  it("browses from a breadcrumb segment and opens selected files", () => {
@@ -454,18 +818,18 @@ describe("ChatSessionWorkspaceFilePreview breadcrumbs", () => {
454
818
 
455
819
  fireEvent.click(screen.getByRole("button", { name: "src" }));
456
820
 
457
- expect(screen.getByTestId("workspace-breadcrumb-popover").className).toContain(
458
- "w-[22rem]",
459
- );
460
- expect(screen.getByTestId("workspace-breadcrumb-browser").className).toContain(
461
- "max-h-72",
462
- );
463
- expect(screen.getAllByRole("button", { name: "workspace" }).at(-1)?.className).toContain(
464
- "h-5",
465
- );
466
- expect(screen.getByRole("button", { name: "components" }).className).toContain(
467
- "h-6",
468
- );
821
+ expect(
822
+ screen.getByTestId("workspace-breadcrumb-popover").className,
823
+ ).toContain("w-[22rem]");
824
+ expect(
825
+ screen.getByTestId("workspace-breadcrumb-browser").className,
826
+ ).toContain("max-h-72");
827
+ expect(
828
+ screen.getAllByRole("button", { name: "workspace" }).at(-1)?.className,
829
+ ).toContain("h-5");
830
+ expect(
831
+ screen.getByRole("button", { name: "components" }).className,
832
+ ).toContain("h-6");
469
833
  });
470
834
 
471
835
  it("keeps line and truncation metadata without the duplicated type badge", () => {