@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
@@ -2,7 +2,6 @@ import { useMemo } from "react";
2
2
  import type {
3
3
  ChatFileOpenActionViewModel,
4
4
  ChatFileOperationBlockViewModel,
5
- ChatFilePreviewViewer,
6
5
  } from "@nextclaw/agent-chat-ui";
7
6
  import {
8
7
  ChatMessageMarkdown,
@@ -11,6 +10,11 @@ import {
11
10
  import type { ChatWorkspaceFileTab } from "@/features/chat/stores/chat-thread.store";
12
11
  import { ChatSessionWorkspaceDirectoryBrowser } from "./chat-session-workspace-directory-browser";
13
12
  import { ChatSessionWorkspaceFileBreadcrumbs } from "./chat-session-workspace-file-breadcrumbs";
13
+ import {
14
+ resolveWorkspaceFileContentKind,
15
+ WorkspaceFileContentPreview,
16
+ type WorkspaceFileContentKind,
17
+ } from "./workspace-file-content-preview";
14
18
  import { useServerPathBrowse } from "@/shared/hooks/use-server-path-browse";
15
19
  import { useServerPathRead } from "@/shared/hooks/use-server-path-read";
16
20
  import { buildServerPathContentUrl } from "@/shared/lib/api";
@@ -21,6 +25,7 @@ import {
21
25
  import { t } from "@/shared/lib/i18n";
22
26
  import { buildWorkspaceFileBreadcrumb } from "@/shared/lib/session-project";
23
27
  import { cn } from "@/shared/lib/utils";
28
+ import { resolveWorkspaceFileViewer } from "@/features/chat/features/workspace/utils/chat-workspace-file-viewer.utils";
24
29
 
25
30
  function inferPreviewKind(params: {
26
31
  path: string;
@@ -32,14 +37,10 @@ function inferPreviewKind(params: {
32
37
  return /\.mdx?$/i.test(params.path) ? "markdown" : "text";
33
38
  }
34
39
 
35
- function resolveFilePreviewViewer(params: {
36
- path: string;
37
- viewer?: ChatFilePreviewViewer | null;
38
- }): "source" | "rendered" {
39
- return params.viewer === "rendered" && /\.html?$/i.test(params.path) ? "rendered" : "source";
40
- }
41
-
42
- function appendPreviewRefreshVersion(url: string, refreshVersion: number): string {
40
+ function appendPreviewRefreshVersion(
41
+ url: string,
42
+ refreshVersion: number,
43
+ ): string {
43
44
  if (refreshVersion <= 0) {
44
45
  return url;
45
46
  }
@@ -155,46 +156,50 @@ function WorkspaceCodeSurface({
155
156
  block,
156
157
  }: {
157
158
  block: ChatFileOperationBlockViewModel;
158
- }) {
159
- return <div className="h-full overflow-auto custom-scrollbar bg-white"><FileOperationCodeSurface block={block} layout="workspace" /></div>;
160
- }
161
-
162
- function WorkspaceHtmlRenderedPreview({
163
- src,
164
- }: {
165
- src: string;
166
159
  }) {
167
160
  return (
168
- <iframe
169
- allowFullScreen
170
- className="h-full w-full border-0 bg-white"
171
- data-testid="workspace-html-preview"
172
- src={src}
173
- title={t("chatWorkspaceHtmlPreviewTitle")}
174
- />
161
+ <div className="h-full overflow-auto custom-scrollbar bg-white">
162
+ <FileOperationCodeSurface block={block} layout="workspace" />
163
+ </div>
175
164
  );
176
165
  }
177
166
 
178
167
  function WorkspacePreviewBody({
168
+ contentUrl,
169
+ contentUrlKind,
170
+ contentLabel,
179
171
  directoryQuery,
172
+ fileBasePath,
180
173
  onFileOpen,
181
174
  previewBlock,
182
175
  previewKind,
176
+ previewViewer,
183
177
  previewQuery,
184
178
  previewText,
185
- previewUrl,
186
- previewViewer,
187
179
  }: {
188
- directoryQuery: ReturnType<typeof useServerPathBrowse>;
180
+ contentUrl: string | null;
181
+ contentUrlKind: WorkspaceFileContentKind | null;
182
+ contentLabel: string;
183
+ directoryQuery: ReturnType<typeof useServerPathBrowse> | null | undefined;
184
+ fileBasePath: string | null;
189
185
  onFileOpen: (action: ChatFileOpenActionViewModel) => void;
190
186
  previewBlock: ChatFileOperationBlockViewModel | null;
191
187
  previewKind: "text" | "markdown" | "binary";
192
- previewQuery: ReturnType<typeof useServerPathRead>;
188
+ previewViewer: "source" | "rendered" | null;
189
+ previewQuery: ReturnType<typeof useServerPathRead> | null | undefined;
193
190
  previewText: string | null;
194
- previewUrl: string | null;
195
- previewViewer: "source" | "rendered";
196
191
  }) {
197
- if (directoryQuery.data) {
192
+ if (contentUrl && contentUrlKind) {
193
+ return (
194
+ <WorkspaceFileContentPreview
195
+ contentUrl={contentUrl}
196
+ kind={contentUrlKind}
197
+ label={contentLabel}
198
+ />
199
+ );
200
+ }
201
+
202
+ if (directoryQuery?.data) {
198
203
  return (
199
204
  <ChatSessionWorkspaceDirectoryBrowser
200
205
  browseQuery={directoryQuery}
@@ -204,25 +209,25 @@ function WorkspacePreviewBody({
204
209
  );
205
210
  }
206
211
 
207
- if (directoryQuery.isLoading && previewQuery.error && !previewBlock) {
212
+ if (directoryQuery?.isLoading && previewQuery?.error && !previewBlock) {
208
213
  return (
209
214
  <WorkspaceFilePreviewStatus text={t("chatWorkspaceLoadingDirectory")} />
210
215
  );
211
216
  }
212
217
 
213
- if ((directoryQuery.isLoading || previewQuery.isLoading) && !previewBlock) {
218
+ if ((directoryQuery?.isLoading || previewQuery?.isLoading) && !previewBlock) {
214
219
  return (
215
220
  <WorkspaceFilePreviewStatus text={t("chatWorkspaceLoadingPreview")} />
216
221
  );
217
222
  }
218
223
 
219
- if (previewQuery.data?.kind === "binary") {
224
+ if (previewQuery?.data?.kind === "binary") {
220
225
  return (
221
226
  <WorkspaceFilePreviewStatus text={t("chatWorkspacePreviewUnsupported")} />
222
227
  );
223
228
  }
224
229
 
225
- if (previewQuery.error && !previewBlock) {
230
+ if (previewQuery?.error && !previewBlock) {
226
231
  return (
227
232
  <WorkspaceFilePreviewStatus
228
233
  tone="error"
@@ -235,11 +240,7 @@ function WorkspacePreviewBody({
235
240
  );
236
241
  }
237
242
 
238
- if (previewViewer === "rendered" && previewUrl) {
239
- return <WorkspaceHtmlRenderedPreview src={previewUrl} />;
240
- }
241
-
242
- if (previewKind === "markdown" && previewText) {
243
+ if (previewKind === "markdown" && previewViewer !== "source" && previewText) {
243
244
  return (
244
245
  <div className="h-full overflow-auto custom-scrollbar px-5 py-4">
245
246
  <ChatMessageMarkdown
@@ -250,6 +251,9 @@ function WorkspacePreviewBody({
250
251
  copiedCodeLabel: t("chatCodeCopied"),
251
252
  }}
252
253
  onFileOpen={onFileOpen}
254
+ resolveFileContentUrl={(action) =>
255
+ buildServerPathContentUrl(action.path, fileBasePath)
256
+ }
253
257
  />
254
258
  </div>
255
259
  );
@@ -278,62 +282,87 @@ export function ChatSessionWorkspaceFilePreview({
278
282
  onFileOpen,
279
283
  }: ChatSessionWorkspaceFilePreviewProps) {
280
284
  const isPreviewMode = file.viewMode === "preview";
285
+ const suppliedContentUrl = file.contentUrl?.trim() || null;
286
+ const usesServerPath = isPreviewMode && !suppliedContentUrl;
281
287
  const previewQuery = useServerPathRead({
282
288
  path: file.path,
283
289
  basePath: sessionWorkingDir,
284
- enabled: isPreviewMode,
290
+ enabled: usesServerPath,
285
291
  });
286
292
  const directoryQuery = useServerPathBrowse({
287
293
  path: file.path,
288
294
  basePath: sessionWorkingDir,
289
295
  includeFiles: true,
290
- enabled: isPreviewMode,
296
+ enabled: usesServerPath,
291
297
  });
292
298
  const diffBlock = useMemo(
293
299
  () => (file.viewMode === "diff" ? buildDiffBlock(file) : null),
294
300
  [file],
295
301
  );
296
- const previewText =
297
- isPreviewMode ? previewQuery.data?.text ?? file.rawText ?? null : null;
302
+ const previewText = isPreviewMode
303
+ ? (previewQuery?.data?.text ?? file.rawText ?? null)
304
+ : null;
298
305
  const previewKind = inferPreviewKind({
299
- path: previewQuery.data?.resolvedPath ?? file.path,
300
- serverKind: previewQuery.data?.kind,
306
+ path: previewQuery?.data?.resolvedPath ?? file.path,
307
+ serverKind: previewQuery?.data?.kind,
301
308
  });
309
+ const suppliedContentKind = suppliedContentUrl
310
+ ? resolveWorkspaceFileContentKind({
311
+ path: file.path,
312
+ mimeType: file.mimeType,
313
+ })
314
+ : null;
302
315
  const resolvedPath =
303
- directoryQuery.data?.currentPath ??
304
- previewQuery.data?.resolvedPath ??
316
+ directoryQuery?.data?.currentPath ??
317
+ previewQuery?.data?.resolvedPath ??
305
318
  file.path;
306
- const previewPathKind = directoryQuery.data ? "directory" : "file";
307
- const previewViewer = resolveFilePreviewViewer({
319
+ const previewPathKind = directoryQuery?.data ? "directory" : "file";
320
+ const previewViewer = resolveWorkspaceFileViewer(
321
+ resolvedPath,
322
+ file.previewViewer,
323
+ );
324
+ const localContentKind = resolveWorkspaceFileContentKind({
308
325
  path: resolvedPath,
309
- viewer: file.previewViewer,
310
326
  });
311
- const previewUrl =
312
- previewViewer === "rendered" && previewQuery.data?.resolvedPath
313
- ? appendPreviewRefreshVersion(
314
- buildServerPathContentUrl(previewQuery.data.resolvedPath),
315
- refreshVersion,
316
- )
327
+ const localContentUrlCandidate = buildServerPathContentUrl(
328
+ file.path,
329
+ sessionWorkingDir,
330
+ );
331
+ const shouldRenderLocalContent = Boolean(
332
+ !suppliedContentUrl &&
333
+ isPreviewMode &&
334
+ localContentUrlCandidate &&
335
+ file.previewViewer !== "source" &&
336
+ localContentKind !== "other" &&
337
+ (localContentKind !== "html" || previewViewer === "rendered"),
338
+ );
339
+ const localContentUrl =
340
+ shouldRenderLocalContent && localContentUrlCandidate
341
+ ? appendPreviewRefreshVersion(localContentUrlCandidate, refreshVersion)
317
342
  : null;
343
+ const contentUrl = suppliedContentUrl ?? localContentUrl;
344
+ const contentUrlKind =
345
+ suppliedContentKind ?? (localContentUrl ? localContentKind : null);
318
346
  const previewBlock = useMemo(() => {
319
- if (!isPreviewMode || !previewText) {
347
+ if (!isPreviewMode || !previewText || contentUrl) {
320
348
  return null;
321
349
  }
322
350
  return buildPreviewBlock({
323
- path: previewQuery.data?.resolvedPath ?? file.path,
351
+ path: previewQuery?.data?.resolvedPath ?? file.path,
324
352
  text: previewText,
325
- languageHint: previewQuery.data?.languageHint ?? null,
353
+ languageHint: previewQuery?.data?.languageHint ?? null,
326
354
  line: file.line,
327
355
  });
328
356
  }, [
357
+ contentUrl,
329
358
  file.line,
330
359
  file.path,
331
360
  isPreviewMode,
332
- previewQuery.data?.languageHint,
333
- previewQuery.data?.resolvedPath,
361
+ previewQuery?.data?.languageHint,
362
+ previewQuery?.data?.resolvedPath,
334
363
  previewText,
335
364
  ]);
336
- const isTruncated = Boolean(previewQuery.data?.truncated);
365
+ const isTextPreviewTruncated = !contentUrl && Boolean(previewQuery?.data?.truncated);
337
366
  const breadcrumbBasePath = sessionProjectRoot ?? sessionWorkingDir;
338
367
  const breadcrumb = useMemo(
339
368
  () =>
@@ -343,13 +372,13 @@ export function ChatSessionWorkspaceFilePreview({
343
372
  sessionProjectRoot: breadcrumbBasePath,
344
373
  line: file.line,
345
374
  column: file.column,
346
- truncated: isTruncated,
375
+ truncated: isTextPreviewTruncated,
347
376
  }),
348
377
  [
349
378
  breadcrumbBasePath,
350
379
  file.column,
351
380
  file.line,
352
- isTruncated,
381
+ isTextPreviewTruncated,
353
382
  previewPathKind,
354
383
  resolvedPath,
355
384
  ],
@@ -367,14 +396,17 @@ export function ChatSessionWorkspaceFilePreview({
367
396
  <WorkspaceDiffBody diffBlock={diffBlock} />
368
397
  ) : (
369
398
  <WorkspacePreviewBody
399
+ contentUrl={contentUrl}
400
+ contentUrlKind={contentUrlKind}
401
+ contentLabel={file.label?.trim() || resolvedPath}
370
402
  directoryQuery={directoryQuery}
403
+ fileBasePath={sessionWorkingDir}
371
404
  onFileOpen={onFileOpen}
372
405
  previewBlock={previewBlock}
373
406
  previewKind={previewKind}
407
+ previewViewer={previewViewer}
374
408
  previewQuery={previewQuery}
375
409
  previewText={previewText}
376
- previewUrl={previewUrl}
377
- previewViewer={previewViewer}
378
410
  />
379
411
  )}
380
412
  </div>
@@ -1,21 +1,234 @@
1
- import { MessageSquarePlus } from "lucide-react";
1
+ import type { ReactNode } from "react";
2
+ import {
3
+ AlarmClock,
4
+ ChevronRight,
5
+ FolderTree,
6
+ GitBranch,
7
+ MessageSquarePlus,
8
+ } from "lucide-react";
2
9
  import type { CronJobView } from "@/shared/lib/api";
3
10
  import { usePresenter } from "@/features/chat/components/providers/chat-presenter.provider";
4
11
  import { SessionConversationArea } from "@/features/chat/features/conversation/components/session-conversation-area";
5
12
  import { ChatSessionWorkspaceFilePreview } from "@/features/chat/features/workspace/components/chat-session-workspace-file-preview";
13
+ import { ChatSessionWorkspaceDirectoryBrowser } from "@/features/chat/features/workspace/components/chat-session-workspace-directory-browser";
6
14
  import { SessionCronJobContent } from "@/features/chat/features/workspace/components/session-cron-job-content";
7
15
  import type { ResolvedChildSessionTab } from "@/features/chat/features/ncp/hooks/use-ncp-child-session-tabs-view";
8
16
  import type { WorkspaceSelection } from "@/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils";
17
+ import { useServerPathBrowse } from "@/shared/hooks/use-server-path-browse";
9
18
  import { t } from "@/shared/lib/i18n";
19
+ import { cn } from "@/shared/lib/utils";
10
20
 
11
21
  type ChatSessionWorkspacePanelContentProps = {
12
22
  activeSelection: WorkspaceSelection;
23
+ childSessionTabs: readonly ResolvedChildSessionTab[];
13
24
  filePreviewRefreshVersion: number;
25
+ sessionKey: string | null;
14
26
  sessionCronJobs: readonly CronJobView[];
15
27
  sessionProjectRoot: string | null;
16
28
  sessionWorkingDir: string | null;
17
29
  };
18
30
 
31
+ function WorkspaceOverviewEntry({
32
+ count,
33
+ description,
34
+ icon,
35
+ onClick,
36
+ title,
37
+ }: {
38
+ count?: number;
39
+ description: string;
40
+ icon: ReactNode;
41
+ onClick: () => void;
42
+ title: string;
43
+ }) {
44
+ return (
45
+ <button
46
+ type="button"
47
+ className="group flex w-full items-center gap-3 rounded-lg border border-gray-200/80 bg-white px-3 py-3 text-left transition-colors hover:border-gray-300 hover:bg-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/25"
48
+ onClick={onClick}
49
+ >
50
+ <span className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-gray-100 text-gray-600">
51
+ {icon}
52
+ </span>
53
+ <span className="min-w-0 flex-1">
54
+ <span className="flex items-center gap-2 text-sm font-medium text-gray-900">
55
+ <span className="truncate">{title}</span>
56
+ {typeof count === "number" ? (
57
+ <span className="rounded-full bg-gray-100 px-1.5 py-0.5 text-[10px] font-semibold tabular-nums text-gray-500">
58
+ {count}
59
+ </span>
60
+ ) : null}
61
+ </span>
62
+ <span className="mt-0.5 block text-xs leading-5 text-gray-500">
63
+ {description}
64
+ </span>
65
+ </span>
66
+ <ChevronRight className="h-4 w-4 shrink-0 text-gray-300 transition-transform group-hover:translate-x-0.5 group-hover:text-gray-500" />
67
+ </button>
68
+ );
69
+ }
70
+
71
+ function WorkspaceOverview({
72
+ childSessionTabs,
73
+ sessionCronJobs,
74
+ sessionKey,
75
+ }: {
76
+ childSessionTabs: readonly ResolvedChildSessionTab[];
77
+ sessionCronJobs: readonly CronJobView[];
78
+ sessionKey: string | null;
79
+ }) {
80
+ const presenter = usePresenter();
81
+
82
+ return (
83
+ <div className="h-full overflow-auto bg-gray-50/45 px-4 py-5 custom-scrollbar">
84
+ <div className="mx-auto max-w-xl">
85
+ <h2 className="text-base font-semibold text-gray-900">
86
+ {t("chatWorkspaceOverview")}
87
+ </h2>
88
+ <p className="mt-1 text-xs leading-5 text-gray-500">
89
+ {t("chatWorkspaceOverviewDescription")}
90
+ </p>
91
+ <div className="mt-4 space-y-2">
92
+ <WorkspaceOverviewEntry
93
+ count={childSessionTabs.length}
94
+ description={t("chatWorkspaceChildSessionsDescription")}
95
+ icon={<GitBranch className="h-4 w-4" />}
96
+ title={t("chatWorkspaceChildSessions")}
97
+ onClick={() => {
98
+ if (sessionKey) {
99
+ presenter.chatThreadManager.openChildSessions(sessionKey);
100
+ }
101
+ }}
102
+ />
103
+ <WorkspaceOverviewEntry
104
+ count={sessionCronJobs.length}
105
+ description={t("chatWorkspaceSessionCronJobsDescription")}
106
+ icon={<AlarmClock className="h-4 w-4" />}
107
+ title={t("chatWorkspaceSessionCronJobs")}
108
+ onClick={() => {
109
+ if (sessionKey) {
110
+ presenter.chatThreadManager.openSessionCronPanel(sessionKey);
111
+ }
112
+ }}
113
+ />
114
+ <WorkspaceOverviewEntry
115
+ description={t("chatWorkspaceProjectFilesDescription")}
116
+ icon={<FolderTree className="h-4 w-4" />}
117
+ title={t("chatWorkspaceProjectFiles")}
118
+ onClick={() => {
119
+ if (sessionKey) {
120
+ presenter.chatThreadManager.openProjectFiles(sessionKey);
121
+ }
122
+ }}
123
+ />
124
+ </div>
125
+ </div>
126
+ </div>
127
+ );
128
+ }
129
+
130
+ function WorkspaceChildSessions({
131
+ childSessionTabs,
132
+ }: {
133
+ childSessionTabs: readonly ResolvedChildSessionTab[];
134
+ }) {
135
+ const presenter = usePresenter();
136
+
137
+ return (
138
+ <div className="h-full overflow-auto bg-gray-50/45 px-4 py-5 custom-scrollbar">
139
+ <div className="mx-auto max-w-xl">
140
+ <div className="flex items-center gap-2">
141
+ <h2 className="text-base font-semibold text-gray-900">
142
+ {t("chatWorkspaceChildSessions")}
143
+ </h2>
144
+ <span className="rounded-full bg-gray-100 px-1.5 py-0.5 text-[10px] font-semibold tabular-nums text-gray-500">
145
+ {childSessionTabs.length}
146
+ </span>
147
+ </div>
148
+ {childSessionTabs.length === 0 ? (
149
+ <div className="py-10 text-center text-sm text-gray-500">
150
+ {t("chatWorkspaceChildSessionsEmpty")}
151
+ </div>
152
+ ) : (
153
+ <div className="mt-4 space-y-2">
154
+ {childSessionTabs.map((tab) => (
155
+ <button
156
+ key={tab.sessionKey}
157
+ type="button"
158
+ className="group flex w-full items-center gap-3 rounded-lg border border-gray-200/80 bg-white px-3 py-3 text-left transition-colors hover:border-gray-300 hover:bg-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/25"
159
+ onClick={() =>
160
+ presenter.chatThreadManager.selectChildSessionDetail(
161
+ tab.sessionKey,
162
+ )
163
+ }
164
+ >
165
+ <span className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-gray-100 text-gray-600">
166
+ <GitBranch className="h-4 w-4" />
167
+ </span>
168
+ <span className="min-w-0 flex-1">
169
+ <span className="block truncate text-sm font-medium text-gray-900">
170
+ {tab.title}
171
+ </span>
172
+ {tab.projectName || tab.sessionTypeLabel ? (
173
+ <span className="mt-0.5 block truncate text-xs text-gray-500">
174
+ {[tab.sessionTypeLabel, tab.projectName]
175
+ .filter(Boolean)
176
+ .join(" · ")}
177
+ </span>
178
+ ) : null}
179
+ </span>
180
+ <ChevronRight className="h-4 w-4 shrink-0 text-gray-300 transition-transform group-hover:translate-x-0.5 group-hover:text-gray-500" />
181
+ </button>
182
+ ))}
183
+ </div>
184
+ )}
185
+ </div>
186
+ </div>
187
+ );
188
+ }
189
+
190
+ function WorkspaceProjectFiles({
191
+ projectRoot,
192
+ workingDir,
193
+ }: {
194
+ projectRoot: string | null;
195
+ workingDir: string | null;
196
+ }) {
197
+ const presenter = usePresenter();
198
+ const rootPath = projectRoot ?? workingDir;
199
+ const browseQuery = useServerPathBrowse({
200
+ path: rootPath,
201
+ includeFiles: true,
202
+ enabled: Boolean(rootPath),
203
+ });
204
+
205
+ if (!rootPath) {
206
+ return (
207
+ <div className="flex h-full items-center justify-center px-6 text-center text-sm text-gray-500">
208
+ {t("chatWorkspaceProjectFilesUnavailable")}
209
+ </div>
210
+ );
211
+ }
212
+
213
+ return (
214
+ <ChatSessionWorkspaceDirectoryBrowser
215
+ browseQuery={browseQuery}
216
+ onFileOpen={presenter.chatThreadManager.openFilePreview}
217
+ renderStatus={({ text, tone = "muted" }) => (
218
+ <div
219
+ className={cn(
220
+ "flex h-full items-center justify-center px-6 text-center text-sm",
221
+ tone === "error" ? "text-rose-600" : "text-gray-500",
222
+ )}
223
+ >
224
+ {text}
225
+ </div>
226
+ )}
227
+ showRoot
228
+ />
229
+ );
230
+ }
231
+
19
232
  function ChildSessionMetaChip({ value }: { value: string }) {
20
233
  return (
21
234
  <span className="inline-flex max-w-full shrink-0 items-center rounded border border-gray-200 bg-gray-50 px-1.5 py-0.5 text-[10px] font-medium text-gray-600">
@@ -68,13 +281,38 @@ function WorkspaceSideChatDraftHeader() {
68
281
 
69
282
  export function ChatSessionWorkspacePanelContent({
70
283
  activeSelection,
284
+ childSessionTabs,
71
285
  filePreviewRefreshVersion,
286
+ sessionKey,
72
287
  sessionCronJobs,
73
288
  sessionProjectRoot,
74
289
  sessionWorkingDir,
75
290
  }: ChatSessionWorkspacePanelContentProps) {
76
291
  const presenter = usePresenter();
77
292
 
293
+ if (activeSelection.kind === "overview") {
294
+ return (
295
+ <WorkspaceOverview
296
+ childSessionTabs={childSessionTabs}
297
+ sessionCronJobs={sessionCronJobs}
298
+ sessionKey={sessionKey}
299
+ />
300
+ );
301
+ }
302
+
303
+ if (activeSelection.kind === "project-files") {
304
+ return (
305
+ <WorkspaceProjectFiles
306
+ projectRoot={sessionProjectRoot}
307
+ workingDir={sessionWorkingDir}
308
+ />
309
+ );
310
+ }
311
+
312
+ if (activeSelection.kind === "child-sessions") {
313
+ return <WorkspaceChildSessions childSessionTabs={childSessionTabs} />;
314
+ }
315
+
78
316
  if (activeSelection.kind === "side-chat-draft") {
79
317
  return (
80
318
  <>
@@ -2,7 +2,11 @@ import {
2
2
  AlarmClock,
3
3
  ArrowLeft,
4
4
  ArrowRight,
5
- FileCode2,
5
+ Code2,
6
+ Eye,
7
+ FolderTree,
8
+ GitBranch,
9
+ LayoutDashboard,
6
10
  Maximize2,
7
11
  MessageSquarePlus,
8
12
  MessageSquareText,
@@ -12,6 +16,7 @@ import {
12
16
  } from "lucide-react";
13
17
  import type { WorkspaceTabViewModel } from "@/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils";
14
18
  import { AgentIdentityAvatar } from "@/shared/components/common/agent-identity";
19
+ import { FileTypeIcon } from "@/shared/components/file-type-icon";
15
20
  import {
16
21
  CompactTabStrip,
17
22
  type CompactTabStripAction,
@@ -19,13 +24,29 @@ import {
19
24
  } from "@/shared/components/ui/tab-strip/compact-tab-strip";
20
25
  import { t } from "@/shared/lib/i18n";
21
26
 
22
- function WorkspaceTabIcon({ agentId, kind }: Pick<WorkspaceTabViewModel, "agentId" | "kind">) {
27
+ function WorkspaceTabIcon({
28
+ agentId,
29
+ fileName,
30
+ kind,
31
+ }: Pick<WorkspaceTabViewModel, "agentId" | "fileName" | "kind">) {
32
+ if (kind === "overview") {
33
+ return <LayoutDashboard className="h-3.5 w-3.5 shrink-0 text-gray-400" />;
34
+ }
35
+
36
+ if (kind === "child-sessions") {
37
+ return <GitBranch className="h-3.5 w-3.5 shrink-0 text-gray-400" />;
38
+ }
39
+
40
+ if (kind === "project-files") {
41
+ return <FolderTree className="h-3.5 w-3.5 shrink-0 text-gray-400" />;
42
+ }
43
+
23
44
  if (kind === "cron") {
24
45
  return <AlarmClock className="h-3.5 w-3.5 shrink-0 text-gray-400" />;
25
46
  }
26
47
 
27
48
  if (kind === "file") {
28
- return <FileCode2 className="h-3.5 w-3.5 shrink-0 text-gray-400" />;
49
+ return <FileTypeIcon fileName={fileName ?? ""} size="compact" />;
29
50
  }
30
51
 
31
52
  if (kind === "side-chat-draft") {
@@ -47,11 +68,17 @@ function buildCompactWorkspaceTabs(
47
68
  return tabs.map((tab) => ({
48
69
  key: tab.key,
49
70
  label: tab.title,
71
+ labelClassName: tab.isRenderedPreview ? "italic font-normal" : undefined,
50
72
  active: tab.active,
51
73
  tooltip: tab.tooltip,
52
- leadingIcon: <WorkspaceTabIcon kind={tab.kind} agentId={tab.agentId} />,
53
- badge:
54
- tab.kind === "file" && tab.viewMode === "diff" ? (
74
+ leadingIcon: (
75
+ <WorkspaceTabIcon
76
+ kind={tab.kind}
77
+ agentId={tab.agentId}
78
+ fileName={tab.fileName}
79
+ />
80
+ ),
81
+ badge: tab.kind === "file" && tab.viewMode === "diff" ? (
55
82
  <span className="shrink-0 rounded border border-amber-200 bg-amber-50 px-1 py-0 text-[9px] font-medium uppercase tracking-[0.08em] text-amber-700">
56
83
  {t("chatWorkspaceDiff")}
57
84
  </span>
@@ -61,6 +88,29 @@ function buildCompactWorkspaceTabs(
61
88
  closePlacement: "leading-hover",
62
89
  onSelect: tab.onSelect,
63
90
  onClose: tab.onClose,
91
+ menuLabel: t("chatWorkspaceFileMoreActions"),
92
+ menuActions: tab.kind === "file"
93
+ ? [
94
+ ...(tab.alternateViewerAction
95
+ ? [{
96
+ key: `viewer:${tab.alternateViewerAction.viewer}`,
97
+ icon: tab.alternateViewerAction.viewer === "rendered"
98
+ ? <Eye className="h-3.5 w-3.5" />
99
+ : <Code2 className="h-3.5 w-3.5" />,
100
+ label: tab.alternateViewerAction.label,
101
+ onClick: tab.alternateViewerAction.onSelect,
102
+ }]
103
+ : []),
104
+ ...(tab.onClose
105
+ ? [{
106
+ key: "close",
107
+ icon: <X className="h-3.5 w-3.5" />,
108
+ label: t("chatWorkspaceCloseFile"),
109
+ onClick: tab.onClose,
110
+ }]
111
+ : []),
112
+ ]
113
+ : undefined,
64
114
  }));
65
115
  }
66
116