@nextclaw/ui 0.15.1 → 0.15.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/assets/{api-b1R9VdeH.js → api-iNBpppzj.js} +3 -3
  3. package/dist/assets/{appearance-settings-page-D2O9pch9.js → appearance-settings-page-DMffQFR3.js} +1 -1
  4. package/dist/assets/{book-open-B77lQ2Tg.js → book-open-C3mJGwiy.js} +1 -1
  5. package/dist/assets/channels-list-page-CCtp0BRS.js +8 -0
  6. package/dist/assets/chat-page-H5a0dWvO.js +1 -0
  7. package/dist/assets/config-split-page-CrOJgB-Y.js +1 -0
  8. package/dist/assets/{confirm-dialog-C-NXSioX.js → confirm-dialog-Chv50USy.js} +2 -2
  9. package/dist/assets/{desktop-update-config-usHpXugP.js → desktop-update-config-KzTuXRf_.js} +1 -1
  10. package/dist/assets/{dist-jdGmbb6w.js → dist-BunzmC4B.js} +1 -1
  11. package/dist/assets/{dist-YtoqU-AT.js → dist-ChmGwvtK.js} +1 -1
  12. package/dist/assets/doc-browser-DNRL4Zvd.js +1 -0
  13. package/dist/assets/doc-browser-Dg0pmkOg.js +1 -0
  14. package/dist/assets/doc-browser-context-BHY-0FSD.js +1 -0
  15. package/dist/assets/{es2015-bNpL2LKy.js → es2015-DQZ5JWwK.js} +1 -1
  16. package/dist/assets/{external-link-BM6fk8q-.js → external-link-CU0kzzIP.js} +1 -1
  17. package/dist/assets/form-actions-BvCp_0lS.js +1 -0
  18. package/dist/assets/{i18n-provider-CntWolRA.js → i18n-provider-Cu8XyZb_.js} +1 -1
  19. package/dist/assets/index-CzfBdvK1.css +1 -0
  20. package/dist/assets/index-DGqHne1Q.js +104 -0
  21. package/dist/assets/loader-circle-CDSFuzmk.js +1 -0
  22. package/dist/assets/mcp-marketplace-page-BeEQK0DL.js +9 -0
  23. package/dist/assets/mcp-marketplace-page-D6idOTLp.js +1 -0
  24. package/dist/assets/model-config-page-bNNo-XyG.js +1 -0
  25. package/dist/assets/plus-D4DsdLN-.js +1 -0
  26. package/dist/assets/{provider-scoped-model-input-Cm_a4GHf.js → provider-scoped-model-input-IlgQCsoT.js} +1 -1
  27. package/dist/assets/providers-config-page-wLo6CAJw.js +1 -0
  28. package/dist/assets/{react-BN8uzPnp.js → react-BPIsQIlO.js} +1 -1
  29. package/dist/assets/{refresh-cw-BpVcFPmJ.js → refresh-cw-CILd1vAP.js} +1 -1
  30. package/dist/assets/remote-uKEYp7AR.js +1 -0
  31. package/dist/assets/rotate-cw-wPFPg1kH.js +1 -0
  32. package/dist/assets/runtime-config-page-DDn1sP4G.js +1 -0
  33. package/dist/assets/{save-BpFW5fpk.js → save-ByJqcPTP.js} +1 -1
  34. package/dist/assets/{search-D0uO7enf.js → search-DVIV6pfD.js} +1 -1
  35. package/dist/assets/search-config-page-B-Cf4iiB.js +1 -0
  36. package/dist/assets/secrets-config-page-DPKsY8Iw.js +3 -0
  37. package/dist/assets/security-config-e9SSBlKr.js +1 -0
  38. package/dist/assets/{select-Bh-CrA4p.js → select-BdgczSNw.js} +1 -1
  39. package/dist/assets/{setting-row-BJfCHRcb.js → setting-row-BkTVUTKx.js} +1 -1
  40. package/dist/assets/{settings-2-Blzi1vr6.js → settings-2-CH5shGg_.js} +1 -1
  41. package/dist/assets/{side-dock-ZdaSrQVB.js → side-dock-BsBvcJ9i.js} +2 -2
  42. package/dist/assets/{skeleton-BLlEzebZ.js → skeleton-DeX1FKw7.js} +1 -1
  43. package/dist/assets/switch-Cv8Ekb3x.js +1 -0
  44. package/dist/assets/{tag-chip-CzGihiQx.js → tag-chip-RB9uKcoC.js} +1 -1
  45. package/dist/assets/{tooltip-CdcrBjrC.js → tooltip-oQlEN-l6.js} +1 -1
  46. package/dist/assets/{use-config-CVPgsBmU.js → use-config-l3W7y8Qz.js} +1 -1
  47. package/dist/assets/{use-confirm-dialog-CSeBk9SF.js → use-confirm-dialog-BAxaVOJI.js} +1 -1
  48. package/dist/assets/x-BBMffEN0.js +1 -0
  49. package/dist/index.html +38 -30
  50. package/index.html +12 -4
  51. package/package.json +9 -9
  52. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +29 -1
  53. package/src/app/components/layout/sidebar-items.tsx +46 -7
  54. package/src/app/components/layout/sidebar-rail.styles.ts +2 -2
  55. package/src/app/components/layout/sidebar.tsx +84 -62
  56. package/src/app/configs/app-navigation.config.ts +49 -0
  57. package/src/app/styles/design-system.css +99 -4
  58. package/src/features/channels/components/config/channel-form.tsx +14 -11
  59. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +40 -0
  60. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +3 -1
  61. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +1 -1
  62. package/src/features/chat/components/layout/__tests__/chat-sidebar-toolbar.test.tsx +48 -0
  63. package/src/features/chat/components/layout/__tests__/chat-sidebar-utility-menu.test.tsx +55 -0
  64. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +0 -3
  65. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +3 -4
  66. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +30 -31
  67. package/src/features/chat/components/layout/chat-sidebar.tsx +21 -7
  68. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +2 -1
  69. package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +1 -1
  70. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +22 -1
  71. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +62 -1
  72. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +10 -5
  73. package/src/features/chat/features/message/components/chat-message-list.container.tsx +47 -1
  74. package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +11 -1
  75. package/src/features/chat/features/message/utils/__tests__/chat-message-texts.utils.test.ts +23 -0
  76. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +0 -11
  77. package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +5 -0
  78. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +138 -0
  79. package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +116 -0
  80. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +108 -46
  81. package/src/features/chat/features/session/components/chat-sidebar-session-entry.tsx +6 -0
  82. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +48 -24
  83. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +0 -3
  84. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +0 -4
  85. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +2 -2
  86. package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +25 -3
  87. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +9 -4
  88. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +1 -1
  89. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +1 -1
  90. package/src/features/chat/features/welcome/components/chat-welcome.tsx +0 -5
  91. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +226 -56
  92. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +1 -0
  93. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +144 -0
  94. package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +99 -0
  95. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +2 -2
  96. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +4 -2
  97. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +232 -21
  98. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +3 -0
  99. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +44 -1
  100. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +45 -3
  101. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +4 -0
  102. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +33 -2
  103. package/src/features/chat/managers/chat-session-list.manager.ts +32 -0
  104. package/src/features/chat/managers/chat-thread.manager.ts +2 -0
  105. package/src/features/chat/stores/chat-session-list.store.ts +28 -5
  106. package/src/features/chat/stores/chat-thread.store.ts +3 -0
  107. package/src/features/cron/components/cron-config.tsx +63 -80
  108. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.tsx +44 -6
  109. package/src/features/marketplace/components/marketplace-detail-doc.tsx +8 -4
  110. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  111. package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +5 -12
  112. package/src/features/marketplace/hooks/use-marketplace-detail-doc-entry.ts +84 -0
  113. package/src/features/panel-apps/components/panel-app-list-item.tsx +6 -6
  114. package/src/features/panel-apps/components/panel-apps-list.tsx +2 -2
  115. package/src/features/panel-apps/index.ts +1 -0
  116. package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +2 -10
  117. package/src/features/panel-apps/utils/panel-app-iframe.utils.ts +6 -0
  118. package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +11 -3
  119. package/src/features/pwa/managers/pwa-shell-theme.manager.ts +3 -1
  120. package/src/features/right-panel-resources/components/right-panel-resource-home-page.tsx +1 -1
  121. package/src/features/service-apps/components/service-apps-panel.tsx +145 -35
  122. package/src/features/settings/components/config/provider-auth-section.tsx +1 -1
  123. package/src/features/settings/components/config/provider-form.tsx +11 -8
  124. package/src/features/settings/components/config/provider-models-section.tsx +7 -7
  125. package/src/features/settings/components/config/provider-pill-selector.tsx +1 -1
  126. package/src/features/settings/components/config/secrets-config-form.tsx +5 -4
  127. package/src/features/settings/pages/model-config-page.tsx +26 -25
  128. package/src/features/settings/pages/providers-config-page.tsx +10 -13
  129. package/src/features/settings/pages/search-config-page.tsx +8 -5
  130. package/src/features/side-dock/components/side-dock.tsx +3 -3
  131. package/src/features/system-status/components/config/runtime-config-editor.tsx +5 -4
  132. package/src/index.css +257 -13
  133. package/src/shared/components/actions/action-link.tsx +1 -1
  134. package/src/shared/components/config-split-page.tsx +6 -6
  135. package/src/shared/components/doc-browser/__tests__/doc-browser-url.utils.test.ts +73 -0
  136. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +48 -0
  137. package/src/shared/components/doc-browser/doc-browser-context.tsx +4 -0
  138. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +6 -2
  139. package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +2 -1
  140. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +10 -10
  141. package/src/shared/components/doc-browser/doc-browser.tsx +6 -4
  142. package/src/shared/components/doc-browser/utils/doc-browser-url.utils.ts +62 -4
  143. package/src/shared/components/resizable-right-panel/__tests__/resizable-right-panel.test.tsx +18 -0
  144. package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +28 -3
  145. package/src/shared/components/ui/actions/__tests__/icon-action-button.test.tsx +36 -0
  146. package/src/shared/components/ui/actions/form-actions.tsx +25 -0
  147. package/src/shared/components/ui/actions/icon-action-button.tsx +25 -2
  148. package/src/shared/components/ui/button.tsx +1 -1
  149. package/src/shared/components/ui/card.tsx +12 -4
  150. package/src/shared/components/ui/dialog.tsx +1 -1
  151. package/src/shared/components/ui/input.tsx +1 -1
  152. package/src/shared/components/ui/select.tsx +1 -1
  153. package/src/shared/components/ui/switch.tsx +1 -1
  154. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +2 -1
  155. package/src/shared/components/ui/tabs.tsx +1 -1
  156. package/src/shared/components/ui/textarea.tsx +1 -1
  157. package/src/shared/hooks/use-server-path-browse.ts +26 -4
  158. package/src/shared/hooks/use-server-path-read.ts +11 -1
  159. package/src/shared/lib/api/utils/server-path.utils.ts +1 -0
  160. package/src/shared/lib/i18n/locales/en-US/chat.json +99 -0
  161. package/src/shared/lib/i18n/locales/en-US/core.json +3 -0
  162. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +5 -0
  163. package/src/shared/lib/i18n/locales/zh-CN/chat.json +99 -0
  164. package/src/shared/lib/i18n/locales/zh-CN/core.json +3 -0
  165. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +5 -0
  166. package/src/shared/lib/session-project/workspace-file-breadcrumb.utils.ts +9 -5
  167. package/src/shared/lib/theme/index.test.ts +21 -0
  168. package/src/shared/lib/theme/index.ts +6 -4
  169. package/src/vite-env.d.ts +2 -0
  170. package/dist/assets/channels-list-page-DcsIQc6A.js +0 -8
  171. package/dist/assets/chat-page-CZ7WxPir.js +0 -1
  172. package/dist/assets/config-split-page-BWQ6_Raq.js +0 -1
  173. package/dist/assets/doc-browser-B_Tf9n70.js +0 -1
  174. package/dist/assets/doc-browser-BdZPFOVa.js +0 -1
  175. package/dist/assets/doc-browser-context-DwokemUn.js +0 -1
  176. package/dist/assets/index-BIcEw90u.js +0 -104
  177. package/dist/assets/index-BiNSTKTU.css +0 -1
  178. package/dist/assets/loader-circle-MBgNSVIH.js +0 -1
  179. package/dist/assets/mcp-marketplace-page-CLmUstLN.js +0 -9
  180. package/dist/assets/mcp-marketplace-page-rZkmWPMy.js +0 -1
  181. package/dist/assets/model-config-page-Di0ySJD2.js +0 -1
  182. package/dist/assets/plus-vDqW5Kxp.js +0 -1
  183. package/dist/assets/providers-config-page-BAf2W0MV.js +0 -1
  184. package/dist/assets/remote-CLQ0IBM8.js +0 -1
  185. package/dist/assets/rotate-cw-CvNBv7_w.js +0 -1
  186. package/dist/assets/runtime-config-page-CflvvoDc.js +0 -1
  187. package/dist/assets/search-config-page-D0klFUXd.js +0 -1
  188. package/dist/assets/secrets-config-page-dVZ631Wp.js +0 -3
  189. package/dist/assets/security-config-C5gnlkuj.js +0 -1
  190. package/dist/assets/switch-CFLJFNSd.js +0 -1
  191. package/dist/assets/x-BF2KQ--V.js +0 -1
  192. /package/dist/assets/{config-hints-fGnUjDe9.js → config-hints-C_iP1zl_.js} +0 -0
