@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,77 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { adaptChatMessage } from '@/components/chat/adapters/chat-message.adapter';
3
+ import { adaptNcpMessageToUiMessage } from '../ncp-session-adapter';
4
+
5
+ const texts = {
6
+ roleLabels: {
7
+ user: 'User',
8
+ assistant: 'Assistant',
9
+ tool: 'Tool',
10
+ system: 'System',
11
+ fallback: 'Message',
12
+ },
13
+ reasoningLabel: 'Reasoning',
14
+ toolCallLabel: 'Tool Call',
15
+ toolResultLabel: 'Tool Result',
16
+ toolInputLabel: 'Input',
17
+ toolNoOutputLabel: 'No output',
18
+ toolOutputLabel: 'Output',
19
+ toolStatusPreparingLabel: 'Preparing',
20
+ toolStatusRunningLabel: 'Running',
21
+ toolStatusCompletedLabel: 'Completed',
22
+ toolStatusFailedLabel: 'Failed',
23
+ toolStatusCancelledLabel: 'Cancelled',
24
+ imageAttachmentLabel: 'Image',
25
+ fileAttachmentLabel: 'File',
26
+ unknownPartLabel: 'Unknown',
27
+ };
28
+
29
+ describe('adaptNcpMessageToUiMessage cancelled tools', () => {
30
+ it('renders cancelled tool invocations as cancelled cards', () => {
31
+ const uiMessage = adaptNcpMessageToUiMessage({
32
+ id: 'ncp-message-tool-cancelled-1',
33
+ sessionId: 'ncp-session-1',
34
+ role: 'assistant',
35
+ status: 'final',
36
+ timestamp: '2026-04-01T00:00:00.000Z',
37
+ parts: [
38
+ {
39
+ type: 'tool-invocation',
40
+ toolCallId: 'tool-cancelled-1',
41
+ toolName: 'write_file',
42
+ state: 'cancelled',
43
+ args: JSON.stringify({
44
+ path: 'src/app.ts',
45
+ content: 'hello',
46
+ }),
47
+ },
48
+ ],
49
+ });
50
+
51
+ const adapted = adaptChatMessage(
52
+ {
53
+ id: uiMessage.id,
54
+ role: uiMessage.role,
55
+ meta: {
56
+ timestamp: uiMessage.meta?.timestamp,
57
+ status: uiMessage.meta?.status,
58
+ },
59
+ parts: uiMessage.parts as never,
60
+ },
61
+ {
62
+ formatTimestamp: (value) => value ?? '',
63
+ texts,
64
+ },
65
+ );
66
+
67
+ expect(adapted.parts[0]).toMatchObject({
68
+ type: 'tool-card',
69
+ card: {
70
+ toolName: 'write_file',
71
+ statusTone: 'cancelled',
72
+ statusLabel: 'Cancelled',
73
+ titleLabel: 'Tool Result',
74
+ },
75
+ });
76
+ });
77
+ });
@@ -2,6 +2,7 @@ import {
2
2
  useEffect,
3
3
  useMemo,
4
4
  useRef,
5
+ useState,
5
6
  } from "react";
