@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,412 @@
1
+ import { type ComponentProps, useRef } from "react";
2
+ import { ArrowLeft } from "lucide-react";
3
+ import { useStickyBottomScroll } from "@nextclaw/agent-chat-ui";
4
+ import type { ChatFileOpenActionViewModel } from "@nextclaw/agent-chat-ui";
5
+ import {
6
+ ChatInputBarContainer,
7
+ ChatMessageListContainer,
8
+ } from "@/components/chat/nextclaw";
9
+ import { ChatWelcome } from "@/components/chat/ChatWelcome";
10
+ import { ChatSessionWorkspacePanel } from "@/components/chat/chat-session-workspace-panel";
11
+ import { AgentAvatar } from "@/components/common/AgentAvatar";
12
+ import { usePresenter } from "@/components/chat/presenter/chat-presenter-context";
13
+ import { ChatSessionHeaderActions } from "@/components/chat/session-header/chat-session-header-actions";
14
+ import { ChatSessionProjectBadge } from "@/components/chat/session-header/chat-session-project-badge";
15
+ import { useChatInputStore } from "@/components/chat/stores/chat-input.store";
16
+ import { useChatThreadStore } from "@/components/chat/stores/chat-thread.store";
17
+ import { resolveAgentRuntimeSessionType } from "@/components/chat/useChatSessionTypeState";
18
+ import { t } from "@/lib/i18n";
19
+ import { cn } from "@/lib/utils";
20
+
21
+ function ChatConversationSkeleton() {
22
+ return (
23
+ <section className="flex-1 min-h-0 flex flex-col overflow-hidden bg-gradient-to-b from-gray-50/60 to-white">
24
+ <div className="flex-1 min-h-0 overflow-y-auto custom-scrollbar">
25
+ <div className="mx-auto w-full max-w-[min(1120px,100%)] px-6 py-5">
26
+ <div className="space-y-4">
27
+ <div className="h-6 w-48 animate-pulse rounded bg-gray-200" />
28
+ <div className="h-24 w-[78%] animate-pulse rounded-2xl bg-gray-200/80" />
29
+ <div className="h-20 w-[62%] animate-pulse rounded-2xl bg-gray-200/80" />
30
+ <div className="h-28 w-[84%] animate-pulse rounded-2xl bg-gray-200/80" />
31
+ </div>
32
+ </div>
33
+ </div>
34
+ <div className="border-t border-gray-200/80 bg-white p-4">
35
+ <div className="mx-auto w-full max-w-[min(1120px,100%)]">
36
+ <div className="rounded-2xl border border-gray-200 bg-white shadow-card p-4">
37
+ <div className="h-16 w-full animate-pulse rounded-xl bg-gray-200/80" />
38
+ <div className="mt-3 flex items-center justify-between">
39
+ <div className="h-8 w-36 animate-pulse rounded-lg bg-gray-200/80" />
40
+ <div className="h-8 w-20 animate-pulse rounded-lg bg-gray-200/80" />
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ </section>
46
+ );
47
+ }
48
+
49
+ type ChatThreadSnapshot = ReturnType<typeof useChatThreadStore.getState>["snapshot"];
50
+ type ChatHeaderDeleteHandler = ComponentProps<
51
+ typeof ChatSessionHeaderActions
52
+ >["onDeleteSession"];
53
+ type ChatToolActionHandler = ComponentProps<
54
+ typeof ChatMessageListContainer
55
+ >["onToolAction"];
56
+ type ChatFileOpenHandler = ComponentProps<
57
+ typeof ChatMessageListContainer
58
+ >["onFileOpen"];
59
+
60
+ type ChatParentSessionBannerProps = {
61
+ parentSessionLabel: string | null;
62
+ onGoToParentSession: () => void;
63
+ };
64
+
65
+ function ChatParentSessionBanner({
66
+ parentSessionLabel,
67
+ onGoToParentSession,
68
+ }: ChatParentSessionBannerProps) {
69
+ if (!parentSessionLabel) {
70
+ return null;
71
+ }
72
+ const trimmedLabel = parentSessionLabel.trim();
73
+ return (
74
+ <div className="border-b border-gray-200/60 bg-white/75 px-5 py-2 backdrop-blur-sm">
75
+ <button
76
+ type="button"
77
+ onClick={onGoToParentSession}
78
+ className="inline-flex items-center gap-2 text-xs font-medium text-gray-600 transition-colors hover:text-gray-900"
79
+ >
80
+ <ArrowLeft className="h-3.5 w-3.5" />
81
+ <span>
82
+ {t("chatBackToParent")}
83
+ {trimmedLabel ? ` · ${trimmedLabel}` : ""}
84
+ </span>
85
+ </button>
86
+ </div>
87
+ );
88
+ }
89
+
90
+ type ChatConversationHeaderProps = {
91
+ snapshot: ChatThreadSnapshot;
92
+ childSessionCount: number;
93
+ normalizedAgentId: string;
94
+ sessionHeaderTitle: string;
95
+ shouldShowHeaderAgentAvatar: boolean;
96
+ shouldShowSessionHeader: boolean;
97
+ onOpenChildSessions: () => void;
98
+ onDeleteSession: ChatHeaderDeleteHandler;
99
+ };
100
+
101
+ function ChatConversationHeader({
102
+ snapshot,
103
+ childSessionCount,
104
+ normalizedAgentId,
105
+ sessionHeaderTitle,
106
+ shouldShowHeaderAgentAvatar,
107
+ shouldShowSessionHeader,
108
+ onOpenChildSessions,
109
+ onDeleteSession,
110
+ }: ChatConversationHeaderProps) {
111
+ return (
112
+ <div
113
+ className={cn(
114
+ "px-5 border-b border-gray-200/60 bg-white/80 backdrop-blur-sm flex items-center justify-between shrink-0 overflow-hidden transition-all duration-200",
115
+ shouldShowSessionHeader ? "py-3 opacity-100" : "h-0 py-0 opacity-0 border-b-0",
116
+ )}
117
+ >
118
+ <div className="min-w-0 flex-1 flex items-center gap-2">
119
+ {shouldShowHeaderAgentAvatar ? (
120
+ <div className="inline-flex shrink-0 items-center">
121
+ <AgentAvatar
122
+ agentId={normalizedAgentId}
123
+ displayName={snapshot.agentDisplayName}
124
+ avatarUrl={snapshot.agentAvatarUrl}
125
+ className="h-5 w-5"
126
+ />
127
+ </div>
128
+ ) : null}
129
+ <span className="text-sm font-medium text-gray-700 truncate">
130
+ {sessionHeaderTitle}
131
+ </span>
132
+ {snapshot.sessionTypeLabel ? (
133
+ <span className="shrink-0 rounded-full border border-gray-200 bg-gray-100 px-2 py-0.5 text-[11px] font-medium text-gray-600">
134
+ {snapshot.sessionTypeLabel}
135
+ </span>
136
+ ) : null}
137
+ {snapshot.sessionProjectName ? (
138
+ <ChatSessionProjectBadge
139
+ sessionKey={snapshot.sessionKey ?? "draft"}
140
+ projectName={snapshot.sessionProjectName}
141
+ projectRoot={snapshot.sessionProjectRoot}
142
+ persistToServer={snapshot.canDeleteSession}
143
+ />
144
+ ) : null}
145
+ </div>
146
+ {snapshot.sessionKey ? (
147
+ <ChatSessionHeaderActions
148
+ sessionKey={snapshot.sessionKey}
149
+ canDeleteSession={snapshot.canDeleteSession}
150
+ isDeletePending={snapshot.isDeletePending}
151
+ projectRoot={snapshot.sessionProjectRoot}
152
+ childSessionCount={childSessionCount}
153
+ onOpenChildSessions={onOpenChildSessions}
154
+ onDeleteSession={onDeleteSession}
155
+ />
156
+ ) : null}
157
+ </div>
158
+ );
159
+ }
160
+
161
+ type ChatConversationAlertsProps = {
162
+ shouldShowProviderHint: boolean;
163
+ sessionTypeUnavailable: boolean;
164
+ sessionTypeUnavailableMessage: string | null;
165
+ onGoToProviders: () => void;
166
+ };
167
+
168
+ function ChatConversationAlerts({
169
+ shouldShowProviderHint,
170
+ sessionTypeUnavailable,
171
+ sessionTypeUnavailableMessage,
172
+ onGoToProviders,
173
+ }: ChatConversationAlertsProps) {
174
+ return (
175
+ <>
176
+ {shouldShowProviderHint ? (
177
+ <div className="px-5 py-2.5 border-b border-amber-200/70 bg-amber-50/70 flex items-center justify-between gap-3 shrink-0">
178
+ <span className="text-xs text-amber-800">
179
+ {t("chatModelNoOptions")}
180
+ </span>
181
+ <button
182
+ type="button"
183
+ onClick={onGoToProviders}
184
+ className="text-xs font-semibold text-amber-900 underline-offset-2 hover:underline"
185
+ >
186
+ {t("chatGoConfigureProvider")}
187
+ </button>
188
+ </div>
189
+ ) : null}
190
+ {sessionTypeUnavailable && sessionTypeUnavailableMessage?.trim() ? (
191
+ <div className="px-5 py-2.5 border-b border-amber-200/70 bg-amber-50/70 shrink-0">
192
+ <span className="text-xs text-amber-800">
193
+ {sessionTypeUnavailableMessage}
194
+ </span>
195
+ </div>
196
+ ) : null}
197
+ </>
198
+ );
199
+ }
200
+
201
+ type ChatConversationContentProps = {
202
+ snapshot: ChatThreadSnapshot;
203
+ availableAgents: NonNullable<ChatThreadSnapshot["availableAgents"]>;
204
+ hideEmptyHint: boolean;
205
+ showWelcome: boolean;
206
+ threadRef: ComponentProps<"div">["ref"];
207
+ onScroll: ComponentProps<"div">["onScroll"];
208
+ onCreateSession: () => void;
209
+ onSelectAgent: (agentId: string) => void;
210
+ onToolAction: ChatToolActionHandler;
211
+ onFileOpen: ChatFileOpenHandler;
212
+ };
213
+
214
+ function ChatConversationContent({
215
+ snapshot,
216
+ availableAgents,
217
+ hideEmptyHint,
218
+ showWelcome,
219
+ threadRef,
220
+ onScroll,
221
+ onCreateSession,
222
+ onSelectAgent,
223
+ onToolAction,
224
+ onFileOpen,
225
+ }: ChatConversationContentProps) {
226
+ return (
227
+ <div
228
+ ref={threadRef}
229
+ onScroll={onScroll}
230
+ className="flex-1 min-h-0 overflow-y-auto custom-scrollbar"
231
+ >
232
+ {showWelcome ? (
233
+ <ChatWelcome
234
+ onCreateSession={onCreateSession}
235
+ agents={availableAgents}
236
+ selectedAgentId={snapshot.agentId ?? "main"}
237
+ onSelectAgent={onSelectAgent}
238
+ />
239
+ ) : hideEmptyHint ? null : snapshot.messages.length === 0 ? (
240
+ <div className="px-5 py-5 text-sm text-gray-500">
241
+ {t("chatNoMessages")}
242
+ </div>
243
+ ) : (
244
+ <div className="mx-auto w-full max-w-[min(1120px,100%)] px-6 py-5">
245
+ <ChatMessageListContainer
246
+ key={snapshot.sessionKey ?? "draft"}
247
+ messages={snapshot.messages}
248
+ isSending={snapshot.isSending && snapshot.isAwaitingAssistantOutput}
249
+ onToolAction={onToolAction}
250
+ onFileOpen={onFileOpen}
251
+ />
252
+ </div>
253
+ )}
254
+ </div>
255
+ );
256
+ }
257
+
258
+ function shouldShowWorkspacePanel(
259
+ snapshot: ChatThreadSnapshot,
260
+ childSessionTabs: ChatThreadSnapshot["childSessionTabs"],
261
+ workspaceFileTabs: ChatThreadSnapshot["workspaceFileTabs"],
262
+ ) {
263
+ if (snapshot.workspacePanelParentKey !== snapshot.sessionKey) {
264
+ return false;
265
+ }
266
+ return childSessionTabs.length > 0 || workspaceFileTabs.length > 0;
267
+ }
268
+
269
+ export function ChatConversationPanel() {
270
+ const presenter = usePresenter();
271
+ const defaultSessionType = useChatInputStore(
272
+ (state) => state.snapshot.defaultSessionType,
273
+ );
274
+ const snapshot = useChatThreadStore((state) => state.snapshot);
275
+ const fallbackThreadRef = useRef<HTMLDivElement | null>(null);
276
+ const threadRef = snapshot.threadRef ?? fallbackThreadRef;
277
+ const childSessionTabs = snapshot.childSessionTabs.filter(
278
+ (tab) => tab.parentSessionKey === snapshot.sessionKey,
279
+ );
280
+ const workspaceFileTabs = snapshot.workspaceFileTabs.filter(
281
+ (tab) => tab.parentSessionKey === snapshot.sessionKey,
282
+ );
283
+ const showWorkspacePanel = shouldShowWorkspacePanel(
284
+ snapshot,
285
+ childSessionTabs,
286
+ workspaceFileTabs,
287
+ );
288
+ const shouldShowSessionHeader = Boolean(
289
+ snapshot.sessionKey || snapshot.sessionTypeLabel,
290
+ );
291
+ const sessionHeaderTitle =
292
+ snapshot.sessionDisplayName ||
293
+ (snapshot.canDeleteSession && snapshot.sessionKey ? snapshot.sessionKey : null) ||
294
+ t("chatSidebarNewTask");
295
+ const normalizedAgentId = snapshot.agentId?.trim() ?? "";
296
+ const shouldShowHeaderAgentAvatar =
297
+ normalizedAgentId.length > 0 &&
298
+ normalizedAgentId.toLowerCase() !== "main";
299
+
300
+ const showWelcome =
301
+ !snapshot.canDeleteSession &&
302
+ snapshot.messages.length === 0 &&
303
+ !snapshot.isSending;
304
+ const hasConfiguredModel = snapshot.modelOptions.length > 0;
305
+ const shouldShowProviderHint =
306
+ snapshot.isProviderStateResolved && !hasConfiguredModel;
307
+ const hideEmptyHint =
308
+ snapshot.isHistoryLoading &&
309
+ snapshot.messages.length === 0 &&
310
+ !snapshot.isSending &&
311
+ !snapshot.isAwaitingAssistantOutput;
312
+ const availableAgents = snapshot.availableAgents ?? [];
313
+ const resolveDraftAgent = (agentId: string) =>
314
+ availableAgents.find((agent) => agent.id === agentId) ?? null;
315
+ const createDraftSessionForAgent = () => {
316
+ const sessionType = resolveAgentRuntimeSessionType(
317
+ resolveDraftAgent(snapshot.agentId ?? "main"),
318
+ defaultSessionType,
319
+ );
320
+ presenter.chatSessionListManager.createSession(sessionType);
321
+ };
322
+ const selectDraftAgent = (agentId: string) => {
323
+ presenter.chatSessionListManager.setSelectedAgentId(agentId);
324
+ presenter.chatInputManager.setPendingSessionType(
325
+ resolveAgentRuntimeSessionType(resolveDraftAgent(agentId), defaultSessionType),
326
+ );
327
+ };
328
+ const openFilePreview = (action: ChatFileOpenActionViewModel) => {
329
+ presenter.chatThreadManager.openFilePreview(action);
330
+ };
331
+ const openChildSessions = () => {
332
+ if (!snapshot.sessionKey) {
333
+ return;
334
+ }
335
+ presenter.chatThreadManager.openChildSessionPanel({
336
+ parentSessionKey: snapshot.sessionKey,
337
+ activeChildSessionKey: childSessionTabs[0]?.sessionKey ?? null,
338
+ });
339
+ };
340
+
341
+ const { onScroll: handleScroll } = useStickyBottomScroll({
342
+ scrollRef: threadRef,
343
+ resetKey: snapshot.sessionKey,
344
+ isLoading: snapshot.isHistoryLoading,
345
+ hasContent: snapshot.messages.length > 0,
346
+ contentVersion: snapshot.messages[snapshot.messages.length - 1] ?? null,
347
+ });
348
+
349
+ if (!snapshot.isProviderStateResolved) {
350
+ return <ChatConversationSkeleton />;
351
+ }
352
+
353
+ return (
354
+ <section className="flex-1 min-h-0 flex overflow-hidden bg-gradient-to-b from-gray-50/60 to-white">
355
+ <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
356
+ <ChatParentSessionBanner
357
+ parentSessionLabel={
358
+ snapshot.parentSessionKey ? (snapshot.parentSessionLabel ?? null) : null
359
+ }
360
+ onGoToParentSession={presenter.chatThreadManager.goToParentSession}
361
+ />
362
+ <ChatConversationHeader
363
+ snapshot={snapshot}
364
+ childSessionCount={childSessionTabs.length}
365
+ normalizedAgentId={normalizedAgentId}
366
+ sessionHeaderTitle={sessionHeaderTitle}
367
+ shouldShowHeaderAgentAvatar={shouldShowHeaderAgentAvatar}
368
+ shouldShowSessionHeader={shouldShowSessionHeader}
369
+ onOpenChildSessions={openChildSessions}
370
+ onDeleteSession={presenter.chatThreadManager.deleteSession}
371
+ />
372
+ <ChatConversationAlerts
373
+ shouldShowProviderHint={shouldShowProviderHint}
374
+ sessionTypeUnavailable={snapshot.sessionTypeUnavailable}
375
+ sessionTypeUnavailableMessage={snapshot.sessionTypeUnavailableMessage ?? null}
376
+ onGoToProviders={presenter.chatThreadManager.goToProviders}
377
+ />
378
+ <ChatConversationContent
379
+ snapshot={snapshot}
380
+ availableAgents={availableAgents}
381
+ hideEmptyHint={hideEmptyHint}
382
+ showWelcome={showWelcome}
383
+ threadRef={threadRef}
384
+ onScroll={handleScroll}
385
+ onCreateSession={createDraftSessionForAgent}
386
+ onSelectAgent={selectDraftAgent}
387
+ onToolAction={presenter.chatThreadManager.openSessionFromToolAction}
388
+ onFileOpen={openFilePreview}
389
+ />
390
+
391
+ <ChatInputBarContainer />
392
+ </div>
393
+
394
+ {showWorkspacePanel ? (
395
+ <ChatSessionWorkspacePanel
396
+ childSessionTabs={childSessionTabs}
397
+ activeChildSessionKey={snapshot.activeChildSessionKey ?? null}
398
+ workspaceFileTabs={workspaceFileTabs}
399
+ activeWorkspaceFileKey={snapshot.activeWorkspaceFileKey ?? null}
400
+ sessionProjectRoot={snapshot.sessionProjectRoot ?? null}
401
+ onSelectSession={presenter.chatThreadManager.selectChildSessionDetail}
402
+ onSelectFile={presenter.chatThreadManager.selectWorkspaceFile}
403
+ onCloseFile={presenter.chatThreadManager.closeWorkspaceFile}
404
+ onClose={presenter.chatThreadManager.closeWorkspacePanel}
405
+ onBackToParent={presenter.chatThreadManager.goToParentSession}
406
+ onToolAction={presenter.chatThreadManager.openSessionFromToolAction}
407
+ onFileOpen={openFilePreview}
408
+ />
409
+ ) : null}
410
+ </section>
411
+ );
412
+ }
@@ -8,7 +8,7 @@ import {
8
8
  } from '@/components/chat/chat-session-preference-governance';
