@nextclaw/ui 0.12.8 → 0.12.10

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 (227) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/dist/assets/ChannelsList-M9FTK1Ak.js +8 -0
  3. package/dist/assets/DocBrowser-CH7-GxlL.js +1 -0
  4. package/dist/assets/{DocBrowser-BMxf9CIK.js → DocBrowser-DMfr0Oow.js} +1 -1
  5. package/dist/assets/{DocBrowserContext-Ce28gRXt.js → DocBrowserContext-BXydqby-.js} +1 -1
  6. package/dist/assets/{LogoBadge-o92MOA2L.js → LogoBadge-hO7tY7hE.js} +1 -1
  7. package/dist/assets/ModelConfig-CNIgLf0e.js +1 -0
  8. package/dist/assets/{ProviderScopedModelInput-CmTIzgI7.js → ProviderScopedModelInput-B3HWP4oz.js} +1 -1
  9. package/dist/assets/ProvidersList-CHjMnRhX.js +1 -0
  10. package/dist/assets/RuntimeConfig-psp8nMSG.js +1 -0
  11. package/dist/assets/SearchConfig-CSoKip1f.js +1 -0
  12. package/dist/assets/{SecretsConfig-Ba1RPJaG.js → SecretsConfig-MEt6MjuD.js} +2 -2
  13. package/dist/assets/SessionsConfig-DifCiXwR.js +2 -0
  14. package/dist/assets/{app-query-client-DniXoIN5.js → app-query-client-9jNewezV.js} +1 -1
  15. package/dist/assets/{book-open-DocgeQtR.js → book-open-DzdUViDm.js} +1 -1
  16. package/dist/assets/chat-page-CLp0UV0Y.js +58 -0
  17. package/dist/assets/chat-session-display-DsYHx0RZ.js +1 -0
  18. package/dist/assets/{chunk-JZWAC4HX-BvKvh1R8.js → chunk-JZWAC4HX-C5dEc8hV.js} +1 -1
  19. package/dist/assets/{client-CVqPF5ie.js → client-C-8fH7-c.js} +1 -1
  20. package/dist/assets/{config-Bop2oB18.js → config-CBScxsdV.js} +1 -1
  21. package/dist/assets/config-split-page-BUout_Ak.js +1 -0
  22. package/dist/assets/{createLucideIcon-DVv8taGY.js → createLucideIcon-dy5ie7Ox.js} +1 -1
  23. package/dist/assets/desktop-update-config-2BS6BMkW.js +1 -0
  24. package/dist/assets/{dist-DmAlInRu.js → dist-BruyLa92.js} +1 -1
  25. package/dist/assets/{dist-Da5Gm_pO.js → dist-Cy7_j6hA.js} +1 -1
  26. package/dist/assets/download-BD0ETkB-.js +1 -0
  27. package/dist/assets/{external-link-DFjw3x1B.js → external-link-kZSAO8nT.js} +1 -1
  28. package/dist/assets/{hash-DJtaCejM.js → hash-BHJC2Ovu.js} +1 -1
  29. package/dist/assets/i18n-CpTZLchQ.js +1 -0
  30. package/dist/assets/index-mW8W2FUu.css +1 -0
  31. package/dist/assets/index-zDZfXoI4.js +6 -0
  32. package/dist/assets/{infiniteQueryBehavior-DHSEQ3OH.js → infiniteQueryBehavior-CyER9hv0.js} +1 -1
  33. package/dist/assets/loader-circle-Bc2gCU33.js +1 -0
  34. package/dist/assets/{logos-DEFUIR12.js → logos-B7gRObP8.js} +1 -1
  35. package/dist/assets/marketplace-page-3qVMnF3d.js +1 -0
  36. package/dist/assets/marketplace-page-BhFIeQzI.js +49 -0
  37. package/dist/assets/mcp-marketplace-page-DYfteJ1D.js +40 -0
  38. package/dist/assets/{page-layout-Da3i3r6G.js → page-layout-0UcO9H9Z.js} +1 -1
  39. package/dist/assets/play-CKDjSQFL.js +1 -0
  40. package/dist/assets/plus-CG0QrVY_.js +1 -0
  41. package/dist/assets/{refresh-ccw-D6HkNtfz.js → refresh-ccw-COVhNHtN.js} +1 -1
  42. package/dist/assets/{refresh-cw-DRcvRrnc.js → refresh-cw-Bcv40SXy.js} +1 -1
  43. package/dist/assets/remote-access-page-CWHG-sug.js +1 -0
  44. package/dist/assets/{rotate-cw-BmDKfXtH.js → rotate-cw-oHMKJMC8.js} +1 -1
  45. package/dist/assets/{save-DHGmi2e9.js → save-EqJPOF0G.js} +1 -1
  46. package/dist/assets/search-BCAlB8nz.js +1 -0
  47. package/dist/assets/security-config-Slh0Mayz.js +1 -0
  48. package/dist/assets/select-CVz0t7MF.js +41 -0
  49. package/dist/assets/setting-row-CbVHAuQt.js +1 -0
  50. package/dist/assets/skeleton-D5rdKvzy.js +1 -0
  51. package/dist/assets/{status-dot-DurKKSwA.js → status-dot-DpPtVzQT.js} +1 -1
  52. package/dist/assets/{switch-0rmPBRKI.js → switch-CM29eCAR.js} +1 -1
  53. package/dist/assets/{tabs-custom-5JLVL6v8.js → tabs-custom-YcZUWn3o.js} +1 -1
  54. package/dist/assets/tag-chip-DMXdnLcj.js +1 -0
  55. package/dist/assets/{trash-2-C6caKPoz.js → trash-2-mJT6oWa2.js} +1 -1
  56. package/dist/assets/{use-infinite-scroll-loader-dwnaa_qi.js → use-infinite-scroll-loader-DJ1L81Dz.js} +1 -1
  57. package/dist/assets/{useConfirmDialog-mMeWD_yo.js → useConfirmDialog-BsVuqu1x.js} +1 -1
  58. package/dist/assets/{useMutation-BmxxvCNf.js → useMutation-CNcz2fgt.js} +1 -1
  59. package/dist/assets/x-Czwxm82I.js +1 -0
  60. package/dist/index.html +95 -21
  61. package/dist/manifest.webmanifest +30 -0
  62. package/dist/offline.html +102 -0
  63. package/dist/pwa-192.png +0 -0
  64. package/dist/pwa-512.png +0 -0
  65. package/dist/runtime-icons/claude.ico +0 -0
  66. package/dist/runtime-icons/codex-openai.svg +6 -0
  67. package/dist/runtime-icons/hermes-agent.png +0 -0
  68. package/dist/sw.js +80 -0
  69. package/index.html +73 -1
  70. package/package.json +5 -5
  71. package/public/manifest.webmanifest +30 -0
  72. package/public/offline.html +102 -0
  73. package/public/pwa-192.png +0 -0
  74. package/public/pwa-512.png +0 -0
  75. package/public/runtime-icons/claude.ico +0 -0
  76. package/public/runtime-icons/codex-openai.svg +6 -0
  77. package/public/runtime-icons/hermes-agent.png +0 -0
  78. package/public/sw.js +80 -0
  79. package/src/account/components/account-panel.tsx +217 -97
  80. package/src/account/managers/account.manager.ts +3 -2
  81. package/src/api/chat-session-type.types.ts +7 -0
  82. package/src/api/runtime-control.types.ts +8 -0
  83. package/src/api/server-path.ts +27 -4
  84. package/src/api/types.ts +25 -10
  85. package/src/app.tsx +227 -54
  86. package/src/components/agents/agent-dialogs.tsx +499 -0
  87. package/src/components/agents/agents-page.test.tsx +238 -0
  88. package/src/components/agents/agents-page.tsx +435 -0
  89. package/src/components/chat/ChatSidebar.test.tsx +43 -1
  90. package/src/components/chat/ChatSidebar.tsx +35 -35
  91. package/src/components/chat/adapters/chat-message.summary-truncation.test.ts +66 -0
  92. package/src/components/chat/adapters/file-operation/card.ts +9 -0
  93. package/src/components/chat/adapters/file-operation/diff.ts +14 -0
  94. package/src/components/chat/{ChatConversationPanel.test.tsx → chat-conversation-panel.test.tsx} +127 -206
  95. package/src/components/chat/chat-conversation-panel.tsx +482 -0
  96. package/src/components/chat/chat-page-shell.tsx +19 -13
  97. package/src/components/chat/chat-session-type-option-item.test.tsx +46 -0
  98. package/src/components/chat/chat-session-type-option-item.tsx +68 -0
  99. package/src/components/chat/chat-session-workspace-file-preview.test.tsx +178 -0
  100. package/src/components/chat/chat-session-workspace-file-preview.tsx +278 -0
  101. package/src/components/chat/chat-session-workspace-panel-nav.tsx +203 -0
  102. package/src/components/chat/chat-session-workspace-panel.tsx +318 -0
  103. package/src/components/chat/chat-sidebar-project-groups.tsx +11 -36
  104. package/src/components/chat/chat-sidebar-session-item.tsx +32 -2
  105. package/src/components/chat/containers/chat-message-list.container.test.tsx +49 -0
  106. package/src/components/chat/containers/chat-message-list.container.tsx +4 -0
  107. package/src/components/chat/managers/chat-session-list.manager.test.ts +12 -0
  108. package/src/components/chat/managers/chat-session-list.manager.ts +7 -0
  109. package/src/components/chat/ncp/__tests__/ncp-session-adapter.cancelled-tool.test.ts +77 -0
  110. package/src/components/chat/ncp/ncp-chat-page.tsx +9 -7
  111. package/src/components/chat/ncp/ncp-chat-thread.manager.ts +179 -41
  112. package/src/components/chat/ncp/ncp-session-adapter.test.ts +36 -1
  113. package/src/components/chat/ncp/ncp-session-adapter.ts +20 -0
  114. package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +62 -13
  115. package/src/components/chat/ncp/tests/ncp-chat-thread.manager.test.ts +189 -0
  116. package/src/components/chat/presenter/chat-presenter-context.tsx +13 -2
  117. package/src/components/chat/session-header/chat-session-header-actions.test.tsx +26 -0
  118. package/src/components/chat/session-header/chat-session-header-actions.tsx +19 -1
  119. package/src/components/chat/stores/chat-input.store.ts +2 -1
  120. package/src/components/chat/stores/chat-thread.store.ts +27 -1
  121. package/src/components/chat/useChatSessionTypeState.ts +10 -1
  122. package/src/components/chat/workspace/chat-session-workspace-file-breadcrumbs.tsx +86 -0
  123. package/src/components/common/BrandHeader.tsx +3 -1
  124. package/src/components/common/session-context-icon.tsx +15 -2
  125. package/src/components/common/{TagInput.tsx → tag-input.tsx} +25 -17
  126. package/src/components/config/ChannelForm.test.tsx +89 -3
  127. package/src/components/config/ChannelForm.tsx +157 -188
  128. package/src/components/config/ChannelsList.test.tsx +163 -119
  129. package/src/components/config/ChannelsList.tsx +90 -101
  130. package/src/components/config/ProviderForm.tsx +108 -146
  131. package/src/components/config/ProvidersList.tsx +100 -123
  132. package/src/components/config/RuntimeConfig.tsx +141 -2
  133. package/src/components/config/SearchConfig.tsx +423 -393
  134. package/src/components/config/channel-form-fields-section.tsx +70 -37
  135. package/src/components/config/config-split-page.tsx +109 -0
  136. package/src/components/config/provider-enabled-field.tsx +17 -10
  137. package/src/components/config/runtime-control-card.test.tsx +56 -0
  138. package/src/components/config/runtime-control-card.tsx +25 -0
  139. package/src/components/config/runtime-presence-card.tsx +93 -79
  140. package/src/components/layout/AppLayout.tsx +25 -37
  141. package/src/components/layout/app-layout.test.tsx +46 -14
  142. package/src/components/layout/runtime-status-entry.test.tsx +157 -0
  143. package/src/components/layout/runtime-status-entry.tsx +143 -0
  144. package/src/components/marketplace/marketplace-detail-doc.ts +93 -0
  145. package/src/components/marketplace/marketplace-list-card.tsx +288 -0
  146. package/src/components/marketplace/marketplace-page-data.ts +129 -0
  147. package/src/components/marketplace/marketplace-page.test.tsx +339 -0
  148. package/src/components/marketplace/marketplace-page.tsx +596 -0
  149. package/src/components/marketplace/mcp/mcp-marketplace-card.tsx +128 -0
  150. package/src/components/marketplace/mcp/mcp-marketplace-dialogs.tsx +191 -0
  151. package/src/components/marketplace/mcp/mcp-marketplace-doc.ts +152 -0
  152. package/src/components/marketplace/mcp/mcp-marketplace-page.test.tsx +223 -0
  153. package/src/components/marketplace/mcp/mcp-marketplace-page.tsx +414 -0
  154. package/src/components/providers/ThemeProvider.tsx +5 -0
  155. package/src/components/remote/remote-access-page.test.tsx +105 -0
  156. package/src/components/remote/remote-access-page.tsx +248 -0
  157. package/src/components/ui/notice-card.tsx +129 -0
  158. package/src/components/ui/setting-row.tsx +51 -0
  159. package/src/components/ui/tag-chip.tsx +39 -0
  160. package/src/components/ui/textarea.tsx +19 -0
  161. package/src/hooks/server-path/use-server-path-read.ts +20 -0
  162. package/src/hooks/useConfig.ts +2 -1
  163. package/src/index.css +24 -0
  164. package/src/lib/app-resource-uri.test.ts +20 -0
  165. package/src/lib/app-resource-uri.ts +29 -0
  166. package/src/lib/chat-message.ts +14 -3
  167. package/src/lib/i18n.chat.ts +12 -1
  168. package/src/lib/i18n.pwa.ts +62 -0
  169. package/src/lib/i18n.remote.ts +1 -1
  170. package/src/lib/i18n.runtime-control.ts +31 -0
  171. package/src/lib/i18n.ts +7 -10
  172. package/src/lib/session-context.utils.test.ts +71 -0
  173. package/src/lib/session-context.utils.ts +28 -3
  174. package/src/lib/session-project/workspace-file-breadcrumb.test.ts +83 -0
  175. package/src/lib/session-project/workspace-file-breadcrumb.ts +188 -0
  176. package/src/pwa/components/pwa-install-entry.test.tsx +110 -0
  177. package/src/pwa/components/pwa-install-entry.tsx +205 -0
  178. package/src/pwa/managers/pwa-install.manager.test.ts +160 -0
  179. package/src/pwa/managers/pwa-install.manager.ts +232 -0
  180. package/src/pwa/managers/pwa-runtime.manager.ts +196 -0
  181. package/src/pwa/managers/pwa-shell-theme.manager.test.ts +30 -0
  182. package/src/pwa/managers/pwa-shell-theme.manager.ts +46 -0
  183. package/src/pwa/pwa-install-banner.storage.ts +55 -0
  184. package/src/pwa/pwa.types.ts +22 -0
  185. package/src/pwa/register-pwa.ts +14 -0
  186. package/src/pwa/stores/pwa.store.ts +17 -0
  187. package/src/vite-env.d.ts +9 -0
  188. package/dist/assets/ChannelsList-KIQIxluX.js +0 -8
  189. package/dist/assets/DocBrowser-CyDgAtO9.js +0 -1
  190. package/dist/assets/MarketplacePage-BySqkYDh.js +0 -49
  191. package/dist/assets/MarketplacePage-C0olZaek.js +0 -1
  192. package/dist/assets/McpMarketplacePage-DqKaiXO9.js +0 -40
  193. package/dist/assets/ModelConfig-IrmzoslW.js +0 -1
  194. package/dist/assets/ProvidersList-8_Kalfwl.js +0 -1
  195. package/dist/assets/RemoteAccessPage-CyQlSjPf.js +0 -1
  196. package/dist/assets/RuntimeConfig-Bk0uYBhf.js +0 -1
  197. package/dist/assets/SearchConfig-DNBR-UbE.js +0 -1
  198. package/dist/assets/SessionsConfig-Doqp5ghH.js +0 -2
  199. package/dist/assets/chat-page-Bph8M5zo.js +0 -58
  200. package/dist/assets/chat-session-display-CoN3Wmn-.js +0 -1
  201. package/dist/assets/config-layout-DmlGaay2.js +0 -1
  202. package/dist/assets/desktop-update-config-1KBrqLBC.js +0 -1
  203. package/dist/assets/i18n-CwHZ-9vt.js +0 -1
  204. package/dist/assets/index-DafCdM4F.css +0 -1
  205. package/dist/assets/index-DdksE6U3.js +0 -6
  206. package/dist/assets/loader-circle-PsSP0H9n.js +0 -1
  207. package/dist/assets/play-DBQbBxTA.js +0 -1
  208. package/dist/assets/plus-DUOVbsyQ.js +0 -1
  209. package/dist/assets/popover-C_mWOFzI.js +0 -1
  210. package/dist/assets/search-MChQRYR1.js +0 -1
  211. package/dist/assets/security-config-CbXfPZzr.js +0 -1
  212. package/dist/assets/select-Caud8QvU.js +0 -41
  213. package/dist/assets/skeleton-B-4vRq_Z.js +0 -1
  214. package/dist/assets/x-DuMhMATD.js +0 -1
  215. package/src/components/agents/AgentDialogs.tsx +0 -400
  216. package/src/components/agents/AgentsPage.test.tsx +0 -217
  217. package/src/components/agents/AgentsPage.tsx +0 -352
  218. package/src/components/chat/ChatConversationPanel.tsx +0 -256
  219. package/src/components/chat/chat-child-session-panel.tsx +0 -270
  220. package/src/components/config/config-layout.ts +0 -10
  221. package/src/components/marketplace/MarketplacePage.test.tsx +0 -322
  222. package/src/components/marketplace/MarketplacePage.tsx +0 -827
  223. package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +0 -208
  224. package/src/components/marketplace/mcp/McpMarketplacePage.tsx +0 -580
  225. package/src/components/remote/RemoteAccessPage.test.tsx +0 -103
  226. package/src/components/remote/RemoteAccessPage.tsx +0 -144
  227. /package/dist/assets/{config-hints-BZoDjXye.js → config-hints-BhTmc9P1.js} +0 -0