6
7
  import {
7
8
  buildNcpRequestEnvelope,
@@ -98,11 +99,7 @@ export function shouldClearPendingProjectRootOverride(params: {
98
99
  }
99
100
 
100
101
  export function NcpChatPage({ view }: ChatPageProps) {
101
- const presenterRef = useRef<NcpChatPresenter | null>(null);
102
- if (!presenterRef.current) {
103
- presenterRef.current = new NcpChatPresenter();
104
- }
105
- const presenter = presenterRef.current;
102
+ const [presenter] = useState(() => new NcpChatPresenter());
106
103
  const query = useChatSessionListStore((state) => state.snapshot.query);
107
104
  const selectedSessionKey = useChatSessionListStore(
108
105
  (state) => state.snapshot.selectedSessionKey,
@@ -306,8 +303,11 @@ export function NcpChatPage({ view }: ChatPageProps) {
306
303
  currentAgentId,
307
304
  currentAgent,
308
305
  parentSession,
309
- currentSessionTypeLabel
306
+ currentSessionTypeLabel,
307
+ currentSessionTypeIcon,
308
+ currentChildSessionTabs,
310
309
  } = useNcpChatDerivedState({
310
+ sessionKey,
311
311
  selectedSession,
312
312
  selectedAgentId,
313
313
  availableAgents,
@@ -341,6 +341,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
341
341
  isSkillsLoading: sessionSkillsQuery.isLoading,
342
342
  sessionTypeUnavailableMessage,
343
343
  currentSessionTypeLabel,
344
+ currentSessionTypeIcon,
344
345
  sessionKey,
345
346
  currentAgentId,
346
347
  currentAgent,
@@ -352,7 +353,8 @@ export function NcpChatPage({ view }: ChatPageProps) {
352
353
  threadRef,
353
354
  agent,
354
355
  isAwaitingAssistantOutput,
355
- parentSession
356
+ parentSession,
357
+ childSessionTabs: currentChildSessionTabs,
356
358
  });
357
359
 
358
360
  return (
@@ -1,14 +1,17 @@
1
1
  import { appQueryClient } from '@/app-query-client';
2
2
  import { deleteNcpSessionSummaryInQueryClient } from '@/api/ncp-session-query-cache';
3
3
  import { deleteNcpSession as deleteNcpSessionApi } from '@/api/ncp-session';
4
- import type { ChatToolActionViewModel } from '@nextclaw/agent-chat-ui';
4
+ import type {
5
+ ChatFileOpenActionViewModel,
6
+ ChatToolActionViewModel,
7
+ } from '@nextclaw/agent-chat-ui';
5
8
  import type { ChatSessionListManager } from '@/components/chat/managers/chat-session-list.manager';
6
9
  import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
7
10
  import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
8
11
  import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
9
12
  import type {
10
- ChatChildSessionTab,
11
13
  ChatThreadSnapshot,
14
+ ChatWorkspaceFileTab,
12
15
  } from '@/components/chat/stores/chat-thread.store';
13
16
  import { useChatThreadStore } from '@/components/chat/stores/chat-thread.store';
14
17
  import { t } from '@/lib/i18n';
@@ -37,6 +40,98 @@ export class NcpChatThreadManager {
37
40
  useChatThreadStore.getState().setSnapshot(patch);
38
41
  };
39
42
 
43
+ private clearDeletedSessionState = (sessionKey: string) => {
44
+ if (useChatSessionListStore.getState().snapshot.selectedSessionKey === sessionKey) {
45
+ this.sessionListManager.setSelectedSessionKey(null);
46
+ }
47
+ useChatThreadStore.getState().setSnapshot({
48
+ sessionKey: null,
49
+ sessionTypeLabel: null,
50
+ agentId: null,
51
+ agentDisplayName: null,
52
+ agentAvatarUrl: null,
53
+ sessionDisplayName: undefined,
54
+ sessionProjectRoot: null,
55
+ sessionProjectName: null,
56
+ canDeleteSession: false,
57
+ isHistoryLoading: false,
58
+ messages: [],
59
+ isSending: false,
60
+ isAwaitingAssistantOutput: false,
61
+ parentSessionKey: null,
62
+ parentSessionLabel: null,
63
+ workspacePanelParentKey: null,
64
+ childSessionTabs: [],
65
+ activeChildSessionKey: null,
66
+ workspaceFileTabs: [],
67
+ activeWorkspaceFileKey: null,
68
+ });
69
+ };
70
+
71
+ private resolveWorkspaceParentSessionKey = (): string | null => {
72
+ const threadSessionKey = useChatThreadStore.getState().snapshot.sessionKey?.trim();
73
+ if (threadSessionKey) {
74
+ return threadSessionKey;
75
+ }
76
+ return useChatSessionListStore.getState().snapshot.selectedSessionKey ?? null;
77
+ };
78
+
79
+ private buildWorkspaceFileTab = (
80
+ action: ChatFileOpenActionViewModel,
81
+ parentSessionKey: string | null,
82
+ ): ChatWorkspaceFileTab | null => {
83
+ const normalizedPath = action.path.trim();
84
+ if (!normalizedPath) {
85
+ return null;
86
+ }
87
+ const normalizedParentSessionKey = parentSessionKey?.trim() || null;
88
+ const key =
89
+ `${normalizedParentSessionKey ?? 'draft'}::${action.viewMode}::${normalizedPath}`;
90
+ return {
91
+ key,
92
+ parentSessionKey: normalizedParentSessionKey,
93
+ path: normalizedPath,
94
+ label: action.label?.trim() || null,
95
+ viewMode: action.viewMode,
96
+ line: action.line ?? null,
97
+ column: action.column ?? null,
98
+ rawText: action.rawText ?? null,
99
+ beforeText: action.beforeText ?? null,
100
+ afterText: action.afterText ?? null,
101
+ patchText: action.patchText ?? null,
102
+ oldStartLine: action.oldStartLine ?? null,
103
+ newStartLine: action.newStartLine ?? null,
104
+ fullLines: action.fullLines,
105
+ };
106
+ };
107
+
108
+ private upsertWorkspaceFileTab = (nextTab: ChatWorkspaceFileTab): ChatWorkspaceFileTab[] => {
109
+ const { workspaceFileTabs } = useChatThreadStore.getState().snapshot;
110
+ const existingIndex = workspaceFileTabs.findIndex((tab) => tab.key === nextTab.key);
111
+ if (existingIndex === -1) {
112
+ return [nextTab, ...workspaceFileTabs];
113
+ }
114
+ const nextTabs = [...workspaceFileTabs];
115
+ nextTabs.splice(existingIndex, 1);
116
+ nextTabs.unshift({
117
+ ...workspaceFileTabs[existingIndex],
118
+ ...nextTab,
119
+ });
120
+ return nextTabs;
121
+ };
122
+
123
+ private ensureWorkspaceParentRoute = (parentSessionKey: string | null) => {
124
+ if (!parentSessionKey) {
125
+ return;
126
+ }
127
+ const {
128
+ snapshot: { selectedSessionKey },
129
+ } = useChatSessionListStore.getState();
130
+ if (selectedSessionKey !== parentSessionKey) {
131
+ this.uiManager.goToSession(parentSessionKey);
132
+ }
133
+ };
134
+
40
135
  deleteSession = () => {
41
136
  void this.deleteCurrentSession();
42
137
  };
@@ -49,21 +144,36 @@ export class NcpChatThreadManager {
49
144
  this.uiManager.goToProviders();
50
145
  };
51
146
 
52
- private upsertChildSessionTab = (tab: ChatChildSessionTab) => {
53
- const { snapshot } = useChatThreadStore.getState();
54
- const existingIndex = snapshot.childSessionTabs.findIndex(
55
- (item) => item.sessionKey === tab.sessionKey,
56
- );
57
- const nextTabs =
58
- existingIndex >= 0
59
- ? snapshot.childSessionTabs.map((item, index) =>
60
- index === existingIndex ? { ...item, ...tab } : item,
61
- )
62
- : [...snapshot.childSessionTabs, tab];
147
+ openChildSessionPanel = (params: {
148
+ parentSessionKey: string;
149
+ activeChildSessionKey?: string | null;
150
+ }) => {
151
+ const parentSessionKey = params.parentSessionKey.trim();
152
+ if (!parentSessionKey) {
153
+ return;
154
+ }
155
+ const activeChildSessionKey = params.activeChildSessionKey?.trim() || null;
156
+ useChatThreadStore.getState().setSnapshot({
157
+ workspacePanelParentKey: parentSessionKey,
158
+ activeChildSessionKey,
159
+ activeWorkspaceFileKey: null,
160
+ });
161
+ this.ensureWorkspaceParentRoute(parentSessionKey);
162
+ };
163
+
164
+ openFilePreview = (action: ChatFileOpenActionViewModel) => {
165
+ const parentSessionKey = this.resolveWorkspaceParentSessionKey();
166
+ const nextTab = this.buildWorkspaceFileTab(action, parentSessionKey);
167
+ if (!nextTab) {
168
+ return;
169
+ }
63
170
  useChatThreadStore.getState().setSnapshot({
64
- childSessionTabs: nextTabs,
65
- activeChildSessionKey: tab.sessionKey,
171
+ workspacePanelParentKey: parentSessionKey,
172
+ workspaceFileTabs: this.upsertWorkspaceFileTab(nextTab),
173
+ activeWorkspaceFileKey: nextTab.key,
174
+ activeChildSessionKey: null,
66
175
  });
176
+ this.ensureWorkspaceParentRoute(parentSessionKey);
67
177
  };
68
178
 
69
179
  openSessionFromToolAction = (action: ChatToolActionViewModel) => {
@@ -75,14 +185,19 @@ export class NcpChatThreadManager {
75
185
  action.parentSessionId?.trim() ||
76
186
  useChatSessionListStore.getState().snapshot.selectedSessionKey ||
77
187
  null;
78
- this.upsertChildSessionTab({
79
- sessionKey: action.sessionId,
80
- parentSessionKey,
81
- label: action.label?.trim() || null,
82
- agentId: action.agentId?.trim() || null,
83
- });
84
- return;
188
+ if (parentSessionKey) {
189
+ this.openChildSessionPanel({
190
+ parentSessionKey,
191
+ activeChildSessionKey: action.sessionId,
192
+ });
193
+ return;
194
+ }
85
195
  }
196
+ useChatThreadStore.getState().setSnapshot({
197
+ workspacePanelParentKey: null,
198
+ activeChildSessionKey: null,
199
+ activeWorkspaceFileKey: null,
200
+ });
86
201
  this.uiManager.goToSession(action.sessionId);
87
202
  };
88
203
 
@@ -97,34 +212,56 @@ export class NcpChatThreadManager {
97
212
  }
98
213
  useChatThreadStore.getState().setSnapshot({
99
214
  activeChildSessionKey: normalizedSessionKey,
215
+ activeWorkspaceFileKey: null,
100
216
  });
101
217
  };
102
218
 
103
- closeChildSessionDetail = () => {
104
- const {
105
- sessionKey,
106
- childSessionTabs,
107
- activeChildSessionKey,
108
- } = useChatThreadStore.getState().snapshot;
109
- if (!sessionKey) {
110
- useChatThreadStore.getState().setSnapshot({
111
- childSessionTabs: [],
112
- activeChildSessionKey: null,
113
- });
219
+ selectWorkspaceFile = (fileKey: string) => {
220
+ const normalizedFileKey = fileKey.trim();
221
+ if (!normalizedFileKey) {
114
222
  return;
115
223
  }
116
- const nextTabs = childSessionTabs.filter(
117
- (tab) => tab.parentSessionKey !== sessionKey,
224
+ const { workspaceFileTabs } = useChatThreadStore.getState().snapshot;
225
+ if (!workspaceFileTabs.some((tab) => tab.key === normalizedFileKey)) {
226
+ return;
227
+ }
228
+ useChatThreadStore.getState().setSnapshot({
229
+ activeWorkspaceFileKey: normalizedFileKey,
230
+ activeChildSessionKey: null,
231
+ });
232
+ };
233
+
234
+ closeWorkspaceFile = (fileKey: string) => {
235
+ const normalizedFileKey = fileKey.trim();
236
+ if (!normalizedFileKey) {
237
+ return;
238
+ }
239
+ const { snapshot } = useChatThreadStore.getState();
240
+ const { activeWorkspaceFileKey, workspaceFileTabs } = snapshot;
241
+ const nextTabs = workspaceFileTabs.filter(
242
+ (tab) => tab.key !== normalizedFileKey,
118
243
  );
119
- const nextActiveKey = nextTabs.some((tab) => tab.sessionKey === activeChildSessionKey)
120
- ? activeChildSessionKey
121
- : null;
244
+ const nextPatch: Partial<ChatThreadSnapshot> = {
245
+ workspaceFileTabs: nextTabs,
246
+ };
247
+ if (activeWorkspaceFileKey === normalizedFileKey) {
248
+ nextPatch.activeWorkspaceFileKey = null;
249
+ }
250
+ useChatThreadStore.getState().setSnapshot(nextPatch);
251
+ };
252
+
253
+ closeWorkspacePanel = () => {
122
254
  useChatThreadStore.getState().setSnapshot({
123
- childSessionTabs: nextTabs,
124
- activeChildSessionKey: nextActiveKey,
255
+ workspacePanelParentKey: null,
256
+ activeChildSessionKey: null,
257
+ activeWorkspaceFileKey: null,
125
258
  });
126
259
  };
127
260
 
261
+ closeChildSessionDetail = () => {
262
+ this.closeWorkspacePanel();
263
+ };
264
+
128
265
  goToParentSession = () => {
129
266
  const {
130
267
  parentSessionKey,
@@ -139,7 +276,7 @@ export class NcpChatThreadManager {
139
276
  if (!resolvedParentSessionKey) {
140
277
  return;
141
278
  }
142
- this.closeChildSessionDetail();
279
+ this.closeWorkspacePanel();
143
280
  this.uiManager.goToSession(resolvedParentSessionKey);
144
281
  };
145
282
 
@@ -171,6 +308,7 @@ export class NcpChatThreadManager {
171
308
  deleteNcpSessionSummaryInQueryClient(appQueryClient, selectedSessionKey);
172
309
  appQueryClient.removeQueries({ queryKey: ['ncp-session-messages', selectedSessionKey] });
173
310
  this.streamActionsManager.resetStreamState();
311
+ this.clearDeletedSessionState(selectedSessionKey);
174
312
  this.uiManager.goToChatRoot({ replace: true });
175
313
  } finally {
176
314
  useChatThreadStore.getState().setSnapshot({ isDeletePending: false });
@@ -71,7 +71,7 @@ describe('adaptNcpSessionSummary', () => {
71
71
  });
72
72
  });
73
73
 
74
- describe('adaptNcpMessageToUiMessage', () => {
74
+ describe('adaptNcpMessageToUiMessage file rendering', () => {
75
75
  it('preserves mixed text and image part order for message rendering', () => {
76
76
  const adapted = adaptNcpMessageToUiMessage({
77
77
  id: 'ncp-message-1',
@@ -111,6 +111,40 @@ describe('adaptNcpMessageToUiMessage', () => {
111
111
  ]);
112
112
  });
113
113
 
114
+ it('maps assetUri file parts into asset content urls for rendering', () => {
115
+ const adapted = adaptNcpMessageToUiMessage({
116
+ id: 'ncp-message-asset-1',
117
+ sessionId: 'ncp-session-1',
118
+ role: 'assistant',
119
+ status: 'final',
120
+ timestamp: '2026-04-16T00:00:00.000Z',
121
+ parts: [
122
+ {
123
+ type: 'file',
124
+ name: 'diagram.png',
125
+ mimeType: 'image/png',
126
+ assetUri: 'asset://store/2026/04/16/asset_123',
127
+ sizeBytes: 42,
128
+ },
129
+ ],
130
+ });
131
+
132
+ expect(adapted.parts).toHaveLength(1);
133
+ expect(adapted.parts[0]).toMatchObject({
134
+ type: 'file',
135
+ name: 'diagram.png',
136
+ mimeType: 'image/png',
137
+ data: '',
138
+ sizeBytes: 42,
139
+ });
140
+ expect((adapted.parts[0] as { url?: string }).url).toMatch(
141
+ /\/api\/ncp\/assets\/content\?uri=asset%3A%2F%2Fstore%2F2026%2F04%2F16%2Fasset_123$/,
142
+ );
143
+ });
144
+
145
+ });
146
+
147
+ describe('adaptNcpMessageToUiMessage tool rendering', () => {
114
148
  it('keeps streamed native file tool args renderable as a preview before the tool result arrives', () => {
115
149
  const uiMessage = adaptNcpMessageToUiMessage({
116
150
  id: 'ncp-message-tool-1',
@@ -282,6 +316,7 @@ describe('adaptNcpMessageToUiMessage', () => {
282
316
  },
283
317
  });
284
318
  });
319
+
285
320
  });
286
321
 
287
322
  describe('readNcpSessionPreferredThinking', () => {
@@ -1,6 +1,7 @@
1
1
  import { ToolInvocationStatus, type UIMessage } from '@nextclaw/agent-chat';
2
2
  import type { NcpMessagePart } from '@nextclaw/ncp';
3
3
  import type { NcpMessageView, NcpSessionSummaryView, SessionEntryView, ThinkingLevel } from '@/api/types';
4
+ import { API_BASE } from '@/api/api-base';
4
5
  import {
5
6
  getSessionProjectName,
6
7
  normalizeSessionProjectRootValue,
@@ -19,6 +20,11 @@ function stringifyUnknown(value: unknown): string {
19
20
  }
20
21
  }
21
22
 
23
+ function buildNcpAssetContentUrl(assetUri: string): string {
24
+ const query = new URLSearchParams({ uri: assetUri });
25
+ return `${API_BASE}/api/ncp/assets/content?${query.toString()}`;
26
+ }
27
+
22
28
  function readOptionalString(value: unknown): string | null {
23
29
  if (typeof value !== 'string') {
24
30
  return null;
@@ -145,6 +151,9 @@ function parseSessionContext(sessionKey: string): { channel?: string; type?: str
145
151
  }
146
152
 
147
153
  function mapToolStatus(part: Extract<NcpMessagePart, { type: 'tool-invocation' }>): ToolInvocationStatus {
154
+ if (part.state === 'cancelled') {
155
+ return ToolInvocationStatus.CANCELLED;
156
+ }
148
157
  if (part.state === 'result') {
149
158
  return ToolInvocationStatus.RESULT;
150
159
  }
@@ -207,6 +216,17 @@ function toUiParts(parts: NcpMessagePart[]): UIMessage['parts'] {
207
216
  });
208
217
  continue;
209
218
  }
219
+ if (part.type === 'file' && part.assetUri) {
220
+ uiParts.push({
221
+ type: 'file',
222
+ ...(part.name ? { name: part.name } : {}),
223
+ mimeType: part.mimeType ?? 'application/octet-stream',
224
+ data: '',
225
+ url: buildNcpAssetContentUrl(part.assetUri),
226
+ ...(typeof part.sizeBytes === 'number' ? { sizeBytes: part.sizeBytes } : {})
227
+ });
228
+ continue;
229
+ }
210
230
  if (part.type === 'step-start') {
211
231
  uiParts.push({ type: 'step-start' });
212
232
  continue;
@@ -10,42 +10,87 @@ import { adaptNcpSessionSummary } from '@/components/chat/ncp/ncp-session-adapte
10
10
  import type { NcpChatPresenter } from '@/components/chat/ncp/ncp-chat.presenter';
11
11
  import type { UseHydratedNcpAgentResult } from '@nextclaw/ncp-react';
12
12
  import type { ChatModelOption } from '@/components/chat/chat-input.types';
13
+ import type { ChatChildSessionTab } from '@/components/chat/stores/chat-thread.store';
14
+ import type { ChatSessionTypeOption } from '@/components/chat/useChatSessionTypeState';
13
15
  import { resolveSessionTypeLabel } from '@/components/chat/useChatSessionTypeState';
16
+
17
+ function buildChildSessionTabs(params: {
18
+ parentSessionKey: string | null;
19
+ sessionSummaries: NcpSessionSummaryView[];
20
+ }): ChatChildSessionTab[] {
21
+ if (!params.parentSessionKey) {
22
+ return [];
23
+ }
24
+ return params.sessionSummaries
25
+ .map(adaptNcpSessionSummary)
26
+ .filter((session) => session.parentSessionId === params.parentSessionKey)
27
+ .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
28
+ .map((session) => ({
29
+ sessionKey: session.key,
30
+ parentSessionKey: session.parentSessionId ?? null,
31
+ label: session.label ?? null,
32
+ agentId: session.agentId ?? null,
33
+ }));
34
+ }
35
+
14
36
  export function useNcpChatDerivedState(params: {
37
+ sessionKey: string | null;
15
38
  selectedSession: SessionEntryView | null;
16
39
  selectedAgentId: string;
17
40
  availableAgents: AgentProfileView[];
18
41
  parentSessionId: string | null;
19
42
  sessionSummaries: NcpSessionSummaryView[];
20
43
  selectedSessionType: string;
21
- sessionTypeOptions: Array<{ value: string; label: string }>;
44
+ sessionTypeOptions: ChatSessionTypeOption[];
22
45
  }) {
23
- const currentSessionDisplayName = params.selectedSession
24
- ? sessionDisplayName(params.selectedSession)
46
+ const {
47
+ availableAgents,
48
+ parentSessionId,
49
+ selectedAgentId,
50
+ selectedSession,
51
+ selectedSessionType,
52
+ sessionKey,
53
+ sessionSummaries,
54
+ sessionTypeOptions,
55
+ } = params;
56
+ const currentSessionDisplayName = selectedSession
57
+ ? sessionDisplayName(selectedSession)
25
58
  : undefined;
26
- const currentAgentId = params.selectedSession?.agentId ?? params.selectedAgentId;
59
+ const currentAgentId = selectedSession?.agentId ?? selectedAgentId;
27
60
  const currentAgent =
28
- params.availableAgents.find((agent) => agent.id === currentAgentId) ?? null;
61
+ availableAgents.find((agent) => agent.id === currentAgentId) ?? null;
29
62
  const parentSession = useMemo(() => {
30
- if (!params.parentSessionId) {
63
+ if (!parentSessionId) {
31
64
  return null;
32
65
  }
33
66
  const parentSummary =
34
- params.sessionSummaries.find(
35
- (summary) => summary.sessionId === params.parentSessionId,
67
+ sessionSummaries.find(
68
+ (summary) => summary.sessionId === parentSessionId,
36
69
  ) ?? null;
37
70
  return parentSummary ? adaptNcpSessionSummary(parentSummary) : null;
38
- }, [params.parentSessionId, params.sessionSummaries]);
71
+ }, [parentSessionId, sessionSummaries]);
72
+ const currentSessionTypeOption =
73
+ sessionTypeOptions.find((option) => option.value === selectedSessionType) ?? null;
39
74
  const currentSessionTypeLabel =
40
- params.sessionTypeOptions.find((option) => option.value === params.selectedSessionType)
41
- ?.label ?? resolveSessionTypeLabel(params.selectedSessionType);
75
+ currentSessionTypeOption?.label ?? resolveSessionTypeLabel(selectedSessionType);
76
+ const currentSessionTypeIcon = currentSessionTypeOption?.icon ?? null;
77
+ const currentChildSessionTabs = useMemo(
78
+ () =>
79
+ buildChildSessionTabs({
80
+ parentSessionKey: sessionKey,
81
+ sessionSummaries,
82
+ }),
83
+ [sessionKey, sessionSummaries],
84
+ );
42
85
 
43
86
  return {
44
87
  currentSessionDisplayName,
45
88
  currentAgentId,
46
89
  currentAgent,
47
90
  parentSession,
48
- currentSessionTypeLabel
91
+ currentSessionTypeLabel,
92
+ currentSessionTypeIcon,
93
+ currentChildSessionTabs,
49
94
  };
50
95
  }
51
96
 
@@ -58,7 +103,7 @@ export function useNcpChatSnapshotSync(params: {
58
103
  lastSendError: string | null;
59
104
  isSending: boolean;
60
105
  modelOptions: ChatModelOption[];
61
- sessionTypeOptions: Array<{ value: string; label: string }>;
106
+ sessionTypeOptions: ChatSessionTypeOption[];
62
107
  selectedSessionType: string;
63
108
  canEditSessionType: boolean;
64
109
  sessionTypeUnavailable: boolean;
@@ -66,6 +111,7 @@ export function useNcpChatSnapshotSync(params: {
66
111
  isSkillsLoading: boolean;
67
112
  sessionTypeUnavailableMessage: string | null;
68
113
  currentSessionTypeLabel: string;
114
+ currentSessionTypeIcon: ChatSessionTypeOption['icon'];
69
115
  sessionKey: string;
70
116
  currentAgentId: string;
71
117
  currentAgent: AgentProfileView | null;
@@ -78,6 +124,7 @@ export function useNcpChatSnapshotSync(params: {
78
124
  agent: Pick<UseHydratedNcpAgentResult, 'isHydrating' | 'visibleMessages'>;
79
125
  isAwaitingAssistantOutput: boolean;
80
126
  parentSession: SessionEntryView | null;
127
+ childSessionTabs: ChatChildSessionTab[];
81
128
  }) {
82
129
  useEffect(() => {
83
130
  params.presenter.chatInputManager.syncSnapshot({
@@ -103,6 +150,7 @@ export function useNcpChatSnapshotSync(params: {
103
150
  sessionTypeUnavailable: params.sessionTypeUnavailable,
104
151
  sessionTypeUnavailableMessage: params.sessionTypeUnavailableMessage,
105
152
  sessionTypeLabel: params.currentSessionTypeLabel,
153
+ sessionTypeIcon: params.currentSessionTypeIcon,
106
154
  sessionKey: params.sessionKey,
107
155
  agentId: params.currentAgentId,
108
156
  agentDisplayName: params.currentAgent?.displayName ?? null,
@@ -121,6 +169,7 @@ export function useNcpChatSnapshotSync(params: {
121
169
  parentSessionLabel: params.parentSession
122
170
  ? sessionDisplayName(params.parentSession)
123
171
  : null,
172
+ childSessionTabs: params.childSessionTabs,
124
173
  });
125
174
  }, [
126
175
  params