@nextclaw/ui 0.15.2 → 0.15.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/assets/aiden0z-pptx-renderer.es-Bd99vut2.js +94 -0
  3. package/dist/assets/api-BOXOrpcJ.js +13 -0
  4. package/dist/assets/app-presenter-provider-CkHLbvZQ.js +106 -0
  5. package/dist/assets/app-presenter-provider-D3BuuOyZ.css +1 -0
  6. package/dist/assets/appearance-settings-page-fLIJbx1C.js +1 -0
  7. package/dist/assets/{book-open-Ba2f7YiG.js → book-open-DWUjS6aW.js} +1 -1
  8. package/dist/assets/channels-list-page-CvCTJraa.js +8 -0
  9. package/dist/assets/chat-page-B42UUIFR.js +1 -0
  10. package/dist/assets/chunk-DseTPa7n.js +1 -0
  11. package/dist/assets/config-split-page-DBgt9S8b.js +1 -0
  12. package/dist/assets/confirm-dialog-GSXIvoRU.js +5 -0
  13. package/dist/assets/desktop-update-config-7bmNeWIC.js +1 -0
  14. package/dist/assets/dist-DC7f4TJH.js +41 -0
  15. package/dist/assets/doc-browser-CgdoLUT92.js +1 -0
  16. package/dist/assets/doc-browser-D0monHjG.js +1 -0
  17. package/dist/assets/doc-browser-context-CGwEOhBy.js +1 -0
  18. package/dist/assets/docx-preview-eFH9DRsS.js +20 -0
  19. package/dist/assets/download-DiP9zx-J.js +1 -0
  20. package/dist/assets/{external-link-DxuPaDoK.js → external-link-C6ZeSPef.js} +1 -1
  21. package/dist/assets/eye-suvKmtwn.js +1 -0
  22. package/dist/assets/folder-CMELbQGG.js +1 -0
  23. package/dist/assets/{form-actions-BztL7P_T.js → form-actions-CUAzpNe_.js} +1 -1
  24. package/dist/assets/index-BFeNDQ4R.css +1 -0
  25. package/dist/assets/index-D_6U-M94.js +2 -0
  26. package/dist/assets/jszip.min-Das2IAnb.js +2 -0
  27. package/dist/assets/loader-circle-tYHStefV.js +1 -0
  28. package/dist/assets/logo-badge-Cu5UGH_4.js +1 -0
  29. package/dist/assets/mcp-marketplace-page-BUzHDwW6.js +9 -0
  30. package/dist/assets/mcp-marketplace-page-ohUAdfqJ.js +1 -0
  31. package/dist/assets/model-config-page-CWP6pyBm.js +1 -0
  32. package/dist/assets/plus-tywizkeY.js +1 -0
  33. package/dist/assets/popover-DImguRok.js +1 -0
  34. package/dist/assets/provider-models-C_yOh6DE.js +1 -0
  35. package/dist/assets/provider-scoped-model-input-CE7p4SbJ.js +1 -0
  36. package/dist/assets/providers-config-page-BKPo8SjH.js +1 -0
  37. package/dist/assets/react-5sjAT_6d.js +8 -0
  38. package/dist/assets/{refresh-cw-eT8BF-8B.js → refresh-cw-CHLBzWWj.js} +1 -1
  39. package/dist/assets/remote-DXJryUxB.js +1 -0
  40. package/dist/assets/rotate-cw-BefO5brU.js +1 -0
  41. package/dist/assets/runtime-config-page-1Axv4QGu.js +1 -0
  42. package/dist/assets/{save-Cb2g0BOi.js → save-BABzyrjB.js} +1 -1
  43. package/dist/assets/{search-BcHjwdsb.js → search-BTqxG3ZG.js} +1 -1
  44. package/dist/assets/search-config-page-BkKKmxmS.js +1 -0
  45. package/dist/assets/secrets-config-page-CWJMyu-k.js +3 -0
  46. package/dist/assets/security-config-DeQUIXfi.js +1 -0
  47. package/dist/assets/select-TYZDBNmq.js +1 -0
  48. package/dist/assets/setting-row-DR23Z_5I.js +1 -0
  49. package/dist/assets/{settings-2-2DWauOjY.js → settings-2-D8WWcBnw.js} +1 -1
  50. package/dist/assets/{skeleton-P5oM14E1.js → skeleton-CloHeg0S.js} +1 -1
  51. package/dist/assets/tabs-CX-ckWoo.js +1 -0
  52. package/dist/assets/tag-chip-mCc23Rhb.js +1 -0
  53. package/dist/assets/trash-2-SAW1Z4Po.js +1 -0
  54. package/dist/assets/use-config-BucdaK23.js +1 -0
  55. package/dist/assets/use-confirm-dialog-C95F6sT1.js +1 -0
  56. package/dist/assets/use-viewport-layout-C6m3Nte7.js +1 -0
  57. package/dist/assets/x-DtCPHwCu.js +1 -0
  58. package/dist/assets/xlsx-B6zVt1RK.js +64 -0
  59. package/dist/index.html +34 -28
  60. package/package.json +14 -9
  61. package/src/app/components/layout/sidebar-items.tsx +3 -3
  62. package/src/app/components/layout/sidebar-rail.styles.ts +2 -2
  63. package/src/app/components/layout/sidebar.tsx +1 -1
  64. package/src/app/styles/design-system.css +17 -4
  65. package/src/features/chat/components/__tests__/chat-popover-content.test.tsx +41 -0
  66. package/src/features/chat/components/chat-popover-content.tsx +26 -0
  67. package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +13 -0
  68. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +21 -11
  69. package/src/features/chat/components/conversation/__tests__/chat-conversation-header.test.tsx +2 -2
  70. package/src/features/chat/components/conversation/__tests__/chat-conversation-workspace-section.test.tsx +28 -1
  71. package/src/features/chat/components/conversation/chat-conversation-content.tsx +5 -4
  72. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +9 -18
  73. package/src/features/chat/components/conversation/chat-conversation-track.tsx +35 -0
  74. package/src/features/chat/components/conversation/chat-conversation-workspace-section.tsx +1 -0
  75. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +7 -7
  76. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +4 -15
  77. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +20 -1
  78. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +69 -54
  79. package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +1 -1
  80. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +62 -1
  81. package/src/features/chat/features/message/components/chat-message-list.container.tsx +63 -2
  82. package/src/features/chat/features/message/utils/__tests__/chat-message-file-operation.utils.test.ts +30 -0
  83. package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +11 -1
  84. package/src/features/chat/features/message/utils/__tests__/chat-message-texts.utils.test.ts +23 -0
  85. package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +5 -0
  86. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +138 -0
  87. package/src/features/chat/features/session/components/__tests__/chat-sidebar-session-item.test.tsx +37 -0
  88. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +5 -4
  89. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +3 -8
  90. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +65 -20
  91. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +26 -28
  92. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -3
  93. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +5 -5
  94. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +4 -4
  95. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +4 -4
  96. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +463 -99
  97. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +113 -2
  98. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +1 -0
  99. package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +197 -44
  100. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +11 -5
  101. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +8 -6
  102. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +99 -67
  103. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +239 -1
  104. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +56 -6
  105. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +29 -7
  106. package/src/features/chat/features/workspace/components/file-content-preview/use-workspace-file-buffer.ts +42 -0
  107. package/src/features/chat/features/workspace/components/file-content-preview/workspace-document-preview-state.tsx +18 -0
  108. package/src/features/chat/features/workspace/components/file-content-preview/workspace-docx-preview.css +89 -0
  109. package/src/features/chat/features/workspace/components/file-content-preview/workspace-docx-preview.tsx +87 -0
  110. package/src/features/chat/features/workspace/components/file-content-preview/workspace-presentation-preview.tsx +82 -0
  111. package/src/features/chat/features/workspace/components/file-content-preview/workspace-spreadsheet-preview.tsx +180 -0
  112. package/src/features/chat/features/workspace/components/workspace-file-content-preview.tsx +191 -0
  113. package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +1 -4
  114. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +53 -10
  115. package/src/features/chat/features/workspace/utils/chat-thread-workspace-session.utils.ts +68 -2
  116. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +21 -0
  117. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab.utils.ts +63 -0
  118. package/src/features/chat/features/workspace/utils/chat-workspace-file-viewer.utils.ts +49 -0
  119. package/src/features/chat/features/workspace/utils/chat-workspace-panel-layout.utils.ts +13 -0
  120. package/src/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils.ts +120 -38
  121. package/src/features/chat/index.ts +1 -0
  122. package/src/features/chat/managers/__tests__/chat-thread-workspace.manager.test.ts +76 -0
  123. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +44 -60
  124. package/src/features/chat/managers/chat-thread.manager.ts +123 -124
  125. package/src/features/chat/pages/ncp-chat-page.tsx +1 -2
  126. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +78 -1
  127. package/src/features/chat/stores/chat-message-layout.store.test.ts +37 -0
  128. package/src/features/chat/stores/chat-message-layout.store.ts +41 -0
  129. package/src/features/chat/stores/chat-thread.store.ts +73 -13
  130. package/src/features/cron/components/cron-config.tsx +63 -80
  131. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.tsx +44 -6
  132. package/src/features/marketplace/components/marketplace-detail-doc.tsx +8 -4
  133. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  134. package/src/features/marketplace/hooks/use-marketplace-detail-doc-entry.ts +84 -0
  135. package/src/features/panel-apps/components/panel-apps-list.tsx +2 -2
  136. package/src/features/right-panel-resources/components/right-panel-resource-home-page.tsx +1 -1
  137. package/src/features/settings/pages/__tests__/appearance-settings-page.test.tsx +25 -8
  138. package/src/features/settings/pages/appearance-settings-page.tsx +92 -13
  139. package/src/features/side-dock/components/side-dock.tsx +3 -3
  140. package/src/index.css +275 -15
  141. package/src/shared/components/__tests__/file-type-icon.test.tsx +61 -0
  142. package/src/shared/components/actions/action-link.tsx +1 -1
  143. package/src/shared/components/doc-browser/__tests__/doc-browser-context.test.tsx +19 -0
  144. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +16 -0
  145. package/src/shared/components/doc-browser/doc-browser-context.tsx +1 -0
  146. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +1 -1
  147. package/src/shared/components/doc-browser/doc-browser.tsx +11 -3
  148. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +5 -0
  149. package/src/shared/components/doc-browser/stores/doc-browser.store.ts +6 -1
  150. package/src/shared/components/doc-browser/types/doc-browser.types.ts +2 -0
  151. package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +11 -0
  152. package/src/shared/components/file-type-icon.tsx +272 -0
  153. package/src/shared/components/resizable-right-panel/__tests__/resizable-right-panel.test.tsx +22 -0
  154. package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +17 -2
  155. package/src/shared/components/ui/actions/icon-action-button.tsx +1 -1
  156. package/src/shared/components/ui/button.tsx +1 -1
  157. package/src/shared/components/ui/dialog.tsx +1 -1
  158. package/src/shared/components/ui/input.tsx +1 -1
  159. package/src/shared/components/ui/select.tsx +1 -1
  160. package/src/shared/components/ui/switch.tsx +1 -1
  161. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +38 -0
  162. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +82 -2
  163. package/src/shared/components/ui/tabs.tsx +1 -1
  164. package/src/shared/components/ui/textarea.tsx +1 -1
  165. package/src/shared/lib/api/__tests__/server-path.utils.test.ts +28 -0
  166. package/src/shared/lib/api/utils/server-path.utils.ts +19 -20
  167. package/src/shared/lib/i18n/locales/en-US/chat.json +99 -0
  168. package/src/shared/lib/i18n/locales/en-US/core.json +6 -0
  169. package/src/shared/lib/i18n/locales/zh-CN/chat.json +99 -0
  170. package/src/shared/lib/i18n/locales/zh-CN/core.json +6 -0
  171. package/dist/assets/api-B7Qigvtm.js +0 -13
  172. package/dist/assets/appearance-settings-page-CuE0Iiqj.js +0 -1
  173. package/dist/assets/channels-list-page-CTsM6TcP.js +0 -8
  174. package/dist/assets/chat-page-C_5dcuE3.js +0 -1
  175. package/dist/assets/config-split-page-Fbb4PtTf.js +0 -1
  176. package/dist/assets/confirm-dialog-D2EG3nqE.js +0 -5
  177. package/dist/assets/desktop-update-config-BGR3HUi1.js +0 -1
  178. package/dist/assets/dist-7JTjycxE.js +0 -1
  179. package/dist/assets/dist-wXLhk4hl.js +0 -1
  180. package/dist/assets/doc-browser-bHyXnxlj.js +0 -1
  181. package/dist/assets/doc-browser-context-CYAGm5d5.js +0 -1
  182. package/dist/assets/doc-browser-gTrQ3jCF.js +0 -1
  183. package/dist/assets/es2015-j1SRCIX5.js +0 -41
  184. package/dist/assets/i18n-provider-B4oMxOFx.js +0 -1
  185. package/dist/assets/index-CvrvblgX.css +0 -1
  186. package/dist/assets/index-Obd4JYU2.js +0 -104
  187. package/dist/assets/loader-circle-CdzgK4eH.js +0 -1
  188. package/dist/assets/mcp-marketplace-page-Bt7CuOoq.js +0 -9
  189. package/dist/assets/mcp-marketplace-page-Ck3t78_n.js +0 -1
  190. package/dist/assets/model-config-page-DeKk9gvE.js +0 -1
  191. package/dist/assets/plus-Bx7wQbhG.js +0 -1
  192. package/dist/assets/provider-scoped-model-input-DsH2LF1q.js +0 -1
  193. package/dist/assets/providers-config-page-BFnGp68F.js +0 -1
  194. package/dist/assets/react-UirzUuST.js +0 -8
  195. package/dist/assets/remote-CDkHZtA8.js +0 -1
  196. package/dist/assets/rotate-cw-1JqxHTuw.js +0 -1
  197. package/dist/assets/runtime-config-page-BelAqiNG.js +0 -1
  198. package/dist/assets/search-config-page-zPm6--Ig.js +0 -1
  199. package/dist/assets/secrets-config-page-Cxyp-KuQ.js +0 -3
  200. package/dist/assets/security-config-B_gCq9fn.js +0 -1
  201. package/dist/assets/select-CdSEOwVM.js +0 -1
  202. package/dist/assets/setting-row-CTAtGexr.js +0 -1
  203. package/dist/assets/side-dock-DPvakJ0a.js +0 -3
  204. package/dist/assets/switch-BU_UEULb.js +0 -1
  205. package/dist/assets/tag-chip-D-WsKoT1.js +0 -1
  206. package/dist/assets/tooltip-CjkOaHW7.js +0 -1
  207. package/dist/assets/use-config--hx5oxQn.js +0 -1
  208. package/dist/assets/use-confirm-dialog-CD9a_SK1.js +0 -1
  209. package/dist/assets/x-DT5x45JU.js +0 -1
  210. /package/dist/assets/{config-hints-C_iP1zl_.js → config-hints-fDrYfl0l.js} +0 -0
  211. /package/dist/assets/{host-capabilities-9bB_998Z.js → host-capabilities-zZvY9U63.js} +0 -0
  212. /package/dist/assets/{middleware-BILWbJE4.js → middleware-BLMHv9YF.js} +0 -0
