@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
@@ -5,10 +5,10 @@ import { ChatSessionTypeOptionItem } from '@/features/chat/features/session-type
5
5
  import type { ChatSessionTypeOption } from '@/features/chat/features/session-type/utils/chat-session-type.utils';
6
6
  import {
7
7
  Popover,
8
- PopoverContent,
9
8
  PopoverTrigger,
10
9
  createPopoverAvailableHeightLimit,
11
10
  } from '@/shared/components/ui/popover';
11
+ import { ChatPopoverContent } from '@/features/chat/components/chat-popover-content';
12
12
  import { t } from '@/shared/lib/i18n';
13
13
 
14
14
  type SessionTypeOption = ChatSessionTypeOption;
@@ -59,7 +59,7 @@ export function ChatWelcomeSessionTypePicker({
59
59
  <ChevronDown className="h-3.5 w-3.5 shrink-0 text-muted-foreground/70" />
60
60
  </button>
61
61
  </PopoverTrigger>
62
- <PopoverContent
62
+ <ChatPopoverContent
63
63
  align="start"
64
64
  className="flex w-[min(16rem,calc(100vw-1rem))] flex-col overflow-hidden rounded-2xl border border-border bg-popover p-1.5 text-popover-foreground shadow-[0_24px_60px_-28px_rgba(15,23,42,0.38)]"
65
65
  style={{ maxHeight: SESSION_TYPE_PICKER_MAX_HEIGHT }}
@@ -76,7 +76,7 @@ export function ChatWelcomeSessionTypePicker({
76
76
  />
77
77
  ))}
78
78
  </div>
79
- </PopoverContent>
79
+ </ChatPopoverContent>
80
80
  </Popover>
81
81
  );
82
82
  }
@@ -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,106 @@ 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
+ );
170
223
  serverPathReadMock.mockReturnValue({
171
224
  isLoading: false,
172
225
  error: null,
@@ -179,6 +232,10 @@ beforeEach(() => {
179
232
  });
180
233
  });
181
234
 