@@ -0,0 +1,189 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { appQueryClient } from '@/app-query-client';
3
+ import { NcpChatThreadManager } from '@/components/chat/ncp/ncp-chat-thread.manager';
4
+ import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
5
+ import { useChatThreadStore } from '@/components/chat/stores/chat-thread.store';
6
+
7
+ const { deleteNcpSessionMock, deleteSummaryMock } = vi.hoisted(() => ({
8
+ deleteNcpSessionMock: vi.fn(async () => ({ deleted: true, sessionId: 'parent-session-1' })),
9
+ deleteSummaryMock: vi.fn(),
10
+ }));
11
+
12
+ vi.mock('@/api/ncp-session', () => ({
13
+ deleteNcpSession: deleteNcpSessionMock,
14
+ }));
15
+
16
+ vi.mock('@/api/ncp-session-query-cache', () => ({
17
+ deleteNcpSessionSummaryInQueryClient: deleteSummaryMock,
18
+ }));
19
+
20
+ describe('NcpChatThreadManager', () => {
21
+ beforeEach(() => {
22
+ useChatSessionListStore.setState({
23
+ optimisticReadAtBySessionKey: {},
24
+ snapshot: {
25
+ ...useChatSessionListStore.getState().snapshot,
26
+ selectedSessionKey: 'parent-session-1',
27
+ },
28
+ });
29
+ useChatThreadStore.setState({
30
+ snapshot: {
31
+ ...useChatThreadStore.getState().snapshot,
32
+ sessionKey: 'parent-session-1',
33
+ workspacePanelParentKey: null,
34
+ childSessionTabs: [
35
+ {
36
+ sessionKey: 'child-session-1',
37
+ parentSessionKey: 'parent-session-1',
38
+ label: 'Child Session 1',
39
+ agentId: 'reviewer',
40
+ },
41
+ ],
42
+ activeChildSessionKey: null,
43
+ },
44
+ });
45
+ });
46
+
47
+ it('opens the child-session panel for the requested parent session and keeps focus on the chosen child', () => {
48
+ const uiManager = {
49
+ goToSession: vi.fn(),
50
+ goToChatRoot: vi.fn(),
51
+ goToProviders: vi.fn(),
52
+ confirm: vi.fn(),
53
+ } as unknown as ConstructorParameters<typeof NcpChatThreadManager>[0];
54
+
55
+ const manager = new NcpChatThreadManager(
56
+ uiManager,
57
+ {} as ConstructorParameters<typeof NcpChatThreadManager>[1],
58
+ {} as ConstructorParameters<typeof NcpChatThreadManager>[2],
59
+ );
60
+
61
+ manager.openChildSessionPanel({
62
+ parentSessionKey: 'parent-session-1',
63
+ activeChildSessionKey: 'child-session-1',
64
+ });
65
+
66
+ expect(useChatThreadStore.getState().snapshot.workspacePanelParentKey).toBe('parent-session-1');
67
+ expect(useChatThreadStore.getState().snapshot.activeChildSessionKey).toBe('child-session-1');
68
+ expect(uiManager.goToSession).not.toHaveBeenCalled();
69
+ });
70
+
71
+ it('routes to the parent session before opening the child-session panel when needed', () => {
72
+ useChatSessionListStore.setState({
73
+ snapshot: {
74
+ ...useChatSessionListStore.getState().snapshot,
75
+ selectedSessionKey: 'another-session',
76
+ },
77
+ });
78
+ const uiManager = {
79
+ goToSession: vi.fn(),
80
+ goToChatRoot: vi.fn(),
81
+ goToProviders: vi.fn(),
82
+ confirm: vi.fn(),
83
+ } as unknown as ConstructorParameters<typeof NcpChatThreadManager>[0];
84
+
85
+ const manager = new NcpChatThreadManager(
86
+ uiManager,
87
+ {} as ConstructorParameters<typeof NcpChatThreadManager>[1],
88
+ {} as ConstructorParameters<typeof NcpChatThreadManager>[2],
89
+ );
90
+
91
+ manager.openChildSessionPanel({
92
+ parentSessionKey: 'parent-session-1',
93
+ activeChildSessionKey: 'child-session-1',
94
+ });
95
+
96
+ expect(uiManager.goToSession).toHaveBeenCalledWith('parent-session-1');
97
+ });
98
+
99
+ it('keeps preview and diff for the same file as separate workspace tabs', () => {
100
+ const uiManager = {
101
+ goToSession: vi.fn(),
102
+ goToChatRoot: vi.fn(),
103
+ goToProviders: vi.fn(),
104
+ confirm: vi.fn(),
105
+ } as unknown as ConstructorParameters<typeof NcpChatThreadManager>[0];
106
+
107
+ const manager = new NcpChatThreadManager(
108
+ uiManager,
109
+ {} as ConstructorParameters<typeof NcpChatThreadManager>[1],
110
+ {} as ConstructorParameters<typeof NcpChatThreadManager>[2],
111
+ );
112
+
113
+ manager.openFilePreview({
114
+ path: 'README.md',
115
+ label: 'README.md',
116
+ viewMode: 'preview',
117
+ });
118
+ manager.openFilePreview({
119
+ path: 'README.md',
120
+ label: 'README.md',
121
+ viewMode: 'diff',
122
+ beforeText: 'old\n',
123
+ afterText: 'new\n',
124
+ });
125
+
126
+ expect(useChatThreadStore.getState().snapshot.workspaceFileTabs).toEqual(
127
+ expect.arrayContaining([
128
+ expect.objectContaining({
129
+ key: 'parent-session-1::preview::README.md',
130
+ path: 'README.md',
131
+ viewMode: 'preview',
132
+ }),
133
+ expect.objectContaining({
134
+ key: 'parent-session-1::diff::README.md',
135
+ path: 'README.md',
136
+ viewMode: 'diff',
137
+ }),
138
+ ]),
139
+ );
140
+ });
141
+
142
+ it('clears the selected thread state after deleting the current session', async () => {
143
+ const removeQueries = vi.spyOn(appQueryClient, 'removeQueries').mockImplementation(async () => undefined);
144
+ const uiManager = {
145
+ goToSession: vi.fn(),
146
+ goToChatRoot: vi.fn(),
147
+ goToProviders: vi.fn(),
148
+ confirm: vi.fn(async () => true),
149
+ } as unknown as ConstructorParameters<typeof NcpChatThreadManager>[0];
150
+ const sessionListManager = {
151
+ setSelectedSessionKey: vi.fn((value: string | null) => {
152
+ useChatSessionListStore.getState().setSnapshot({
153
+ selectedSessionKey: value,
154
+ });
155
+ }),
156
+ } as unknown as ConstructorParameters<typeof NcpChatThreadManager>[1];
157
+ const streamActionsManager = {
158
+ resetStreamState: vi.fn(),
159
+ } as unknown as ConstructorParameters<typeof NcpChatThreadManager>[2];
160
+ const manager = new NcpChatThreadManager(
161
+ uiManager,
162
+ sessionListManager,
163
+ streamActionsManager,
164
+ );
165
+
166
+ await (manager as unknown as { deleteCurrentSession: () => Promise<void> }).deleteCurrentSession();
167
+
168
+ expect(sessionListManager.setSelectedSessionKey).toHaveBeenCalledWith(null);
169
+ expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
170
+ expect(useChatThreadStore.getState().snapshot).toMatchObject({
171
+ sessionKey: null,
172
+ canDeleteSession: false,
173
+ messages: [],
174
+ workspacePanelParentKey: null,
175
+ childSessionTabs: [],
176
+ activeChildSessionKey: null,
177
+ workspaceFileTabs: [],
178
+ activeWorkspaceFileKey: null,
179
+ });
180
+ expect(streamActionsManager.resetStreamState).toHaveBeenCalledTimes(1);
181
+ expect(deleteSummaryMock).toHaveBeenCalledWith(appQueryClient, 'parent-session-1');
182
+ expect(removeQueries).toHaveBeenCalledWith({
183
+ queryKey: ['ncp-session-messages', 'parent-session-1'],
184
+ });
185
+ expect(uiManager.goToChatRoot).toHaveBeenCalledWith({ replace: true });
186
+
187
+ removeQueries.mockRestore();
188
+ });
189
+ });
@@ -1,4 +1,8 @@
1
- import type { ChatComposerNode, ChatToolActionViewModel } from '@nextclaw/agent-chat-ui';
1
+ import type {
2
+ ChatComposerNode,
3
+ ChatFileOpenActionViewModel,
4
+ ChatToolActionViewModel,
5
+ } from '@nextclaw/agent-chat-ui';
2
6
  import type { NcpDraftAttachment } from '@nextclaw/ncp-react';