@@ -1,13 +1,22 @@
1
1
  import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
2
3
  import { expect, it, vi } from "vitest";
3
4
  import { ChatSessionWorkspacePanelContent } from "@/features/chat/features/workspace/components/chat-session-workspace-panel-content";
4
5
  import type { ResolvedChildSessionTab } from "@/features/chat/features/ncp/hooks/use-ncp-child-session-tabs-view";
5
6
 
7
+ const mocks = vi.hoisted(() => ({
8
+ materializeSideChatDraft: vi.fn(),
9
+ openChildSessions: vi.fn(),
10
+ openFilePreview: vi.fn(),
11
+ openProjectFiles: vi.fn(),
12
+ openSessionCronPanel: vi.fn(),
13
+ selectChildSessionDetail: vi.fn(),
14
+ }));
15
+
6
16
  vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
7
17
  usePresenter: () => ({
8
18
  chatThreadManager: {
9
- materializeSideChatDraft: vi.fn(),
10
- openFilePreview: vi.fn(),
19
+ ...mocks,
11
20
  },
12
21
  }),
13
22
  }));
@@ -16,6 +25,33 @@ vi.mock("@/features/chat/features/conversation/components/session-conversation-a
16
25
  SessionConversationArea: () => <div data-testid="session-conversation-area" />,
17
26
  }));