@@ -32,6 +32,71 @@ export function buildChatMessageAdapterTexts(
32
32
  };
33
33
  }
34
34
 
35
+ function buildBuiltInToolStatusLabels() {
36
+ return {
37
+ directory: {
38
+ running: t("chatToolDirectoryRunning"),
39
+ success: t("chatToolDirectorySuccess"),
40
+ error: t("chatToolDirectoryError"),
41
+ cancelled: t("chatToolDirectoryCancelled"),
42
+ },
43
+ web: {
44
+ running: t("chatToolWebRunning"),
45
+ success: t("chatToolWebSuccess"),
46
+ error: t("chatToolWebError"),
47
+ cancelled: t("chatToolWebCancelled"),
48
+ },
49
+ message: {
50
+ running: t("chatToolMessageRunning"),
51
+ success: t("chatToolMessageSuccess"),
52
+ error: t("chatToolMessageError"),
53
+ cancelled: t("chatToolMessageCancelled"),
54
+ },
55
+ session: {
56
+ running: t("chatToolSessionRunning"),
57
+ success: t("chatToolSessionSuccess"),
58
+ error: t("chatToolSessionError"),
59
+ cancelled: t("chatToolSessionCancelled"),
60
+ },
61
+ agent: {
62
+ running: t("chatToolAgentRunning"),
63
+ success: t("chatToolAgentSuccess"),
64
+ error: t("chatToolAgentError"),
65
+ cancelled: t("chatToolAgentCancelled"),
66
+ },
67
+ memory: {
68
+ running: t("chatToolMemoryRunning"),
69
+ success: t("chatToolMemorySuccess"),
70
+ error: t("chatToolMemoryError"),
71
+ cancelled: t("chatToolMemoryCancelled"),
72
+ },
73
+ schedule: {
74
+ running: t("chatToolScheduleRunning"),
75
+ success: t("chatToolScheduleSuccess"),
76
+ error: t("chatToolScheduleError"),
77
+ cancelled: t("chatToolScheduleCancelled"),
78
+ },
79
+ system: {
80
+ running: t("chatToolSystemRunning"),
81
+ success: t("chatToolSystemSuccess"),
82
+ error: t("chatToolSystemError"),
83
+ cancelled: t("chatToolSystemCancelled"),
84
+ },
85
+ image: {
86
+ running: t("chatToolImageRunning"),
87
+ success: t("chatToolImageSuccess"),
88
+ error: t("chatToolImageError"),
89
+ cancelled: t("chatToolImageCancelled"),
90
+ },
91
+ display: {
92
+ running: t("chatToolDisplayRunning"),
93
+ success: t("chatToolDisplaySuccess"),
94
+ error: t("chatToolDisplayError"),
95
+ cancelled: t("chatToolDisplayCancelled"),
96
+ },
97
+ };
98
+ }
99
+
35
100
  export function buildChatMessageTexts(
36
101
  language: string,
37
102
  ): ChatMessageTexts {
@@ -42,8 +107,41 @@ export function buildChatMessageTexts(
42
107
  copyMessageLabel: t("chatMessageCopy"),
43
108
  copiedMessageLabel: t("chatMessageCopied"),
44
109
  typingLabel: t("chatTyping"),
110
+ reasoningCharacterCountTemplates: {
111
+ inProgress: t("chatReasoningInProgressCharacterCount"),
112
+ completed: t("chatReasoningCompletedCharacterCount"),
113
+ },
114
+ toolStatusLabels: {
115
+ terminal: {
116
+ running: t("chatToolTerminalRunning"),
117
+ success: t("chatToolTerminalSuccess"),
118
+ error: t("chatToolTerminalError"),
119
+ cancelled: t("chatToolTerminalCancelled"),
120
+ },
121
+ fileRead: {
122
+ running: t("chatToolFileReadRunning"),
123
+ success: t("chatToolFileReadSuccess"),
124
+ error: t("chatToolFileReadError"),
125
+ cancelled: t("chatToolFileReadCancelled"),
126
+ },
127
+ fileEdit: {
128
+ running: t("chatToolFileEditRunning"),
129
+ success: t("chatToolFileEditSuccess"),
130
+ error: t("chatToolFileEditError"),
131
+ cancelled: t("chatToolFileEditCancelled"),
132
+ },
133
+ search: {
134
+ running: t("chatToolSearchRunning"),
135
+ success: t("chatToolSearchSuccess"),
136
+ error: t("chatToolSearchError"),
137
+ cancelled: t("chatToolSearchCancelled"),
138
+ },
139
+ builtIn: buildBuiltInToolStatusLabels(),
140
+ },
45
141
  attachmentOpenLabel: t("chatAttachmentOpen"),
46
142
  attachmentAttachedLabel: t("chatAttachmentAttached"),
143
+ attachmentExpandLabel: t("chatAttachmentExpand"),
144
+ attachmentCloseLabel: t("chatAttachmentClosePreview"),
47
145
  attachmentCategoryLabels: {
48
146
  archive: t("chatAttachmentCategoryArchive"),
49
147
  audio: t("chatAttachmentCategoryAudio"),
@@ -56,5 +154,45 @@ export function buildChatMessageTexts(
56
154
  sheet: t("chatAttachmentCategorySheet"),
57
155
  video: t("chatAttachmentCategoryVideo"),
58
156
  },
157
+ toolActivitySegmentTemplates: {
158
+ read: {
159
+ one: t("chatToolActivityReadOne"),
160
+ other: t("chatToolActivityReadOther"),
161
+ },
162
+ edit: {
163
+ one: t("chatToolActivityEditOne"),
164
+ other: t("chatToolActivityEditOther"),
165
+ },
166
+ directory: {
167
+ one: t("chatToolActivityDirectoryOne"),
168
+ other: t("chatToolActivityDirectoryOther"),
169
+ },
170
+ search: {
171
+ one: t("chatToolActivitySearchOne"),
172
+ other: t("chatToolActivitySearchOther"),
173
+ },
174
+ bash: {
175
+ one: t("chatToolActivityBashOne"),
176
+ other: t("chatToolActivityBashOther"),
177
+ },
178
+ web: {
179
+ one: t("chatToolActivityWebOne"),
180
+ other: t("chatToolActivityWebOther"),
181
+ },
182
+ agent: {
183
+ one: t("chatToolActivityAgentOne"),
184
+ other: t("chatToolActivityAgentOther"),
185
+ },
186
+ panel: {
187
+ one: t("chatToolActivityPanelOne"),
188
+ other: t("chatToolActivityPanelOther"),
189
+ },
190
+ other: {
191
+ one: t("chatToolActivityOtherOne"),
192
+ other: t("chatToolActivityOtherOther"),
193
+ },
194
+ },
195
+ toolActivityFailedLabel: t("chatProcessSummaryFailed"),
196
+ toolActivityCancelledLabel: t("chatProcessSummaryCancelled"),
59
197
  };
60
198
  }
@@ -0,0 +1,116 @@
1
+ import { fireEvent, render, screen } from "@testing-library/react";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { ChatSidebarProjectGroups } from "@/features/chat/features/session/components/chat-sidebar-project-groups";
4
+ import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
5
+
6
+ const mocks = vi.hoisted(() => ({
7
+ createSession: vi.fn(),
8
+ toggleProjectCollapsed: vi.fn(),
9
+ toggleProjectPinned: vi.fn(),
10
+ }));
11
+
12
+ vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
13
+ usePresenter: () => ({
14
+ chatSessionListManager: mocks,
15
+ }),
16
+ }));
17
+
18
+ const projectGroup = {
19
+ projectRoot: "/tmp/analysis-project",
20
+ projectName: "analysis-project",
21
+ items: [
22
+ {
23
+ session: {
24
+ key: "session:analysis",
25
+ createdAt: "2026-07-12T00:00:00.000Z",
26
+ updatedAt: "2026-07-12T00:00:00.000Z",
27
+ sessionType: "native",
28
+ sessionTypeMutable: false,
29
+ messageCount: 1,
30
+ },
31
+ },
32
+ ],
33
+ latestUpdatedAt: 1,
34
+ isPinned: false,
35
+ };
36
+
37
+ function renderProjectGroups(isPinned = false) {
38
+ return render(
39
+ <ChatSidebarProjectGroups
40
+ groups={[{ ...projectGroup, isPinned }]}
41
+ defaultSessionType="native"
42
+ sessionTypeOptions={[
43
+ { value: "native", label: "Native", icon: null, ready: true },
44
+ ]}
45
+ renderSessionItem={() => <div>Project session</div>}
46
+ />,
47
+ );
48
+ }
49
+
50
+ describe("ChatSidebarProjectGroups", () => {
51
+ beforeEach(() => {
52
+ mocks.createSession.mockReset();
53
+ mocks.toggleProjectCollapsed.mockReset();
54
+ mocks.toggleProjectPinned.mockReset();
55
+ useChatSessionListStore.setState({
56
+ snapshot: {
57
+ ...useChatSessionListStore.getState().snapshot,
58
+ collapsedProjectRoots: [],
59
+ selectedSessionKey: "session:current",
60
+ },
61
+ });
62
+ mocks.toggleProjectCollapsed.mockImplementation((projectRoot: string) => {
63
+ useChatSessionListStore.getState().setSnapshot({
64
+ collapsedProjectRoots: [projectRoot],
65
+ });
66
+ });
67
+ });
68
+
69
+ it("keeps the project header as the full-width folder row and collapses its sessions", () => {
70
+ renderProjectGroups();
71
+
72
+ const header = screen.getByLabelText("Collapse project");
73
+ expect(header.parentElement?.className).toContain("h-10");
74
+ expect(header.parentElement?.className).toContain("hover:bg-gray-200/60");
75
+ expect(header.parentElement?.className).not.toContain("focus-within:bg");
76
+ expect(screen.getByText("analysis-project").nextElementSibling?.tagName).toBe("svg");
77
+
78
+ fireEvent.click(header);
79
+
80
+ expect(mocks.toggleProjectCollapsed).toHaveBeenCalledWith(
81
+ "/tmp/analysis-project",
82
+ );
83
+ expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBe(
84
+ "session:current",
85
+ );
86
+ expect(screen.queryByText("Project session")).toBeNull();
87
+ });
88
+
89
+ it("keeps project creation and pinning in one trailing action cluster", () => {
90
+ renderProjectGroups();
91
+
92
+ fireEvent.click(screen.getByLabelText("Pin project"));
93
+
94
+ expect(mocks.toggleProjectPinned).toHaveBeenCalledWith(
95
+ "/tmp/analysis-project",
96
+ );
97
+ expect(screen.getByLabelText("New Task · analysis-project")).not.toBeNull();
98
+ });
99
+
100
+ it("uses the same pin control to show and clear the project pin state", () => {
101
+ renderProjectGroups(true);
102
+
103
+ const unpinButton = screen.getByLabelText("Unpin project");
104
+ const pinIcon = unpinButton.querySelector("svg");
105
+
106
+ expect(pinIcon?.getAttribute("class")).toContain("fill-current");
107
+ expect(pinIcon?.getAttribute("class")).toContain("text-foreground");
108
+ expect(document.querySelectorAll("svg.fill-current")).toHaveLength(1);
109
+
110
+ fireEvent.click(unpinButton);
111
+
112
+ expect(mocks.toggleProjectPinned).toHaveBeenCalledWith(
113
+ "/tmp/analysis-project",
114
+ );
115
+ });
116
+ });
@@ -1,11 +1,13 @@
1
1
  import { useMemo, useState, type ReactNode } from 'react';
