@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,318 @@
1
+ import { useEffect, useMemo, useRef } from "react";
2
+ import { ArrowLeft, FolderGit2, Loader2, X } from "lucide-react";
3
+ import type {
4
+ ChatFileOpenActionViewModel,
5
+ ChatToolActionViewModel,
6
+ } from "@nextclaw/agent-chat-ui";
7
+ import { useStickyBottomScroll } from "@nextclaw/agent-chat-ui";
8
+ import { ChatMessageListContainer } from "@/components/chat/containers/chat-message-list.container";
9
+ import {
10
+ useNcpChildSessionTabsView,
11
+ type ResolvedChildSessionTab,
12
+ } from "@/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view";
13
+ import { useNcpSessionConversation } from "@/components/chat/ncp/session-conversation/use-ncp-session-conversation";
14
+ import {
15
+ shouldShowUnreadSessionIndicator,
16
+ useChatSessionListStore,
17
+ } from "@/components/chat/stores/chat-session-list.store";
18
+ import type {
19
+ ChatChildSessionTab,
20
+ ChatWorkspaceFileTab,
21
+ } from "@/components/chat/stores/chat-thread.store";
22
+ import {
23
+ readWorkspaceFileTitle,
24
+ resolveWorkspaceSelection,
25
+ type WorkspaceTabViewModel,
26
+ WorkspaceTabsBar,
27
+ } from "@/components/chat/chat-session-workspace-panel-nav";
28
+ import { usePresenter } from "@/components/chat/presenter/chat-presenter-context";
29
+ import { ChatSessionWorkspaceFilePreview } from "@/components/chat/chat-session-workspace-file-preview";
30
+ import { AgentIdentityAvatar } from "@/components/common/agent-identity";
31
+ import { t } from "@/lib/i18n";
32
+ import { cn } from "@/lib/utils";
33
+
34
+ type ChatSessionWorkspacePanelProps = {
35
+ childSessionTabs: readonly ChatChildSessionTab[];
36
+ activeChildSessionKey: string | null;
37
+ workspaceFileTabs: readonly ChatWorkspaceFileTab[];
38
+ activeWorkspaceFileKey: string | null;
39
+ sessionProjectRoot: string | null;
40
+ onSelectSession: (sessionKey: string) => void;
41
+ onSelectFile: (fileKey: string) => void;
42
+ onCloseFile: (fileKey: string) => void;
43
+ onClose: () => void;
44
+ onBackToParent: () => void;
45
+ onToolAction?: (action: ChatToolActionViewModel) => void;
46
+ onFileOpen: (action: ChatFileOpenActionViewModel) => void;
47
+ };
48
+
49
+ function ChildSessionContent({
50
+ sessionKey,
51
+ onToolAction,
52
+ onFileOpen,
53
+ }: {
54
+ sessionKey: string;
55
+ onToolAction?: (action: ChatToolActionViewModel) => void;
56
+ onFileOpen: (action: ChatFileOpenActionViewModel) => void;
57
+ }) {
58
+ const agent = useNcpSessionConversation(sessionKey);
59
+ const messages = agent.visibleMessages;
60
+ const scrollRef = useRef<HTMLDivElement>(null);
61
+ const { onScroll } = useStickyBottomScroll({
62
+ scrollRef,
63
+ resetKey: sessionKey,
64
+ isLoading: agent.isHydrating,
65
+ hasContent: messages.length > 0,
66
+ contentVersion: messages[messages.length - 1] ?? null,
67
+ stickyThresholdPx: 20,
68
+ });
69
+
70
+ return (
71
+ <div
72
+ ref={scrollRef}
73
+ onScroll={onScroll}
74
+ className="h-full overflow-y-auto custom-scrollbar"
75
+ >
76
+ {agent.isHydrating ? (
77
+ <div className="flex h-full items-center justify-center text-sm text-gray-500">
78
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
79
+ {t("chatChildSessionLoading")}
80
+ </div>
81
+ ) : agent.hydrateError ? (
82
+ <div className="px-4 py-5 text-sm text-rose-600">
83
+ {agent.hydrateError.message}
84
+ </div>
85
+ ) : messages.length === 0 && !agent.isRunning ? (
86
+ <div className="px-4 py-5 text-sm text-gray-500">
87
+ {t("chatChildSessionEmpty")}
88
+ </div>
89
+ ) : (
90
+ <div className="px-4 py-5">
91
+ <ChatMessageListContainer
92
+ messages={messages}
93
+ isSending={agent.isRunning}
94
+ onToolAction={onToolAction}
95
+ onFileOpen={onFileOpen}
96
+ />
97
+ </div>
98
+ )}
99
+ </div>
100
+ );
101
+ }
102
+
103
+ function ChildSessionMetaChip({ value }: { value: string }) {
104
+ return (
105
+ <span className="inline-flex max-w-full items-center rounded border border-gray-200 bg-gray-50 px-2 py-0.5 text-[10px] font-medium text-gray-600">
106
+ <span className="truncate">{value}</span>
107
+ </span>
108
+ );
109
+ }
110
+
111
+ function ChildSessionMetaStrip({ tab }: { tab: ResolvedChildSessionTab }) {
112
+ const metaItems = [
113
+ tab.sessionTypeLabel,
114
+ tab.preferredModel,
115
+ tab.projectName,
116
+ ].filter((value): value is string => Boolean(value?.trim()));
117
+
118
+ if (metaItems.length === 0 && !tab.projectRoot) {
119
+ return null;
120
+ }
121
+
122
+ return (
123
+ <div className="mt-3 space-y-2">
124
+ {metaItems.length > 0 ? (
125
+ <div className="flex flex-wrap gap-1.5">
126
+ {metaItems.map((item) => (
127
+ <ChildSessionMetaChip key={item} value={item} />
128
+ ))}
129
+ </div>
130
+ ) : null}
131
+ {tab.projectRoot ? (
132
+ <div
133
+ title={tab.projectRoot}
134
+ className="truncate rounded border border-gray-200 bg-gray-50 px-2 py-1.5 font-mono text-[11px] text-gray-500"
135
+ >
136
+ {tab.projectRoot}
137
+ </div>
138
+ ) : null}
139
+ </div>
140
+ );
141
+ }
142
+
143
+ function WorkspaceActiveChildHeader({
144
+ tab,
145
+ }: {
146
+ tab: ResolvedChildSessionTab;
147
+ }) {
148
+ return (
149
+ <div className="border-b border-gray-200/70 px-4 py-3">
150
+ <div className="flex min-w-0 items-center gap-2 text-sm font-semibold text-gray-900">
151
+ {tab.agentId ? (
152
+ <AgentIdentityAvatar
153
+ agentId={tab.agentId}
154
+ className="h-4 w-4 shrink-0"
155
+ />
156
+ ) : (
157
+ <FolderGit2 className="h-4 w-4 shrink-0 text-gray-400" />
158
+ )}
159
+ <span className="truncate">{tab.title}</span>
160
+ </div>
161
+ <ChildSessionMetaStrip tab={tab} />
162
+ </div>
163
+ );
164
+ }
165
+
166
+ export function ChatSessionWorkspacePanel({
167
+ childSessionTabs,
168
+ activeChildSessionKey,
169
+ workspaceFileTabs,
170
+ activeWorkspaceFileKey,
171
+ sessionProjectRoot,
172
+ onSelectSession,
173
+ onSelectFile,
174
+ onCloseFile,
175
+ onClose,
176
+ onBackToParent,
177
+ onToolAction,
178
+ onFileOpen,
179
+ }: ChatSessionWorkspacePanelProps) {
180
+ const presenter = usePresenter();
181
+ const resolvedChildTabs = useNcpChildSessionTabsView(childSessionTabs);
182
+ const optimisticReadAtBySessionKey = useChatSessionListStore(
183
+ (state) => state.optimisticReadAtBySessionKey,
184
+ );
185
+ const activeSelection = resolveWorkspaceSelection({
186
+ activeChildSessionKey,
187
+ activeWorkspaceFileKey,
188
+ childSessionTabs: resolvedChildTabs,
189
+ workspaceFileTabs,
190
+ });
191
+ const hasParentSession = resolvedChildTabs.some((tab) =>
192
+ Boolean(tab.parentSessionKey),
193
+ );
194
+
195
+ useEffect(() => {
196
+ if (activeSelection?.kind !== "child-session") {
197
+ return;
198
+ }
199
+ const activeTabReadAt = activeSelection.tab.lastMessageAt?.trim() ?? null;
200
+ if (!activeTabReadAt) {
201
+ return;
202
+ }
203
+ presenter.chatSessionListManager.markSessionRead(
204
+ activeSelection.tab.sessionKey,
205
+ activeTabReadAt,
206
+ activeSelection.tab.readAt ?? null,
207
+ );
208
+ }, [activeSelection, presenter]);
209
+
210
+ const workspaceTabs = useMemo<WorkspaceTabViewModel[]>(() => {
211
+ const childTabs = resolvedChildTabs.map((tab) => {
212
+ const optimisticReadAt = optimisticReadAtBySessionKey[tab.sessionKey];
213
+ const effectiveReadAt =
214
+ optimisticReadAt && tab.readAt
215
+ ? optimisticReadAt.localeCompare(tab.readAt) > 0
216
+ ? optimisticReadAt
217
+ : tab.readAt
218
+ : optimisticReadAt ?? tab.readAt;
219
+ return {
220
+ key: `child:${tab.sessionKey}`,
221
+ kind: "child-session" as const,
222
+ title: tab.title,
223
+ tooltip: tab.title,
224
+ agentId: tab.agentId,
225
+ active:
226
+ activeSelection?.kind === "child-session" &&
227
+ activeSelection.tab.sessionKey === tab.sessionKey,
228
+ showUnreadDot: shouldShowUnreadSessionIndicator({
229
+ active:
230
+ activeSelection?.kind === "child-session" &&
231
+ activeSelection.tab.sessionKey === tab.sessionKey,
232
+ lastMessageAt: tab.lastMessageAt,
233
+ readAt: effectiveReadAt,
234
+ runStatus: tab.runStatus,
235
+ }),
236
+ onSelect: () => onSelectSession(tab.sessionKey),
237
+ };
238
+ });
239
+
240
+ const fileTabs = workspaceFileTabs.map((file) => ({
241
+ key: `file:${file.key}`,
242
+ kind: "file" as const,
243
+ title: readWorkspaceFileTitle(file),
244
+ tooltip: file.path,
245
+ viewMode: file.viewMode,
246
+ active:
247
+ activeSelection?.kind === "file" &&
248
+ activeSelection.file.key === file.key,
249
+ onSelect: () => onSelectFile(file.key),
250
+ onClose: () => onCloseFile(file.key),
251
+ }));
252
+
253
+ return [...childTabs, ...fileTabs];
254
+ }, [
255
+ activeSelection,
256
+ onCloseFile,
257
+ onSelectFile,
258
+ onSelectSession,
259
+ optimisticReadAtBySessionKey,
260
+ resolvedChildTabs,
261
+ workspaceFileTabs,
262
+ ]);
263
+
264
+ if (!activeSelection) {
265
+ return null;
266
+ }
267
+
268
+ return (
269
+ <aside className="hidden shrink-0 border-l border-gray-200/70 bg-white/95 backdrop-blur-sm md:flex md:w-[26rem] lg:w-[30rem] xl:w-[34rem]">
270
+ <div className="flex h-full min-h-0 w-full flex-col">
271
+ <div className="flex items-center justify-between gap-3 border-b border-gray-200/70 px-4 py-2.5">
272
+ <button
273
+ type="button"
274
+ onClick={onBackToParent}
275
+ className={cn(
276
+ "inline-flex items-center gap-1 text-xs font-medium text-gray-600 transition-colors hover:text-gray-900",
277
+ !hasParentSession && "pointer-events-none opacity-0",
278
+ )}
279
+ >
280
+ <ArrowLeft className="h-3.5 w-3.5" />
281
+ <span>{t("chatBackToParent")}</span>
282
+ </button>
283
+ <button
284
+ type="button"
285
+ onClick={onClose}
286
+ className="rounded-full border border-gray-200/80 p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-900"
287
+ aria-label={t("chatWorkspaceClosePanel")}
288
+ >
289
+ <X className="h-4 w-4" />
290
+ </button>
291
+ </div>
292
+
293
+ <WorkspaceTabsBar tabs={workspaceTabs} />
294
+
295
+ <div className="flex min-h-0 flex-1 flex-col bg-white">
296
+ {activeSelection.kind === "child-session" ? (
297
+ <>
298
+ <WorkspaceActiveChildHeader tab={activeSelection.tab} />
299
+ <div className="flex-1 min-h-0">
300
+ <ChildSessionContent
301
+ sessionKey={activeSelection.tab.sessionKey}
302
+ onToolAction={onToolAction}
303
+ onFileOpen={onFileOpen}
304
+ />
305
+ </div>
306
+ </>
307
+ ) : (
308
+ <ChatSessionWorkspaceFilePreview
309
+ file={activeSelection.file}
310
+ sessionProjectRoot={sessionProjectRoot}
311
+ onFileOpen={onFileOpen}
312
+ />
313
+ )}
314
+ </div>
315
+ </div>
316
+ </aside>
317
+ );
318
+ }
@@ -1,11 +1,11 @@
1
1
  import { useMemo, useState, type ReactNode } from 'react';