3
7
  import { createContext, useContext } from 'react';
4
8
  import type { ReactNode } from 'react';
@@ -37,9 +41,16 @@ export type ChatThreadManagerLike = {
37
41
  deleteSession: () => void;
38
42
  createSession: () => void;
39
43
  goToProviders: () => void;
44
+ openChildSessionPanel: (params: {
45
+ parentSessionKey: string;
46
+ activeChildSessionKey?: string | null;
47
+ }) => void;
48
+ openFilePreview: (action: ChatFileOpenActionViewModel) => void;
40
49
  openSessionFromToolAction: (action: ChatToolActionViewModel) => void;
41
50
  selectChildSessionDetail: (sessionKey: string) => void;
42
- closeChildSessionDetail: () => void;
51
+ selectWorkspaceFile: (fileKey: string) => void;
52
+ closeWorkspaceFile: (fileKey: string) => void;
53
+ closeWorkspacePanel: () => void;
43
54
  goToParentSession: () => void;
44
55
  };
45
56
 
@@ -6,6 +6,7 @@ import { ChatSessionHeaderActions } from '@/components/chat/session-header/chat-
6
6
  const mocks = vi.hoisted(() => ({
7
7
  updateSessionProject: vi.fn(),
8
8
  onDeleteSession: vi.fn(),
9
+ onOpenChildSessions: vi.fn(),
9
10
  }));
