@nextclaw/ui 0.15.0 → 0.15.2

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 (191) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/dist/assets/{api-DLtsiBKm.js → api-B7Qigvtm.js} +2 -2
  3. package/dist/assets/{appearance-settings-page-CtkeYXux.js → appearance-settings-page-CuE0Iiqj.js} +1 -1
  4. package/dist/assets/{book-open-BQwWBDKb.js → book-open-Ba2f7YiG.js} +1 -1
  5. package/dist/assets/channels-list-page-CTsM6TcP.js +8 -0
  6. package/dist/assets/chat-page-C_5dcuE3.js +1 -0
  7. package/dist/assets/config-split-page-Fbb4PtTf.js +1 -0
  8. package/dist/assets/{confirm-dialog-iIJFFp2T.js → confirm-dialog-D2EG3nqE.js} +1 -1
  9. package/dist/assets/desktop-update-config-BGR3HUi1.js +1 -0
  10. package/dist/assets/{dist-D2eZC3OR.js → dist-7JTjycxE.js} +1 -1
  11. package/dist/assets/{dist-CBxWOazz.js → dist-wXLhk4hl.js} +1 -1
  12. package/dist/assets/doc-browser-bHyXnxlj.js +1 -0
  13. package/dist/assets/doc-browser-context-CYAGm5d5.js +1 -0
  14. package/dist/assets/doc-browser-gTrQ3jCF.js +1 -0
  15. package/dist/assets/{es2015-DhQo_oS6.js → es2015-j1SRCIX5.js} +1 -1
  16. package/dist/assets/{external-link-CM5dnUI6.js → external-link-DxuPaDoK.js} +1 -1
  17. package/dist/assets/form-actions-BztL7P_T.js +1 -0
  18. package/dist/assets/{i18n-provider-di1waTGX.js → i18n-provider-B4oMxOFx.js} +1 -1
  19. package/dist/assets/index-CvrvblgX.css +1 -0
  20. package/dist/assets/index-Obd4JYU2.js +104 -0
  21. package/dist/assets/loader-circle-CdzgK4eH.js +1 -0
  22. package/dist/assets/{mcp-marketplace-page-C_MVQUbT.js → mcp-marketplace-page-Bt7CuOoq.js} +2 -2
  23. package/dist/assets/mcp-marketplace-page-Ck3t78_n.js +1 -0
  24. package/dist/assets/model-config-page-DeKk9gvE.js +1 -0
  25. package/dist/assets/plus-Bx7wQbhG.js +1 -0
  26. package/dist/assets/{provider-scoped-model-input-g1WkGiJ8.js → provider-scoped-model-input-DsH2LF1q.js} +1 -1
  27. package/dist/assets/providers-config-page-BFnGp68F.js +1 -0
  28. package/dist/assets/{react-DjpzoZXB.js → react-UirzUuST.js} +1 -1
  29. package/dist/assets/{refresh-cw-DaTv3zxE.js → refresh-cw-eT8BF-8B.js} +1 -1
  30. package/dist/assets/remote-CDkHZtA8.js +1 -0
  31. package/dist/assets/rotate-cw-1JqxHTuw.js +1 -0
  32. package/dist/assets/runtime-config-page-BelAqiNG.js +1 -0
  33. package/dist/assets/{save-BZof1uie.js → save-Cb2g0BOi.js} +1 -1
  34. package/dist/assets/{search-MxAarS_g.js → search-BcHjwdsb.js} +1 -1
  35. package/dist/assets/search-config-page-zPm6--Ig.js +1 -0
  36. package/dist/assets/secrets-config-page-Cxyp-KuQ.js +3 -0
  37. package/dist/assets/security-config-B_gCq9fn.js +1 -0
  38. package/dist/assets/{select-D7GcNP0z.js → select-CdSEOwVM.js} +1 -1
  39. package/dist/assets/{setting-row-CuozlnJd.js → setting-row-CTAtGexr.js} +1 -1
  40. package/dist/assets/settings-2-2DWauOjY.js +1 -0
  41. package/dist/assets/{side-dock-BcLau5S_.js → side-dock-DPvakJ0a.js} +2 -2
  42. package/dist/assets/{skeleton-shfmXuYp.js → skeleton-P5oM14E1.js} +1 -1
  43. package/dist/assets/{switch-C1BWMU5c.js → switch-BU_UEULb.js} +1 -1
  44. package/dist/assets/{tag-chip-DCUrQUCP.js → tag-chip-D-WsKoT1.js} +1 -1
  45. package/dist/assets/{tooltip-DfAHHA76.js → tooltip-CjkOaHW7.js} +1 -1
  46. package/dist/assets/{use-config-DAdbT5QJ.js → use-config--hx5oxQn.js} +1 -1
  47. package/dist/assets/{use-confirm-dialog-eWcJ6V-s.js → use-confirm-dialog-CD9a_SK1.js} +1 -1
  48. package/dist/assets/x-DT5x45JU.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 +43 -4
  54. package/src/app/components/layout/sidebar.tsx +83 -61
  55. package/src/app/configs/app-navigation.config.ts +49 -0
  56. package/src/app/styles/design-system.css +82 -0
  57. package/src/features/agents/components/__tests__/agents-page.test.tsx +135 -2
  58. package/src/features/agents/components/agent-advanced-config-fields.tsx +70 -0
  59. package/src/features/agents/components/agent-details-dialog.tsx +398 -0
  60. package/src/features/agents/components/agent-dialogs.tsx +20 -47
  61. package/src/features/agents/components/agents-page.tsx +76 -10
  62. package/src/features/agents/types/agent-form.types.ts +21 -0
  63. package/src/features/agents/utils/agent-form.utils.ts +39 -0
  64. package/src/features/channels/components/config/channel-form.tsx +14 -11
  65. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +172 -1
  66. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +11 -1
  67. package/src/features/chat/components/conversation/chat-conversation-header.tsx +7 -3
  68. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +1 -1
  69. package/src/features/chat/components/layout/__tests__/chat-sidebar-toolbar.test.tsx +48 -0
  70. package/src/features/chat/components/layout/__tests__/chat-sidebar-utility-menu.test.tsx +55 -0
  71. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +0 -3
  72. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +3 -4
  73. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +29 -30
  74. package/src/features/chat/components/layout/chat-sidebar.tsx +21 -7
  75. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +18 -3
  76. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +2 -1
  77. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +22 -1
  78. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +1 -1
  79. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +10 -5
  80. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +0 -11
  81. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime.test.tsx +23 -1
  82. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-session-list-view.test.tsx +74 -0
  83. package/src/features/chat/features/ncp/hooks/use-ncp-session-list-view.ts +3 -2
  84. package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +116 -0
  85. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +108 -46
  86. package/src/features/chat/features/session/components/chat-sidebar-session-entry.tsx +6 -0
  87. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +48 -24
  88. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +0 -3
  89. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +0 -4
  90. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +294 -0
  91. package/src/features/chat/features/session/utils/__tests__/chat-session-display.utils.test.ts +15 -1
  92. package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts +24 -4
  93. package/src/features/chat/features/session/utils/chat-session-display.utils.ts +3 -0
  94. package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +25 -3
  95. package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +1 -0
  96. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +9 -4
  97. package/src/features/chat/features/welcome/components/chat-welcome.tsx +0 -5
  98. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +193 -56
  99. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +1 -0
  100. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +144 -0
  101. package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +99 -0
  102. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +3 -1
  103. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +67 -14
  104. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +3 -0
  105. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +44 -1
  106. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +45 -3
  107. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +46 -0
  108. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +33 -2
  109. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +47 -0
  110. package/src/features/chat/managers/chat-session-list.manager.ts +32 -0
  111. package/src/features/chat/managers/chat-thread.manager.ts +8 -2
  112. package/src/features/chat/stores/chat-session-list.store.ts +28 -5
  113. package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +5 -12
  114. package/src/features/panel-apps/components/panel-app-list-item.tsx +6 -6
  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/service-apps/components/service-apps-panel.tsx +145 -35
  121. package/src/features/settings/components/config/provider-auth-section.tsx +1 -1
  122. package/src/features/settings/components/config/provider-form.tsx +11 -8
  123. package/src/features/settings/components/config/provider-models-section.tsx +7 -7
  124. package/src/features/settings/components/config/provider-pill-selector.tsx +1 -1
  125. package/src/features/settings/components/config/secrets-config-form.tsx +5 -4
  126. package/src/features/settings/pages/model-config-page.tsx +26 -25
  127. package/src/features/settings/pages/providers-config-page.tsx +10 -13
  128. package/src/features/settings/pages/search-config-page.tsx +8 -5
  129. package/src/features/system-status/components/__tests__/desktop-update-config.test.tsx +97 -5
  130. package/src/features/system-status/components/config/runtime-config-editor.tsx +5 -4
  131. package/src/features/system-status/components/desktop-update-config.tsx +91 -2
  132. package/src/features/system-status/utils/update-release-notes.utils.ts +94 -0
  133. package/src/index.css +18 -0
  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 +5 -1
  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/card.tsx +12 -4
  149. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +2 -1
  150. package/src/shared/hooks/use-server-path-browse.ts +26 -4
  151. package/src/shared/hooks/use-server-path-read.ts +11 -1
  152. package/src/shared/lib/api/ncp-session.types.ts +1 -1
  153. package/src/shared/lib/api/types.ts +12 -3
  154. package/src/shared/lib/api/utils/server-path.utils.ts +1 -0
  155. package/src/shared/lib/i18n/locales/en-US/agents.json +41 -0
  156. package/src/shared/lib/i18n/locales/en-US/chat.json +19 -0
  157. package/src/shared/lib/i18n/locales/en-US/core.json +3 -0
  158. package/src/shared/lib/i18n/locales/en-US/desktop-update.json +8 -0
  159. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +5 -0
  160. package/src/shared/lib/i18n/locales/zh-CN/agents.json +41 -0
  161. package/src/shared/lib/i18n/locales/zh-CN/chat.json +19 -0
  162. package/src/shared/lib/i18n/locales/zh-CN/core.json +3 -0
  163. package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +8 -0
  164. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +5 -0
  165. package/src/shared/lib/session-project/workspace-file-breadcrumb.utils.ts +9 -5
  166. package/src/shared/lib/theme/index.test.ts +21 -0
  167. package/src/shared/lib/theme/index.ts +6 -4
  168. package/src/vite-env.d.ts +2 -0
  169. package/dist/assets/channels-list-page-Cv8o9Hxg.js +0 -8
  170. package/dist/assets/chat-page-BtA7H1Ea.js +0 -1
  171. package/dist/assets/config-split-page-EidRBpP2.js +0 -1
  172. package/dist/assets/desktop-update-config-C6n_TB1r.js +0 -1
  173. package/dist/assets/doc-browser-B76XOXmW.js +0 -1
  174. package/dist/assets/doc-browser-DBnj9UMB.js +0 -1
  175. package/dist/assets/doc-browser-context-Sv-044Cl.js +0 -1
  176. package/dist/assets/ellipsis-2uzhypw5.js +0 -1
  177. package/dist/assets/index-CBRP-7-a.css +0 -1
  178. package/dist/assets/index-Z1fYpbNd.js +0 -104
  179. package/dist/assets/loader-circle-BjmN5DeJ.js +0 -1
  180. package/dist/assets/mcp-marketplace-page-CoZIiD6G.js +0 -1
  181. package/dist/assets/model-config-page-Cdr9P6TQ.js +0 -1
  182. package/dist/assets/plus-3ztRZD_l.js +0 -1
  183. package/dist/assets/providers-config-page-CWlO7J4A.js +0 -1
  184. package/dist/assets/remote-CKWZfVVB.js +0 -1
  185. package/dist/assets/rotate-cw-CHcaRUDz.js +0 -1
  186. package/dist/assets/runtime-config-page-CfTqh1nv.js +0 -1
  187. package/dist/assets/search-config-page-Dm8daYny.js +0 -1
  188. package/dist/assets/secrets-config-page-D8AbcR05.js +0 -3
  189. package/dist/assets/security-config-Ds0LsJmo.js +0 -1
  190. package/dist/assets/x-DqL1mYLC.js +0 -1
  191. /package/dist/assets/{config-hints-fGnUjDe9.js → config-hints-C_iP1zl_.js} +0 -0
