@nextclaw/ui 0.12.25 → 0.12.26

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 (42) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/dist/assets/{channels-list-page-FJDuPwU6.js → channels-list-page-HgLgrEg4.js} +1 -1
  3. package/dist/assets/chat-page-DAKMFDrS.js +1 -0
  4. package/dist/assets/{desktop-kk7qvZ-v.js → desktop-DVUbOWbR.js} +1 -1
  5. package/dist/assets/{index-D-AAMKCt.js → index-Cuwst6cc.js} +34 -37
  6. package/dist/assets/index-dlcqieQ0.css +1 -0
  7. package/dist/assets/{marketplace-page-BrCLRIc4.js → marketplace-page-BeFbwxR-.js} +2 -2
  8. package/dist/assets/marketplace-page-CR4xq-TM.js +1 -0
  9. package/dist/assets/mcp-marketplace-page-DlRrSCj3.js +1 -0
  10. package/dist/assets/{mcp-marketplace-page-DIq_SpMe.js → mcp-marketplace-page-DwnaLNTx.js} +1 -1
  11. package/dist/assets/{model-config-Bc6VVnxy.js → model-config-L2l6YAlQ.js} +1 -1
  12. package/dist/assets/{providers-list-DN0tvISH.js → providers-list-DYAEunOp.js} +1 -1
  13. package/dist/assets/{runtime-config-page-CRWOwBbl.js → runtime-config-page-BdeU8PEK.js} +1 -1
  14. package/dist/assets/{search-config-C4c1yZSP.js → search-config-CQUhd5RU.js} +1 -1
  15. package/dist/assets/{secrets-config-zAF30YfO.js → secrets-config-D-NWlW9q.js} +1 -1
  16. package/dist/assets/{use-infinite-scroll-loader-Cvz8ZteY.js → use-infinite-scroll-loader-CFVdPpNv.js} +1 -1
  17. package/dist/index.html +3 -3
  18. package/package.json +9 -9
  19. package/src/features/agents/components/agents-page.test.tsx +1 -1
  20. package/src/features/agents/components/agents-page.tsx +1 -1
  21. package/src/features/chat/components/chat-session-workspace-panel.tsx +31 -45
  22. package/src/features/chat/components/chat-sidebar-session-item.tsx +7 -9
  23. package/src/features/chat/components/conversation/chat-conversation-header.test.tsx +5 -2
  24. package/src/features/chat/components/conversation/chat-conversation-header.tsx +2 -2
  25. package/src/features/chat/components/conversation/chat-conversation-panel.test.tsx +106 -78
  26. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +172 -167
  27. package/src/features/chat/components/conversation/chat-input-bar.container.tsx +11 -1
  28. package/src/features/chat/components/conversation/session-header/chat-session-header-actions.tsx +2 -2
  29. package/src/features/chat/components/providers/chat-presenter.provider.tsx +2 -0
  30. package/src/features/chat/hooks/use-ncp-agent-runtime.test.tsx +147 -88
  31. package/src/features/chat/managers/ncp-chat-input.manager.test.ts +20 -0
  32. package/src/features/chat/managers/ncp-chat-input.manager.ts +18 -0
  33. package/src/features/chat/managers/ncp-chat-presenter.manager.ts +1 -0
  34. package/src/features/chat/pages/ncp-chat-page.tsx +4 -1
  35. package/src/features/chat/stores/chat-input.store.ts +3 -1
  36. package/src/features/chat/utils/ncp-chat-input-availability.utils.test.ts +1 -0
  37. package/src/platforms/desktop/components/desktop-app-shell.test.tsx +1 -0
  38. package/src/platforms/desktop/components/desktop-app-shell.tsx +1 -1
  39. package/dist/assets/chat-page-D1fMNBrT.js +0 -1
  40. package/dist/assets/index-DnBeV2Xm.css +0 -1
  41. package/dist/assets/marketplace-page-odDpPYEs.js +0 -1
  42. package/dist/assets/mcp-marketplace-page-CfbOBgKK.js +0 -1
@@ -1,6 +1,5 @@
1
- import { type ComponentProps, useMemo, useRef } from "react";
1
+ import { useMemo, useRef } from "react";
2
2
  import { useStickyBottomScroll } from "@nextclaw/agent-chat-ui";