2
- import { Plus } from 'lucide-react';
2
+ import { ChevronDown, ChevronRight, Folder, Pin, Plus } from 'lucide-react';
3
+ import { usePresenter } from '@/features/chat/components/providers/chat-presenter.provider';
3
4
  import { ChatSessionTypeMenu } from "@/features/chat/features/session-type/components/chat-session-type-menu";
4
5
  import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
5
6
  import { IconActionButton } from '@/shared/components/ui/actions/icon-action-button';
6
7
  import type { ChatSessionTypeOption } from "@/features/chat/features/session-type/utils/chat-session-type.utils";
7
8
  import type { NcpSessionListItemView } from '@/features/chat/features/ncp/hooks/use-ncp-session-list-view';
8
9
  import type { ChatSidebarProjectGroup } from '@/features/chat/features/session/utils/chat-sidebar-session-groups.utils';
10
+ import { useChatSessionListStore } from '@/features/chat/stores/chat-session-list.store';
9
11
  import { t } from '@/shared/lib/i18n';
10
12
 
11
13
  export type { ChatSidebarProjectGroup };
@@ -17,7 +19,6 @@ type ChatSidebarProjectGroupsProps = {
17
19
  defaultSessionType: string;
18
20
  sessionTypeOptions: SessionTypeOption[];
19
21
  renderSessionItem: (item: NcpSessionListItemView) => ReactNode;
20
- onCreateSession: (sessionType: string, projectRoot: string) => void;
21
22
  };
