@nextclaw/ui 0.15.1 → 0.15.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/assets/{api-b1R9VdeH.js → api-iNBpppzj.js} +3 -3
  3. package/dist/assets/{appearance-settings-page-D2O9pch9.js → appearance-settings-page-DMffQFR3.js} +1 -1
  4. package/dist/assets/{book-open-B77lQ2Tg.js → book-open-C3mJGwiy.js} +1 -1
  5. package/dist/assets/channels-list-page-CCtp0BRS.js +8 -0
  6. package/dist/assets/chat-page-H5a0dWvO.js +1 -0
  7. package/dist/assets/config-split-page-CrOJgB-Y.js +1 -0
  8. package/dist/assets/{confirm-dialog-C-NXSioX.js → confirm-dialog-Chv50USy.js} +2 -2
  9. package/dist/assets/{desktop-update-config-usHpXugP.js → desktop-update-config-KzTuXRf_.js} +1 -1
  10. package/dist/assets/{dist-jdGmbb6w.js → dist-BunzmC4B.js} +1 -1
  11. package/dist/assets/{dist-YtoqU-AT.js → dist-ChmGwvtK.js} +1 -1
  12. package/dist/assets/doc-browser-DNRL4Zvd.js +1 -0
  13. package/dist/assets/doc-browser-Dg0pmkOg.js +1 -0
  14. package/dist/assets/doc-browser-context-BHY-0FSD.js +1 -0
  15. package/dist/assets/{es2015-bNpL2LKy.js → es2015-DQZ5JWwK.js} +1 -1
  16. package/dist/assets/{external-link-BM6fk8q-.js → external-link-CU0kzzIP.js} +1 -1
  17. package/dist/assets/form-actions-BvCp_0lS.js +1 -0
  18. package/dist/assets/{i18n-provider-CntWolRA.js → i18n-provider-Cu8XyZb_.js} +1 -1
  19. package/dist/assets/index-CzfBdvK1.css +1 -0
  20. package/dist/assets/index-DGqHne1Q.js +104 -0
  21. package/dist/assets/loader-circle-CDSFuzmk.js +1 -0
  22. package/dist/assets/mcp-marketplace-page-BeEQK0DL.js +9 -0
  23. package/dist/assets/mcp-marketplace-page-D6idOTLp.js +1 -0
  24. package/dist/assets/model-config-page-bNNo-XyG.js +1 -0
  25. package/dist/assets/plus-D4DsdLN-.js +1 -0
  26. package/dist/assets/{provider-scoped-model-input-Cm_a4GHf.js → provider-scoped-model-input-IlgQCsoT.js} +1 -1
  27. package/dist/assets/providers-config-page-wLo6CAJw.js +1 -0
  28. package/dist/assets/{react-BN8uzPnp.js → react-BPIsQIlO.js} +1 -1
  29. package/dist/assets/{refresh-cw-BpVcFPmJ.js → refresh-cw-CILd1vAP.js} +1 -1
  30. package/dist/assets/remote-uKEYp7AR.js +1 -0
  31. package/dist/assets/rotate-cw-wPFPg1kH.js +1 -0
  32. package/dist/assets/runtime-config-page-DDn1sP4G.js +1 -0
  33. package/dist/assets/{save-BpFW5fpk.js → save-ByJqcPTP.js} +1 -1
  34. package/dist/assets/{search-D0uO7enf.js → search-DVIV6pfD.js} +1 -1
  35. package/dist/assets/search-config-page-B-Cf4iiB.js +1 -0
  36. package/dist/assets/secrets-config-page-DPKsY8Iw.js +3 -0
  37. package/dist/assets/security-config-e9SSBlKr.js +1 -0
  38. package/dist/assets/{select-Bh-CrA4p.js → select-BdgczSNw.js} +1 -1
  39. package/dist/assets/{setting-row-BJfCHRcb.js → setting-row-BkTVUTKx.js} +1 -1
  40. package/dist/assets/{settings-2-Blzi1vr6.js → settings-2-CH5shGg_.js} +1 -1
  41. package/dist/assets/{side-dock-ZdaSrQVB.js → side-dock-BsBvcJ9i.js} +2 -2
  42. package/dist/assets/{skeleton-BLlEzebZ.js → skeleton-DeX1FKw7.js} +1 -1
  43. package/dist/assets/switch-Cv8Ekb3x.js +1 -0
  44. package/dist/assets/{tag-chip-CzGihiQx.js → tag-chip-RB9uKcoC.js} +1 -1
  45. package/dist/assets/{tooltip-CdcrBjrC.js → tooltip-oQlEN-l6.js} +1 -1
  46. package/dist/assets/{use-config-CVPgsBmU.js → use-config-l3W7y8Qz.js} +1 -1
  47. package/dist/assets/{use-confirm-dialog-CSeBk9SF.js → use-confirm-dialog-BAxaVOJI.js} +1 -1
  48. package/dist/assets/x-BBMffEN0.js +1 -0
  49. package/dist/index.html +38 -30
  50. package/index.html +12 -4
  51. package/package.json +9 -9
  52. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +29 -1
  53. package/src/app/components/layout/sidebar-items.tsx +46 -7
  54. package/src/app/components/layout/sidebar-rail.styles.ts +2 -2
  55. package/src/app/components/layout/sidebar.tsx +84 -62
  56. package/src/app/configs/app-navigation.config.ts +49 -0
  57. package/src/app/styles/design-system.css +99 -4
  58. package/src/features/channels/components/config/channel-form.tsx +14 -11
  59. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +40 -0
  60. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +3 -1
  61. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +1 -1
  62. package/src/features/chat/components/layout/__tests__/chat-sidebar-toolbar.test.tsx +48 -0
  63. package/src/features/chat/components/layout/__tests__/chat-sidebar-utility-menu.test.tsx +55 -0
  64. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +0 -3
  65. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +3 -4
  66. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +30 -31
  67. package/src/features/chat/components/layout/chat-sidebar.tsx +21 -7
  68. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +2 -1
  69. package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +1 -1
  70. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +22 -1
  71. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +62 -1
  72. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +10 -5
  73. package/src/features/chat/features/message/components/chat-message-list.container.tsx +47 -1
  74. package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +11 -1
  75. package/src/features/chat/features/message/utils/__tests__/chat-message-texts.utils.test.ts +23 -0
  76. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +0 -11
  77. package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +5 -0
  78. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +138 -0
  79. package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +116 -0
  80. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +108 -46
  81. package/src/features/chat/features/session/components/chat-sidebar-session-entry.tsx +6 -0
  82. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +48 -24
  83. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +0 -3
  84. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +0 -4
  85. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +2 -2
  86. package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +25 -3
  87. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +9 -4
  88. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +1 -1
  89. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +1 -1
  90. package/src/features/chat/features/welcome/components/chat-welcome.tsx +0 -5
  91. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +226 -56
  92. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +1 -0
  93. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +144 -0
  94. package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +99 -0
  95. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +2 -2
  96. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +4 -2
  97. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +232 -21
  98. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +3 -0
  99. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +44 -1
  100. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +45 -3
  101. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +4 -0
  102. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +33 -2
  103. package/src/features/chat/managers/chat-session-list.manager.ts +32 -0
  104. package/src/features/chat/managers/chat-thread.manager.ts +2 -0
  105. package/src/features/chat/stores/chat-session-list.store.ts +28 -5
  106. package/src/features/chat/stores/chat-thread.store.ts +3 -0
  107. package/src/features/cron/components/cron-config.tsx +63 -80
  108. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.tsx +44 -6
  109. package/src/features/marketplace/components/marketplace-detail-doc.tsx +8 -4
  110. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  111. package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +5 -12
  112. package/src/features/marketplace/hooks/use-marketplace-detail-doc-entry.ts +84 -0
  113. package/src/features/panel-apps/components/panel-app-list-item.tsx +6 -6
  114. package/src/features/panel-apps/components/panel-apps-list.tsx +2 -2
  115. package/src/features/panel-apps/index.ts +1 -0
  116. package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +2 -10
  117. package/src/features/panel-apps/utils/panel-app-iframe.utils.ts +6 -0
  118. package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +11 -3
  119. package/src/features/pwa/managers/pwa-shell-theme.manager.ts +3 -1
  120. package/src/features/right-panel-resources/components/right-panel-resource-home-page.tsx +1 -1
  121. package/src/features/service-apps/components/service-apps-panel.tsx +145 -35
  122. package/src/features/settings/components/config/provider-auth-section.tsx +1 -1
  123. package/src/features/settings/components/config/provider-form.tsx +11 -8
  124. package/src/features/settings/components/config/provider-models-section.tsx +7 -7
  125. package/src/features/settings/components/config/provider-pill-selector.tsx +1 -1
  126. package/src/features/settings/components/config/secrets-config-form.tsx +5 -4
  127. package/src/features/settings/pages/model-config-page.tsx +26 -25
  128. package/src/features/settings/pages/providers-config-page.tsx +10 -13
  129. package/src/features/settings/pages/search-config-page.tsx +8 -5
  130. package/src/features/side-dock/components/side-dock.tsx +3 -3
  131. package/src/features/system-status/components/config/runtime-config-editor.tsx +5 -4
  132. package/src/index.css +257 -13
  133. package/src/shared/components/actions/action-link.tsx +1 -1
  134. package/src/shared/components/config-split-page.tsx +6 -6
  135. package/src/shared/components/doc-browser/__tests__/doc-browser-url.utils.test.ts +73 -0
  136. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +48 -0
  137. package/src/shared/components/doc-browser/doc-browser-context.tsx +4 -0
  138. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +6 -2
  139. package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +2 -1
  140. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +10 -10
  141. package/src/shared/components/doc-browser/doc-browser.tsx +6 -4
  142. package/src/shared/components/doc-browser/utils/doc-browser-url.utils.ts +62 -4
  143. package/src/shared/components/resizable-right-panel/__tests__/resizable-right-panel.test.tsx +18 -0
  144. package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +28 -3
  145. package/src/shared/components/ui/actions/__tests__/icon-action-button.test.tsx +36 -0
  146. package/src/shared/components/ui/actions/form-actions.tsx +25 -0
  147. package/src/shared/components/ui/actions/icon-action-button.tsx +25 -2
  148. package/src/shared/components/ui/button.tsx +1 -1
  149. package/src/shared/components/ui/card.tsx +12 -4
  150. package/src/shared/components/ui/dialog.tsx +1 -1
  151. package/src/shared/components/ui/input.tsx +1 -1
  152. package/src/shared/components/ui/select.tsx +1 -1
  153. package/src/shared/components/ui/switch.tsx +1 -1
  154. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +2 -1
  155. package/src/shared/components/ui/tabs.tsx +1 -1
  156. package/src/shared/components/ui/textarea.tsx +1 -1
  157. package/src/shared/hooks/use-server-path-browse.ts +26 -4
  158. package/src/shared/hooks/use-server-path-read.ts +11 -1
  159. package/src/shared/lib/api/utils/server-path.utils.ts +1 -0
  160. package/src/shared/lib/i18n/locales/en-US/chat.json +99 -0
  161. package/src/shared/lib/i18n/locales/en-US/core.json +3 -0
  162. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +5 -0
  163. package/src/shared/lib/i18n/locales/zh-CN/chat.json +99 -0
  164. package/src/shared/lib/i18n/locales/zh-CN/core.json +3 -0
  165. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +5 -0
  166. package/src/shared/lib/session-project/workspace-file-breadcrumb.utils.ts +9 -5
  167. package/src/shared/lib/theme/index.test.ts +21 -0
  168. package/src/shared/lib/theme/index.ts +6 -4
  169. package/src/vite-env.d.ts +2 -0
  170. package/dist/assets/channels-list-page-DcsIQc6A.js +0 -8
  171. package/dist/assets/chat-page-CZ7WxPir.js +0 -1
  172. package/dist/assets/config-split-page-BWQ6_Raq.js +0 -1
  173. package/dist/assets/doc-browser-B_Tf9n70.js +0 -1
  174. package/dist/assets/doc-browser-BdZPFOVa.js +0 -1
  175. package/dist/assets/doc-browser-context-DwokemUn.js +0 -1
  176. package/dist/assets/index-BIcEw90u.js +0 -104
  177. package/dist/assets/index-BiNSTKTU.css +0 -1
  178. package/dist/assets/loader-circle-MBgNSVIH.js +0 -1
  179. package/dist/assets/mcp-marketplace-page-CLmUstLN.js +0 -9
  180. package/dist/assets/mcp-marketplace-page-rZkmWPMy.js +0 -1
  181. package/dist/assets/model-config-page-Di0ySJD2.js +0 -1
  182. package/dist/assets/plus-vDqW5Kxp.js +0 -1
  183. package/dist/assets/providers-config-page-BAf2W0MV.js +0 -1
  184. package/dist/assets/remote-CLQ0IBM8.js +0 -1
  185. package/dist/assets/rotate-cw-CvNBv7_w.js +0 -1
  186. package/dist/assets/runtime-config-page-CflvvoDc.js +0 -1
  187. package/dist/assets/search-config-page-D0klFUXd.js +0 -1
  188. package/dist/assets/secrets-config-page-dVZ631Wp.js +0 -3
  189. package/dist/assets/security-config-C5gnlkuj.js +0 -1
  190. package/dist/assets/switch-CFLJFNSd.js +0 -1
  191. package/dist/assets/x-BF2KQ--V.js +0 -1
  192. /package/dist/assets/{config-hints-fGnUjDe9.js → config-hints-C_iP1zl_.js} +0 -0