2
2
  import { Plus } from 'lucide-react';
3
3
  import { Button } from '@/components/ui/button';
4
+ import { ChatSessionTypeOptionItem } from '@/components/chat/chat-session-type-option-item';
4
5
  import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
5
6
  import type { ChatInputSnapshot } from '@/components/chat/stores/chat-input.store';
6
7
  import type { NcpSessionListItemView } from '@/components/chat/ncp/use-ncp-session-list-view';
7
8
  import { t } from '@/lib/i18n';
8
- import { cn } from '@/lib/utils';
9
9
 
10
10
  export type ChatSidebarProjectGroup = {
11
11
  projectRoot: string;
@@ -34,16 +34,6 @@ function resolveProjectGroupDefaultSessionType(
34
34
  return sessionTypeOptions[0]?.value ?? defaultSessionType;
35
35
  }
36
36
 
37
- function resolveSessionTypeStatusText(option: {
38
- ready?: boolean;
39
- reasonMessage?: string | null;
40
- }): string {
41
- if (option.ready === false) {
42
- return option.reasonMessage?.trim() || t('statusSetup');
43
- }
44
- return t('statusReady');
45
- }
46
-
47
37
  export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
48
38
  const { groups, defaultSessionType, sessionTypeOptions, renderSessionItem, onCreateSession } = props;
49
39
  const [openProjectRoot, setOpenProjectRoot] = useState<string | null>(null);
@@ -91,38 +81,23 @@ export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
91
81
  <Plus className="h-3.5 w-3.5" />
92
82
  </Button>
93
83
  </PopoverTrigger>
94
- <PopoverContent align="end" className="w-64 p-2">
95
- <div className="px-2 py-1 text-[11px] font-medium uppercase tracking-wider text-gray-400">
84
+ <PopoverContent
85
+ align="end"
86
+ className="w-56 rounded-2xl border border-gray-200/80 bg-white p-1.5 shadow-[0_24px_60px_-28px_rgba(15,23,42,0.38)]"
87
+ >
88
+ <div className="px-3 pb-1 pt-2 text-[10px] font-semibold uppercase tracking-[0.18em] text-gray-400">
96
89
  {t('chatSessionTypeLabel')}
97
90
  </div>
98
- <div className="mt-1 space-y-1">
91
+ <div className="space-y-1">
99
92
  {sessionTypeOptions.map((option) => (
100
- <button
93
+ <ChatSessionTypeOptionItem
101
94
  key={`${group.projectRoot}:${option.value}`}
102
- type="button"
103
- onClick={() => {
95
+ option={option}
96
+ onSelect={() => {
104
97
  onCreateSession(option.value, group.projectRoot);
105
98
  setOpenProjectRoot(null);
106
99
  }}
107
- className="w-full rounded-xl px-3 py-2 text-left transition-colors hover:bg-gray-100"
108
- >
109
- <div className="flex items-center justify-between gap-3">
110
- <div className="text-[13px] font-medium text-gray-900">{option.label}</div>
111
- <span
112
- className={cn(
113
- 'shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold',
114
- option.ready === false
115
- ? 'bg-amber-100 text-amber-800'
116
- : 'bg-emerald-100 text-emerald-700'
117
- )}
118
- >
119
- {option.ready === false ? t('statusSetup') : t('statusReady')}
120
- </span>
121
- </div>
122
- <div className="mt-0.5 text-[11px] text-gray-500">
123
- {resolveSessionTypeStatusText(option)}
124
- </div>
125
- </button>
100
+ />
126
101
  ))}
127
102
  </div>
128
103
  </PopoverContent>
@@ -8,7 +8,7 @@ import { type SessionContextView } from '@/lib/session-context.utils';
8
8
  import type { SessionRunStatus } from '@/lib/session-run-status';
9
9
  import { cn } from '@/lib/utils';
10
10
  import { formatDateTime, t } from '@/lib/i18n';
11
- import { Check, Pencil, X } from 'lucide-react';
11
+ import { Check, GitBranch, Pencil, X } from 'lucide-react';
12
12
 
13
13
  type ChatSidebarSessionItemProps = {
14
14
  session: SessionEntryView;
@@ -20,10 +20,12 @@ type ChatSidebarSessionItemProps = {
20
20
  agentId?: string | null;
21
21
  agentLabel?: string | null;
22
22
  agentAvatarUrl?: string | null;
23
+ childSessionCount?: number;
23
24
  isEditing: boolean;
24
25
  draftLabel: string;
25
26
  isSaving: boolean;
26
27
  onSelect: () => void;
28
+ onOpenChildSessions?: () => void;
27
29
  onStartEditing: () => void;
28
30
  onDraftLabelChange: (value: string) => void;
29
31
  onSave: () => void | Promise<void>;
@@ -108,7 +110,9 @@ function ChatSidebarSessionDisplayView({
108
110
  agentId,
109
111
  agentLabel,
110
112
  agentAvatarUrl,
113
+ childSessionCount = 0,
111
114
  onSelect,
115
+ onOpenChildSessions,
112
116
  onStartEditing
113
117
  }: ChatSidebarSessionDisplayViewProps) {
114
118
  const iconTone = active ? 'text-gray-700' : 'text-gray-500';
@@ -162,9 +166,31 @@ function ChatSidebarSessionDisplayView({
162
166
  </span>
163
167
  </div>
164
168
  <div className="mt-0.5 text-[11px] text-gray-400 truncate">
165
- {agentLabel?.trim() ? `${agentLabel} · ` : ''}{session.messageCount} · {formatDateTime(session.updatedAt)}
169
+ <span>
170
+ {agentLabel?.trim() ? `${agentLabel} · ` : ''}{session.messageCount} · {formatDateTime(session.updatedAt)}
171
+ </span>
166
172
  </div>
167
173
  </button>
174
+ {childSessionCount > 0 && onOpenChildSessions ? (
175
+ <button
176
+ type="button"
177
+ onClick={(event) => {
178
+ event.stopPropagation();
179
+ onOpenChildSessions();
180
+ }}
181
+ className={cn(
182
+ 'absolute right-7 top-0 inline-flex h-7 items-center gap-1 rounded-lg px-1.5 text-[10px] font-medium text-gray-400 transition-all hover:bg-white hover:text-gray-900',
183
+ active
184
+ ? 'opacity-100'
185
+ : 'opacity-0 group-hover/session:opacity-100 group-focus-within/session:opacity-100'
186
+ )}
187
+ aria-label={t('chatSessionOpenChildSessions')}
188
+ title={t('chatSessionOpenChildSessions')}
189
+ >
190
+ <GitBranch className="h-3.5 w-3.5" />
191
+ <span>{childSessionCount}</span>
192
+ </button>
193
+ ) : null}
168
194
  <button
169
195
  type="button"
170
196
  onClick={(event) => {
@@ -195,10 +221,12 @@ export function ChatSidebarSessionItem({
195
221
  agentId,
196
222
  agentLabel,
197
223
  agentAvatarUrl,
224
+ childSessionCount,
198
225
  isEditing,
199
226
  draftLabel,
200
227
  isSaving,
201
228
  onSelect,
229
+ onOpenChildSessions,
202
230
  onStartEditing,
203
231
  onDraftLabelChange,
204
232
  onSave,
@@ -234,6 +262,8 @@ export function ChatSidebarSessionItem({
234
262
  agentLabel={agentLabel}
235
263
  agentAvatarUrl={agentAvatarUrl}
236
264
  onSelect={onSelect}
265
+ childSessionCount={childSessionCount}
266
+ onOpenChildSessions={onOpenChildSessions}
237
267
  onStartEditing={onStartEditing}
238
268
  />
239
269
  )}
@@ -147,6 +147,55 @@ it("adapts persisted inline token metadata into rich message parts", () => {
147
147
  });
148
148
  });
149
149
 
150
+ it("keeps Hermes tool invocation parts as tool cards instead of flattening them into plain text", () => {
151
+ const message = {
152
+ id: "assistant-hermes-tool-1",
153
+ sessionId: "session-1",
154
+ role: "assistant",
155
+ status: "final",
156
+ timestamp: "2026-04-16T00:00:00.000Z",
157
+ parts: [
158
+ {
159
+ type: "reasoning",
160
+ text: "The user wants Python files.",
161
+ },
162
+ {
163
+ type: "text",
164
+ text: "\n",
165
+ },
166
+ {
167
+ type: "tool-invocation",
168
+ toolCallId: "hermes-inline-tool-1",
169
+ toolName: "search_files",
170
+ state: "call",
171
+ args: "{\"pattern\":\"*.py\"}",
172
+ },
173
+ {
174
+ type: "text",
175
+ text: "\nFound them.",
176
+ },
177
+ ],
178
+ } satisfies NcpMessage;
179
+
180
+ render(<ChatMessageListContainer messages={[message]} isSending={false} />);
181
+
182
+ const renderedMessages =
183
+ captures.renders[captures.renders.length - 1]?.messages ?? [];
184
+ expect(renderedMessages[0]).toMatchObject({
185
+ parts: expect.arrayContaining([
186
+ expect.objectContaining({
187
+ type: "tool-card",
188
+ card: expect.objectContaining({
189
+ toolName: "search_files",
190
+ titleLabel: "chatToolCall",
191
+ statusTone: "running",
192
+ statusLabel: "chatToolStatusRunning",
193
+ }),
194
+ }),
195
+ ]),
196
+ });
197
+ });
198
+
150
199
  it("passes localized attachment card texts to the shared chat UI", () => {
151
200
  captures.language = "zh";
152
201
 
@@ -1,6 +1,7 @@
1
1
  import { useMemo } from "react";
2
2
  import type { NcpMessage } from "@nextclaw/ncp";
3
3
  import {
4
+ type ChatFileOpenActionViewModel,
4
5
  type ChatToolActionViewModel,
5
6
  type ChatMessageViewModel,
6
7
  ChatMessageList,
@@ -21,6 +22,7 @@ type ChatMessageListContainerProps = {
21
22
  isSending: boolean;
22
23
  className?: string;
23
24
  onToolAction?: (action: ChatToolActionViewModel) => void;
25
+ onFileOpen?: (action: ChatFileOpenActionViewModel) => void;
24
26
  };
25
27
 
26
28
  const messageViewModelCache = new WeakMap<
@@ -87,6 +89,7 @@ export function ChatMessageListContainer({
87
89
  isSending,
88
90
  className,
89
91
  onToolAction,
92
+ onFileOpen,
90
93
  }: ChatMessageListContainerProps) {
91
94
  const { language } = useI18n();
92
95
  const texts = useMemo<ChatMessageAdapterTexts>(
@@ -144,6 +147,7 @@ export function ChatMessageListContainer({
144
147
  className={className}
145
148
  texts={messageTexts}
146
149
  onToolAction={onToolAction}
150
+ onFileOpen={onFileOpen}
147
151
  renderToolAgent={(agentId) => (
148
152
  <AgentIdentityAvatar
149
153
  agentId={agentId}
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
2
  import { ChatSessionListManager } from '@/components/chat/managers/chat-session-list.manager';
3
3
  import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
4
4
  import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
5
+ import { useChatThreadStore } from '@/components/chat/stores/chat-thread.store';
5
6
 
6
7
  const mocks = vi.hoisted(() => ({
7
8
  updateNcpSession: vi.fn(),
@@ -33,6 +34,14 @@ describe('ChatSessionListManager', () => {
33
34
  listMode: 'time-first'
34
35
  }
35
36
  });
37
+ useChatThreadStore.setState({
38
+ snapshot: {
39
+ ...useChatThreadStore.getState().snapshot,
40
+ workspacePanelParentKey: 'session-1',
41
+ activeChildSessionKey: 'child-session-1',
42
+ activeWorkspaceFileKey: 'session-1::/tmp/demo.md',
43
+ },
44
+ });
36
45
  });
37
46
 
38
47
  it('applies the requested session type when creating a session', () => {
@@ -133,6 +142,9 @@ describe('ChatSessionListManager', () => {
133
142
 
134
143
  expect(uiManager.goToSession).toHaveBeenCalledWith('session-2');
135
144
  expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBe('session-1');
145
+ expect(useChatThreadStore.getState().snapshot.workspacePanelParentKey).toBeNull();
146
+ expect(useChatThreadStore.getState().snapshot.activeChildSessionKey).toBeNull();
147
+ expect(useChatThreadStore.getState().snapshot.activeWorkspaceFileKey).toBeNull();
136
148
  });
137
149
 
138
150
  it('updates the sidebar list mode without touching other session list state', () => {
@@ -25,8 +25,10 @@ export class ChatSessionListManager {
25
25
  isAwaitingAssistantOutput: false,
26
26
  parentSessionKey: null,
27
27
  parentSessionLabel: null,
28
+ workspacePanelParentKey: null,
28
29
  childSessionTabs: [],
29
30
  activeChildSessionKey: null,
31
+ activeWorkspaceFileKey: null,
30
32
  });
31
33
  };
32
34
 
@@ -157,6 +159,11 @@ export class ChatSessionListManager {
157
159
  };
158
160
 
159
161
  selectSession = (sessionKey: string) => {
162
+ useChatThreadStore.getState().setSnapshot({
163
+ workspacePanelParentKey: null,
164
+ activeChildSessionKey: null,
165
+ activeWorkspaceFileKey: null,
166
+ });
160
167
  this.uiManager.goToSession(sessionKey);
161
168
  };
162
169