@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
@@ -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,40 +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
- type ContentUrlPreviewKind = "image" | "audio" | "video" | "pdf" | "html" | "other";
43
-
44
- function resolveContentUrlPreviewKind(params: {
45
- path: string;
46
- mimeType?: string | null;
47
- }): ContentUrlPreviewKind {
48
- const mime = params.mimeType?.trim().toLowerCase() ?? "";
49
- const path = params.path.trim().toLowerCase();
50
- if (mime.startsWith("image/") || /\.(avif|bmp|gif|heic|heif|ico|jpe?g|png|svg|tiff?|webp)$/i.test(path)) {
51
- return "image";
52
- }
53
- if (mime.startsWith("audio/") || /\.(aac|flac|m4a|mp3|ogg|opus|wav|weba)$/i.test(path)) {
54
- return "audio";
55
- }
56
- if (mime.startsWith("video/") || /\.(avi|m4v|mkv|mov|mp4|webm|wmv)$/i.test(path)) {
57
- return "video";
58
- }
59
- if (mime.includes("pdf") || path.endsWith(".pdf")) {
60
- return "pdf";
61
- }
62
- if (mime.includes("html") || /\.html?$/i.test(path)) {
63
- return "html";
64
- }
65
- return "other";
66
- }
67
-
68
- function appendPreviewRefreshVersion(url: string, refreshVersion: number): string {
40
+ function appendPreviewRefreshVersion(
41
+ url: string,
42
+ refreshVersion: number,
43
+ ): string {
69
44
  if (refreshVersion <= 0) {
70
45
  return url;
71
46
  }
@@ -182,119 +157,9 @@ function WorkspaceCodeSurface({
182
157
  }: {
183
158
  block: ChatFileOperationBlockViewModel;
184
159
  }) {
185
- return <div className="h-full overflow-auto custom-scrollbar bg-white"><FileOperationCodeSurface block={block} layout="workspace" /></div>;
186
- }
187
-
188
- function WorkspaceHtmlRenderedPreview({
189
- src,
190
- }: {
191
- src: string;
192
- }) {
193
- return (
194
- <iframe
195
- allowFullScreen
196
- className="h-full w-full border-0 bg-white"
197
- data-testid="workspace-html-preview"
198
- src={src}
199
- title={t("chatWorkspaceHtmlPreviewTitle")}
200
- />
201
- );
202
- }
203
-
204
- function WorkspaceContentUrlPreview({
205
- contentUrl,
206
- kind,
207
- label,
208
- }: {
209
- contentUrl: string;
210
- kind: ContentUrlPreviewKind;
211
- label: string;
212
- }) {
213
- if (kind === "image") {
214
- return (
215
- <div className="flex h-full items-center justify-center overflow-auto custom-scrollbar bg-white p-4">
216
- <img
217
- src={contentUrl}
218
- alt={label}
219
- className="max-h-full max-w-full object-contain"
220
- data-testid="workspace-content-image"
221
- />
222
- </div>
223
- );
224
- }
225
- if (kind === "audio") {
226
- return (
227
- <div className="flex h-full items-center justify-center bg-white px-6">
228
- <audio
229
- controls
230
- preload="metadata"
231
- aria-label={label}
232
- className="w-full max-w-xl"
233
- data-testid="workspace-content-audio"
234
- src={contentUrl}
235
- />
236
- </div>
237
- );
238
- }
239
- if (kind === "video") {
240
- return (
241
- <div className="flex h-full items-center justify-center bg-black">
242
- <video
243
- controls
244
- playsInline
245
- preload="metadata"
246
- aria-label={label}
247
- className="max-h-full max-w-full"
248
- data-testid="workspace-content-video"
249
- src={contentUrl}
250
- />
251
- </div>
252
- );
253
- }
254
- if (kind === "pdf" || kind === "html") {
255
- return (
256
- <iframe
257
- allowFullScreen
258
- className="h-full w-full border-0 bg-white"
259
- data-testid={kind === "pdf" ? "workspace-content-pdf" : "workspace-html-preview"}
260
- src={contentUrl}
261
- title={label}
262
- />
263
- );
264
- }
265
160
  return (
266
- <div
267
- className="flex h-full flex-col items-center justify-center gap-4 px-6 text-center"
268
- data-testid="workspace-content-unsupported"
269
- >
270
- <div className="max-w-sm space-y-1.5">
271
- <p className="truncate text-sm font-medium text-foreground" title={label}>
272
- {label}
273
- </p>
274
- <p className="text-sm text-muted-foreground">
275
- {t("chatWorkspacePreviewUnsupported")}
276
- </p>
277
- <p className="text-xs leading-5 text-muted-foreground/80">
278
- {t("chatWorkspacePreviewUnsupportedHint")}
279
- </p>
280
- </div>
281
- <div className="flex flex-wrap items-center justify-center gap-2">
282
- <a
283
- href={contentUrl}
284
- download={label}
285
- className="inline-flex h-8 items-center rounded-lg border border-border bg-card px-3 text-xs font-medium text-foreground transition-colors hover:bg-muted"
286
- >
287
- {t("chatWorkspacePreviewDownload")}
288
- </a>
289
- <a
290
- href={contentUrl}
291
- target="_blank"
292
- rel="noreferrer"
293
- className="inline-flex h-8 items-center rounded-lg bg-primary px-3 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90"
294
- >
295
- {t("chatWorkspacePreviewOpenExternally")}
296
- </a>
297
- </div>
161
+ <div className="h-full overflow-auto custom-scrollbar bg-white">
162
+ <FileOperationCodeSurface block={block} layout="workspace" />
298
163
  </div>
299
164
  );
300
165
  }
@@ -304,29 +169,29 @@ function WorkspacePreviewBody({
304
169
  contentUrlKind,
305
170
  contentLabel,
306
171
  directoryQuery,
172
+ fileBasePath,
307
173
  onFileOpen,
308
174
  previewBlock,
309
175
  previewKind,
176
+ previewViewer,
310
177
  previewQuery,
311
178
  previewText,
312
- previewUrl,
313
- previewViewer,
314
179
  }: {
315
180
  contentUrl: string | null;
316
- contentUrlKind: ContentUrlPreviewKind | null;
181
+ contentUrlKind: WorkspaceFileContentKind | null;
317
182
  contentLabel: string;
318
183
  directoryQuery: ReturnType<typeof useServerPathBrowse> | null | undefined;
184
+ fileBasePath: string | null;
319
185
  onFileOpen: (action: ChatFileOpenActionViewModel) => void;
320
186
  previewBlock: ChatFileOperationBlockViewModel | null;
321
187
  previewKind: "text" | "markdown" | "binary";
188
+ previewViewer: "source" | "rendered" | null;
322
189
  previewQuery: ReturnType<typeof useServerPathRead> | null | undefined;
323
190
  previewText: string | null;
324
- previewUrl: string | null;
325
- previewViewer: "source" | "rendered";
326
191
  }) {
327
192
  if (contentUrl && contentUrlKind) {
328
193
  return (
329
- <WorkspaceContentUrlPreview
194
+ <WorkspaceFileContentPreview
330
195
  contentUrl={contentUrl}
331
196
  kind={contentUrlKind}
332
197
  label={contentLabel}
@@ -375,11 +240,7 @@ function WorkspacePreviewBody({
375
240
  );
376
241
  }
377
242
 
378
- if (previewViewer === "rendered" && previewUrl) {
379
- return <WorkspaceHtmlRenderedPreview src={previewUrl} />;
380
- }
381
-
382
- if (previewKind === "markdown" && previewText) {
243
+ if (previewKind === "markdown" && previewViewer !== "source" && previewText) {
383
244
  return (
384
245
  <div className="h-full overflow-auto custom-scrollbar px-5 py-4">
385
246
  <ChatMessageMarkdown
@@ -390,6 +251,9 @@ function WorkspacePreviewBody({
390
251
  copiedCodeLabel: t("chatCodeCopied"),
391
252
  }}
392
253
  onFileOpen={onFileOpen}
254
+ resolveFileContentUrl={(action) =>
255
+ buildServerPathContentUrl(action.path, fileBasePath)
256
+ }
393
257
  />
394
258
  </div>
395
259
  );
@@ -418,8 +282,8 @@ export function ChatSessionWorkspaceFilePreview({
418
282
  onFileOpen,
419
283
  }: ChatSessionWorkspaceFilePreviewProps) {
420
284
  const isPreviewMode = file.viewMode === "preview";
421
- const contentUrl = file.contentUrl?.trim() || null;
422
- const usesServerPath = isPreviewMode && !contentUrl;
285
+ const suppliedContentUrl = file.contentUrl?.trim() || null;
286
+ const usesServerPath = isPreviewMode && !suppliedContentUrl;
423
287
  const previewQuery = useServerPathRead({
424
288
  path: file.path,
425
289
  basePath: sessionWorkingDir,
@@ -435,14 +299,15 @@ export function ChatSessionWorkspaceFilePreview({
435
299
  () => (file.viewMode === "diff" ? buildDiffBlock(file) : null),
436
300
  [file],
437
301
  );
438
- const previewText =
439
- isPreviewMode ? previewQuery?.data?.text ?? file.rawText ?? null : null;
302
+ const previewText = isPreviewMode
303
+ ? (previewQuery?.data?.text ?? file.rawText ?? null)
304
+ : null;
440
305
  const previewKind = inferPreviewKind({
441
306
  path: previewQuery?.data?.resolvedPath ?? file.path,
442
307
  serverKind: previewQuery?.data?.kind,
443
308
  });
444
- const contentUrlKind = contentUrl
445
- ? resolveContentUrlPreviewKind({
309
+ const suppliedContentKind = suppliedContentUrl
310
+ ? resolveWorkspaceFileContentKind({
446
311
  path: file.path,
447
312
  mimeType: file.mimeType,
448
313
  })
@@ -452,17 +317,32 @@ export function ChatSessionWorkspaceFilePreview({
452
317
  previewQuery?.data?.resolvedPath ??
453
318
  file.path;
454
319
  const previewPathKind = directoryQuery?.data ? "directory" : "file";
455
- const previewViewer = resolveFilePreviewViewer({
320
+ const previewViewer = resolveWorkspaceFileViewer(
321
+ resolvedPath,
322
+ file.previewViewer,
323
+ );
324
+ const localContentKind = resolveWorkspaceFileContentKind({
456
325
  path: resolvedPath,
457
- viewer: file.previewViewer,
458
326
  });
459
- const previewUrl =
460
- previewViewer === "rendered" && previewQuery?.data?.resolvedPath
461
- ? appendPreviewRefreshVersion(
462
- buildServerPathContentUrl(previewQuery.data.resolvedPath),
463
- refreshVersion,
464
- )
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)
465
342
  : null;
343
+ const contentUrl = suppliedContentUrl ?? localContentUrl;
344
+ const contentUrlKind =
345
+ suppliedContentKind ?? (localContentUrl ? localContentKind : null);
466
346
  const previewBlock = useMemo(() => {
467
347
  if (!isPreviewMode || !previewText || contentUrl) {
468
348
  return null;
@@ -482,7 +362,7 @@ export function ChatSessionWorkspaceFilePreview({
482
362
  previewQuery?.data?.resolvedPath,
483
363
  previewText,
484
364
  ]);
485
- const isTruncated = Boolean(previewQuery?.data?.truncated);
365
+ const isTextPreviewTruncated = !contentUrl && Boolean(previewQuery?.data?.truncated);
486
366
  const breadcrumbBasePath = sessionProjectRoot ?? sessionWorkingDir;
487
367
  const breadcrumb = useMemo(
488
368
  () =>
@@ -492,13 +372,13 @@ export function ChatSessionWorkspaceFilePreview({
492
372
  sessionProjectRoot: breadcrumbBasePath,
493
373
  line: file.line,
494
374
  column: file.column,
495
- truncated: isTruncated,
375
+ truncated: isTextPreviewTruncated,
496
376
  }),
497
377
  [
498
378
  breadcrumbBasePath,
499
379
  file.column,
500
380
  file.line,
501
- isTruncated,
381
+ isTextPreviewTruncated,
502
382
  previewPathKind,
503
383
  resolvedPath,
504
384
  ],
@@ -520,13 +400,13 @@ export function ChatSessionWorkspaceFilePreview({
520
400
  contentUrlKind={contentUrlKind}
521
401
  contentLabel={file.label?.trim() || resolvedPath}
522
402
  directoryQuery={directoryQuery}
403
+ fileBasePath={sessionWorkingDir}
523
404
  onFileOpen={onFileOpen}
524
405
  previewBlock={previewBlock}
525
406
  previewKind={previewKind}
407
+ previewViewer={previewViewer}
526
408
  previewQuery={previewQuery}
527
409
  previewText={previewText}
528
- previewUrl={previewUrl}
529
- previewViewer={previewViewer}
530
410
  />
531
411
  )}
532
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
  <>