@@ -4,12 +4,20 @@ import type {
4
4
  ChatFileOperationBlockViewModel,
5
5
  ChatFilePreviewViewer,
6
6
  } from "@nextclaw/agent-chat-ui";
7
- import { ChatMessageMarkdown, FileOperationCodeSurface } from "@nextclaw/agent-chat-ui";
7
+ import {
8
+ ChatMessageMarkdown,
9
+ FileOperationCodeSurface,
10
+ } from "@nextclaw/agent-chat-ui";
8
11
  import type { ChatWorkspaceFileTab } from "@/features/chat/stores/chat-thread.store";
12
+ import { ChatSessionWorkspaceDirectoryBrowser } from "./chat-session-workspace-directory-browser";
9
13
  import { ChatSessionWorkspaceFileBreadcrumbs } from "./chat-session-workspace-file-breadcrumbs";
14
+ import { useServerPathBrowse } from "@/shared/hooks/use-server-path-browse";
10
15
  import { useServerPathRead } from "@/shared/hooks/use-server-path-read";
11
16
  import { buildServerPathContentUrl } from "@/shared/lib/api";
12
- import { buildLineDiff, buildPreviewLines } from "@/features/chat/features/message/utils/file-operation/line-builder.utils";
17
+ import {
18
+ buildLineDiff,
19
+ buildPreviewLines,
20
+ } from "@/features/chat/features/message/utils/file-operation/line-builder.utils";
13
21
  import { t } from "@/shared/lib/i18n";