@@ -1,6 +1,6 @@
1
1
  import { Fragment, useState } from "react";
2
2
  import type { ChatFileOpenActionViewModel } from "@nextclaw/agent-chat-ui";
3
- import { ChevronRight, FileCode2, FolderTree } from "lucide-react";
3
+ import { ChevronRight, FileCode2, Folder, FolderTree } from "lucide-react";
4
4
  import { Popover, PopoverContent, PopoverTrigger } from "@/shared/components/ui/popover";
5
5
  import { WorkspaceBreadcrumbBrowser } from "./chat-session-workspace-file-breadcrumb-browser";
6
6
  import type {
@@ -59,6 +59,8 @@ function WorkspaceBreadcrumbSegmentButton({
59
59
  >
60
60
  {segment.kind === "workspace" ? (
61
61
  <FolderTree className="h-3 w-3 shrink-0" />
62
+ ) : segment.kind === "directory" && segment.isCurrent ? (
63
+ <Folder className="h-3 w-3 shrink-0" />
62
64
  ) : segment.isCurrent ? (
63
65
  <FileCode2 className="h-3 w-3 shrink-0" />
64
66
  ) : null}
@@ -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";
@@ -24,17 +32,18 @@ function inferPreviewKind(params: {
24
32
  return /\.mdx?$/i.test(params.path) ? "markdown" : "text";
25
33
  }
26
34
 
27
- function isHtmlFilePath(path: string): boolean {
28
- return /\.html?$/i.test(path);
29
- }
30
-
31
35
  function resolveFilePreviewViewer(params: {
32
36
  path: string;
33
37
  viewer?: ChatFilePreviewViewer | null;
34
38
  }): "source" | "rendered" {
35
- return params.viewer === "rendered" && isHtmlFilePath(params.path)
36
- ? "rendered"
37
- : "source";
39
+ return params.viewer === "rendered" && /\.html?$/i.test(params.path) ? "rendered" : "source";
40
+ }
41
+
42
+ function appendPreviewRefreshVersion(url: string, refreshVersion: number): string {
43
+ if (refreshVersion <= 0) {
44
+ return url;
45
+ }
46
+ return `${url}${url.includes("?") ? "&" : "?"}refresh=${refreshVersion}`;
38
47
  }
39
48
 
40
49
  function buildPreviewBlock(params: {
@@ -167,6 +176,7 @@ function WorkspaceHtmlRenderedPreview({
167
176
  }
168
177
 
169
178
  function WorkspacePreviewBody({
179
+ directoryQuery,
170
180
  onFileOpen,
171
181
  previewBlock,
172
182
  previewKind,
@@ -175,6 +185,7 @@ function WorkspacePreviewBody({
175
185
  previewUrl,
176
186
  previewViewer,
177
187
  }: {
188
+ directoryQuery: ReturnType<typeof useServerPathBrowse>;
178
189
  onFileOpen: (action: ChatFileOpenActionViewModel) => void;
179
190
  previewBlock: ChatFileOperationBlockViewModel | null;
180
191
  previewKind: "text" | "markdown" | "binary";
@@ -183,8 +194,26 @@ function WorkspacePreviewBody({
183
194
  previewUrl: string | null;
184
195
  previewViewer: "source" | "rendered";
185
196
  }) {
186
- if (previewQuery.isLoading && !previewBlock) {
187
- return <WorkspaceFilePreviewStatus text={t("chatWorkspaceLoadingFile")} />;
197
+ if (directoryQuery.data) {
198
+ return (
199
+ <ChatSessionWorkspaceDirectoryBrowser
200
+ browseQuery={directoryQuery}
201
+ onFileOpen={onFileOpen}
202
+ renderStatus={(params) => <WorkspaceFilePreviewStatus {...params} />}
203
+ />
204
+ );
205
+ }
206
+
207
+ if (directoryQuery.isLoading && previewQuery.error && !previewBlock) {
208
+ return (
209
+ <WorkspaceFilePreviewStatus text={t("chatWorkspaceLoadingDirectory")} />
210
+ );
211
+ }
212
+
213
+ if ((directoryQuery.isLoading || previewQuery.isLoading) && !previewBlock) {
214
+ return (
215
+ <WorkspaceFilePreviewStatus text={t("chatWorkspaceLoadingPreview")} />
216
+ );
188
217
  }
189
218
 
190
219
  if (previewQuery.data?.kind === "binary") {
@@ -235,6 +264,7 @@ function WorkspacePreviewBody({
235
264
 
236
265
  type ChatSessionWorkspaceFilePreviewProps = {
237
266
  file: ChatWorkspaceFileTab;
267
+ refreshVersion?: number;
238
268
  sessionProjectRoot: string | null;
239
269
  sessionWorkingDir: string | null;
240
270
  onFileOpen: (action: ChatFileOpenActionViewModel) => void;
@@ -242,6 +272,7 @@ type ChatSessionWorkspaceFilePreviewProps = {
242
272
 
243
273
  export function ChatSessionWorkspaceFilePreview({
244
274
  file,
275
+ refreshVersion = 0,
245
276
  sessionProjectRoot,
246
277
  sessionWorkingDir,
247
278
  onFileOpen,
@@ -252,6 +283,12 @@ export function ChatSessionWorkspaceFilePreview({
252
283
  basePath: sessionWorkingDir,
253
284
  enabled: isPreviewMode,
254
285
  });
286
+ const directoryQuery = useServerPathBrowse({
287
+ path: file.path,
288
+ basePath: sessionWorkingDir,
289
+ includeFiles: true,
290
+ enabled: isPreviewMode,
291
+ });
255
292
  const diffBlock = useMemo(
256
293
  () => (file.viewMode === "diff" ? buildDiffBlock(file) : null),
257
294
  [file],
@@ -262,14 +299,21 @@ export function ChatSessionWorkspaceFilePreview({
262
299
  path: previewQuery.data?.resolvedPath ?? file.path,
263
300
  serverKind: previewQuery.data?.kind,
264
301
  });
265
- const resolvedPath = previewQuery.data?.resolvedPath ?? file.path;
302
+ const resolvedPath =
303
+ directoryQuery.data?.currentPath ??
304
+ previewQuery.data?.resolvedPath ??
305
+ file.path;
306
+ const previewPathKind = directoryQuery.data ? "directory" : "file";
266
307
  const previewViewer = resolveFilePreviewViewer({
267
308
  path: resolvedPath,
268
309
  viewer: file.previewViewer,
269
310
  });
270
311
  const previewUrl =
271
312
  previewViewer === "rendered" && previewQuery.data?.resolvedPath
272
- ? buildServerPathContentUrl(previewQuery.data.resolvedPath)
313
+ ? appendPreviewRefreshVersion(
314
+ buildServerPathContentUrl(previewQuery.data.resolvedPath),
315
+ refreshVersion,
316
+ )
273
317
  : null;
274
318
  const previewBlock = useMemo(() => {
275
319
  if (!isPreviewMode || !previewText) {
@@ -295,12 +339,20 @@ export function ChatSessionWorkspaceFilePreview({
295
339
  () =>
296
340
  buildWorkspaceFileBreadcrumb({
297
341
  path: resolvedPath,
342
+ kind: previewPathKind,
298
343
  sessionProjectRoot: breadcrumbBasePath,
299
344
  line: file.line,
300
345
  column: file.column,
301
346
  truncated: isTruncated,
302
347
  }),
303
- [breadcrumbBasePath, file.column, file.line, isTruncated, resolvedPath],
348
+ [
349
+ breadcrumbBasePath,
350
+ file.column,
351
+ file.line,
352
+ isTruncated,
353
+ previewPathKind,
354
+ resolvedPath,
355
+ ],
304
356
  );
305
357
 
306
358
  return (
@@ -315,6 +367,7 @@ export function ChatSessionWorkspaceFilePreview({
315
367
  <WorkspaceDiffBody diffBlock={diffBlock} />
316
368
  ) : (
317
369
  <WorkspacePreviewBody
370
+ directoryQuery={directoryQuery}
318
371
  onFileOpen={onFileOpen}
319
372
  previewBlock={previewBlock}
320
373
  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}
@@ -158,4 +158,50 @@ describe("buildWorkspaceTabsViewModel", () => {
158
158
  active: true,
159
159
  });
160
160
  });
161
+
162
+ it("keeps distinct source and rendered file tabs without collapsing by path", () => {
163
+ const renderedTab = {
164
+ key: "parent::preview:rendered::demo.html",
165
+ parentSessionKey: "parent-1",
166
+ path: "demo.html",
167
+ label: "demo.html",
168
+ viewMode: "preview" as const,
169
+ previewViewer: "rendered" as const,
170
+ };
171
+ const sourceTab = {
172
+ key: "parent::preview::demo.html",
173
+ parentSessionKey: "parent-1",
174
+ path: "demo.html",
175
+ label: "demo.html",
176
+ viewMode: "preview" as const,
177
+ previewViewer: "source" as const,
178
+ };
179
+
180
+ const tabs = buildWorkspaceTabsViewModel({
181
+ resolvedChildTabs: [],
182
+ activeSideChatDraft: null,
183
+ workspaceFileTabs: [sourceTab, renderedTab],
184
+ sessionCronJobCount: 0,
185
+ activeSelection: {
186
+ kind: "file",
187
+ file: sourceTab,
188
+ },
189
+ optimisticReadAtBySessionKey: {},
190
+ onSelectSession: vi.fn(),
191
+ onSelectFile: vi.fn(),
192
+ onCloseFile: vi.fn(),
193
+ onSelectCronJobs: vi.fn(),
194
+ });
195
+
196
+ expect(tabs).toEqual([
197
+ expect.objectContaining({
198
+ key: "file:parent::preview::demo.html",
199
+ active: true,
200
+ }),
201
+ expect.objectContaining({
202
+ key: "file:parent::preview:rendered::demo.html",
203
+ active: false,
204
+ }),
205
+ ]);
206
+ });
161
207
  });
@@ -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
  });
@@ -331,6 +331,53 @@ describe('ChatThreadManager', () => {
331
331
  });
332
332
 
333
333
  describe('ChatThreadManager workspace file tabs', () => {
334
+ it('opens HTML previews in source mode when the viewer is omitted or auto', () => {
335
+ const manager = new ChatThreadManager(
336
+ createUiManager(),
337
+ {} as ConstructorParameters<typeof ChatThreadManager>[1],
338
+ );
339
+
340
+ manager.openFilePreview({
341
+ path: 'demo.html',
342
+ label: 'demo.html',
343
+ viewMode: 'preview',
344
+ });
345
+
346
+ expect(useChatThreadStore.getState().snapshot).toMatchObject({
347
+ activeWorkspaceFileKey: 'parent-session-1::preview::demo.html',
348
+ workspaceFileTabs: [
349
+ {
350
+ key: 'parent-session-1::preview::demo.html',
351
+ path: 'demo.html',
352
+ viewMode: 'preview',
353
+ previewViewer: 'source',
354
+ },
355
+ ],
356
+ });
357
+
358
+ manager.openFilePreview({
359
+ path: 'auto.html',
360
+ label: 'auto.html',
361
+ viewMode: 'preview',
362
+ previewViewer: 'auto',
363
+ });
364
+
365
+ expect(useChatThreadStore.getState().snapshot).toMatchObject({
366
+ activeWorkspaceFileKey: 'parent-session-1::preview::auto.html',
367
+ workspaceFileTabs: [
368
+ {
369
+ key: 'parent-session-1::preview::auto.html',
370
+ path: 'auto.html',
371
+ viewMode: 'preview',
372
+ previewViewer: 'source',
373
+ },
374
+ expect.objectContaining({
375
+ key: 'parent-session-1::preview::demo.html',
376
+ }),
377
+ ],
378
+ });
379
+ });
380
+
334
381
  it('keeps source and rendered previews for the same file as separate workspace tabs', () => {
335
382
  const manager = new ChatThreadManager(
336
383
  createUiManager(),
@@ -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,
@@ -111,14 +111,20 @@ export class ChatThreadManager {
111
111
  return null;
112
112
  }
113
113
  const normalizedParentSessionKey = parentSessionKey?.trim() || null;
114
- const tabViewIdentity = action.viewMode === 'preview' && action.previewViewer === 'rendered' ? 'preview:rendered' : action.viewMode;
114
+ const previewViewer =
115
+ action.viewMode === 'preview' &&
116
+ action.previewViewer !== 'rendered' &&
117
+ (action.previewViewer === 'auto' || action.previewViewer === 'source' || /\.html?$/i.test(normalizedPath))
118
+ ? 'source'
119
+ : action.previewViewer ?? null;
120
+ const tabViewIdentity = action.viewMode === 'preview' && previewViewer === 'rendered' ? 'preview:rendered' : action.viewMode;
115
121
  return {
116
122
  key: `${normalizedParentSessionKey ?? 'draft'}::${tabViewIdentity}::${normalizedPath}`,
117
123
  parentSessionKey: normalizedParentSessionKey,
118
124
  path: normalizedPath,
119
125
  label: action.label?.trim() || null,
120
126
  viewMode: action.viewMode,
121
- previewViewer: action.previewViewer ?? null,
127
+ previewViewer,
122
128
  line: action.line ?? null,
123
129
  column: action.column ?? null,
124
130
  rawText: action.rawText ?? null,