@nextclaw/ui 0.15.1 → 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 (158) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/assets/{api-b1R9VdeH.js → api-B7Qigvtm.js} +2 -2
  3. package/dist/assets/{appearance-settings-page-D2O9pch9.js → appearance-settings-page-CuE0Iiqj.js} +1 -1
  4. package/dist/assets/{book-open-B77lQ2Tg.js → book-open-Ba2f7YiG.js} +1 -1
  5. package/dist/assets/channels-list-page-CTsM6TcP.js +8 -0
  6. package/dist/assets/{chat-page-CZ7WxPir.js → chat-page-C_5dcuE3.js} +1 -1
  7. package/dist/assets/config-split-page-Fbb4PtTf.js +1 -0
  8. package/dist/assets/{confirm-dialog-C-NXSioX.js → confirm-dialog-D2EG3nqE.js} +1 -1
  9. package/dist/assets/{desktop-update-config-usHpXugP.js → desktop-update-config-BGR3HUi1.js} +1 -1
  10. package/dist/assets/{dist-jdGmbb6w.js → dist-7JTjycxE.js} +1 -1
  11. package/dist/assets/{dist-YtoqU-AT.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-bNpL2LKy.js → es2015-j1SRCIX5.js} +1 -1
  16. package/dist/assets/{external-link-BM6fk8q-.js → external-link-DxuPaDoK.js} +1 -1
  17. package/dist/assets/form-actions-BztL7P_T.js +1 -0
  18. package/dist/assets/{i18n-provider-CntWolRA.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-CLmUstLN.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-Cm_a4GHf.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-BN8uzPnp.js → react-UirzUuST.js} +1 -1
  29. package/dist/assets/{refresh-cw-BpVcFPmJ.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-BpFW5fpk.js → save-Cb2g0BOi.js} +1 -1
  34. package/dist/assets/{search-D0uO7enf.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-Bh-CrA4p.js → select-CdSEOwVM.js} +1 -1
  39. package/dist/assets/{setting-row-BJfCHRcb.js → setting-row-CTAtGexr.js} +1 -1
  40. package/dist/assets/{settings-2-Blzi1vr6.js → settings-2-2DWauOjY.js} +1 -1
  41. package/dist/assets/{side-dock-ZdaSrQVB.js → side-dock-DPvakJ0a.js} +2 -2
  42. package/dist/assets/{skeleton-BLlEzebZ.js → skeleton-P5oM14E1.js} +1 -1
  43. package/dist/assets/{switch-CFLJFNSd.js → switch-BU_UEULb.js} +1 -1
  44. package/dist/assets/{tag-chip-CzGihiQx.js → tag-chip-D-WsKoT1.js} +1 -1
  45. package/dist/assets/{tooltip-CdcrBjrC.js → tooltip-CjkOaHW7.js} +1 -1
  46. package/dist/assets/{use-config-CVPgsBmU.js → use-config--hx5oxQn.js} +1 -1
  47. package/dist/assets/{use-confirm-dialog-CSeBk9SF.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/channels/components/config/channel-form.tsx +14 -11
  58. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +40 -0
  59. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +3 -1
  60. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +1 -1
  61. package/src/features/chat/components/layout/__tests__/chat-sidebar-toolbar.test.tsx +48 -0
  62. package/src/features/chat/components/layout/__tests__/chat-sidebar-utility-menu.test.tsx +55 -0
  63. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +0 -3
  64. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +3 -4
  65. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +29 -30
  66. package/src/features/chat/components/layout/chat-sidebar.tsx +21 -7
  67. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +2 -1
  68. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +22 -1
  69. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +10 -5
  70. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +0 -11
  71. package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +116 -0
  72. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +108 -46
  73. package/src/features/chat/features/session/components/chat-sidebar-session-entry.tsx +6 -0
  74. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +48 -24
  75. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +0 -3
  76. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +0 -4
  77. package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +25 -3
  78. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +9 -4
  79. package/src/features/chat/features/welcome/components/chat-welcome.tsx +0 -5
  80. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +159 -56
  81. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +1 -0
  82. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +144 -0
  83. package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +99 -0
  84. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +3 -1
  85. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +66 -7
  86. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +3 -0
  87. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +44 -1
  88. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +45 -3
  89. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +33 -2
  90. package/src/features/chat/managers/chat-session-list.manager.ts +32 -0
  91. package/src/features/chat/stores/chat-session-list.store.ts +28 -5
  92. package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +5 -12
  93. package/src/features/panel-apps/components/panel-app-list-item.tsx +6 -6
  94. package/src/features/panel-apps/index.ts +1 -0
  95. package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +2 -10
  96. package/src/features/panel-apps/utils/panel-app-iframe.utils.ts +6 -0
  97. package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +11 -3
  98. package/src/features/pwa/managers/pwa-shell-theme.manager.ts +3 -1
  99. package/src/features/service-apps/components/service-apps-panel.tsx +145 -35
  100. package/src/features/settings/components/config/provider-auth-section.tsx +1 -1
  101. package/src/features/settings/components/config/provider-form.tsx +11 -8
  102. package/src/features/settings/components/config/provider-models-section.tsx +7 -7
  103. package/src/features/settings/components/config/provider-pill-selector.tsx +1 -1
  104. package/src/features/settings/components/config/secrets-config-form.tsx +5 -4
  105. package/src/features/settings/pages/model-config-page.tsx +26 -25
  106. package/src/features/settings/pages/providers-config-page.tsx +10 -13
  107. package/src/features/settings/pages/search-config-page.tsx +8 -5
  108. package/src/features/system-status/components/config/runtime-config-editor.tsx +5 -4
  109. package/src/index.css +18 -0
  110. package/src/shared/components/config-split-page.tsx +6 -6
  111. package/src/shared/components/doc-browser/__tests__/doc-browser-url.utils.test.ts +73 -0
  112. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +48 -0
  113. package/src/shared/components/doc-browser/doc-browser-context.tsx +4 -0
  114. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +5 -1
  115. package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +2 -1
  116. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +10 -10
  117. package/src/shared/components/doc-browser/doc-browser.tsx +6 -4
  118. package/src/shared/components/doc-browser/utils/doc-browser-url.utils.ts +62 -4
  119. package/src/shared/components/resizable-right-panel/__tests__/resizable-right-panel.test.tsx +18 -0
  120. package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +28 -3
  121. package/src/shared/components/ui/actions/__tests__/icon-action-button.test.tsx +36 -0
  122. package/src/shared/components/ui/actions/form-actions.tsx +25 -0
  123. package/src/shared/components/ui/actions/icon-action-button.tsx +25 -2
  124. package/src/shared/components/ui/card.tsx +12 -4
  125. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +2 -1
  126. package/src/shared/hooks/use-server-path-browse.ts +26 -4
  127. package/src/shared/hooks/use-server-path-read.ts +11 -1
  128. package/src/shared/lib/api/utils/server-path.utils.ts +1 -0
  129. package/src/shared/lib/i18n/locales/en-US/chat.json +16 -0
  130. package/src/shared/lib/i18n/locales/en-US/core.json +3 -0
  131. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +5 -0
  132. package/src/shared/lib/i18n/locales/zh-CN/chat.json +16 -0
  133. package/src/shared/lib/i18n/locales/zh-CN/core.json +3 -0
  134. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +5 -0
  135. package/src/shared/lib/session-project/workspace-file-breadcrumb.utils.ts +9 -5
  136. package/src/shared/lib/theme/index.test.ts +21 -0
  137. package/src/shared/lib/theme/index.ts +6 -4
  138. package/src/vite-env.d.ts +2 -0
  139. package/dist/assets/channels-list-page-DcsIQc6A.js +0 -8
  140. package/dist/assets/config-split-page-BWQ6_Raq.js +0 -1
  141. package/dist/assets/doc-browser-B_Tf9n70.js +0 -1
  142. package/dist/assets/doc-browser-BdZPFOVa.js +0 -1
  143. package/dist/assets/doc-browser-context-DwokemUn.js +0 -1
  144. package/dist/assets/index-BIcEw90u.js +0 -104
  145. package/dist/assets/index-BiNSTKTU.css +0 -1
  146. package/dist/assets/loader-circle-MBgNSVIH.js +0 -1
  147. package/dist/assets/mcp-marketplace-page-rZkmWPMy.js +0 -1
  148. package/dist/assets/model-config-page-Di0ySJD2.js +0 -1
  149. package/dist/assets/plus-vDqW5Kxp.js +0 -1
  150. package/dist/assets/providers-config-page-BAf2W0MV.js +0 -1
  151. package/dist/assets/remote-CLQ0IBM8.js +0 -1
  152. package/dist/assets/rotate-cw-CvNBv7_w.js +0 -1
  153. package/dist/assets/runtime-config-page-CflvvoDc.js +0 -1
  154. package/dist/assets/search-config-page-D0klFUXd.js +0 -1
  155. package/dist/assets/secrets-config-page-dVZ631Wp.js +0 -3
  156. package/dist/assets/security-config-C5gnlkuj.js +0 -1
  157. package/dist/assets/x-BF2KQ--V.js +0 -1
  158. /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,13 @@ function resolveFilePreviewViewer(params: {
31
39
  return params.viewer === "rendered" && /\.html?$/i.test(params.path) ? "rendered" : "source";
32
40
  }
33
41
 
42
+ function appendPreviewRefreshVersion(url: string, refreshVersion: number): string {
43
+ if (refreshVersion <= 0) {
44
+ return url;
45
+ }
46
+ return `${url}${url.includes("?") ? "&" : "?"}refresh=${refreshVersion}`;
47
+ }
48
+
34
49
  function buildPreviewBlock(params: {
35
50
  path: string;
36
51
  text: string;
@@ -161,6 +176,7 @@ function WorkspaceHtmlRenderedPreview({
161
176
  }
162
177
 
163
178
  function WorkspacePreviewBody({
179
+ directoryQuery,
164
180
  onFileOpen,
165
181
  previewBlock,
166
182
  previewKind,
@@ -169,6 +185,7 @@ function WorkspacePreviewBody({
169
185
  previewUrl,
170
186
  previewViewer,
171
187
  }: {
188
+ directoryQuery: ReturnType<typeof useServerPathBrowse>;
172
189
  onFileOpen: (action: ChatFileOpenActionViewModel) => void;
173
190
  previewBlock: ChatFileOperationBlockViewModel | null;
174
191
  previewKind: "text" | "markdown" | "binary";
@@ -177,8 +194,26 @@ function WorkspacePreviewBody({
177
194
  previewUrl: string | null;
178
195
  previewViewer: "source" | "rendered";
179
196
  }) {
180
- if (previewQuery.isLoading && !previewBlock) {
181
- 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
+ );
182
217
  }
183
218
 
184
219
  if (previewQuery.data?.kind === "binary") {
@@ -229,6 +264,7 @@ function WorkspacePreviewBody({
229
264
 
230
265
  type ChatSessionWorkspaceFilePreviewProps = {
231
266
  file: ChatWorkspaceFileTab;
267
+ refreshVersion?: number;
232
268
  sessionProjectRoot: string | null;
233
269
  sessionWorkingDir: string | null;
234
270
  onFileOpen: (action: ChatFileOpenActionViewModel) => void;
@@ -236,6 +272,7 @@ type ChatSessionWorkspaceFilePreviewProps = {
236
272
 
237
273
  export function ChatSessionWorkspaceFilePreview({
238
274
  file,
275
+ refreshVersion = 0,
239
276
  sessionProjectRoot,
240
277
  sessionWorkingDir,
241
278
  onFileOpen,
@@ -246,6 +283,12 @@ export function ChatSessionWorkspaceFilePreview({
246
283
  basePath: sessionWorkingDir,
247
284
  enabled: isPreviewMode,
248
285
  });
286
+ const directoryQuery = useServerPathBrowse({
287
+ path: file.path,
288
+ basePath: sessionWorkingDir,
289
+ includeFiles: true,
290
+ enabled: isPreviewMode,
291
+ });
249
292
  const diffBlock = useMemo(
250
293
  () => (file.viewMode === "diff" ? buildDiffBlock(file) : null),
251
294
  [file],
@@ -256,14 +299,21 @@ export function ChatSessionWorkspaceFilePreview({
256
299
  path: previewQuery.data?.resolvedPath ?? file.path,
257
300
  serverKind: previewQuery.data?.kind,
258
301
  });
259
- 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";
260
307
  const previewViewer = resolveFilePreviewViewer({
261
308
  path: resolvedPath,
262
309
  viewer: file.previewViewer,
263
310
  });
264
311
  const previewUrl =
265
312
  previewViewer === "rendered" && previewQuery.data?.resolvedPath
266
- ? buildServerPathContentUrl(previewQuery.data.resolvedPath)
313
+ ? appendPreviewRefreshVersion(
314
+ buildServerPathContentUrl(previewQuery.data.resolvedPath),
315
+ refreshVersion,
316
+ )
267
317
  : null;
268
318
  const previewBlock = useMemo(() => {
269
319
  if (!isPreviewMode || !previewText) {
@@ -289,12 +339,20 @@ export function ChatSessionWorkspaceFilePreview({
289
339
  () =>
290
340
  buildWorkspaceFileBreadcrumb({
291
341
  path: resolvedPath,
342
+ kind: previewPathKind,
292
343
  sessionProjectRoot: breadcrumbBasePath,
293
344
  line: file.line,
294
345
  column: file.column,
295
346
  truncated: isTruncated,
296
347
  }),
297
- [breadcrumbBasePath, file.column, file.line, isTruncated, resolvedPath],
348
+ [
349
+ breadcrumbBasePath,
350
+ file.column,
351
+ file.line,
352
+ isTruncated,
353
+ previewPathKind,
354
+ resolvedPath,
355
+ ],
298
356
  );
299
357
 
300
358
  return (
@@ -309,6 +367,7 @@ export function ChatSessionWorkspaceFilePreview({
309
367
  <WorkspaceDiffBody diffBlock={diffBlock} />
310
368
  ) : (
311
369
  <WorkspacePreviewBody
370
+ directoryQuery={directoryQuery}
312
371
  onFileOpen={onFileOpen}
313
372
  previewBlock={previewBlock}
314
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}
@@ -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,
@@ -7,6 +7,9 @@ export type ChatSessionListSnapshot = {
7
7
  selectedAgentId: string;
8
8
  query: string;
9
9
  listMode: ChatSessionListMode;
10
+ pinnedSessionKeys: string[];
11
+ pinnedProjectRoots: string[];
12
+ collapsedProjectRoots: string[];
10
13
  };
11
14
 
12
15
  const CHAT_SESSION_LIST_MODE_STORAGE_KEY = 'nextclaw.chat.session-list.mode';
@@ -15,6 +18,9 @@ const CHAT_SESSION_LIST_DEFAULT_MODE: ChatSessionListMode = 'time-first';
15
18
  type PersistedChatSessionListStore = {
16
19
  snapshot?: {
17
20
  listMode?: unknown;
21
+ pinnedSessionKeys?: unknown;
22
+ pinnedProjectRoots?: unknown;
23
+ collapsedProjectRoots?: unknown;
18
24
  };
19
25
  };
20
26
 
@@ -22,6 +28,13 @@ function isChatSessionListMode(value: unknown): value is ChatSessionListMode {
22
28
  return value === 'time-first' || value === 'project-first';
23
29
  }
24
30
 
31
+ function normalizePersistedStringList(value: unknown): string[] {
32
+ if (!Array.isArray(value)) {
33
+ return [];
34
+ }
35
+ return [...new Set(value.filter((item): item is string => typeof item === 'string').map((item) => item.trim()).filter(Boolean))];
36
+ }
37
+
25
38
  function resolvePersistedChatSessionListMode(persistedState: unknown): ChatSessionListMode | null {
26
39
  if (!persistedState || typeof persistedState !== 'object') {
27
40
  return null;
@@ -75,7 +88,10 @@ const initialSnapshot: ChatSessionListSnapshot = {
75
88
  selectedSessionKey: null,
76
89
  selectedAgentId: 'main',
77
90
  query: '',
78
- listMode: CHAT_SESSION_LIST_DEFAULT_MODE
91
+ listMode: CHAT_SESSION_LIST_DEFAULT_MODE,
92
+ pinnedSessionKeys: [],
93
+ pinnedProjectRoots: [],
94
+ collapsedProjectRoots: [],
79
95
  };
80
96
 
81
97
  function createSetSnapshotAction(set: ChatSessionListStoreSet) {
@@ -123,19 +139,26 @@ export const useChatSessionListStore = create<ChatSessionListStore>()(
123
139
  storage: createJSONStorage(() => window.localStorage),
124
140
  partialize: (state): PersistedChatSessionListStore => ({
125
141
  snapshot: {
126
- listMode: state.snapshot.listMode
142
+ listMode: state.snapshot.listMode,
143
+ pinnedSessionKeys: state.snapshot.pinnedSessionKeys,
144
+ pinnedProjectRoots: state.snapshot.pinnedProjectRoots,
145
+ collapsedProjectRoots: state.snapshot.collapsedProjectRoots,
127
146
  }
128
147
  }),
129
148
  merge: (persistedState, currentState) => {
130
- const listMode = resolvePersistedChatSessionListMode(persistedState);
131
- if (!listMode) {
149
+ if (!persistedState || typeof persistedState !== 'object') {
132
150
  return currentState;
133
151
  }
152
+ const { snapshot } = persistedState as PersistedChatSessionListStore;
153
+ const listMode = resolvePersistedChatSessionListMode(persistedState);
134
154
  return {
135
155
  ...currentState,
136
156
  snapshot: {
137
157
  ...currentState.snapshot,
138
- listMode
158
+ ...(listMode ? { listMode } : {}),
159
+ pinnedSessionKeys: normalizePersistedStringList(snapshot?.pinnedSessionKeys),
160
+ pinnedProjectRoots: normalizePersistedStringList(snapshot?.pinnedProjectRoots),
161
+ collapsedProjectRoots: normalizePersistedStringList(snapshot?.collapsedProjectRoots),
139
162
  }
140
163
  };
141
164
  }
@@ -266,21 +266,14 @@ export function McpMarketplacePage() {
266
266
  return (
267
267
  <PageLayout className="flex h-full min-h-0 flex-col pb-0 px-0">
268
268
  <div className="flex flex-col gap-6 w-full max-w-[1400px] h-full min-h-0 mx-auto">
269
-
270
- {/* Modern App Store Hero for MCP */}
271
- <div className="relative overflow-hidden rounded-[32px] bg-gradient-to-br from-[#0F172A] via-[#1E1B4B] to-[#312E81] px-10 py-14 text-white shadow-xl isolate">
272
- <div className="absolute top-0 right-0 -m-32 opacity-30 pointer-events-none mix-blend-screen scale-150 transform-gpu">
273
- <div className="w-[500px] h-[500px] rounded-full bg-gradient-to-tl from-fuchsia-500/40 via-purple-500/30 to-indigo-500/20 blur-[80px]"></div>
274
- </div>
275
- <div className="absolute bottom-0 left-0 -m-32 opacity-20 pointer-events-none mix-blend-screen transform-gpu">
276
- <div className="w-[400px] h-[400px] rounded-full bg-gradient-to-tr from-blue-500/40 to-cyan-500/20 blur-[80px]"></div>
277
- </div>
278
-
269
+ <div className="relative isolate overflow-hidden rounded-[28px] border border-border bg-card px-10 py-12 text-card-foreground shadow-card">
270
+ <div className="absolute inset-y-0 left-0 w-1 bg-primary/80" />
271
+
279
272
  <div className="relative z-10 flex flex-col gap-3">
280
- <h1 className="text-[38px] font-extrabold tracking-[-0.02em] leading-tight drop-shadow-sm text-transparent bg-clip-text bg-gradient-to-r from-white via-white to-white/70">
273
+ <h1 className="text-[38px] font-extrabold leading-tight text-foreground">
281
274
  {t("marketplaceMcpPageTitle")}
282
275
  </h1>
283
- <p className="text-[17px] font-medium text-purple-100/70 max-w-2xl leading-relaxed tracking-wide">
276
+ <p className="max-w-2xl text-[17px] font-medium leading-relaxed text-muted-foreground">
284
277
  {t("marketplaceMcpPageDescription")}
285
278
  </p>
286
279
  </div>
@@ -38,11 +38,11 @@ export function PanelAppListItem({
38
38
  };
39
39
 
40
40
  return (
41
- <div className="group w-full min-w-0 rounded-lg border border-border bg-card px-2.5 py-2.5 transition-colors hover:border-primary/35 hover:bg-accent/60">
41
+ <div className="group w-full min-w-0 rounded-lg border border-border/60 bg-card px-2.5 py-2.5 transition-colors hover:bg-muted/40">
42
42
  <div className="flex min-w-0 items-start gap-2">
43
43
  <button type="button" onClick={onOpen} className="min-w-0 flex-1 text-left">
44
44
  <span className="flex min-w-0 items-center gap-2">
45
- <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-accent text-base text-accent-foreground">
45
+ <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-muted text-base text-muted-foreground">
46
46
  <PanelAppIcon icon={entry.icon} title={entry.title} />
47
47
  </span>
48
48
  <span className="block min-w-0 flex-1 truncate text-sm font-medium text-foreground">{entry.title}</span>
@@ -52,22 +52,22 @@ export function PanelAppListItem({
52
52
  ) : null}
53
53
  <span className="mt-0.5 block truncate text-[11px] leading-4 text-muted-foreground/70">{secondary}</span>
54
54
  </button>
55
- <div className="flex shrink-0 items-center gap-1">
55
+ <div className="flex shrink-0 items-center gap-0.5 opacity-70 transition-opacity group-hover:opacity-100">
56
56
  <button
57
57
  type="button"
58
58
  onClick={handleFavorite}
59
59
  disabled={favoritePending}
60
- className="rounded-md p-1.5 text-muted-foreground/70 transition-colors hover:bg-background hover:text-amber-500 disabled:opacity-50"
60
+ className="rounded-md p-1.5 text-muted-foreground/70 transition-colors hover:bg-muted hover:text-foreground disabled:opacity-50"
61
61
  title={favoriteLabel}
62
62
  aria-label={favoriteLabel}
63
63
  >
64
- <Star className={entry.favorite ? 'h-4 w-4 fill-amber-400 text-amber-500' : 'h-4 w-4'} />
64
+ <Star className={entry.favorite ? 'h-4 w-4 fill-current text-foreground' : 'h-4 w-4'} />
65
65
  </button>
66
66
  <Popover open={isMenuOpen} onOpenChange={setIsMenuOpen}>
67
67
  <PopoverTrigger asChild>
68
68
  <button
69
69
  type="button"
70
- className="rounded-md p-1.5 text-muted-foreground/70 transition-colors hover:bg-background hover:text-foreground"
70
+ className="rounded-md p-1.5 text-muted-foreground/70 transition-colors hover:bg-muted hover:text-foreground"
71
71
  aria-label={t('panelAppsMoreActions')}
72
72
  disabled={deletePending}
73
73
  >
@@ -2,6 +2,7 @@ export { PanelAppsList } from './components/panel-apps-list';
2
2
  export { usePanelApps } from './hooks/use-panel-apps';
3
3
  export { PanelAppBridgeManager } from './managers/panel-app-bridge.manager';
4
4
  export { findPanelAppEntryByDisplayId } from './utils/panel-app-entry-match.utils';
5
+ export { PANEL_APP_IFRAME_SANDBOX, focusPanelAppIframe } from './utils/panel-app-iframe.utils';
5
6
  export {
6
7
  openApps,
7
8
  PANEL_APPS_DOC_BROWSER_RENDERERS,
@@ -14,20 +14,11 @@ import {
14
14
  RightPanelResourceHomePage,
15
15
  } from '@/features/right-panel-resources';
16
16
  import { t } from '@/shared/lib/i18n';
17
+ import { PANEL_APP_IFRAME_SANDBOX, focusPanelAppIframe } from './panel-app-iframe.utils';
17
18
 
18
19
  export const APPS_TAB_KIND = RIGHT_PANEL_APPS_TAB_KIND;
19
20
  export const PANEL_APP_TAB_KIND = RIGHT_PANEL_PANEL_APP_TAB_KIND;
20
21
  const DEFAULT_APPS_PANEL_TAB: AppsPanelTab = 'panel-apps';
21
- const PANEL_APP_IFRAME_SANDBOX = [
22
- 'allow-scripts',
23
- 'allow-forms',
24
- 'allow-modals',
25
- 'allow-popups',
26
- 'allow-popups-to-escape-sandbox',
27
- 'allow-downloads',
28
- 'allow-pointer-lock',
29
- 'allow-presentation',
30
- ].join(' ');
31
22
 
32
23
  function isAppsPanelTab(value: unknown): value is AppsPanelTab {
33
24
  return value === 'panel-apps' || value === 'service-apps';
@@ -79,6 +70,7 @@ export const PANEL_APPS_DOC_BROWSER_RENDERERS: DocBrowserCustomTabRenderers = {
79
70
  getIframeSandbox: () => PANEL_APP_IFRAME_SANDBOX,
80
71
  getTitle: (tab) => tab.title || t('panelAppsTitle'),
81
72
  onIframeMessage: (params) => getPresenter().panelAppBridgeManager.handleIframeMessage(params),
73
+ onIframePointerOver: (event) => focusPanelAppIframe(event.currentTarget),
82
74
  renderIcon: () => <AppWindow className="w-4 h-4 text-primary shrink-0" />,
83
75
  renderToolbar: ({ open, refreshIframe, tab }) => (
84
76
  <PanelAppToolbar
@@ -0,0 +1,6 @@
1
+ export const PANEL_APP_IFRAME_SANDBOX = 'allow-scripts allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-downloads allow-pointer-lock allow-presentation';
2
+
3
+ export function focusPanelAppIframe(iframe: HTMLIFrameElement | null): void {
4
+ iframe?.focus({ preventScroll: true });
5
+ iframe?.contentWindow?.focus();
6
+ }