@nextclaw/ui 0.12.7 → 0.12.9

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 (171) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/dist/assets/ChannelsList-Ita2Zm1_.js +8 -0
  3. package/dist/assets/{DocBrowser-Cse_F8Nn.js → DocBrowser-6ReNjvzF.js} +1 -1
  4. package/dist/assets/DocBrowser-BNwbPHf4.js +1 -0
  5. package/dist/assets/{DocBrowserContext-Bai1WU2H.js → DocBrowserContext-B6SpA7Qs.js} +1 -1
  6. package/dist/assets/{LogoBadge-BdxMPc9v.js → LogoBadge-ByNLYg65.js} +1 -1
  7. package/dist/assets/MarketplacePage-CjX2MWww.js +1 -0
  8. package/dist/assets/{MarketplacePage-BbpAkllU.js → MarketplacePage-D0sDlYX4.js} +1 -1
  9. package/dist/assets/McpMarketplacePage-BGKJm1sJ.js +40 -0
  10. package/dist/assets/{ModelConfig-3GLqQ5GY.js → ModelConfig-BzZenCH-.js} +1 -1
  11. package/dist/assets/{ProviderScopedModelInput-BYNouw-i.js → ProviderScopedModelInput-Da7khnBA.js} +1 -1
  12. package/dist/assets/{ProvidersList-BR1gJ4Dm.js → ProvidersList-BbVzRxjY.js} +1 -1
  13. package/dist/assets/RemoteAccessPage-BaDH_X1Q.js +1 -0
  14. package/dist/assets/RuntimeConfig-F_XKGgLm.js +1 -0
  15. package/dist/assets/{SearchConfig-DTeJvp8m.js → SearchConfig-BGkzXQP-.js} +1 -1
  16. package/dist/assets/{SecretsConfig-CCYO6NcV.js → SecretsConfig-D281Rotl.js} +2 -2
  17. package/dist/assets/{SessionsConfig-Du39vDgt.js → SessionsConfig-ChHQ7M5c.js} +2 -2
  18. package/dist/assets/{app-query-client-Dr5d-K8d.js → app-query-client-VnFElj4E.js} +1 -1
  19. package/dist/assets/{book-open-Da4OEPqB.js → book-open-BdcxxoQu.js} +1 -1
  20. package/dist/assets/chat-page-Doe0yTtB.js +58 -0
  21. package/dist/assets/chat-session-display-cw78aiI_.js +1 -0
  22. package/dist/assets/{chunk-JZWAC4HX-CoFVxHXV.js → chunk-JZWAC4HX-DK5HPmIK.js} +1 -1
  23. package/dist/assets/{client-CSk58DcF.js → client-_i4MU2bB.js} +1 -1
  24. package/dist/assets/{config-D8KzikVB.js → config-DtIQwrHF.js} +1 -1
  25. package/dist/assets/{createLucideIcon-83gaZMtv.js → createLucideIcon-BSeTgkZW.js} +1 -1
  26. package/dist/assets/desktop-update-config-Dpcf4BKG.js +1 -0
  27. package/dist/assets/{dist-toEYs-MZ.js → dist-6TrrnPCR.js} +1 -1
  28. package/dist/assets/{dist-aTmhMDVh.js → dist-ccBFUi-o.js} +1 -1
  29. package/dist/assets/download-BhDxnyvU.js +1 -0
  30. package/dist/assets/{external-link-QQ0TC6X4.js → external-link-BgErLCNT.js} +1 -1
  31. package/dist/assets/{hash-DaFBEkmi.js → hash-Bl7dr_UG.js} +1 -1
  32. package/dist/assets/i18n-eDHeDY0n.js +1 -0
  33. package/dist/assets/index-CF9xve0E.js +6 -0
  34. package/dist/assets/index-FgA52VBt.css +1 -0
  35. package/dist/assets/{infiniteQueryBehavior-BmHX_ayZ.js → infiniteQueryBehavior-ZDS92Qpp.js} +1 -1
  36. package/dist/assets/loader-circle-ACM1s51e.js +1 -0
  37. package/dist/assets/{logos-Dzlz30M3.js → logos-x89HbrZ4.js} +1 -1
  38. package/dist/assets/{page-layout-D2eRufRQ.js → page-layout-vZnghcFy.js} +1 -1
  39. package/dist/assets/play-CFUwCA2E.js +1 -0
  40. package/dist/assets/plus-rYsv72JG.js +1 -0
  41. package/dist/assets/{popover-BSXxm5bj.js → popover-Bg1VoTZ6.js} +1 -1
  42. package/dist/assets/{refresh-ccw-B3zMtN-_.js → refresh-ccw-DT98i__E.js} +1 -1
  43. package/dist/assets/{refresh-cw-DlZkIHnJ.js → refresh-cw-C47QSEwg.js} +1 -1
  44. package/dist/assets/rotate-cw-JtFzpNn6.js +1 -0
  45. package/dist/assets/{save-Us9fg4Sj.js → save-3S6-H3Xw.js} +1 -1
  46. package/dist/assets/search-3kFR_zh9.js +1 -0
  47. package/dist/assets/{security-config-BGWYwxNr.js → security-config-BWaiARNk.js} +1 -1
  48. package/dist/assets/{select-DLYqySQK.js → select-DJ2MUjBB.js} +1 -1
  49. package/dist/assets/skeleton-ByQepn0M.js +1 -0
  50. package/dist/assets/{status-dot-DGayudyB.js → status-dot-vbanNPFU.js} +1 -1
  51. package/dist/assets/{switch-Dz2ScsKx.js → switch-BsLtHOH-.js} +1 -1
  52. package/dist/assets/{tabs-custom-CdKyjiGk.js → tabs-custom-D3HYMt6k.js} +1 -1
  53. package/dist/assets/{trash-2-Db-mZOZs.js → trash-2-G48scll7.js} +1 -1
  54. package/dist/assets/{use-infinite-scroll-loader-DBJX5hj0.js → use-infinite-scroll-loader-DkNhD-42.js} +1 -1
  55. package/dist/assets/{useConfirmDialog-DL0a-oGC.js → useConfirmDialog-BkvTN-vd.js} +1 -1
  56. package/dist/assets/{useMutation-BdZm-9PL.js → useMutation-CBWjE2uj.js} +1 -1
  57. package/dist/assets/x-ByDbItbq.js +1 -0
  58. package/dist/index.html +95 -21
  59. package/dist/manifest.webmanifest +30 -0
  60. package/dist/offline.html +102 -0
  61. package/dist/pwa-192.png +0 -0
  62. package/dist/pwa-512.png +0 -0
  63. package/dist/sw.js +80 -0
  64. package/index.html +73 -1
  65. package/package.json +6 -6
  66. package/public/manifest.webmanifest +30 -0
  67. package/public/offline.html +102 -0
  68. package/public/pwa-192.png +0 -0
  69. package/public/pwa-512.png +0 -0
  70. package/public/sw.js +80 -0
  71. package/src/api/runtime-control.ts +34 -0
  72. package/src/api/runtime-control.types.ts +58 -0
  73. package/src/api/server-path.ts +27 -4
  74. package/src/api/types.ts +30 -10
  75. package/src/{App.test.tsx → app.test.tsx} +1 -1
  76. package/src/{App.tsx → app.tsx} +10 -1
  77. package/src/components/chat/ChatSidebar.test.tsx +79 -8
  78. package/src/components/chat/ChatSidebar.tsx +43 -26
  79. package/src/components/chat/adapters/chat-message.summary-truncation.test.ts +66 -0
  80. package/src/components/chat/adapters/file-operation/card.ts +9 -0
  81. package/src/components/chat/adapters/file-operation/diff.ts +14 -0
  82. package/src/components/chat/{ChatConversationPanel.test.tsx → chat-conversation-panel.test.tsx} +118 -155
  83. package/src/components/chat/chat-conversation-panel.tsx +412 -0
  84. package/src/components/chat/chat-page-runtime.test.ts +1 -1
  85. package/src/components/chat/chat-page-shell.tsx +1 -1
  86. package/src/components/chat/{ChatPage.tsx → chat-page.tsx} +1 -1
  87. package/src/components/chat/chat-session-workspace-file-preview.test.tsx +91 -0
  88. package/src/components/chat/chat-session-workspace-file-preview.tsx +307 -0
  89. package/src/components/chat/chat-session-workspace-panel-nav.tsx +197 -0
  90. package/src/components/chat/chat-session-workspace-panel.tsx +318 -0
  91. package/src/components/chat/chat-sidebar-session-item.tsx +32 -2
  92. package/src/components/chat/containers/chat-message-list.container.test.tsx +49 -0
  93. package/src/components/chat/containers/chat-message-list.container.tsx +4 -0
  94. package/src/components/chat/managers/chat-session-list.manager.test.ts +94 -31
  95. package/src/components/chat/managers/chat-session-list.manager.ts +86 -14
  96. package/src/components/chat/managers/chat-ui.manager.ts +2 -0
  97. package/src/components/chat/ncp/README.md +1 -1
  98. package/src/components/chat/ncp/ncp-chat-input.manager.ts +7 -1
  99. package/src/components/chat/ncp/ncp-chat-page-data.test.ts +1 -1
  100. package/src/components/chat/ncp/{NcpChatPage.tsx → ncp-chat-page.tsx} +7 -7
  101. package/src/components/chat/ncp/ncp-chat-thread.manager.ts +179 -41
  102. package/src/components/chat/ncp/ncp-session-adapter.test.ts +40 -2
  103. package/src/components/chat/ncp/ncp-session-adapter.ts +29 -0
  104. package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +54 -11
  105. package/src/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view.ts +4 -0
  106. package/src/components/chat/ncp/tests/ncp-chat-input.manager.test.ts +99 -0
  107. package/src/components/chat/ncp/tests/ncp-chat-thread.manager.test.ts +189 -0
  108. package/src/components/chat/presenter/chat-presenter-context.tsx +13 -2
  109. package/src/components/chat/session-header/chat-session-header-actions.test.tsx +26 -0
  110. package/src/components/chat/session-header/chat-session-header-actions.tsx +19 -1
  111. package/src/components/chat/stores/chat-session-list.store.ts +25 -54
  112. package/src/components/chat/stores/chat-thread.store.ts +24 -0
  113. package/src/components/common/ProviderScopedModelInput.tsx +12 -2
  114. package/src/components/config/ModelConfig.test.tsx +108 -2
  115. package/src/components/config/RuntimeConfig.tsx +154 -7
  116. package/src/components/config/desktop-update-config.test.tsx +85 -0
  117. package/src/components/config/desktop-update-config.tsx +44 -3
  118. package/src/components/config/runtime-control-card.test.tsx +255 -0
  119. package/src/components/config/runtime-control-card.tsx +301 -0
  120. package/src/components/config/runtime-presence-card.test.tsx +154 -0
  121. package/src/components/config/runtime-presence-card.tsx +163 -0
  122. package/src/components/layout/AppLayout.tsx +1 -1
  123. package/src/components/providers/ThemeProvider.tsx +5 -0
  124. package/src/desktop/desktop-update.types.ts +25 -0
  125. package/src/desktop/managers/desktop-presence.manager.ts +91 -0
  126. package/src/desktop/managers/desktop-update.manager.ts +37 -1
  127. package/src/desktop/stores/desktop-presence.store.ts +18 -0
  128. package/src/desktop/stores/desktop-update.store.ts +7 -1
  129. package/src/hooks/server-path/use-server-path-read.ts +20 -0
  130. package/src/hooks/use-runtime-control.ts +24 -0
  131. package/src/lib/chat-message.ts +14 -3
  132. package/src/lib/desktop-update-labels.utils.ts +28 -2
  133. package/src/lib/i18n.chat.ts +12 -1
  134. package/src/lib/i18n.pwa.ts +62 -0
  135. package/src/lib/i18n.runtime-control.ts +120 -0
  136. package/src/lib/i18n.ts +4 -6
  137. package/src/main.tsx +1 -1
  138. package/src/pwa/components/pwa-install-entry.test.tsx +110 -0
  139. package/src/pwa/components/pwa-install-entry.tsx +205 -0
  140. package/src/pwa/managers/pwa-install.manager.test.ts +160 -0
  141. package/src/pwa/managers/pwa-install.manager.ts +232 -0
  142. package/src/pwa/managers/pwa-runtime.manager.ts +196 -0
  143. package/src/pwa/managers/pwa-shell-theme.manager.test.ts +30 -0
  144. package/src/pwa/managers/pwa-shell-theme.manager.ts +46 -0
  145. package/src/pwa/pwa-install-banner.storage.ts +55 -0
  146. package/src/pwa/pwa.types.ts +22 -0
  147. package/src/pwa/register-pwa.ts +14 -0
  148. package/src/pwa/stores/pwa.store.ts +17 -0
  149. package/src/runtime-control/runtime-control.manager.ts +118 -0
  150. package/src/vite-env.d.ts +9 -0
  151. package/dist/assets/ChannelsList-D8p4OlM6.js +0 -8
  152. package/dist/assets/ChatPage-A45t1Rmf.js +0 -58
  153. package/dist/assets/DocBrowser-B2MpsnU9.js +0 -1
  154. package/dist/assets/MarketplacePage-BNZ3Jx5d.js +0 -1
  155. package/dist/assets/McpMarketplacePage-CxPFOgxv.js +0 -40
  156. package/dist/assets/RemoteAccessPage-DyYVWsyK.js +0 -1
  157. package/dist/assets/RuntimeConfig-ChdfK4Y_.js +0 -1
  158. package/dist/assets/chat-session-display-CAlPrnlV.js +0 -1
  159. package/dist/assets/desktop-update-config-CfoVwf-w.js +0 -1
  160. package/dist/assets/i18n-C3jb83S6.js +0 -1
  161. package/dist/assets/index-CE4N7ItL.css +0 -1
  162. package/dist/assets/index-riX7Sg0_.js +0 -6
  163. package/dist/assets/loader-circle-BjMg63eu.js +0 -1
  164. package/dist/assets/plus-CIXME2pD.js +0 -1
  165. package/dist/assets/search-B_Qr0f6C.js +0 -1
  166. package/dist/assets/skeleton-CYQJazv6.js +0 -1
  167. package/dist/assets/x-B8Tho_xC.js +0 -1
  168. package/src/components/chat/ChatConversationPanel.tsx +0 -256
  169. package/src/components/chat/chat-child-session-panel.tsx +0 -262
  170. /package/dist/assets/{config-hints-GSUMvmSo.js → config-hints-BhTmc9P1.js} +0 -0
  171. /package/dist/assets/{config-layout-CgBMG7OL.js → config-layout-CHs0mAaR.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
+ }
@@ -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}