9
9
  import {
10
10
  shouldClearPendingProjectRootOverride
11
- } from '@/components/chat/ncp/NcpChatPage';
11
+ } from '@/components/chat/ncp/ncp-chat-page';
12
12
 
13
13
  const modelOptions = [
14
14
  {
@@ -1,7 +1,7 @@
1
1
  import { useEffect } from 'react';
2
2
  import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
3
3
  import { ChatSidebar } from '@/components/chat/ChatSidebar';
4
- import { ChatConversationPanel } from '@/components/chat/ChatConversationPanel';
4
+ import { ChatConversationPanel } from '@/components/chat/chat-conversation-panel';
5
5
  import { AgentsPage } from '@/components/agents/AgentsPage';
6
6
  import { CronConfig } from '@/components/config/CronConfig';
7
7
  import { MarketplacePage } from '@/components/marketplace/MarketplacePage';
@@ -1,5 +1,5 @@
1
1
  import type { ChatPageProps } from '@/components/chat/chat-page-shell';
2
- import { NcpChatPage } from '@/components/chat/ncp/NcpChatPage';
2
+ import { NcpChatPage } from '@/components/chat/ncp/ncp-chat-page';
3
3
 
4
4
  export function ChatPage({ view }: ChatPageProps) {
5
5
  return <NcpChatPage view={view} />;
@@ -0,0 +1,91 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { ChatSessionWorkspaceFilePreview } from "@/components/chat/chat-session-workspace-file-preview";
4
+ import type { ChatWorkspaceFileTab } from "@/components/chat/stores/chat-thread.store";
5
+
6
+ const serverPathReadMock = vi.fn();
7
+
8
+ vi.mock("@/hooks/server-path/use-server-path-read", () => ({
9
+ useServerPathRead: (...args: unknown[]) => serverPathReadMock(...args),
10
+ }));
11
+
12
+ vi.mock("@nextclaw/agent-chat-ui", () => ({
13
+ ChatMessageMarkdown: ({ text }: { text: string }) => (
14
+ <div data-testid="markdown-preview">{text}</div>
15
+ ),
16
+ FileOperationCodeSurface: ({
17
+ layout,
18
+ }: {
19
+ layout?: "compact" | "workspace";
20
+ }) => <div data-testid="file-code-surface" data-layout={layout ?? "compact"} />,
21
+ }));
22
+
23
+ function buildWorkspaceFile(
24
+ overrides: Partial<ChatWorkspaceFileTab>,
25
+ ): ChatWorkspaceFileTab {
26
+ return {
27
+ key: "workspace-file",
28
+ parentSessionKey: null,
29
+ path: "/tmp/example.ts",
30
+ label: "example.ts",
31
+ viewMode: "preview",
32
+ ...overrides,
33
+ };
34
+ }
35
+
36
+ describe("ChatSessionWorkspaceFilePreview", () => {
37
+ beforeEach(() => {
38
+ serverPathReadMock.mockReset();
39
+ });
40
+
41
+ it("renders preview files inside a full-height workspace code surface", () => {
42
+ serverPathReadMock.mockReturnValue({
43
+ isLoading: false,
44
+ error: null,
45
+ data: {
46
+ kind: "text",
47
+ resolvedPath: "/tmp/example.ts",
48
+ text: "const answer = 42;\n",
49
+ truncated: false,
50
+ },
51
+ });
52
+
53
+ render(
54
+ <ChatSessionWorkspaceFilePreview
55
+ file={buildWorkspaceFile({ viewMode: "preview" })}
56
+ sessionProjectRoot="/tmp"
57
+ onFileOpen={vi.fn()}
58
+ />,
59
+ );
60
+
61
+ expect(screen.getByTestId("file-code-surface").getAttribute("data-layout")).toBe(
62
+ "workspace",
63
+ );
64
+ });
65
+
66
+ it("renders diff files inside a full-height workspace code surface", () => {
67
+ serverPathReadMock.mockReturnValue({
68
+ isLoading: false,
69
+ error: null,
70
+ data: null,
71
+ });
72
+
73
+ render(
74
+ <ChatSessionWorkspaceFilePreview
75
+ file={buildWorkspaceFile({
76
+ viewMode: "diff",
77
+ beforeText: "const answer = 41;\n",
78
+ afterText: "const answer = 42;\n",
79
+ oldStartLine: 1,
80
+ newStartLine: 1,
81
+ })}
82
+ sessionProjectRoot="/tmp"
83
+ onFileOpen={vi.fn()}
84
+ />,
85
+ );
86
+
87
+ expect(screen.getByTestId("file-code-surface").getAttribute("data-layout")).toBe(
88
+ "workspace",
89
+ );
90
+ });
91
+ });