235
+ afterEach(() => {
236
+ vi.unstubAllGlobals();
237
+ });
238
+
182
239
  describe("ChatSessionWorkspaceFilePreview rendering", () => {
183
240
  it("renders attachment content URLs as workspace-native media previews", () => {
184
241
  renderWorkspaceFilePreview({
@@ -205,25 +262,240 @@ describe("ChatSessionWorkspaceFilePreview rendering", () => {
205
262
  expect(image.getAttribute("alt")).toBe("photo.png");
206
263
  });
207
264
 
208
- it("offers download and system open actions for unsupported office attachments", () => {
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
+ );
209
346
  renderWorkspaceFilePreview({
210
347
  file: {
211
348
  key: "attachment-docx",
212
349
  path: "overview.zh-CN.docx",
213
350
  label: "overview.zh-CN.docx",
214
351
  viewMode: "preview",
215
- contentUrl: "/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2Foverview",
352
+ contentUrl:
353
+ "/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2Foverview",
216
354
  mimeType:
217
355
  "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
218
356
  },
219
357
  });
220
358
 
221
- expect(screen.getByTestId("workspace-content-unsupported")).toBeTruthy();
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());
222
429
  expect(
223
- screen.getByTestId("workspace-content-unsupported").textContent,
224
- ).toContain("overview.zh-CN.docx");
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();
225
498
  expect(screen.getByText(t("chatWorkspacePreviewUnsupported"))).toBeTruthy();
226
- expect(screen.getByText(t("chatWorkspacePreviewUnsupportedHint"))).toBeTruthy();
227
499
 
228
500
  const download = screen.getByRole("link", {
229
501
  name: t("chatWorkspacePreviewDownload"),
@@ -231,7 +503,7 @@ describe("ChatSessionWorkspaceFilePreview rendering", () => {
231
503
  expect(download.getAttribute("href")).toBe(
232
504
  "/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2Foverview",
233
505
  );
234
- expect(download.getAttribute("download")).toBe("overview.zh-CN.docx");
506
+ expect(download.getAttribute("download")).toBe("overview.ppt");
235
507
 
236
508
  const openExternally = screen.getByRole("link", {
237
509
  name: t("chatWorkspacePreviewOpenExternally"),
@@ -241,14 +513,16 @@ describe("ChatSessionWorkspaceFilePreview rendering", () => {
241
513
  );
242
514
  expect(openExternally.getAttribute("target")).toBe("_blank");
243
515
  });
516
+ });
244
517
 
518
+ describe("ChatSessionWorkspaceFilePreview text rendering", () => {
245
519
  it("renders preview files inside a full-height workspace code surface", () => {
246
520
  mockTextRead();
247
521
  renderWorkspaceFilePreview();
248
522
 
249
- expect(screen.getByTestId("file-code-surface").getAttribute("data-layout")).toBe(
250
- "workspace",
251
- );
523
+ expect(
524
+ screen.getByTestId("file-code-surface").getAttribute("data-layout"),
525
+ ).toBe("workspace");
252
526
  });
253
527
 
254
528
  it("passes server language hints to the workspace code surface", () => {
@@ -262,10 +536,34 @@ describe("ChatSessionWorkspaceFilePreview rendering", () => {
262
536
  });
263
537
 
264
538
  expect(
265
- screen.getByTestId("file-code-surface").getAttribute("data-language-hint"),
539
+ screen
540
+ .getByTestId("file-code-surface")
541
+ .getAttribute("data-language-hint"),
266
542
  ).toBe("js");
267
543
  });
268
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
+
269
567
  it("keeps HTML files in the source preview when preview viewer is automatic", () => {
270
568
  mockTextRead({
271
569
  resolvedPath: "/tmp/example.html",
@@ -301,7 +599,7 @@ describe("ChatSessionWorkspaceFilePreview rendering", () => {
301
599
  expect(frame.getAttribute("sandbox")).toBeNull();
302
600
  expect(frame.getAttribute("srcdoc")).toBeNull();
303
601
  expect(frame.getAttribute("src")).toContain(
304
- "/api/server-paths/content/__abs__/tmp/example.html",
602
+ "/api/server-paths/content?path=%2Ftmp%2Fexample.html",
305
603
  );
306
604
  expect(screen.queryByTestId("file-code-surface")).toBeNull();
307
605
  });
@@ -321,9 +619,9 @@ describe("ChatSessionWorkspaceFilePreview rendering", () => {
321
619
  refreshVersion: 3,
322
620
  });
323
621
 
324
- expect(screen.getByTestId("workspace-html-preview").getAttribute("src")).toContain(
325
- "refresh=3",
326
- );
622
+ expect(
623
+ screen.getByTestId("workspace-html-preview").getAttribute("src"),
624
+ ).toContain("refresh=3");
327
625
  });
328
626
 
329
627
  it("keeps HTML files in the source preview when source is requested", () => {
@@ -374,9 +672,9 @@ describe("ChatSessionWorkspaceFilePreview rendering", () => {
374
672
  },
375
673
  });
376
674
 
377
- expect(screen.getByTestId("file-code-surface").getAttribute("data-layout")).toBe(
378
- "workspace",
379
- );
675
+ expect(
676
+ screen.getByTestId("file-code-surface").getAttribute("data-layout"),
677
+ ).toBe("workspace");
380
678
  });
381
679
 
382
680
  it("does not repeat the preview badge inside the workspace header", () => {
@@ -389,7 +687,7 @@ describe("ChatSessionWorkspaceFilePreview rendering", () => {
389
687
  expect(screen.getByText("example.ts")).toBeTruthy();
390
688
  });
391
689
 
392
- it("renders directory entries and opens child directories or files", () => {
690
+ it("renders directory entries, expands child directories, and opens files", () => {
393
691
  serverPathReadMock.mockReturnValue({
394
692
  isLoading: false,
395
693
  error: new Error("server path must point to a file"),
@@ -443,18 +741,17 @@ describe("ChatSessionWorkspaceFilePreview rendering", () => {
443
741
  });
444
742
  expect(screen.getByTestId("workspace-directory-browser")).toBeTruthy();
445
743
 
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",
744
+ const componentsTreeItem = screen.getByRole("treeitem", {
745
+ name: "Open directory: components",
453
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();
454
753
 
455
- fireEvent.click(
456
- screen.getByRole("button", { name: "Open file: index.ts" }),
457
- );
754
+ fireEvent.click(indexButton);
458
755
  expect(onFileOpen).toHaveBeenCalledWith({
459
756
  path: "/tmp/workspace/src/index.ts",
460
757
  label: "index.ts",
@@ -477,9 +774,9 @@ describe("ChatSessionWorkspaceFilePreview breadcrumbs", () => {
477
774
  expect(
478
775
  screen.getByTestId("workspace-file-breadcrumb-scroll").className,
479
776
  ).toContain("py-1.5");
480
- expect(screen.getByTestId("workspace-file-breadcrumbs").className).toContain(
481
- "workspace-horizontal-scrollbar",
482
- );
777
+ expect(
778
+ screen.getByTestId("workspace-file-breadcrumbs").className,
779
+ ).toContain("workspace-horizontal-scrollbar");
483
780
  });
484
781
 
485
782
  it("browses from a breadcrumb segment and opens selected files", () => {
@@ -521,18 +818,18 @@ describe("ChatSessionWorkspaceFilePreview breadcrumbs", () => {
521
818
 
522
819
  fireEvent.click(screen.getByRole("button", { name: "src" }));
523
820
 
524
- expect(screen.getByTestId("workspace-breadcrumb-popover").className).toContain(
525
- "w-[22rem]",
526
- );
527
- expect(screen.getByTestId("workspace-breadcrumb-browser").className).toContain(
528
- "max-h-72",
529
- );
530
- expect(screen.getAllByRole("button", { name: "workspace" }).at(-1)?.className).toContain(
531
- "h-5",
532
- );
533
- expect(screen.getByRole("button", { name: "components" }).className).toContain(
534
- "h-6",
535
- );
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");
536
833
  });
537
834
 
538
835
  it("keeps line and truncation metadata without the duplicated type badge", () => {