22
23
 
23
24
  function resolveProjectGroupDefaultSessionType(
@@ -31,7 +32,11 @@ function resolveProjectGroupDefaultSessionType(
31
32
  }
32
33
 
33
34
  export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
34
- const { groups, defaultSessionType, sessionTypeOptions, renderSessionItem, onCreateSession } = props;
35
+ const { groups, defaultSessionType, sessionTypeOptions, renderSessionItem } = props;
36
+ const presenter = usePresenter();
37
+ const collapsedProjectRoots = useChatSessionListStore(
38
+ (state) => state.snapshot.collapsedProjectRoots,
39
+ );
35
40
  const [openProjectRoot, setOpenProjectRoot] = useState<string | null>(null);
36
41
  const preferredSessionType = useMemo(
37
42
  () => resolveProjectGroupDefaultSessionType(defaultSessionType, sessionTypeOptions),
@@ -43,62 +48,119 @@ export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
43
48
  <div className="space-y-3">
44
49
  {groups.map((group) => {
45
50
  const actionLabel = `${t('chatSidebarNewTask')} · ${group.projectName}`;
51
+ const isCollapsed = collapsedProjectRoots.includes(group.projectRoot);
52
+ const pinLabel = t(
53
+ group.isPinned ? 'chatSidebarUnpinProject' : 'chatSidebarPinProject',
54
+ );
46
55
 
47
56
  return (
48
57
  <div key={group.projectRoot}>
49
- <div className="flex items-center justify-between gap-2 px-2 py-0.5">
50
- <div className="flex min-w-0 items-center gap-1.5">
51
- <div
52
- className="truncate text-[11px] font-medium uppercase tracking-wider text-muted-foreground"
58
+ <div className="group/project relative h-10 rounded-lg px-2 text-muted-foreground transition-colors hover:bg-gray-200/60 hover:text-gray-900">
59
+ <button
60
+ type="button"
61
+ aria-expanded={!isCollapsed}
62
+ aria-label={t(
63
+ isCollapsed
64
+ ? 'chatSidebarExpandProject'
65
+ : 'chatSidebarCollapseProject',
66
+ )}
67
+ className="flex h-full w-full min-w-0 items-center gap-1.5 pr-14 text-left focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
68
+ onClick={() =>
69
+ presenter.chatSessionListManager.toggleProjectCollapsed(
70
+ group.projectRoot,
71
+ )
72
+ }
73
+ >
74
+ <Folder className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
75
+ <span
76
+ className="truncate text-[11px] font-medium uppercase tracking-wider"
53
77
  title={group.projectRoot}
54
78
  >
55
79
  {group.projectName}
56
- </div>
80
+ </span>
81
+ {isCollapsed ? (
82
+ <ChevronRight
83
+ className="h-3.5 w-3.5 shrink-0"
84
+ aria-hidden="true"
85
+ />
86
+ ) : (
87
+ <ChevronDown
88
+ className="h-3.5 w-3.5 shrink-0"
89
+ aria-hidden="true"
90
+ />
91
+ )}
57
92
  <span className="shrink-0 text-[10px] text-muted-foreground/70">
58
93
  {group.items.length}
59
94
  </span>
60
- </div>
61
- {supportsSessionTypeChoice ? (
62
- <Popover
63
- open={openProjectRoot === group.projectRoot}
64
- onOpenChange={(nextOpen) => {
65
- setOpenProjectRoot(nextOpen ? group.projectRoot : null);
66
- }}
67
- >
68
- <PopoverTrigger asChild>
69
- <IconActionButton
70
- icon={<Plus className="h-3.5 w-3.5" />}
71
- label={actionLabel}
72
- tooltip={false}
73
- className="h-7 w-7 shrink-0 rounded-lg text-muted-foreground hover:bg-gray-200/60 hover:text-gray-900"
74
- />
75
- </PopoverTrigger>
76
- <PopoverContent
77
- align="end"
78
- className="w-56 rounded-2xl border border-border bg-popover p-1.5 text-popover-foreground shadow-[0_24px_60px_-28px_rgba(15,23,42,0.38)]"
95
+ </button>
96
+ <div className="pointer-events-none absolute right-2 top-1/2 flex -translate-y-1/2 items-center opacity-0 transition-opacity group-hover/project:pointer-events-auto group-hover/project:opacity-100 focus-within:pointer-events-auto focus-within:opacity-100">
97
+ {supportsSessionTypeChoice ? (
98
+ <Popover
99
+ open={openProjectRoot === group.projectRoot}
100
+ onOpenChange={(nextOpen) => {
101
+ setOpenProjectRoot(nextOpen ? group.projectRoot : null);
102
+ }}
79
103
  >
80
- <ChatSessionTypeMenu
81
- options={sessionTypeOptions}
82
- selectedSessionType={preferredSessionType}
83
- onSelect={(sessionType) => {
84
- onCreateSession(sessionType, group.projectRoot);
85
- setOpenProjectRoot(null);
86
- }}
87
- />
88
- </PopoverContent>
89
- </Popover>
90
- ) : (
104
+ <PopoverTrigger asChild>
105
+ <IconActionButton
106
+ icon={<Plus className="h-3.5 w-3.5" />}
107
+ label={actionLabel}
108
+ />
109
+ </PopoverTrigger>
110
+ <PopoverContent
111
+ align="end"
112
+ className="w-56 rounded-2xl border border-border bg-popover p-1.5 text-popover-foreground shadow-[0_24px_60px_-28px_rgba(15,23,42,0.38)]"
113
+ >
114
+ <ChatSessionTypeMenu
115
+ options={sessionTypeOptions}
116
+ selectedSessionType={preferredSessionType}
117
+ onSelect={(sessionType) => {
118
+ presenter.chatSessionListManager.createSession(
119
+ sessionType,
120
+ group.projectRoot,
121
+ );
122
+ setOpenProjectRoot(null);
123
+ }}
124
+ />
125
+ </PopoverContent>
126
+ </Popover>
127
+ ) : (
128
+ <IconActionButton
129
+ icon={<Plus className="h-3.5 w-3.5" />}
130
+ label={actionLabel}
131
+ onClick={() =>
132
+ presenter.chatSessionListManager.createSession(
133
+ preferredSessionType,
134
+ group.projectRoot,
135
+ )
136
+ }
137
+ />
138
+ )}
91
139
  <IconActionButton
92
- icon={<Plus className="h-3.5 w-3.5" />}
93
- label={actionLabel}
94
- className="h-7 w-7 shrink-0 rounded-lg text-muted-foreground hover:bg-gray-200/60 hover:text-gray-900"
95
- onClick={() => onCreateSession(preferredSessionType, group.projectRoot)}
140
+ icon={
141
+ <Pin
142
+ className={
143
+ group.isPinned
144
+ ? 'h-3.5 w-3.5 fill-current text-foreground'
145
+ : 'h-3.5 w-3.5'
146
+ }
147
+ />
148
+ }
149
+ label={pinLabel}
150
+ tooltipSide="right"
151
+ onClick={() =>
152
+ presenter.chatSessionListManager.toggleProjectPinned(
153
+ group.projectRoot,
154
+ )
155
+ }
96
156
  />
97
- )}
98
- </div>
99
- <div className="space-y-0.5 pl-2">
100
- {group.items.map(renderSessionItem)}
157
+ </div>
101
158
  </div>
159
+ {isCollapsed ? null : (
160
+ <div className="mt-0.5 space-y-0.5 pl-2">
161
+ {group.items.map(renderSessionItem)}
162
+ </div>
163
+ )}
102
164
  </div>
103
165
  );
104
166
  })}
@@ -22,6 +22,7 @@ export function ChatSidebarSessionEntry(props: {
22
22
  draftLabel: string;
23
23
  savingSessionKey: string | null;
24
24
  sessionTypeOptions: ChatSessionTypeOption[];
25
+ isPinned: boolean;
25
26
  sessionTitle: (session: SessionEntryView) => string;
26
27
  onSelectSession: (sessionKey: string) => void;
27
28
  onOpenChildSessions: (
@@ -32,6 +33,7 @@ export function ChatSidebarSessionEntry(props: {
32
33
  onDraftLabelChange: (value: string) => void;
33
34
  onSaveSessionLabel: (session: SessionEntryView) => void;
34
35
  onCancelEditingSessionLabel: () => void;
36
+ onTogglePinned: () => void;
35
37
  }) {
36
38
  const {
37
39
  item,
@@ -43,6 +45,7 @@ export function ChatSidebarSessionEntry(props: {
43
45
  draftLabel,
44
46
  savingSessionKey,
45
47
  sessionTypeOptions,
48
+ isPinned,
46
49
  sessionTitle,
47
50
  onSelectSession,
48
51
  onOpenChildSessions,
@@ -50,6 +53,7 @@ export function ChatSidebarSessionEntry(props: {
50
53
  onDraftLabelChange,
51
54
  onSaveSessionLabel,
52
55
  onCancelEditingSessionLabel,
56
+ onTogglePinned,
53
57
  } = props;
54
58
  const { session, runStatus } = item;
55
59
  const active = selectedSessionKey === session.key;
@@ -80,6 +84,7 @@ export function ChatSidebarSessionEntry(props: {
80
84
  })}
81
85
  runStatus={runStatus}
82
86
  context={resolveSessionContextView(session, sessionTypeOptions)}
87
+ isPinned={isPinned}
83
88
  title={sessionTitle(session)}
84
89
  previewText={previewText}
85
90
  trailingText={formatSessionListTime(
@@ -104,6 +109,7 @@ export function ChatSidebarSessionEntry(props: {
104
109
  onDraftLabelChange={onDraftLabelChange}
105
110
  onSave={() => onSaveSessionLabel(session)}
106
111
  onCancel={onCancelEditingSessionLabel}
112
+ onTogglePinned={onTogglePinned}
107
113
  />
108
114
  );
109
115
  }