18
27
 
28
+ vi.mock("@/shared/hooks/use-server-path-browse", () => ({
29
+ useServerPathBrowse: () => ({
30
+ isLoading: false,
31
+ error: null,
32
+ data: {
33
+ currentPath: "/Users/peiwang/Projects/nextbot",
34
+ parentPath: "/Users/peiwang/Projects",
35
+ homePath: "/Users/peiwang",
36
+ breadcrumbs: [],
37
+ entries: [
38
+ {
39
+ name: "src",
40
+ path: "/Users/peiwang/Projects/nextbot/src",
41
+ kind: "directory",
42
+ hidden: false,
43
+ },
44
+ {
45
+ name: "package.json",
46
+ path: "/Users/peiwang/Projects/nextbot/package.json",
47
+ kind: "file",
48
+ hidden: false,
49
+ },
50
+ ],
51
+ },
52
+ }),
53
+ }));
54
+
19
55
  function createChildTab(): ResolvedChildSessionTab {
20
56
  return {
21
57
  sessionKey: "child-1",
@@ -40,7 +76,9 @@ it("shows compact child session metadata without repeating the tab title", () =>
40
76
  kind: "child-session",
41
77
  tab: createChildTab(),
42
78
  }}
79
+ childSessionTabs={[createChildTab()]}
43
80
  filePreviewRefreshVersion={0}
81
+ sessionKey="parent-1"
44
82
  sessionCronJobs={[]}
45
83
  sessionProjectRoot={null}
46
84
  sessionWorkingDir={null}
@@ -53,3 +91,76 @@ it("shows compact child session metadata without repeating the tab title", () =>
53
91
  expect(screen.getByText("nextbot")).toBeTruthy();
54
92
  expect(screen.getByTitle("/Users/peiwang/Projects/nextbot")).toBeTruthy();
55
93
  });