10
11
 
11
12
  vi.mock('@/components/chat/hooks/use-chat-session-project', () => ({
@@ -20,6 +21,7 @@ describe('ChatSessionHeaderActions', () => {
20
21
  beforeEach(() => {
21
22
  mocks.updateSessionProject.mockReset();
22
23
  mocks.onDeleteSession.mockReset();
24
+ mocks.onOpenChildSessions.mockReset();
23
25
  });
24
26
 
25
27
  it('keeps only the set-project action in the more-actions menu when a project is already attached', async () => {
@@ -31,6 +33,8 @@ describe('ChatSessionHeaderActions', () => {
31
33
  canDeleteSession
32
34
  isDeletePending={false}
33
35
  projectRoot="/tmp/project-alpha"
36
+ childSessionCount={0}
37
+ onOpenChildSessions={mocks.onOpenChildSessions}
34
38
  onDeleteSession={mocks.onDeleteSession}
35
39
  />
36
40
  );
@@ -51,6 +55,8 @@ describe('ChatSessionHeaderActions', () => {
51
55
  canDeleteSession={false}
52
56
  isDeletePending={false}
53
57
  projectRoot={null}
58
+ childSessionCount={0}
59
+ onOpenChildSessions={mocks.onOpenChildSessions}
54
60
  onDeleteSession={mocks.onDeleteSession}
55
61
  />
56
62
  );
@@ -60,4 +66,24 @@ describe('ChatSessionHeaderActions', () => {
60
66
  expect(screen.getByText('Set Project Directory')).toBeTruthy();
61
67
  expect(screen.queryByText('Clear Project Directory')).toBeNull();
62
68
  });
69
+
70
+ it('shows a dedicated child-session entry button when the current session has child sessions', async () => {
71
+ const user = userEvent.setup();
72
+
73
+ render(
74
+ <ChatSessionHeaderActions
75
+ sessionKey="session-children"
76
+ canDeleteSession
77
+ isDeletePending={false}
78
+ projectRoot={null}
79
+ childSessionCount={2}
80
+ onOpenChildSessions={mocks.onOpenChildSessions}
81
+ onDeleteSession={mocks.onDeleteSession}
82
+ />
83
+ );
84
+
85
+ await user.click(screen.getByRole('button', { name: 'View child sessions' }));
86
+
87
+ expect(mocks.onOpenChildSessions).toHaveBeenCalledTimes(1);
88
+ });
63
89
  });
@@ -1,5 +1,5 @@
1
1
  import { useState } from 'react';
2
- import { FolderOpen, MoreHorizontal, Trash2 } from 'lucide-react';
2
+ import { FolderOpen, GitBranch, MoreHorizontal, Trash2 } from 'lucide-react';
3
3
  import { Button } from '@/components/ui/button';
4
4
  import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
5
5
  import { useChatSessionProject } from '@/components/chat/hooks/use-chat-session-project';
@@ -12,6 +12,8 @@ type ChatSessionHeaderActionsProps = {
12
12
  canDeleteSession: boolean;
13
13
  isDeletePending: boolean;
14
14
  projectRoot?: string | null;
15
+ childSessionCount?: number;
16
+ onOpenChildSessions?: () => void;
15
17
  onDeleteSession: () => void;
16
18
  };
17
19
 
@@ -20,6 +22,8 @@ export function ChatSessionHeaderActions({
20
22
  canDeleteSession,
21
23
  isDeletePending,
22
24
  projectRoot,
25
+ childSessionCount = 0,
26
+ onOpenChildSessions,
23
27
  onDeleteSession,
24
28
  }: ChatSessionHeaderActionsProps) {
25
29
  const updateSessionProject = useChatSessionProject();
@@ -46,6 +50,20 @@ export function ChatSessionHeaderActions({
46
50
 
47
51
  return (
48
52
  <>
53
+ {childSessionCount > 0 && onOpenChildSessions ? (
54
+ <Button
55
+ type="button"
56
+ variant="ghost"
57
+ size="icon"
58
+ className="rounded-lg shrink-0 text-gray-400 hover:text-gray-700"
59
+ aria-label={t('chatSessionOpenChildSessions')}
60
+ title={t('chatSessionOpenChildSessions')}
61
+ onClick={onOpenChildSessions}
62
+ disabled={isBusy}
63
+ >
64
+ <GitBranch className="h-4 w-4" />
65
+ </Button>
66
+ ) : null}
49
67
  <Popover open={isMenuOpen} onOpenChange={setIsMenuOpen}>
50
68
  <PopoverTrigger asChild>
51
69
  <Button
@@ -1,7 +1,7 @@
1
1
  import { create } from 'zustand';
2
2
  import type { ChatComposerNode } from '@nextclaw/agent-chat-ui';
3
3
  import type { NcpDraftAttachment } from '@nextclaw/ncp-react';
4
- import type { SessionSkillEntryView, ThinkingLevel } from '@/api/types';
4
+ import type { SessionSkillEntryView, SessionTypeIconView, ThinkingLevel } from '@/api/types';
5
5
  import type { ChatModelOption } from '@/components/chat/chat-input.types';
6
6
  import { createInitialChatComposerNodes } from '@/components/chat/chat-composer-state';
7
7
 
@@ -24,6 +24,7 @@ export type ChatInputSnapshot = {
24
24
  sessionTypeOptions: Array<{
25
25
  value: string;
26
26
  label: string;
27
+ icon?: SessionTypeIconView | null;
27
28
  ready?: boolean;
28
29
  reason?: string | null;
29
30
  reasonMessage?: string | null;
@@ -1,8 +1,9 @@
1
1
  import { create } from 'zustand';
2
2
  import type { MutableRefObject } from 'react';
3
3
  import type { NcpMessage } from '@nextclaw/ncp';
4
+ import type { ChatFileOperationLineViewModel } from '@nextclaw/agent-chat-ui';
4
5
  import type { ChatModelOption } from '@/components/chat/chat-input.types';
5
- import type { AgentProfileView } from '@/api/types';
6
+ import type { AgentProfileView, SessionTypeIconView } from '@/api/types';
6
7
 
7
8
  export type ChatChildSessionTab = {
8
9
  sessionKey: string;
@@ -11,12 +12,30 @@ export type ChatChildSessionTab = {
11
12
  agentId?: string | null;
12
13
  };
13
14
 
15
+ export type ChatWorkspaceFileTab = {
16
+ key: string;
17
+ parentSessionKey: string | null;
18
+ path: string;
19
+ label?: string | null;
20
+ viewMode: 'preview' | 'diff';
21
+ line?: number | null;
22
+ column?: number | null;
23
+ rawText?: string | null;
24
+ beforeText?: string | null;
25
+ afterText?: string | null;
26
+ patchText?: string | null;
27
+ oldStartLine?: number | null;
28
+ newStartLine?: number | null;
29
+ fullLines?: ChatFileOperationLineViewModel[];
30
+ };
31
+
14
32
  export type ChatThreadSnapshot = {
15
33
  isProviderStateResolved: boolean;
16
34
  modelOptions: ChatModelOption[];
17
35
  sessionTypeUnavailable: boolean;
18
36
  sessionTypeUnavailableMessage?: string | null;
19
37
  sessionTypeLabel?: string | null;
38
+ sessionTypeIcon?: SessionTypeIconView | null;
20
39
  sessionKey: string | null;
21
40
  agentId?: string | null;
22
41
  agentDisplayName?: string | null;
@@ -34,8 +53,11 @@ export type ChatThreadSnapshot = {
34
53
  isAwaitingAssistantOutput: boolean;
35
54
  parentSessionKey?: string | null;
36
55
  parentSessionLabel?: string | null;
56
+ workspacePanelParentKey?: string | null;
37
57
  childSessionTabs: ChatChildSessionTab[];
38
58
  activeChildSessionKey?: string | null;
59
+ workspaceFileTabs: ChatWorkspaceFileTab[];
60
+ activeWorkspaceFileKey?: string | null;
39
61
  };
40
62
 
41
63
  type ChatThreadStore = {
@@ -49,6 +71,7 @@ const initialSnapshot: ChatThreadSnapshot = {
49
71
  sessionTypeUnavailable: false,
50
72
  sessionTypeUnavailableMessage: null,
51
73
  sessionTypeLabel: null,
74
+ sessionTypeIcon: null,
52
75
  sessionKey: null,
53
76
  agentId: null,
54
77
  agentDisplayName: null,
@@ -66,8 +89,11 @@ const initialSnapshot: ChatThreadSnapshot = {
66
89
  isAwaitingAssistantOutput: false,
67
90
  parentSessionKey: null,
68
91
  parentSessionLabel: null,
92
+ workspacePanelParentKey: null,
69
93
  childSessionTabs: [],
70
94
  activeChildSessionKey: null,
95
+ workspaceFileTabs: [],
96
+ activeWorkspaceFileKey: null,
71
97
  };
72
98
 
73
99
  export const useChatThreadStore = create<ChatThreadStore>((set) => ({
@@ -1,6 +1,11 @@
1
1
  import { useEffect, useMemo, useRef } from 'react';
2
2
  import type { Dispatch, SetStateAction } from 'react';
3
- import type { AgentProfileView, ChatSessionTypeOptionView, SessionEntryView } from '@/api/types';
3
+ import type {
4
+ AgentProfileView,
5
+ ChatSessionTypeOptionView,
6
+ SessionEntryView,
7
+ SessionTypeIconView
8
+ } from '@/api/types';
4
9
  import { t } from '@/lib/i18n';
5
10
 
6
11
  export const DEFAULT_SESSION_TYPE = 'native';
@@ -8,6 +13,7 @@ export const DEFAULT_SESSION_TYPE = 'native';
8
13
  export type ChatSessionTypeOption = {
9
14
  value: string;
10
15
  label: string;
16
+ icon: SessionTypeIconView | null;
11
17
  ready: boolean;
12
18
  reason?: string | null;
13
19
  reasonMessage?: string | null;
@@ -71,6 +77,7 @@ export function buildSessionTypeOptions(
71
77
  deduped.set(value, {
72
78
  value,
73
79
  label: option.label?.trim() || resolveSessionTypeLabel(value),
80
+ icon: option.icon ?? null,
74
81
  ready: option.ready ?? true,
75
82
  reason: option.reason ?? null,
76
83
  reasonMessage: option.reasonMessage ?? null,
@@ -83,6 +90,7 @@ export function buildSessionTypeOptions(
83
90
  deduped.set(DEFAULT_SESSION_TYPE, {
84
91
  value: DEFAULT_SESSION_TYPE,
85
92
  label: resolveSessionTypeLabel(DEFAULT_SESSION_TYPE),
93
+ icon: null,
86
94
  ready: true,
87
95
  reason: null,
88
96
  reasonMessage: null,
@@ -129,6 +137,7 @@ export function useChatSessionTypeState(params: UseChatSessionTypeStateParams):
129
137
  options.push({
130
138
  value: currentSessionType,
131
139
  label: resolveSessionTypeLabel(currentSessionType),
140
+ icon: null,
132
141
  ready: true,
133
142
  reason: null,
134
143
  reasonMessage: null,
@@ -0,0 +1,86 @@
1
+ import { Fragment } from "react";
2
+ import { ChevronRight, FileCode2, FolderTree } from "lucide-react";
3
+ import type { WorkspaceFileBreadcrumbViewModel } from "@/lib/session-project/workspace-file-breadcrumb";
4
+ import { t } from "@/lib/i18n";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ function WorkspaceBreadcrumbMetaChip({
8
+ tone = "neutral",
9
+ value,
10
+ }: {
11
+ tone?: "neutral" | "warning";
12
+ value: string;
13
+ }) {
14
+ return (
15
+ <span
16
+ className={cn(
17
+ "inline-flex h-5 items-center rounded-sm border px-1.5 text-[10px] font-medium leading-none",
18
+ tone === "warning"
19
+ ? "border-amber-200 bg-amber-50 text-amber-700"
20
+ : "border-gray-200 bg-gray-50 text-gray-500",
21
+ )}
22
+ >
23
+ {value}
24
+ </span>
25
+ );
26
+ }
27
+
28
+ export function ChatSessionWorkspaceFileBreadcrumbs({
29
+ breadcrumb,
30
+ }: {
31
+ breadcrumb: WorkspaceFileBreadcrumbViewModel;
32
+ }) {
33
+ return (
34
+ <div
35
+ data-testid="workspace-file-breadcrumbs"
36
+ title={breadcrumb.fullPath}
37
+ className="workspace-horizontal-scrollbar overflow-x-auto overflow-y-hidden border-b border-gray-200/80 bg-gray-50/55"
38
+ >
39
+ <div
40
+ data-testid="workspace-file-breadcrumb-scroll"
41
+ className="flex min-w-max items-center gap-2.5 px-3 py-1.5"
42
+ >
43
+ <div className="flex min-w-0 flex-1 items-center gap-1 pr-1">
44
+ {breadcrumb.segments.map((segment, index) => (
45
+ <Fragment key={segment.key}>
46
+ <span
47
+ className={cn(
48
+ "inline-flex h-5 items-center gap-1 rounded-sm px-1 text-[11px] leading-none",
49
+ segment.kind === "workspace"
50
+ ? "bg-primary/8 text-primary"
51
+ : segment.isCurrent
52
+ ? "bg-gray-200/70 text-gray-900"
53
+ : "text-gray-500",
54
+ )}
55
+ >
56
+ {segment.kind === "workspace" ? (
57
+ <FolderTree className="h-3 w-3 shrink-0" />
58
+ ) : segment.isCurrent ? (
59
+ <FileCode2 className="h-3 w-3 shrink-0" />
60
+ ) : null}
61
+ <span>{segment.label}</span>
62
+ </span>
63
+ {index < breadcrumb.segments.length - 1 ? (
64
+ <ChevronRight className="h-3 w-3 shrink-0 text-gray-300" />
65
+ ) : null}
66
+ </Fragment>
67
+ ))}
68
+ </div>
69
+
70
+ {breadcrumb.locationLabel || breadcrumb.truncated ? (
71
+ <div className="flex shrink-0 flex-wrap items-center justify-end gap-1">
72
+ {breadcrumb.locationLabel ? (
73
+ <WorkspaceBreadcrumbMetaChip value={breadcrumb.locationLabel} />
74
+ ) : null}
75
+ {breadcrumb.truncated ? (
76
+ <WorkspaceBreadcrumbMetaChip
77
+ tone="warning"
78
+ value={t("chatWorkspacePreviewTruncated")}
79
+ />
80
+ ) : null}
81
+ </div>
82
+ ) : null}
83
+ </div>
84
+ </div>
85
+ );
86
+ }
@@ -1,5 +1,6 @@
1
1
  import { useAppMeta } from '@/hooks/useConfig';
2
2
  import type { ReactNode } from 'react';
3
+ import { RuntimeStatusEntry } from '@/components/layout/runtime-status-entry';
3
4
 
4
5
  type BrandHeaderProps = {
5
6
  className?: string;
@@ -10,6 +11,7 @@ export function BrandHeader({ className, suffix }: BrandHeaderProps) {
10
11
  const { data } = useAppMeta();
11
12
  const productName = data?.name ?? 'NextClaw';
12
13
  const productVersion = data?.productVersion?.trim();
14
+ const resolvedSuffix = suffix ?? <RuntimeStatusEntry />;
13
15
 
14
16
  return (
15
17
  <div className={className ?? 'flex items-center gap-2.5'}>
@@ -19,7 +21,7 @@ export function BrandHeader({ className, suffix }: BrandHeaderProps) {
19
21
  <div className="flex items-baseline gap-2 min-w-0">
20
22
  <span className="truncate text-[15px] font-semibold tracking-[-0.01em] text-gray-800">{productName}</span>
21
23
  {productVersion ? <span className="text-[13px] font-medium text-gray-500">v{productVersion}</span> : null}
22
- {suffix ? <span className="inline-flex items-center shrink-0">{suffix}</span> : null}
24
+ {resolvedSuffix ? <span className="inline-flex items-center shrink-0">{resolvedSuffix}</span> : null}
23
25
  </div>
24
26
  </div>
25
27
  );
@@ -1,4 +1,5 @@
1
1
  import { type SessionContextIcon } from '@/lib/session-context.utils';
2
+ import { resolveAppResourceUri } from '@/lib/app-resource-uri';
2
3
  import { LogoBadge } from '@/components/common/LogoBadge';
3
4
  import { getChannelLogo } from '@/lib/logos';
4
5
  import { cn } from '@/lib/utils';
@@ -8,6 +9,18 @@ export function SessionContextIconNode({ icon, className }: { icon: SessionConte
8
9
  if (icon.kind === 'channel-logo') {
9
10
  return <ChannelLogoIcon channel={icon.channel} className={className} />;
10
11
  }
12
+ if (icon.kind === 'runtime-image') {
13
+ const runtimeIconSrc = resolveAppResourceUri(icon.src);
14
+ return (
15
+ <LogoBadge
16
+ name={icon.name?.trim() || icon.alt?.trim() || 'runtime'}
17
+ src={runtimeIconSrc ?? undefined}
18
+ className={cn('h-[1.125rem] w-[1.125rem]', className)}
19
+ imgClassName="h-full w-full object-contain"
20
+ fallback={<Bot className={cn('h-3 w-3 text-gray-500', className)} />}
21
+ />
22
+ );
23
+ }
11
24
  if (icon.icon === 'heartbeat') {
12
25
  return <HeartPulse className={cn('h-3.5 w-3.5', className)} />;
13
26
  }
@@ -22,8 +35,8 @@ function ChannelLogoIcon(
22
35
  <LogoBadge
23
36
  name={channel}
24
37
  src={logoSrc}
25
- className={cn('h-4 w-4 rounded-[4px] border border-gray-200/80 bg-white', className)}
26
- imgClassName="h-3 w-3 object-contain"
38
+ className={cn('h-[1.125rem] w-[1.125rem]', className)}
39
+ imgClassName="h-full w-full object-contain"
27
40
  fallback={<Bot className="h-3 w-3 text-gray-500" />}
28
41
  />
29
42
  );
@@ -1,7 +1,8 @@
1
- import { useState } from 'react';
2
- import { X } from 'lucide-react';
3
- import { cn } from '@/lib/utils';
4
- import { t } from '@/lib/i18n';
1
+ import { useState } from "react";
2
+ import { X } from "lucide-react";
3
+ import { cn } from "@/lib/utils";
4
+ import { t } from "@/lib/i18n";
5
+ import { TagChip } from "@/components/ui/tag-chip";
5
6
 
6
7
  interface TagInputProps {
7
8
  value: string[];
@@ -10,15 +11,20 @@ interface TagInputProps {
10
11
  placeholder?: string;
11
12
  }
12
13
 
13
- export function TagInput({ value, onChange, className, placeholder = '' }: TagInputProps) {
14
- const [input, setInput] = useState('');
14
+ export function TagInput({
15
+ value,
16
+ onChange,
17
+ className,
18
+ placeholder = "",
19
+ }: TagInputProps) {
20
+ const [input, setInput] = useState("");
15
21
 
16
22
  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
17
- if (e.key === 'Enter' && input.trim()) {
23
+ if (e.key === "Enter" && input.trim()) {
18
24
  e.preventDefault();
19
25
  onChange([...value, input.trim()]);
20
- setInput('');
21
- } else if (e.key === 'Backspace' && !input && value.length > 0) {
26
+ setInput("");
27
+ } else if (e.key === "Backspace" && !input && value.length > 0) {
22
28
  onChange(value.slice(0, -1));
23
29
  }
24
30
  };
@@ -28,21 +34,23 @@ export function TagInput({ value, onChange, className, placeholder = '' }: TagIn
28
34
  };
29
35
 
30
36
  return (
31
- <div className={cn('flex flex-wrap gap-2 p-2 border rounded-md min-h-[42px]', className)}>
37
+ <div
38
+ className={cn(
39
+ "flex min-h-[42px] flex-wrap gap-2 rounded-xl border border-gray-200/80 bg-white p-2",
40
+ className,
41
+ )}
42
+ >
32
43
  {value.map((tag, index) => (
33
- <span
34
- key={index}
35
- className="inline-flex items-center gap-1 px-2 py-1 bg-primary text-primary-foreground rounded text-sm"
36
- >
44
+ <TagChip key={index} tone="info" className="gap-1 px-2 py-1 text-sm">
37
45
  {tag}
38
46
  <button
39
47
  type="button"
40
48
  onClick={() => removeTag(index)}
41
- className="hover:text-red-300 transition-colors"
49
+ className="transition-colors hover:text-rose-200"
42
50
  >
43
51
  <X className="h-3 w-3" />
44
52
  </button>
45
- </span>
53
+ </TagChip>
46
54
  ))}
47
55
  <input
48
56
  type="text"
@@ -50,7 +58,7 @@ export function TagInput({ value, onChange, className, placeholder = '' }: TagIn
50
58
  onChange={(e) => setInput(e.target.value)}
51
59
  onKeyDown={handleKeyDown}
52
60
  className="flex-1 outline-none min-w-[100px] bg-transparent text-sm"
53
- placeholder={placeholder || t('enterTag')}
61
+ placeholder={placeholder || t("enterTag")}
54
62
  />
55
63
  </div>
56
64
  );