14
22
  import { buildWorkspaceFileBreadcrumb } from "@/shared/lib/session-project";
15
23
  import { cn } from "@/shared/lib/utils";
@@ -31,6 +39,39 @@ function resolveFilePreviewViewer(params: {
31
39
  return params.viewer === "rendered" && /\.html?$/i.test(params.path) ? "rendered" : "source";
32
40
  }
33
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 {
69
+ if (refreshVersion <= 0) {
70
+ return url;
71
+ }
72
+ return `${url}${url.includes("?") ? "&" : "?"}refresh=${refreshVersion}`;
73
+ }
74
+
34
75
  function buildPreviewBlock(params: {
35
76
  path: string;
36
77
  text: string;
@@ -160,7 +201,109 @@ function WorkspaceHtmlRenderedPreview({
160
201
  );
161
202
  }
162
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
+ 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>
298
+ </div>
299
+ );
300
+ }
301
+
163
302
  function WorkspacePreviewBody({
303
+ contentUrl,
304
+ contentUrlKind,
305
+ contentLabel,
306
+ directoryQuery,
164
307
  onFileOpen,
165
308
  previewBlock,
166
309
  previewKind,
@@ -169,25 +312,57 @@ function WorkspacePreviewBody({
169
312
  previewUrl,
170
313
  previewViewer,
171
314
  }: {
315
+ contentUrl: string | null;
316
+ contentUrlKind: ContentUrlPreviewKind | null;
317
+ contentLabel: string;
318
+ directoryQuery: ReturnType<typeof useServerPathBrowse> | null | undefined;
172
319
  onFileOpen: (action: ChatFileOpenActionViewModel) => void;
173
320
  previewBlock: ChatFileOperationBlockViewModel | null;
174
321
  previewKind: "text" | "markdown" | "binary";
175
- previewQuery: ReturnType<typeof useServerPathRead>;
322
+ previewQuery: ReturnType<typeof useServerPathRead> | null | undefined;
176
323
  previewText: string | null;
177
324
  previewUrl: string | null;
178
325
  previewViewer: "source" | "rendered";
179
326
  }) {
180
- if (previewQuery.isLoading && !previewBlock) {
181
- return <WorkspaceFilePreviewStatus text={t("chatWorkspaceLoadingFile")} />;
327
+ if (contentUrl && contentUrlKind) {
328
+ return (
329
+ <WorkspaceContentUrlPreview
330
+ contentUrl={contentUrl}
331
+ kind={contentUrlKind}
332
+ label={contentLabel}
333
+ />
334
+ );
182
335
  }
183
336
 
184
- if (previewQuery.data?.kind === "binary") {
337
+ if (directoryQuery?.data) {
338
+ return (
339
+ <ChatSessionWorkspaceDirectoryBrowser
340
+ browseQuery={directoryQuery}
341
+ onFileOpen={onFileOpen}
342
+ renderStatus={(params) => <WorkspaceFilePreviewStatus {...params} />}
343
+ />
344
+ );
345
+ }
346
+
347
+ if (directoryQuery?.isLoading && previewQuery?.error && !previewBlock) {
348
+ return (
349
+ <WorkspaceFilePreviewStatus text={t("chatWorkspaceLoadingDirectory")} />
350
+ );
351
+ }
352
+
353
+ if ((directoryQuery?.isLoading || previewQuery?.isLoading) && !previewBlock) {
354
+ return (
355
+ <WorkspaceFilePreviewStatus text={t("chatWorkspaceLoadingPreview")} />
356
+ );
357
+ }
358
+
359
+ if (previewQuery?.data?.kind === "binary") {
185
360
  return (
186
361
  <WorkspaceFilePreviewStatus text={t("chatWorkspacePreviewUnsupported")} />
187
362
  );
188
363
  }
189
364
 
190
- if (previewQuery.error && !previewBlock) {
365
+ if (previewQuery?.error && !previewBlock) {
191
366
  return (
192
367
  <WorkspaceFilePreviewStatus
193
368
  tone="error"
@@ -229,6 +404,7 @@ function WorkspacePreviewBody({
229
404
 
230
405
  type ChatSessionWorkspaceFilePreviewProps = {
231
406
  file: ChatWorkspaceFileTab;
407
+ refreshVersion?: number;
232
408
  sessionProjectRoot: string | null;
233
409
  sessionWorkingDir: string | null;
234
410
  onFileOpen: (action: ChatFileOpenActionViewModel) => void;
@@ -236,65 +412,96 @@ type ChatSessionWorkspaceFilePreviewProps = {
236
412
 
237
413
  export function ChatSessionWorkspaceFilePreview({
238
414
  file,
415
+ refreshVersion = 0,
239
416
  sessionProjectRoot,
240
417
  sessionWorkingDir,
241
418
  onFileOpen,
242
419
  }: ChatSessionWorkspaceFilePreviewProps) {
243
420
  const isPreviewMode = file.viewMode === "preview";
421
+ const contentUrl = file.contentUrl?.trim() || null;
422
+ const usesServerPath = isPreviewMode && !contentUrl;
244
423
  const previewQuery = useServerPathRead({
245
424
  path: file.path,
246
425
  basePath: sessionWorkingDir,
247
- enabled: isPreviewMode,
426
+ enabled: usesServerPath,
427
+ });
428
+ const directoryQuery = useServerPathBrowse({
429
+ path: file.path,
430
+ basePath: sessionWorkingDir,
431
+ includeFiles: true,
432
+ enabled: usesServerPath,
248
433
  });
249
434
  const diffBlock = useMemo(
250
435
  () => (file.viewMode === "diff" ? buildDiffBlock(file) : null),
251
436
  [file],
252
437
  );
253
438
  const previewText =
254
- isPreviewMode ? previewQuery.data?.text ?? file.rawText ?? null : null;
439
+ isPreviewMode ? previewQuery?.data?.text ?? file.rawText ?? null : null;
255
440
  const previewKind = inferPreviewKind({
256
- path: previewQuery.data?.resolvedPath ?? file.path,
257
- serverKind: previewQuery.data?.kind,
441
+ path: previewQuery?.data?.resolvedPath ?? file.path,
442
+ serverKind: previewQuery?.data?.kind,
258
443
  });
259
- const resolvedPath = previewQuery.data?.resolvedPath ?? file.path;
444
+ const contentUrlKind = contentUrl
445
+ ? resolveContentUrlPreviewKind({
446
+ path: file.path,
447
+ mimeType: file.mimeType,
448
+ })
449
+ : null;
450
+ const resolvedPath =
451
+ directoryQuery?.data?.currentPath ??
452
+ previewQuery?.data?.resolvedPath ??
453
+ file.path;
454
+ const previewPathKind = directoryQuery?.data ? "directory" : "file";
260
455
  const previewViewer = resolveFilePreviewViewer({
261
456
  path: resolvedPath,
262
457
  viewer: file.previewViewer,
263
458
  });
264
459
  const previewUrl =
265
- previewViewer === "rendered" && previewQuery.data?.resolvedPath
266
- ? buildServerPathContentUrl(previewQuery.data.resolvedPath)
460
+ previewViewer === "rendered" && previewQuery?.data?.resolvedPath
461
+ ? appendPreviewRefreshVersion(
462
+ buildServerPathContentUrl(previewQuery.data.resolvedPath),
463
+ refreshVersion,
464
+ )
267
465
  : null;
268
466
  const previewBlock = useMemo(() => {
269
- if (!isPreviewMode || !previewText) {
467
+ if (!isPreviewMode || !previewText || contentUrl) {
270
468
  return null;
271
469
  }
272
470
  return buildPreviewBlock({
273
- path: previewQuery.data?.resolvedPath ?? file.path,
471
+ path: previewQuery?.data?.resolvedPath ?? file.path,
274
472
  text: previewText,
275
- languageHint: previewQuery.data?.languageHint ?? null,
473
+ languageHint: previewQuery?.data?.languageHint ?? null,
276
474
  line: file.line,
277
475
  });
278
476
  }, [
477
+ contentUrl,
279
478
  file.line,
280
479
  file.path,
281
480
  isPreviewMode,
282
- previewQuery.data?.languageHint,
283
- previewQuery.data?.resolvedPath,
481
+ previewQuery?.data?.languageHint,
482
+ previewQuery?.data?.resolvedPath,
284
483
  previewText,
285
484
  ]);
286
- const isTruncated = Boolean(previewQuery.data?.truncated);
485
+ const isTruncated = Boolean(previewQuery?.data?.truncated);
287
486
  const breadcrumbBasePath = sessionProjectRoot ?? sessionWorkingDir;
288
487
  const breadcrumb = useMemo(
289
488
  () =>
290
489
  buildWorkspaceFileBreadcrumb({
291
490
  path: resolvedPath,
491
+ kind: previewPathKind,
292
492
  sessionProjectRoot: breadcrumbBasePath,
293
493
  line: file.line,
294
494
  column: file.column,
295
495
  truncated: isTruncated,
296
496
  }),
297
- [breadcrumbBasePath, file.column, file.line, isTruncated, resolvedPath],
497
+ [
498
+ breadcrumbBasePath,
499
+ file.column,
500
+ file.line,
501
+ isTruncated,
502
+ previewPathKind,
503
+ resolvedPath,
504
+ ],
298
505
  );
299
506
 
300
507
  return (
@@ -309,6 +516,10 @@ export function ChatSessionWorkspaceFilePreview({
309
516
  <WorkspaceDiffBody diffBlock={diffBlock} />
310
517
  ) : (
311
518
  <WorkspacePreviewBody
519
+ contentUrl={contentUrl}
520
+ contentUrlKind={contentUrlKind}
521
+ contentLabel={file.label?.trim() || resolvedPath}
522
+ directoryQuery={directoryQuery}
312
523
  onFileOpen={onFileOpen}
313
524
  previewBlock={previewBlock}
314
525
  previewKind={previewKind}
@@ -10,6 +10,7 @@ import { t } from "@/shared/lib/i18n";
10
10
 
11
11
  type ChatSessionWorkspacePanelContentProps = {
12
12
  activeSelection: WorkspaceSelection;
13
+ filePreviewRefreshVersion: number;
13
14
  sessionCronJobs: readonly CronJobView[];
14
15
  sessionProjectRoot: string | null;
15
16
  sessionWorkingDir: string | null;
@@ -67,6 +68,7 @@ function WorkspaceSideChatDraftHeader() {
67
68
 
68
69
  export function ChatSessionWorkspacePanelContent({
69
70
  activeSelection,
71
+ filePreviewRefreshVersion,
70
72
  sessionCronJobs,
71
73
  sessionProjectRoot,
72
74
  sessionWorkingDir,
@@ -113,6 +115,7 @@ export function ChatSessionWorkspacePanelContent({
113
115
  return (
114
116
  <ChatSessionWorkspaceFilePreview
115
117
  file={activeSelection.file}
118
+ refreshVersion={filePreviewRefreshVersion}
116
119
  sessionProjectRoot={sessionProjectRoot}
117
120
  sessionWorkingDir={sessionWorkingDir}
118
121
  onFileOpen={presenter.chatThreadManager.openFilePreview}
@@ -1,4 +1,15 @@
1
- import { AlarmClock, ArrowLeft, ArrowRight, FileCode2, MessageSquarePlus, MessageSquareText, X } from "lucide-react";
1
+ import {
2
+ AlarmClock,
3
+ ArrowLeft,
4
+ ArrowRight,
5
+ FileCode2,
6
+ Maximize2,
7
+ MessageSquarePlus,
8
+ MessageSquareText,
9
+ Minimize2,
10
+ RefreshCw,
11
+ X,
12
+ } from "lucide-react";
2
13
  import type { WorkspaceTabViewModel } from "@/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils";
3
14
  import { AgentIdentityAvatar } from "@/shared/components/common/agent-identity";
4
15
  import {
@@ -56,22 +67,54 @@ function buildCompactWorkspaceTabs(
56
67
  export function WorkspaceTabsBar({
57
68
  canGoBack,
58
69
  canGoForward,
70
+ isMaximized = false,
59
71
  onClose,
60
72
  onGoBack,
61
73
  onGoForward,
74
+ onRefreshFile,
75
+ onToggleMaximize,
62
76
  tabs,
63
77
  }: {
64
78
  canGoBack: boolean;
65
79
  canGoForward: boolean;
80
+ isMaximized?: boolean;
66
81
  onClose: () => void;
67
82
  onGoBack: () => void;
68
83
  onGoForward: () => void;
84
+ onRefreshFile?: () => void;
85
+ onToggleMaximize?: () => void;
69
86
  tabs: readonly WorkspaceTabViewModel[];
70
87
  }) {
71
88
  const compactTabs = buildCompactWorkspaceTabs(tabs);
72
89
  const actions: CompactTabStripAction[] = [
73
90
  { key: "back", icon: <ArrowLeft className="h-4 w-4" />, label: t("chatWorkspaceBack"), disabled: !canGoBack, onClick: onGoBack },
74
91
  { key: "forward", icon: <ArrowRight className="h-4 w-4" />, label: t("chatWorkspaceForward"), disabled: !canGoForward, onClick: onGoForward },
92
+ ...(onRefreshFile
93
+ ? [
94
+ {
95
+ key: "refresh-file",
96
+ icon: <RefreshCw className="h-4 w-4" />,
97
+ label: t("chatWorkspaceRefreshFile"),
98
+ onClick: onRefreshFile,
99
+ },
100
+ ]
101
+ : []),
102
+ ...(onToggleMaximize
103
+ ? [
104
+ {
105
+ key: "maximize",
106
+ icon: isMaximized ? (
107
+ <Minimize2 className="h-4 w-4" />
108
+ ) : (
109
+ <Maximize2 className="h-4 w-4" />
110
+ ),
111
+ label: isMaximized
112
+ ? t("chatWorkspaceRestorePanel")
113
+ : t("chatWorkspaceMaximizePanel"),
114
+ onClick: onToggleMaximize,
115
+ },
116
+ ]
117
+ : []),
75
118
  { key: "close", icon: <X className="h-4 w-4" />, label: t("chatWorkspaceClosePanel"), onClick: onClose },
76
119
  ];
77
120
 
@@ -1,4 +1,5 @@
1
- import { useEffect, useMemo } from "react";
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import { useQueryClient } from "@tanstack/react-query";
2
3
  import type { CronJobView } from "@/shared/lib/api";
3
4
  import { useNcpChildSessionTabsView } from "@/features/chat/features/ncp/hooks/use-ncp-child-session-tabs-view";
4
5
  import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
@@ -22,6 +23,8 @@ import {
22
23
  canGoBackInNavigationHistory,
23
24
  canGoForwardInNavigationHistory,
24
25
  } from "@/shared/lib/navigation-history";
26
+ import { buildServerPathBrowseQueryKey } from "@/shared/hooks/use-server-path-browse";
27
+ import { buildServerPathReadQueryKey } from "@/shared/hooks/use-server-path-read";
25
28
 
26
29
  type ChatSessionWorkspacePanelProps = {
27
30
  sessionKey: string | null;
@@ -55,6 +58,9 @@ export function ChatSessionWorkspacePanel({
55
58
  displayMode = "docked",
56
59
  }: ChatSessionWorkspacePanelProps) {
57
60
  const presenter = usePresenter();
61
+ const queryClient = useQueryClient();
62
+ const [isMaximized, setIsMaximized] = useState(false);
63
+ const [filePreviewRefreshVersion, setFilePreviewRefreshVersion] = useState(0);
58
64
  const resolvedChildTabs = useNcpChildSessionTabsView(childSessionTabs);
59
65
  const optimisticReadAtBySessionKey = useChatSessionListStore(
60
66
  (state) => state.optimisticReadAtBySessionKey,
@@ -110,30 +116,66 @@ export function ChatSessionWorkspacePanel({
110
116
  return null;
111
117
  }
112
118
 
119
+ const isContainerMaximized = displayMode === "docked" && isMaximized;
120
+ const isOverlayPanel = displayMode === "overlay" || isContainerMaximized;
121
+ const activeFile =
122
+ activeSelection.kind === "file" && activeSelection.file.viewMode === "preview"
123
+ ? activeSelection.file
124
+ : null;
125
+ const refreshActiveFile = activeFile
126
+ ? () => {
127
+ void queryClient.invalidateQueries({
128
+ queryKey: buildServerPathReadQueryKey({
129
+ path: activeFile.path,
130
+ basePath: sessionWorkingDir,
131
+ }),
132
+ });
133
+ void queryClient.invalidateQueries({
134
+ queryKey: buildServerPathBrowseQueryKey({
135
+ path: activeFile.path,
136
+ basePath: sessionWorkingDir,
137
+ includeFiles: true,
138
+ }),
139
+ });
140
+ setFilePreviewRefreshVersion((value) => value + 1);
141
+ }
142
+ : undefined;
143
+
113
144
  return (
114
145
  <ResizableRightPanel
146
+ data-testid="chat-session-workspace-panel"
115
147
  className={cn(
116
- displayMode === "overlay"
148
+ isOverlayPanel
117
149
  ? "bg-white"
118
150
  : "hidden border-gray-200/70 bg-white/95 backdrop-blur-sm md:flex",
151
+ isContainerMaximized ? "shadow-xl" : null,
119
152
  )}
120
153
  defaultWidth={480}
121
154
  minWidth={360}
122
155
  maxWidth={860}
123
- overlay={displayMode === "overlay"}
156
+ overlay={isOverlayPanel}
157
+ overlayScope={isContainerMaximized ? "container" : "viewport"}
124
158
  >
125
159
  <WorkspaceTabsBar
126
160
  tabs={workspaceTabs}
127
161
  canGoBack={canGoBackInNavigationHistory(workspaceHistory)}
128
162
  canGoForward={canGoForwardInNavigationHistory(workspaceHistory)}
163
+ isMaximized={isContainerMaximized}
129
164
  onGoBack={presenter.chatThreadManager.goBackWorkspacePanel}
130
165
  onGoForward={presenter.chatThreadManager.goForwardWorkspacePanel}
166
+ onRefreshFile={refreshActiveFile}
167
+ onToggleMaximize={
168
+ displayMode === "docked"
169
+ ? () => setIsMaximized((value) => !value)
170
+ : undefined
171
+ }
131
172
  onClose={presenter.chatThreadManager.closeWorkspacePanel}
132
173
  />
133
174
 
134
175
  <div className="flex min-h-0 flex-1 flex-col bg-white">
135
176
  <ChatSessionWorkspacePanelContent
136
177
  activeSelection={activeSelection}
178
+ filePreviewRefreshVersion={filePreviewRefreshVersion}
137
179
  sessionCronJobs={sessionCronJobs}
138
180
  sessionProjectRoot={sessionProjectRoot}
139
181
  sessionWorkingDir={sessionWorkingDir}
@@ -53,6 +53,8 @@ export function normalizePersistedWorkspaceFileTab(
53
53
  line: normalizeOptionalNumber(value.line),
54
54
  column: normalizeOptionalNumber(value.column),
55
55
  rawText: normalizeOptionalText(value.rawText),
56
+ contentUrl: normalizeOptionalString(value.contentUrl),
57
+ mimeType: normalizeOptionalString(value.mimeType),
56
58
  beforeText: normalizeOptionalText(value.beforeText),
57
59
  afterText: normalizeOptionalText(value.afterText),
58
60
  patchText: normalizeOptionalText(value.patchText),
@@ -74,6 +76,8 @@ export function toPersistedWorkspaceFileTab(
74
76
  line: tab.line,
75
77
  column: tab.column,
76
78
  rawText: tab.rawText,
79
+ contentUrl: tab.contentUrl,
80
+ mimeType: tab.mimeType,
77
81
  beforeText: tab.beforeText,
78
82
  afterText: tab.afterText,
79
83
  patchText: tab.patchText,
@@ -42,7 +42,10 @@ function resetChatSessionListManagerState() {
42
42
  snapshot: {
43
43
  ...useChatSessionListStore.getState().snapshot,
44
44
  selectedSessionKey: 'session-1',
45
- listMode: 'time-first'
45
+ listMode: 'time-first',
46
+ pinnedSessionKeys: [],
47
+ pinnedProjectRoots: [],
48
+ collapsedProjectRoots: [],
46
49
  }
47
50
  });
48
51
  useChatThreadStore.setState({
@@ -191,6 +194,31 @@ describe('ChatSessionListManager list preference and read state', () => {
191
194
  });
192
195
  });
193
196
 
197
+ it('persists session and project list preferences through the list owner', () => {
198
+ const manager = new ChatSessionListManager(
199
+ {} as ConstructorParameters<typeof ChatSessionListManager>[0]
200
+ );
201
+
202
+ manager.toggleSessionPinned('session-2');
203
+ manager.toggleProjectPinned('/tmp/project-alpha');
204
+ manager.toggleProjectCollapsed('/tmp/project-alpha');
205
+
206
+ expect(useChatSessionListStore.getState().snapshot).toMatchObject({
207
+ pinnedSessionKeys: ['session-2'],
208
+ pinnedProjectRoots: ['/tmp/project-alpha'],
209
+ collapsedProjectRoots: ['/tmp/project-alpha'],
210
+ });
211
+ expect(persistStorage.get(chatSessionListModeStorageKey)).toMatchObject({
212
+ state: {
213
+ snapshot: {
214
+ pinnedSessionKeys: ['session-2'],
215
+ pinnedProjectRoots: ['/tmp/project-alpha'],
216
+ collapsedProjectRoots: ['/tmp/project-alpha'],
217
+ },
218
+ },
219
+ });
220
+ });
221
+
194
222
  it('marks a session as read through the session list owner boundary', () => {
195
223
  const manager = new ChatSessionListManager(
196
224
  {} as ConstructorParameters<typeof ChatSessionListManager>[0]
@@ -266,7 +294,10 @@ describe('ChatSessionListStore persistence', () => {
266
294
  useChatSessionListStore.setState({
267
295
  snapshot: {
268
296
  ...useChatSessionListStore.getState().snapshot,
269
- listMode: 'time-first'
297
+ listMode: 'time-first',
298
+ pinnedSessionKeys: [],
299
+ pinnedProjectRoots: [],
300
+ collapsedProjectRoots: [],
270
301
  }
271
302
  });
272
303
  });
@@ -13,6 +13,17 @@ type WorkspaceChildReadState = {
13
13
  readAt?: string | null;
14
14
  runStatus?: string | null;
15
15
  };
16
+
17
+ function toggleListValue(values: string[], value: string): string[] {
18
+ const normalizedValue = value.trim();
19
+ if (!normalizedValue) {
20
+ return values;
21
+ }
22
+ return values.includes(normalizedValue)
23
+ ? values.filter((item) => item !== normalizedValue)
24
+ : [normalizedValue, ...values];
25
+ }
26
+
16
27
  export class ChatSessionListManager {
17
28
  constructor(private uiManager: ChatUiManager) {}
18
29
 
@@ -110,6 +121,27 @@ export class ChatSessionListManager {
110
121
  useChatSessionListStore.getState().setSnapshot({ listMode: value });
111
122
  };
112
123
 
124
+ toggleSessionPinned = (sessionKey: string) => {
125
+ const { pinnedSessionKeys } = useChatSessionListStore.getState().snapshot;
126
+ useChatSessionListStore.getState().setSnapshot({
127
+ pinnedSessionKeys: toggleListValue(pinnedSessionKeys, sessionKey),
128
+ });
129
+ };
130
+
131
+ toggleProjectPinned = (projectRoot: string) => {
132
+ const { pinnedProjectRoots } = useChatSessionListStore.getState().snapshot;
133
+ useChatSessionListStore.getState().setSnapshot({
134
+ pinnedProjectRoots: toggleListValue(pinnedProjectRoots, projectRoot),
135
+ });
136
+ };
137
+
138
+ toggleProjectCollapsed = (projectRoot: string) => {
139
+ const { collapsedProjectRoots } = useChatSessionListStore.getState().snapshot;
140
+ useChatSessionListStore.getState().setSnapshot({
141
+ collapsedProjectRoots: toggleListValue(collapsedProjectRoots, projectRoot),
142
+ });
143
+ };
144
+
113
145
  markSessionRead = (
114
146
  sessionKey: string | null | undefined,
115
147
  readAt: string | null | undefined,
@@ -128,6 +128,8 @@ export class ChatThreadManager {
128
128
  line: action.line ?? null,
129
129
  column: action.column ?? null,
130
130
  rawText: action.rawText ?? null,
131
+ contentUrl: action.contentUrl?.trim() || null,
132
+ mimeType: action.mimeType?.trim() || null,
131
133
  beforeText: action.beforeText ?? null,
132
134
  afterText: action.afterText ?? null,
133
135
  patchText: action.patchText ?? null,