94
+
95
+ it("shows all session workspace entries in the overview", async () => {
96
+ const user = userEvent.setup();
97
+ const childTab = createChildTab();
98
+
99
+ render(
100
+ <ChatSessionWorkspacePanelContent
101
+ activeSelection={{ kind: "overview" }}
102
+ childSessionTabs={[childTab]}
103
+ filePreviewRefreshVersion={0}
104
+ sessionKey="parent-1"
105
+ sessionCronJobs={[]}
106
+ sessionProjectRoot="/tmp/project"
107
+ sessionWorkingDir="/tmp/project"
108
+ />,
109
+ );
110
+
111
+ expect(screen.getByText("Overview")).toBeTruthy();
112
+ const childSessionsButton = screen.getByRole("button", {
113
+ name: /Child sessions/,
114
+ });
115
+ const cronJobsButton = screen.getByRole("button", {
116
+ name: /Session cron jobs/,
117
+ });
118
+
119
+ expect((childSessionsButton as HTMLButtonElement).disabled).toBe(false);
120
+ expect((cronJobsButton as HTMLButtonElement).disabled).toBe(false);
121
+
122
+ await user.click(childSessionsButton);
123
+ await user.click(cronJobsButton);
124
+ await user.click(screen.getByRole("button", { name: /Project files/ }));
125
+
126
+ expect(mocks.openChildSessions).toHaveBeenCalledWith("parent-1");
127
+ expect(mocks.openSessionCronPanel).toHaveBeenCalledWith("parent-1");
128
+ expect(mocks.openProjectFiles).toHaveBeenCalledWith("parent-1");
129
+ });
130
+
131
+ it("shows an empty child sessions page instead of disabling the entry", () => {
132
+ render(
133
+ <ChatSessionWorkspacePanelContent
134
+ activeSelection={{ kind: "child-sessions" }}
135
+ childSessionTabs={[]}
136
+ filePreviewRefreshVersion={0}
137
+ sessionKey="parent-1"
138
+ sessionCronJobs={[]}
139
+ sessionProjectRoot={null}
140
+ sessionWorkingDir={null}
141
+ />,
142
+ );
143
+
144
+ expect(screen.getByText("No child sessions yet.")).toBeTruthy();
145
+ });
146
+
147
+ it("shows the selected session project as a hierarchical file tree", () => {
148
+ render(
149
+ <ChatSessionWorkspacePanelContent
150
+ activeSelection={{ kind: "project-files" }}
151
+ childSessionTabs={[]}
152
+ filePreviewRefreshVersion={0}
153
+ sessionKey="parent-1"
154
+ sessionCronJobs={[]}
155
+ sessionProjectRoot="/Users/peiwang/Projects/nextbot"
156
+ sessionWorkingDir="/Users/peiwang/Projects/nextbot"
157
+ />,
158
+ );
159
+
160
+ expect(screen.getByRole("tree", { name: "Project files" })).toBeTruthy();
161
+ expect(screen.getByText("nextbot")).toBeTruthy();
162
+ expect(
163
+ screen.getByRole("treeitem", { name: "Open directory: src" }),
164
+ ).toBeTruthy();
165
+ expect(document.querySelector('[data-file-type-icon="npm"]')).toBeTruthy();
166
+ });
@@ -14,6 +14,7 @@ const mocks = vi.hoisted(() => ({
14
14
  goBackWorkspacePanel: vi.fn(),
15
15
  goForwardWorkspacePanel: vi.fn(),
16
16
  closeWorkspacePanel: vi.fn(),
17
+ setWorkspacePanelWidth: vi.fn(),
17
18
  invalidateQueries: vi.fn(),
18
19
  }));