3
- import type { ChatFileOpenActionViewModel } from "@nextclaw/agent-chat-ui";
4
3
  import { ChatInputBarContainer } from "@/features/chat/components/conversation/chat-input-bar.container";
5
4
  import { ChatMessageListContainer } from "@/features/chat/components/conversation/chat-message-list.container";
6
5
  import {
@@ -8,7 +7,7 @@ import {
8
7
  ChatParentSessionBanner,
9
8
  } from "@/features/chat/components/conversation/chat-conversation-header";
10
9
  import { ChatWelcome } from "@/features/chat/components/chat-welcome";
11
- import { ChatSessionWorkspacePanel } from "@/features/chat";
10
+ import { ChatSessionWorkspacePanel } from "@/features/chat/components/chat-session-workspace-panel";
12
11
  import { usePresenter } from "@/features/chat/components/providers/chat-presenter.provider";
13
12
  import { resolveAgentRuntimeSessionType } from "@/features/chat/hooks/use-chat-session-type-state";
14
13
  import { useChatInputStore } from "@/features/chat/stores/chat-input.store";
@@ -103,26 +102,17 @@ function ChatConversationSkeleton() {
103
102
  );
104
103
  }
105
104
 
106
- type ChatThreadSnapshot = ReturnType<typeof useChatThreadStore.getState>["snapshot"];
107
- type ChatToolActionHandler = ComponentProps<
108
- typeof ChatMessageListContainer
109
- >["onToolAction"];
110
- type ChatFileOpenHandler = ComponentProps<
111
- typeof ChatMessageListContainer
112
- >["onFileOpen"];
105
+ type ChatThreadSnapshot = ReturnType<
106
+ typeof useChatThreadStore.getState
107
+ >["snapshot"];
108
+ type ChatConversationLayoutMode = "desktop" | "mobile";
109
+
110
+ function ChatConversationAlerts() {
111
+ const presenter = usePresenter();
112
+ const snapshot = useChatThreadStore((state) => state.snapshot);
113
+ const shouldShowProviderHint =
114
+ snapshot.isProviderStateResolved && snapshot.modelOptions.length === 0;
113
115
 
114
- type ChatConversationAlertsProps = {
115
- shouldShowProviderHint: boolean;
116
- sessionTypeUnavailable: boolean;
117
- sessionTypeUnavailableMessage: string | null;
118
- onGoToProviders: () => void;
119
- };
120
- function ChatConversationAlerts({
121
- shouldShowProviderHint,
122
- sessionTypeUnavailable,
123
- sessionTypeUnavailableMessage,
124
- onGoToProviders,
125
- }: ChatConversationAlertsProps) {
126
116
  return (
127
117
  <>
128
118
  {shouldShowProviderHint ? (
@@ -132,17 +122,18 @@ function ChatConversationAlerts({
132
122
  </span>
133
123
  <button
134
124
  type="button"
135
- onClick={onGoToProviders}
125
+ onClick={presenter.chatThreadManager.goToProviders}
136
126
  className="text-xs font-semibold text-amber-900 underline-offset-2 hover:underline"
137
127
  >
138
128
  {t("chatGoConfigureProvider")}
139
129
  </button>
140
130
  </div>
141
131
  ) : null}
142
- {sessionTypeUnavailable && sessionTypeUnavailableMessage?.trim() ? (
132
+ {snapshot.sessionTypeUnavailable &&
133
+ snapshot.sessionTypeUnavailableMessage?.trim() ? (
143
134
  <div className="px-4 py-2.5 border-b border-amber-200/70 bg-amber-50/70 shrink-0 sm:px-5">
144
135
  <span className="text-xs text-amber-800">
145
- {sessionTypeUnavailableMessage}
136
+ {snapshot.sessionTypeUnavailableMessage}
146
137
  </span>
147
138
  </div>
148
139
  ) : null}
@@ -150,35 +141,59 @@ function ChatConversationAlerts({
150
141
  );
151
142
  }
152
143
 
153
- type ChatConversationContentProps = {
154
- snapshot: ChatThreadSnapshot;
155
- availableAgents: NonNullable<ChatThreadSnapshot["availableAgents"]>;
156
- hideEmptyHint: boolean;
157
- showWelcome: boolean;
158
- threadRef: ComponentProps<"div">["ref"];
159
- onScroll: ComponentProps<"div">["onScroll"];
160
- onCreateSession: () => void;
161
- onSelectAgent: (agentId: string) => void;
162
- onToolAction: ChatToolActionHandler;
163
- onFileOpen: ChatFileOpenHandler;
164
- };
165
-
166
144
  function ChatConversationContent({
167
- snapshot,
168
- availableAgents,
169
- hideEmptyHint,
170
- showWelcome,
171
- threadRef,
172
- onScroll,
173
- onCreateSession,
174
- onSelectAgent,
175
- onToolAction,
176
- onFileOpen,
177
- }: ChatConversationContentProps) {
145
+ layoutMode,
146
+ }: {
147
+ layoutMode: ChatConversationLayoutMode;
148
+ }) {
149
+ const presenter = usePresenter();
150
+ const defaultSessionType = useChatInputStore(
151
+ (state) => state.snapshot.defaultSessionType,
152
+ );
153
+ const snapshot = useChatThreadStore((state) => state.snapshot);
154
+ const fallbackThreadRef = useRef<HTMLDivElement | null>(null);
155
+ const threadRef = snapshot.threadRef ?? fallbackThreadRef;
156
+ const availableAgents = snapshot.availableAgents ?? [];
157
+ const showWelcome =
158
+ !snapshot.canDeleteSession &&
159
+ !snapshot.hasSubmittedDraftMessage &&
160
+ snapshot.messages.length === 0 &&
161
+ !snapshot.isSending;
162
+ const hideEmptyHint =
163
+ snapshot.isHistoryLoading &&
164
+ snapshot.messages.length === 0 &&
165
+ !snapshot.isSending &&
166
+ !snapshot.isAwaitingAssistantOutput;
167
+ const resolveDraftAgent = (agentId: string) =>
168
+ availableAgents.find((agent) => agent.id === agentId) ?? null;
169
+ const createDraftSessionForAgent = () => {
170
+ const sessionType = resolveAgentRuntimeSessionType(
171
+ resolveDraftAgent(snapshot.agentId ?? "main"),
172
+ defaultSessionType,
173
+ );
174
+ presenter.chatSessionListManager.createSession(sessionType);
175
+ if (layoutMode === "mobile") presenter.chatUiManager.goToChatRoot();
176
+ };
177
+ const selectDraftAgent = (agentId: string) => {
178
+ presenter.chatSessionListManager.setSelectedAgentId(agentId);
179
+ presenter.chatInputManager.setPendingSessionType(
180
+ resolveAgentRuntimeSessionType(
181
+ resolveDraftAgent(agentId),
182
+ defaultSessionType,
183
+ ),
184
+ );
185
+ };
186
+ const { onScroll } = useStickyBottomScroll({
187
+ scrollRef: threadRef,
188
+ resetKey: snapshot.sessionKey,
189
+ isLoading: snapshot.isHistoryLoading,
190
+ hasContent: snapshot.messages.length > 0,
191
+ contentVersion: snapshot.messages[snapshot.messages.length - 1] ?? null,
192
+ });
193
+ const hasMessages = snapshot.messages.length > 0;
178
194
  const isAwaitingAssistantOutput =
179
- snapshot.isSending && snapshot.isAwaitingAssistantOutput;
180
- const shouldShowMessages =
181
- snapshot.messages.length > 0 || isAwaitingAssistantOutput;
195
+ hasMessages && snapshot.isSending && snapshot.isAwaitingAssistantOutput;
196
+ const shouldShowMessages = hasMessages;
182
197
 
183
198
  return (
184
199
  <div
@@ -188,18 +203,18 @@ function ChatConversationContent({
188
203
  >
189
204
  {showWelcome ? (
190
205
  <ChatWelcome
191
- onCreateSession={onCreateSession}
206
+ onCreateSession={createDraftSessionForAgent}
192
207
  agents={availableAgents}
193
208
  selectedAgentId={snapshot.agentId ?? "main"}
194
- onSelectAgent={onSelectAgent}
209
+ onSelectAgent={selectDraftAgent}
195
210
  />
196
211
  ) : hideEmptyHint || !shouldShowMessages ? null : (
197
212
  <div className="mx-auto w-full max-w-[min(1120px,100%)] px-4 py-4 sm:px-6 sm:py-5">
198
213
  <ChatMessageListContainer
199
214
  messages={snapshot.messages}
200
215
  isSending={isAwaitingAssistantOutput}
201
- onToolAction={onToolAction}
202
- onFileOpen={onFileOpen}
216
+ onToolAction={presenter.chatThreadManager.openSessionFromToolAction}
217
+ onFileOpen={presenter.chatThreadManager.openFilePreview}
203
218
  />
204
219
  </div>
205
220
  )}
@@ -216,21 +231,34 @@ function shouldShowWorkspacePanel(
216
231
  if (snapshot.workspacePanelParentKey !== snapshot.sessionKey) {
217
232
  return false;
218
233
  }
219
- return childSessionTabs.length > 0 || workspaceFileTabs.length > 0 || sessionCronJobCount > 0;
234
+ return (
235
+ childSessionTabs.length > 0 ||
236
+ workspaceFileTabs.length > 0 ||
237
+ sessionCronJobCount > 0
238
+ );
220
239
  }
221
240
 
222
241
  function useSessionWorkspaceState(snapshot: ChatThreadSnapshot) {
223
242
  const childSessionTabs = useMemo(
224
- () => snapshot.childSessionTabs.filter((tab) => tab.parentSessionKey === snapshot.sessionKey),
243
+ () =>
244
+ snapshot.childSessionTabs.filter(
245
+ (tab) => tab.parentSessionKey === snapshot.sessionKey,
246
+ ),
225
247
  [snapshot.childSessionTabs, snapshot.sessionKey],
226
248
  );
227
249
  const workspaceFileTabs = useMemo(
228
- () => snapshot.workspaceFileTabs.filter((tab) => tab.parentSessionKey === snapshot.sessionKey),
250
+ () =>
251
+ snapshot.workspaceFileTabs.filter(
252
+ (tab) => tab.parentSessionKey === snapshot.sessionKey,
253
+ ),
229
254
  [snapshot.sessionKey, snapshot.workspaceFileTabs],
230
255
  );
231
256
  const cronQuery = useCronJobs({ all: true });
232
257
  const sessionCronJobs = useMemo(
233
- () => (cronQuery.data?.jobs ?? []).filter((job) => isCronJobForSession(job, snapshot.sessionKey)),
258
+ () =>
259
+ (cronQuery.data?.jobs ?? []).filter((job) =>
260
+ isCronJobForSession(job, snapshot.sessionKey),
261
+ ),
234
262
  [cronQuery.data?.jobs, snapshot.sessionKey],
235
263
  );
236
264
  return {
@@ -246,65 +274,42 @@ function useSessionWorkspaceState(snapshot: ChatThreadSnapshot) {
246
274
  };
247
275
  }
248
276
 
249
- export function ChatConversationPanel({
250
- layoutMode = "desktop",
277
+ function ChatParentSessionBannerContainer() {
278
+ const presenter = usePresenter();
279
+ const snapshot = useChatThreadStore((state) => state.snapshot);
280
+ return (
281
+ <ChatParentSessionBanner
282
+ parentSessionLabel={
283
+ snapshot.parentSessionKey ? (snapshot.parentSessionLabel ?? null) : null
284
+ }
285
+ onGoToParentSession={presenter.chatThreadManager.goToParentSession}
286
+ />
287
+ );
288
+ }
289
+
290
+ function ChatConversationHeaderContainer({
291
+ layoutMode,
251
292
  onBackToList,
252
293
  }: {
253
- layoutMode?: "desktop" | "mobile";
294
+ layoutMode: ChatConversationLayoutMode;
254
295
  onBackToList?: () => void;
255
296
  }) {
256
297
  const presenter = usePresenter();
257
- const defaultSessionType = useChatInputStore((state) => state.snapshot.defaultSessionType);
258
298
  const snapshot = useChatThreadStore((state) => state.snapshot);
259
- const fallbackThreadRef = useRef<HTMLDivElement | null>(null);
260
- const threadRef = snapshot.threadRef ?? fallbackThreadRef;
261
- const { childSessionTabs, workspaceFileTabs, sessionCronJobs, showWorkspacePanel } =
299
+ const { childSessionTabs, sessionCronJobs } =
262
300
  useSessionWorkspaceState(snapshot);
263
301
  const shouldShowSessionHeader = Boolean(
264
302
  snapshot.sessionKey || snapshot.sessionTypeLabel,
265
303
  );
266
304
  const sessionHeaderTitle =
267
305
  snapshot.sessionDisplayName ||
268
- (snapshot.canDeleteSession && snapshot.sessionKey ? snapshot.sessionKey : null) ||
306
+ (snapshot.canDeleteSession && snapshot.sessionKey
307
+ ? snapshot.sessionKey
308
+ : null) ||
269
309
  t("chatSidebarNewTask");
270
310
  const normalizedAgentId = snapshot.agentId?.trim() ?? "";
271
311
  const shouldShowHeaderAgentAvatar =
272
- normalizedAgentId.length > 0 &&
273
- normalizedAgentId.toLowerCase() !== "main";
274
-
275
- const showWelcome =
276
- !snapshot.canDeleteSession &&
277
- !snapshot.hasSubmittedDraftMessage &&
278
- snapshot.messages.length === 0 &&
279
- !snapshot.isSending;
280
- const hasConfiguredModel = snapshot.modelOptions.length > 0;
281
- const shouldShowProviderHint =
282
- snapshot.isProviderStateResolved && !hasConfiguredModel;
283
- const hideEmptyHint =
284
- snapshot.isHistoryLoading &&
285
- snapshot.messages.length === 0 &&
286
- !snapshot.isSending &&
287
- !snapshot.isAwaitingAssistantOutput;
288
- const availableAgents = snapshot.availableAgents ?? [];
289
- const resolveDraftAgent = (agentId: string) =>
290
- availableAgents.find((agent) => agent.id === agentId) ?? null;
291
- const createDraftSessionForAgent = () => {
292
- const sessionType = resolveAgentRuntimeSessionType(
293
- resolveDraftAgent(snapshot.agentId ?? "main"),
294
- defaultSessionType,
295
- );
296
- presenter.chatSessionListManager.createSession(sessionType);
297
- if (layoutMode === "mobile") presenter.chatUiManager.goToChatRoot();
298
- };
299
- const selectDraftAgent = (agentId: string) => {
300
- presenter.chatSessionListManager.setSelectedAgentId(agentId);
301
- presenter.chatInputManager.setPendingSessionType(
302
- resolveAgentRuntimeSessionType(resolveDraftAgent(agentId), defaultSessionType),
303
- );
304
- };
305
- const openFilePreview = (action: ChatFileOpenActionViewModel) => {
306
- presenter.chatThreadManager.openFilePreview(action);
307
- };
312
+ normalizedAgentId.length > 0 && normalizedAgentId.toLowerCase() !== "main";
308
313
  const openChildSessions = () => {
309
314
  if (!snapshot.sessionKey) {
310
315
  return;
@@ -321,13 +326,64 @@ export function ChatConversationPanel({
321
326
  presenter.chatThreadManager.openSessionCronPanel(snapshot.sessionKey);
322
327
  };
323
328
 
324
- const { onScroll: handleScroll } = useStickyBottomScroll({
325
- scrollRef: threadRef,
326
- resetKey: snapshot.sessionKey,
327
- isLoading: snapshot.isHistoryLoading,
328
- hasContent: snapshot.messages.length > 0,
329
- contentVersion: snapshot.messages[snapshot.messages.length - 1] ?? null,
330
- });
329
+ return (
330
+ <ChatConversationHeader
331
+ snapshot={snapshot}
332
+ childSessionCount={childSessionTabs.length}
333
+ sessionCronJobCount={sessionCronJobs.length}
334
+ layoutMode={layoutMode}
335
+ normalizedAgentId={normalizedAgentId}
336
+ sessionHeaderTitle={sessionHeaderTitle}
337
+ shouldShowHeaderAgentAvatar={shouldShowHeaderAgentAvatar}
338
+ shouldShowSessionHeader={shouldShowSessionHeader}
339
+ onBackToList={onBackToList}
340
+ onOpenChildSessions={openChildSessions}
341
+ onOpenSessionCronJobs={openSessionCronJobs}
342
+ onDeleteSession={presenter.chatThreadManager.deleteSession}
343
+ />
344
+ );
345
+ }
346
+
347
+ function ChatSessionWorkspacePanelContainer({
348
+ layoutMode,
349
+ }: {
350
+ layoutMode: ChatConversationLayoutMode;
351
+ }) {
352
+ const snapshot = useChatThreadStore((state) => state.snapshot);
353
+ const {
354
+ childSessionTabs,
355
+ workspaceFileTabs,
356
+ sessionCronJobs,
357
+ showWorkspacePanel,
358
+ } = useSessionWorkspaceState(snapshot);
359
+
360
+ if (!showWorkspacePanel) {
361
+ return null;
362
+ }
363
+
364
+ return (
365
+ <ChatSessionWorkspacePanel
366
+ sessionKey={snapshot.sessionKey}
367
+ childSessionTabs={childSessionTabs}
368
+ activeChildSessionKey={snapshot.activeChildSessionKey ?? null}
369
+ workspaceFileTabs={workspaceFileTabs}
370
+ activeWorkspaceFileKey={snapshot.activeWorkspaceFileKey ?? null}
371
+ activePanelKind={snapshot.activeWorkspacePanelKind ?? null}
372
+ sessionCronJobs={sessionCronJobs}
373
+ sessionProjectRoot={snapshot.sessionProjectRoot ?? null}
374
+ displayMode={layoutMode === "mobile" ? "overlay" : "docked"}
375
+ />
376
+ );
377
+ }
378
+
379
+ export function ChatConversationPanel({
380
+ layoutMode = "desktop",
381
+ onBackToList,
382
+ }: {
383
+ layoutMode?: ChatConversationLayoutMode;
384
+ onBackToList?: () => void;
385
+ }) {
386
+ const snapshot = useChatThreadStore((state) => state.snapshot);
331
387
 
332
388
  if (!snapshot.isProviderStateResolved) {
333
389
  return <ChatConversationSkeleton />;
@@ -336,68 +392,17 @@ export function ChatConversationPanel({
336
392
  return (
337
393
  <section className="flex-1 min-h-0 flex overflow-hidden bg-gradient-to-b from-gray-50/60 to-white">
338
394
  <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
339
- <ChatParentSessionBanner
340
- parentSessionLabel={
341
- snapshot.parentSessionKey ? (snapshot.parentSessionLabel ?? null) : null
342
- }
343
- onGoToParentSession={presenter.chatThreadManager.goToParentSession}
344
- />
345
- <ChatConversationHeader
346
- snapshot={snapshot}
347
- childSessionCount={childSessionTabs.length}
348
- sessionCronJobCount={sessionCronJobs.length}
395
+ <ChatParentSessionBannerContainer />
396
+ <ChatConversationHeaderContainer
349
397
  layoutMode={layoutMode}
350
- normalizedAgentId={normalizedAgentId}
351
- sessionHeaderTitle={sessionHeaderTitle}
352
- shouldShowHeaderAgentAvatar={shouldShowHeaderAgentAvatar}
353
- shouldShowSessionHeader={shouldShowSessionHeader}
354
398
  onBackToList={onBackToList}
355
- onOpenChildSessions={openChildSessions}
356
- onOpenSessionCronJobs={openSessionCronJobs}
357
- onDeleteSession={presenter.chatThreadManager.deleteSession}
358
399
  />
359
- <ChatConversationAlerts
360
- shouldShowProviderHint={shouldShowProviderHint}
361
- sessionTypeUnavailable={snapshot.sessionTypeUnavailable}
362
- sessionTypeUnavailableMessage={snapshot.sessionTypeUnavailableMessage ?? null}
363
- onGoToProviders={presenter.chatThreadManager.goToProviders}
364
- />
365
- <ChatConversationContent
366
- snapshot={snapshot}
367
- availableAgents={availableAgents}
368
- hideEmptyHint={hideEmptyHint}
369
- showWelcome={showWelcome}
370
- threadRef={threadRef}
371
- onScroll={handleScroll}
372
- onCreateSession={createDraftSessionForAgent}
373
- onSelectAgent={selectDraftAgent}
374
- onToolAction={presenter.chatThreadManager.openSessionFromToolAction}
375
- onFileOpen={openFilePreview}
376
- />
377
-
400
+ <ChatConversationAlerts />
401
+ <ChatConversationContent layoutMode={layoutMode} />
378
402
  <ChatInputBarContainer />
379
403
  </div>
380
404
 
381
- {showWorkspacePanel ? (
382
- <ChatSessionWorkspacePanel
383
- childSessionTabs={childSessionTabs}
384
- activeChildSessionKey={snapshot.activeChildSessionKey ?? null}
385
- workspaceFileTabs={workspaceFileTabs}
386
- activeWorkspaceFileKey={snapshot.activeWorkspaceFileKey ?? null}
387
- activePanelKind={snapshot.activeWorkspacePanelKind ?? null}
388
- sessionCronJobs={sessionCronJobs}
389
- sessionProjectRoot={snapshot.sessionProjectRoot ?? null}
390
- displayMode={layoutMode === "mobile" ? "overlay" : "docked"}
391
- onSelectSession={presenter.chatThreadManager.selectChildSessionDetail}
392
- onSelectFile={presenter.chatThreadManager.selectWorkspaceFile}
393
- onCloseFile={presenter.chatThreadManager.closeWorkspaceFile}
394
- onSelectCronJobs={() => snapshot.sessionKey ? presenter.chatThreadManager.openSessionCronPanel(snapshot.sessionKey) : undefined}
395
- onClose={presenter.chatThreadManager.closeWorkspacePanel}
396
- onBackToParent={presenter.chatThreadManager.goToParentSession}
397
- onToolAction={presenter.chatThreadManager.openSessionFromToolAction}
398
- onFileOpen={openFilePreview}
399
- />
400
- ) : null}
405
+ <ChatSessionWorkspacePanelContainer layoutMode={layoutMode} />
401
406
  </section>
402
407
  );
403
408
  }
@@ -1,4 +1,4 @@
1
- import { useCallback, useMemo, useRef, useState, type ChangeEvent, type RefObject } from 'react';
1
+ import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type RefObject } from 'react';
2
2
  import { ChatInputBar, type ChatInputBarHandle } from '@nextclaw/agent-chat-ui';
3
3
  import { DEFAULT_NCP_ATTACHMENT_MAX_BYTES, uploadFilesAsNcpDraftAttachments } from '@nextclaw/ncp-react';
4
4
  import { uploadNcpAssets } from '@/shared/lib/api';
@@ -234,6 +234,16 @@ export function ChatInputBarContainer() {
234
234
  ? t('chatStopPreparing')
235
235
  : snapshot.stopDisabledReason?.trim() || t('chatStopUnavailable');
236
236
  const { handleFilesAdd, handleFileInputChange } = useChatInputBarAttachments({ attachmentSupported, inputBarRef, presenter });
237
+ useEffect(() => {
238
+ const request = snapshot.composerFocusRequest;
239
+ if (!request) {
240
+ return;
241
+ }
242
+ if (request.placement === 'end') {
243
+ inputBarRef.current?.focusComposerAtEnd();
244
+ }
245
+ presenter.chatInputManager.consumeComposerFocusRequest(request.id);
246
+ }, [presenter.chatInputManager, snapshot.composerFocusRequest]);
237
247
  const toolbarSelects = buildToolbarSelects({
238
248
  allModelsLabel: labels.allModelsLabel,
239
249
  hasModelOptions,
@@ -1,5 +1,5 @@
1
1
  import { useState } from 'react';
2
- import { AlarmClock, FolderOpen, GitBranch, MoreHorizontal, Trash2 } from 'lucide-react';
2
+ import { AlarmClock, FolderOpen, GitBranch, MoreVertical, Trash2 } from 'lucide-react';
3
3
  import { Button } from '@/shared/components/ui/button';
4
4
  import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
5
5
  import { useChatSessionProject } from '@/features/chat/hooks/use-chat-session-project';
@@ -94,7 +94,7 @@ export function ChatSessionHeaderActions({
94
94
  aria-label={t('chatSessionMoreActions')}
95
95
  disabled={isBusy}
96
96
  >
97
- <MoreHorizontal className="h-4 w-4" />
97
+ <MoreVertical className="h-4 w-4" />
98
98
  </Button>
99
99
  </PopoverTrigger>
100
100
  <PopoverContent align="end" className="w-56 p-2">
@@ -10,6 +10,8 @@ import type { ChatThreadSnapshot } from '@/features/chat/stores/chat-thread.stor
10
10
  export type ChatInputManagerLike = {
11
11
  syncSnapshot: (patch: Record<string, unknown>) => void;
12
12
  setDraft: (next: SetStateAction<string>) => void;
13
+ requestComposerFocusAtEnd: () => void;
14
+ consumeComposerFocusRequest: (requestId: number) => void;
13
15
  setComposerNodes: (next: SetStateAction<ChatComposerNode[]>) => void;
14
16
  addAttachments?: (attachments: NcpDraftAttachment[]) => NcpDraftAttachment[];
15
17
  restoreComposerState?: (nodes: ChatComposerNode[], attachments: NcpDraftAttachment[]) => void;