19
20
 
@@ -1,7 +1,8 @@
1
- import type { ReactNode } from "react";
1
+ import { useState, type ReactNode } from "react";
2
2
  import type { ChatFileOpenActionViewModel } from "@nextclaw/agent-chat-ui";
3
- import { ChevronRight, FileCode2, Folder } from "lucide-react";
4
- import type { useServerPathBrowse } from "@/shared/hooks/use-server-path-browse";
3
+ import { ChevronDown, ChevronRight, Folder, FolderOpen } from "lucide-react";
4
+ import { FileTypeIcon } from "@/shared/components/file-type-icon";
5
+ import { useServerPathBrowse } from "@/shared/hooks/use-server-path-browse";
5
6
  import type { ServerPathEntryView } from "@/shared/lib/api";
6
7
  import { t } from "@/shared/lib/i18n";
7
8
  import { cn } from "@/shared/lib/utils";
@@ -13,20 +14,9 @@ type ChatSessionWorkspaceDirectoryBrowserProps = {
13
14
  text: string;
14
15
  tone?: "muted" | "error";
15
16
  }) => ReactNode;
17
+ showRoot?: boolean;
16
18
  };
17
19
 
18
- function WorkspaceDirectoryEntryIcon({
19
- kind,
20
- }: {
21
- kind: ServerPathEntryView["kind"];
22
- }) {
23
- return kind === "directory" ? (
24
- <Folder className="h-4 w-4 shrink-0 text-emerald-600" />
25
- ) : (
26
- <FileCode2 className="h-4 w-4 shrink-0 text-sky-600" />
27
- );
28
- }
29
-
30
20
  function buildDirectoryEntryLabel(entry: ServerPathEntryView): string {
31
21
  const actionLabel =
32
22
  entry.kind === "directory"
@@ -35,18 +25,115 @@ function buildDirectoryEntryLabel(entry: ServerPathEntryView): string {
35
25
  return `${actionLabel}: ${entry.name}`;
36
26
  }
37
27
 
38
- export function ChatSessionWorkspaceDirectoryBrowser({
28
+ function readPathName(path: string): string {
29
+ return path.split(/[\\/]/).filter(Boolean).pop() ?? path;
30
+ }
31
+
32
+ function readBrowseError(
33
+ browseQuery: ReturnType<typeof useServerPathBrowse>,
34
+ ): string | null {
35
+ if (!browseQuery.error) {
36
+ return null;
37
+ }
38
+ return browseQuery.error instanceof Error
39
+ ? browseQuery.error.message
40
+ : String(browseQuery.error);
41
+ }
42
+
43
+ function WorkspaceDirectoryEntryIcon({
44
+ fileName,
45
+ isDirectory,
46
+ isExpanded,
47
+ }: {
48
+ fileName: string;
49
+ isDirectory: boolean;
50
+ isExpanded: boolean;
51
+ }) {
52
+ if (!isDirectory) {
53
+ return <FileTypeIcon fileName={fileName} />;
54
+ }
55
+ return isExpanded ? (
56
+ <FolderOpen className="h-4 w-4 shrink-0 text-amber-500" />
57
+ ) : (
58
+ <Folder className="h-4 w-4 shrink-0 text-amber-500" />
59
+ );
60
+ }
61
+
62
+ function WorkspaceDirectoryTreeChildren({
39
63
  browseQuery,
64
+ level,
40
65
  onFileOpen,
41
- renderStatus,
42
- }: ChatSessionWorkspaceDirectoryBrowserProps) {
66
+ }: {
67
+ browseQuery: ReturnType<typeof useServerPathBrowse>;
68
+ level: number;
69
+ onFileOpen: (action: ChatFileOpenActionViewModel) => void;
70
+ }) {
43
71
  const entries = browseQuery.data?.entries ?? [];
44
- const errorMessage = browseQuery.error
45
- ? browseQuery.error instanceof Error
46
- ? browseQuery.error.message
47
- : String(browseQuery.error)
48
- : null;
49
- const openEntry = (entry: ServerPathEntryView) => {
72
+ const errorMessage = readBrowseError(browseQuery);
73
+ const statusStyle = { paddingLeft: `${36 + level * 14}px` };
74
+
75
+ if (browseQuery.isLoading && !browseQuery.data) {
76
+ return (
77
+ <div
78
+ className="h-7 truncate pr-2 text-[11px] leading-7 text-gray-400"
79
+ style={statusStyle}
80
+ >
81
+ {t("chatWorkspaceLoadingDirectory")}
82
+ </div>
83
+ );
84
+ }
85
+ if (errorMessage && !browseQuery.data) {
86
+ return (
87
+ <div
88
+ className="h-7 truncate pr-2 text-[11px] leading-7 text-rose-600"
89
+ style={statusStyle}
90
+ title={errorMessage}
91
+ >
92
+ {t("chatWorkspaceDirectoryLoadFailed")}
93
+ </div>
94
+ );
95
+ }
96
+ if (entries.length === 0) {
97
+ return (
98
+ <div
99
+ className="h-7 truncate pr-2 text-[11px] leading-7 text-gray-400"
100
+ style={statusStyle}
101
+ >
102
+ {t("chatWorkspaceDirectoryEmpty")}
103
+ </div>
104
+ );
105
+ }
106
+ return entries.map((entry) => (
107
+ <WorkspaceDirectoryTreeEntry
108
+ key={entry.path}
109
+ entry={entry}
110
+ level={level + 1}
111
+ onFileOpen={onFileOpen}
112
+ />
113
+ ));
114
+ }
115
+
116
+ function WorkspaceDirectoryTreeEntry({
117
+ entry,
118
+ level,
119
+ onFileOpen,
120
+ }: {
121
+ entry: ServerPathEntryView;
122
+ level: number;
123
+ onFileOpen: (action: ChatFileOpenActionViewModel) => void;
124
+ }) {
125
+ const isDirectory = entry.kind === "directory";
126
+ const [isExpanded, setIsExpanded] = useState(false);
127
+ const browseQuery = useServerPathBrowse({
128
+ path: entry.path,
129
+ includeFiles: true,
130
+ enabled: isDirectory && isExpanded,
131
+ });
132
+ const activateEntry = () => {
133
+ if (isDirectory) {
134
+ setIsExpanded((value) => !value);
135
+ return;
136
+ }
50
137
  onFileOpen({
51
138
  path: entry.path,
52
139
  label: entry.name,
@@ -54,6 +141,60 @@ export function ChatSessionWorkspaceDirectoryBrowser({
54
141
  });
55
142
  };
56
143
 
144
+ return (
145
+ <div
146
+ role="treeitem"
147
+ aria-expanded={isDirectory ? isExpanded : undefined}
148
+ aria-label={buildDirectoryEntryLabel(entry)}
149
+ >
150
+ <button
151
+ type="button"
152
+ className={cn(
153
+ "flex h-7 w-full min-w-0 items-center gap-1.5 pr-2 text-left text-[13px] text-gray-700 transition-colors hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-border",
154
+ entry.hidden ? "opacity-65" : null,
155
+ )}
156
+ style={{ paddingLeft: `${8 + level * 14}px` }}
157
+ onClick={activateEntry}
158
+ >
159
+ <span className="inline-flex h-4 w-4 shrink-0 items-center justify-center text-gray-400">
160
+ {isDirectory ? (
161
+ isExpanded ? (
162
+ <ChevronDown className="h-3.5 w-3.5" />
163
+ ) : (
164
+ <ChevronRight className="h-3.5 w-3.5" />
165
+ )
166
+ ) : null}
167
+ </span>
168
+ <WorkspaceDirectoryEntryIcon
169
+ fileName={entry.name}
170
+ isDirectory={isDirectory}
171
+ isExpanded={isExpanded}
172
+ />
173
+ <span className="min-w-0 flex-1 truncate">{entry.name}</span>
174
+ </button>
175
+
176
+ {isDirectory && isExpanded ? (
177
+ <div role="group">
178
+ <WorkspaceDirectoryTreeChildren
179
+ browseQuery={browseQuery}
180
+ level={level}
181
+ onFileOpen={onFileOpen}
182
+ />
183
+ </div>
184
+ ) : null}
185
+ </div>
186
+ );
187
+ }
188
+
189
+ export function ChatSessionWorkspaceDirectoryBrowser({
190
+ browseQuery,
191
+ onFileOpen,
192
+ renderStatus,
193
+ showRoot = false,
194
+ }: ChatSessionWorkspaceDirectoryBrowserProps) {
195
+ const entries = browseQuery.data?.entries ?? [];
196
+ const errorMessage = readBrowseError(browseQuery);
197
+
57
198
  if (browseQuery.isLoading && !browseQuery.data) {
58
199
  return renderStatus({ text: t("chatWorkspaceLoadingDirectory") });
59
200
  }
@@ -72,28 +213,40 @@ export function ChatSessionWorkspaceDirectoryBrowser({
72
213
  return (
73
214
  <div
74
215
  data-testid="workspace-directory-browser"
75
- className="h-full overflow-auto bg-white px-2 py-2 custom-scrollbar"
216
+ role="tree"
217
+ aria-label={t("chatWorkspaceProjectFiles")}
218
+ className="h-full overflow-auto bg-white py-2 custom-scrollbar"
76
219
  >
77
- <div className="space-y-0.5">
78
- {entries.map((entry) => (
79
- <button
220
+ {showRoot && browseQuery.data ? (
221
+ <div role="treeitem" aria-expanded="true">
222
+ <div className="flex h-8 items-center gap-1.5 border-b border-gray-100 px-2 text-xs font-semibold text-gray-700">
223
+ <ChevronDown className="h-3.5 w-3.5 shrink-0 text-gray-400" />
224
+ <FolderOpen className="h-4 w-4 shrink-0 text-amber-500" />
225
+ <span className="min-w-0 flex-1 truncate" title={browseQuery.data.currentPath}>
226
+ {readPathName(browseQuery.data.currentPath)}
227
+ </span>
228
+ </div>
229
+ <div role="group" className="pt-1">
230
+ {entries.map((entry) => (
231
+ <WorkspaceDirectoryTreeEntry
232
+ key={entry.path}
233
+ entry={entry}
234
+ level={1}
235
+ onFileOpen={onFileOpen}
236
+ />
237
+ ))}
238
+ </div>
239
+ </div>
240
+ ) : (
241
+ entries.map((entry) => (
242
+ <WorkspaceDirectoryTreeEntry
80
243
  key={entry.path}
81
- type="button"
82
- aria-label={buildDirectoryEntryLabel(entry)}
83
- className={cn(
84
- "flex h-8 w-full min-w-0 items-center gap-2 rounded-sm px-2 text-left text-sm text-gray-700 transition-colors hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40",
85
- entry.hidden ? "opacity-65" : null,
86
- )}
87
- onClick={() => openEntry(entry)}
88
- >
89
- <WorkspaceDirectoryEntryIcon kind={entry.kind} />
90
- <span className="min-w-0 flex-1 truncate">{entry.name}</span>
91
- {entry.kind === "directory" ? (
92
- <ChevronRight className="h-3.5 w-3.5 shrink-0 text-gray-300" />
93
- ) : null}
94
- </button>
95
- ))}
96
- </div>
244
+ entry={entry}
245
+ level={0}
246
+ onFileOpen={onFileOpen}
247
+ />
248
+ ))
249
+ )}
97
250
  </div>
98
251
  );
99
252
  }
@@ -1,6 +1,7 @@
1
1
  import { Fragment } from "react";
2
2
  import type { ChatFileOpenActionViewModel } from "@nextclaw/agent-chat-ui";
3
- import { ChevronRight, File, Folder, Loader2 } from "lucide-react";
3
+ import { ChevronRight, Folder, Loader2 } from "lucide-react";
4
+ import { FileTypeIcon } from "@/shared/components/file-type-icon";
4
5
  import { ScrollArea } from "@/shared/components/ui/scroll-area";
5
6
  import { useServerPathBrowse } from "@/shared/hooks/use-server-path-browse";
6
7
  import type { ServerPathEntryView } from "@/shared/lib/api";
@@ -8,14 +9,16 @@ import { t } from "@/shared/lib/i18n";
8
9
  import { cn } from "@/shared/lib/utils";
9
10
 
10
11
  function WorkspaceBreadcrumbEntryIcon({
12
+ name,
11
13
  kind,
12
14
  }: {
15
+ name: string;
13
16
  kind: ServerPathEntryView["kind"];
14
17
  }) {
15
18
  return kind === "directory" ? (
16
19
  <Folder className="h-3.5 w-3.5 shrink-0 text-emerald-600" />
17
20
  ) : (
18
- <File className="h-3.5 w-3.5 shrink-0 text-sky-600" />
21
+ <FileTypeIcon fileName={name} size="compact" />
19
22
  );
20
23
  }
21
24
 
@@ -67,7 +70,7 @@ export function WorkspaceBreadcrumbBrowser({
67
70
  <Fragment key={breadcrumb.path}>
68
71
  <button
69
72
  type="button"
70
- className="h-5 rounded-sm px-1 text-left hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
73
+ className="h-5 rounded-sm px-1 text-left hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
71
74
  onClick={() => onBrowsePathChange(breadcrumb.path)}
72
75
  >
73
76
  {breadcrumb.label}
@@ -97,12 +100,15 @@ export function WorkspaceBreadcrumbBrowser({
97
100
  key={entry.path}
98
101
  type="button"
99
102
  className={cn(
100
- "flex h-6 w-full items-center gap-1.5 rounded-sm px-1.5 text-left text-[11px] text-gray-700 transition-colors hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40",
103
+ "flex h-6 w-full items-center gap-1.5 rounded-sm px-1.5 text-left text-[11px] text-gray-700 transition-colors hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border",
101
104
  entry.hidden ? "opacity-65" : null,
102
105
  )}
103
106
  onClick={() => openEntry(entry)}
104
107
  >
105
- <WorkspaceBreadcrumbEntryIcon kind={entry.kind} />
108
+ <WorkspaceBreadcrumbEntryIcon
109
+ kind={entry.kind}
110
+ name={entry.name}
111
+ />
106
112
  <span className="min-w-0 flex-1 truncate">{entry.name}</span>
107
113
  {entry.kind === "directory" ? (
108
114
  <ChevronRight className="h-3 w-3 shrink-0 text-gray-300" />
@@ -1,7 +1,9 @@
1
1
  import { Fragment, useState } from "react";
2
2
  import type { ChatFileOpenActionViewModel } from "@nextclaw/agent-chat-ui";
3
- import { ChevronRight, FileCode2, Folder, FolderTree } from "lucide-react";
4
- import { Popover, PopoverContent, PopoverTrigger } from "@/shared/components/ui/popover";
3
+ import { ChevronRight, Folder, FolderTree } from "lucide-react";
4
+ import { Popover, PopoverTrigger } from "@/shared/components/ui/popover";
5
+ import { FileTypeIcon } from "@/shared/components/file-type-icon";
6
+ import { ChatPopoverContent } from "@/features/chat/components/chat-popover-content";
5
7
  import { WorkspaceBreadcrumbBrowser } from "./chat-session-workspace-file-breadcrumb-browser";
6
8
  import type {
7
9
  WorkspaceFileBreadcrumbSegmentViewModel,
@@ -48,7 +50,7 @@ function WorkspaceBreadcrumbSegmentButton({
48
50
  type="button"
49
51
  aria-current={segment.isCurrent ? "page" : undefined}
50
52
  className={cn(
51
- "inline-flex h-5 items-center gap-1 rounded-sm px-1 text-[11px] leading-none transition-colors hover:bg-gray-200/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40",
53
+ "inline-flex h-5 items-center gap-1 rounded-sm px-1 text-[11px] leading-none transition-colors hover:bg-gray-200/80 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border",
52
54
  segment.kind === "workspace"
53
55
  ? "bg-primary/8 text-primary hover:bg-primary/12"
54
56
  : segment.isCurrent
@@ -62,12 +64,12 @@ function WorkspaceBreadcrumbSegmentButton({
62
64
  ) : segment.kind === "directory" && segment.isCurrent ? (
63
65
  <Folder className="h-3 w-3 shrink-0" />
64
66
  ) : segment.isCurrent ? (
65
- <FileCode2 className="h-3 w-3 shrink-0" />
67
+ <FileTypeIcon fileName={segment.label} size="compact" />
66
68
  ) : null}
67
69
  <span>{segment.label}</span>
68
70
  </button>
69
71
  </PopoverTrigger>
70
- <PopoverContent
72
+ <ChatPopoverContent
71
73
  data-testid="workspace-breadcrumb-popover"
72
74
  className="w-[22rem] rounded-md p-0"
73
75
  align="start"
@@ -78,7 +80,7 @@ function WorkspaceBreadcrumbSegmentButton({
78
80
  onClose={() => setOpen(false)}
79
81
  onFileOpen={onFileOpen}
80
82
  />
81
- </PopoverContent>
83
+ </ChatPopoverContent>
82
84
  </Popover>
83
85
  );
84
86
  }