@nextclaw/ui 0.14.4 → 0.15.1
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.
- package/CHANGELOG.md +73 -0
- package/dist/assets/api-b1R9VdeH.js +13 -0
- package/dist/assets/appearance-settings-page-D2O9pch9.js +1 -0
- package/dist/assets/book-open-B77lQ2Tg.js +1 -0
- package/dist/assets/channels-list-page-DcsIQc6A.js +8 -0
- package/dist/assets/chat-page-CZ7WxPir.js +1 -0
- package/dist/assets/{config-split-page-CRhlZ_pH.js → config-split-page-BWQ6_Raq.js} +1 -1
- package/dist/assets/confirm-dialog-C-NXSioX.js +5 -0
- package/dist/assets/desktop-update-config-usHpXugP.js +1 -0
- package/dist/assets/{dist-CikkhOlq.js → dist-YtoqU-AT.js} +1 -1
- package/dist/assets/{dist-PVA_xL1D.js → dist-jdGmbb6w.js} +1 -1
- package/dist/assets/doc-browser-B_Tf9n70.js +1 -0
- package/dist/assets/doc-browser-BdZPFOVa.js +1 -0
- package/dist/assets/{doc-browser-context-MTw4EvKd.js → doc-browser-context-DwokemUn.js} +1 -1
- package/dist/assets/es2015-bNpL2LKy.js +41 -0
- package/dist/assets/{external-link-DVJIQf_G.js → external-link-BM6fk8q-.js} +1 -1
- package/dist/assets/host-capabilities-9bB_998Z.js +1 -0
- package/dist/assets/i18n-provider-CntWolRA.js +1 -0
- package/dist/assets/index-BIcEw90u.js +104 -0
- package/dist/assets/index-BiNSTKTU.css +1 -0
- package/dist/assets/loader-circle-MBgNSVIH.js +1 -0
- package/dist/assets/mcp-marketplace-page-CLmUstLN.js +9 -0
- package/dist/assets/mcp-marketplace-page-rZkmWPMy.js +1 -0
- package/dist/assets/model-config-page-Di0ySJD2.js +1 -0
- package/dist/assets/plus-vDqW5Kxp.js +1 -0
- package/dist/assets/provider-scoped-model-input-Cm_a4GHf.js +1 -0
- package/dist/assets/providers-config-page-BAf2W0MV.js +1 -0
- package/dist/assets/{react-VNqT5Zp7.js → react-BN8uzPnp.js} +1 -1
- package/dist/assets/refresh-cw-BpVcFPmJ.js +1 -0
- package/dist/assets/remote-CLQ0IBM8.js +1 -0
- package/dist/assets/rotate-cw-CvNBv7_w.js +1 -0
- package/dist/assets/runtime-config-page-CflvvoDc.js +1 -0
- package/dist/assets/{save-BOJ477H-.js → save-BpFW5fpk.js} +1 -1
- package/dist/assets/{search-BEti5beA.js → search-D0uO7enf.js} +1 -1
- package/dist/assets/search-config-page-D0klFUXd.js +1 -0
- package/dist/assets/{secrets-config-page--IzrIOid.js → secrets-config-page-dVZ631Wp.js} +2 -2
- package/dist/assets/security-config-C5gnlkuj.js +1 -0
- package/dist/assets/select-Bh-CrA4p.js +1 -0
- package/dist/assets/setting-row-BJfCHRcb.js +1 -0
- package/dist/assets/settings-2-Blzi1vr6.js +1 -0
- package/dist/assets/side-dock-ZdaSrQVB.js +3 -0
- package/dist/assets/skeleton-BLlEzebZ.js +1 -0
- package/dist/assets/switch-CFLJFNSd.js +1 -0
- package/dist/assets/tag-chip-CzGihiQx.js +1 -0
- package/dist/assets/tooltip-CdcrBjrC.js +1 -0
- package/dist/assets/use-config-CVPgsBmU.js +1 -0
- package/dist/assets/use-confirm-dialog-CSeBk9SF.js +1 -0
- package/dist/assets/x-BF2KQ--V.js +1 -0
- package/dist/index.html +53 -17
- package/index.html +25 -2
- package/package.json +9 -9
- package/src/app/components/layout/__tests__/app-layout.test.tsx +27 -0
- package/src/app/components/layout/__tests__/settings-entry-page.test.tsx +9 -1
- package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +6 -3
- package/src/app/components/layout/app-layout.tsx +10 -3
- package/src/app/components/layout/sidebar-items.tsx +8 -8
- package/src/app/components/layout/sidebar-rail.styles.ts +4 -2
- package/src/app/components/layout/sidebar.tsx +21 -28
- package/src/app/configs/app-navigation.config.ts +11 -5
- package/src/app/index.tsx +8 -0
- package/src/app/managers/__tests__/viewport-layout.manager.test.ts +59 -0
- package/src/app/managers/viewport-layout.manager.ts +39 -0
- package/src/app/presenters/app.presenter.ts +25 -2
- package/src/app/styles/design-system.css +91 -4
- package/src/features/agents/components/__tests__/agents-page.test.tsx +135 -2
- package/src/features/agents/components/agent-advanced-config-fields.tsx +70 -0
- package/src/features/agents/components/agent-details-dialog.tsx +398 -0
- package/src/features/agents/components/agent-dialogs.tsx +20 -47
- package/src/features/agents/components/agents-page.tsx +76 -10
- package/src/features/agents/types/agent-form.types.ts +21 -0
- package/src/features/agents/utils/agent-form.utils.ts +39 -0
- package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +86 -0
- package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +132 -1
- package/src/features/chat/components/conversation/chat-conversation-content.tsx +47 -17
- package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +8 -0
- package/src/features/chat/components/conversation/chat-conversation-header.tsx +7 -3
- package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +7 -0
- package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +9 -5
- package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +5 -5
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +66 -2
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +170 -1
- package/src/features/chat/features/conversation/components/session-conversation-area.tsx +23 -1
- package/src/features/chat/features/conversation/components/session-conversation-input.tsx +24 -7
- package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +96 -0
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +181 -0
- package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +351 -89
- package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +29 -4
- package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +28 -3
- package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -8
- package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +50 -14
- package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +77 -6
- package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +21 -12
- package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +167 -1
- package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +19 -11
- package/src/features/chat/features/message/components/chat-message-list.container.tsx +38 -64
- package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +59 -0
- package/src/features/chat/features/message/utils/__tests__/chat-message-show-content-tool-card.utils.test.ts +56 -1
- package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +0 -2
- package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +80 -0
- package/src/features/chat/features/message/utils/chat-message-show-content-tool-card.utils.ts +12 -3
- package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +60 -0
- package/src/features/chat/features/message/utils/chat-message.utils.ts +3 -0
- package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime.test.tsx +62 -1
- package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-session-list-view.test.tsx +74 -0
- package/src/features/chat/features/ncp/hooks/use-ncp-session-list-view.ts +3 -2
- package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +2 -2
- package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +1 -1
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +3 -1
- package/src/features/chat/features/session/components/session-header/chat-session-metadata-dialog.tsx +1 -1
- package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +294 -0
- package/src/features/chat/features/session/utils/__tests__/chat-session-display.utils.test.ts +15 -1
- package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts +24 -4
- package/src/features/chat/features/session/utils/chat-session-display.utils.ts +3 -0
- package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +1 -0
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +304 -115
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +121 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +75 -20
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +64 -5
- package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +46 -0
- package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +83 -0
- package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +126 -1
- package/src/features/chat/managers/__tests__/chat-ui.manager.test.ts +2 -0
- package/src/features/chat/managers/chat-thread.manager.ts +15 -3
- package/src/features/chat/managers/chat-ui.manager.ts +5 -1
- package/src/features/chat/presenters/chat.presenter.ts +2 -1
- package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +4 -0
- package/src/features/chat/stores/chat-thread.store.ts +6 -64
- package/src/features/chat/types/chat-message.types.ts +4 -0
- package/src/features/marketplace/components/__tests__/marketplace-page-detail.test.tsx +33 -26
- package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +1 -0
- package/src/features/marketplace/components/curated-shelves/__tests__/marketplace-curated-scene-route.test.tsx +1 -0
- package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.tsx +115 -0
- package/src/features/marketplace/components/marketplace-detail-doc.tsx +441 -0
- package/src/features/marketplace/components/mcp/__tests__/mcp-marketplace-page.test.tsx +3 -3
- package/src/features/marketplace/components/mcp/mcp-marketplace-doc.ts +0 -60
- package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +64 -29
- package/src/features/marketplace/hooks/use-marketplace-item-detail.ts +42 -31
- package/src/features/marketplace/index.ts +3 -0
- package/src/features/marketplace/stores/marketplace-detail-doc.store.ts +38 -0
- package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +32 -2
- package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +11 -0
- package/src/features/pwa/managers/pwa-shell-theme.manager.ts +8 -3
- package/src/features/settings/pages/__tests__/appearance-settings-page.test.tsx +25 -0
- package/src/features/settings/pages/appearance-settings-page.tsx +40 -0
- package/src/features/side-dock/components/__tests__/side-dock.test.tsx +44 -2
- package/src/features/side-dock/components/side-dock.tsx +100 -13
- package/src/features/side-dock/configs/side-dock-built-in-items.config.ts +10 -0
- package/src/features/side-dock/managers/__tests__/side-dock.manager.test.ts +32 -2
- package/src/features/side-dock/managers/side-dock.manager.ts +17 -7
- package/src/features/side-dock/stores/side-dock.store.ts +9 -0
- package/src/features/side-dock/types/side-dock.types.ts +10 -3
- package/src/features/side-dock/utils/side-dock-item.utils.ts +15 -2
- package/src/features/system-status/components/__tests__/desktop-update-config.test.tsx +97 -5
- package/src/features/system-status/components/desktop-update-config.tsx +91 -2
- package/src/features/system-status/utils/update-release-notes.utils.ts +94 -0
- package/src/index.css +501 -27
- package/src/platforms/desktop/components/desktop-window-chrome.tsx +1 -1
- package/src/shared/components/doc-browser/__tests__/doc-browser-context.test.tsx +30 -1
- package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +206 -38
- package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +34 -11
- package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +1 -1
- package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +23 -5
- package/src/shared/components/doc-browser/doc-browser.tsx +128 -75
- package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +9 -1
- package/src/shared/components/doc-browser/utils/doc-browser-floating-panel.utils.ts +83 -0
- package/src/shared/components/ui/__tests__/dialog.test.tsx +27 -0
- package/src/shared/components/ui/actions/icon-action-button.tsx +14 -2
- package/src/shared/components/ui/dialog.tsx +2 -2
- package/src/shared/components/ui/popover.tsx +1 -1
- package/src/shared/components/ui/select.tsx +1 -1
- package/src/shared/components/ui/switch.tsx +4 -3
- package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +54 -0
- package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +4 -5
- package/src/shared/components/ui/tooltip.tsx +1 -1
- package/src/shared/lib/api/ncp-session.types.ts +1 -1
- package/src/shared/lib/api/types.ts +12 -3
- package/src/shared/lib/api/utils/server-path.utils.ts +27 -1
- package/src/shared/lib/i18n/locales/en-US/agents.json +41 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +18 -6
- package/src/shared/lib/i18n/locales/en-US/core.json +2 -0
- package/src/shared/lib/i18n/locales/en-US/desktop-update.json +8 -0
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +11 -0
- package/src/shared/lib/i18n/locales/en-US/marketplace.json +5 -0
- package/src/shared/lib/i18n/locales/zh-CN/agents.json +41 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +18 -6
- package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -0
- package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +8 -0
- package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +11 -0
- package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +5 -0
- package/src/shared/lib/session-project/__tests__/workspace-file-breadcrumb.test.ts +14 -0
- package/src/shared/lib/session-project/index.ts +1 -1
- package/src/shared/lib/session-project/{workspace-file-breadcrumb.ts → workspace-file-breadcrumb.utils.ts} +62 -8
- package/src/shared/lib/theme/index.ts +51 -13
- package/dist/assets/api-BP_8IaxB.js +0 -13
- package/dist/assets/channels-list-page-DEh5Jjqx.js +0 -8
- package/dist/assets/chat-page-BRB0N5WW.js +0 -105
- package/dist/assets/confirm-dialog-DsivFJ4r.js +0 -5
- package/dist/assets/desktop-update-config-B0w9p2ep.js +0 -1
- package/dist/assets/doc-browser-BoNJ7-tE.js +0 -1
- package/dist/assets/doc-browser-CdJ_d4ik.js +0 -1
- package/dist/assets/ellipsis-y6vDtq4M.js +0 -1
- package/dist/assets/index-BiE-K8-q.css +0 -1
- package/dist/assets/index-D9BAE_L_.js +0 -105
- package/dist/assets/mcp-marketplace-page-B1SZNY5m.js +0 -40
- package/dist/assets/mcp-marketplace-page-Cq_Tw42T.js +0 -1
- package/dist/assets/model-config-page-DotaTUHb.js +0 -1
- package/dist/assets/plus-BbZqquJe.js +0 -1
- package/dist/assets/provider-scoped-model-input-DsFRuWJH.js +0 -1
- package/dist/assets/providers-config-page-DW4tQSLN.js +0 -1
- package/dist/assets/remote-B8TiUfwW.js +0 -1
- package/dist/assets/rotate-cw-DjHDTkCv.js +0 -1
- package/dist/assets/runtime-config-page-S6ErRHR8.js +0 -1
- package/dist/assets/search-config-page-DGj5am03.js +0 -1
- package/dist/assets/select-hoj8dK4K.js +0 -41
- package/dist/assets/tag-chip-T8-h-die.js +0 -1
- package/dist/assets/use-config-CjRV2hwL.js +0 -1
- package/dist/assets/x-D9PXs_xP.js +0 -1
- package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.ts +0 -40
- package/src/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer.ts +0 -201
- package/src/features/marketplace/components/marketplace-detail-doc.ts +0 -142
- /package/dist/assets/{config-hints-BkYBc7z0.js → config-hints-fGnUjDe9.js} +0 -0
- /package/dist/assets/{middleware-CkFT3R4v.js → middleware-BILWbJE4.js} +0 -0
package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import type { NcpMessage } from "@nextclaw/ncp";
|
|
4
|
+
import { beforeEach, expect, it, vi } from "vitest";
|
|
5
|
+
import { ChatConversationContent } from "@/features/chat/components/conversation/chat-conversation-content";
|
|
6
|
+
|
|
7
|
+
const captures = vi.hoisted(() => ({
|
|
8
|
+
isAtBottom: true,
|
|
9
|
+
onScroll: vi.fn(),
|
|
10
|
+
scrollToBottom: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock("@nextclaw/agent-chat-ui", () => ({
|
|
14
|
+
useStickyBottomScroll: () => ({
|
|
15
|
+
isAtBottom: captures.isAtBottom,
|
|
16
|
+
onScroll: captures.onScroll,
|
|
17
|
+
scrollToBottom: captures.scrollToBottom,
|
|
18
|
+
}),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock("@/features/chat/features/message/components/chat-message-list.container", () => ({
|
|
22
|
+
ChatMessageListContainer: () => <div data-testid="chat-message-list" />,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock("@/shared/lib/i18n", () => ({
|
|
26
|
+
t: (key: string) => key,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
const messages = [
|
|
30
|
+
{
|
|
31
|
+
id: "message-1",
|
|
32
|
+
sessionId: "session-1",
|
|
33
|
+
role: "user",
|
|
34
|
+
status: "final",
|
|
35
|
+
timestamp: "2026-07-04T10:00:00.000Z",
|
|
36
|
+
parts: [{ type: "text", text: "hello" }],
|
|
37
|
+
},
|
|
38
|
+
] as unknown as readonly NcpMessage[];
|
|
39
|
+
|
|
40
|
+
function renderContent(options: { bottomSlot?: React.ReactNode } = {}) {
|
|
41
|
+
return render(
|
|
42
|
+
<ChatConversationContent
|
|
43
|
+
bottomSlot={options.bottomSlot}
|
|
44
|
+
isAwaitingAssistantOutput={false}
|
|
45
|
+
isHistoryLoading={false}
|
|
46
|
+
isSending={false}
|
|
47
|
+
messages={messages}
|
|
48
|
+
sessionKey="session-1"
|
|
49
|
+
showWelcome={false}
|
|
50
|
+
/>,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
captures.isAtBottom = true;
|
|
56
|
+
captures.onScroll.mockReset();
|
|
57
|
+
captures.scrollToBottom.mockReset();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("hides the scroll-to-bottom action while the conversation is already at the bottom", () => {
|
|
61
|
+
renderContent();
|
|
62
|
+
|
|
63
|
+
expect(screen.queryByRole("button", { name: "chatScrollToBottom" })).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("scrolls to the latest message when the user clicks the floating action", async () => {
|
|
67
|
+
const user = userEvent.setup();
|
|
68
|
+
captures.isAtBottom = false;
|
|
69
|
+
|
|
70
|
+
renderContent();
|
|
71
|
+
await user.click(screen.getByRole("button", { name: "chatScrollToBottom" }));
|
|
72
|
+
|
|
73
|
+
expect(captures.scrollToBottom).toHaveBeenCalledOnce();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("renders bottom slot after the message list", () => {
|
|
77
|
+
renderContent({
|
|
78
|
+
bottomSlot: <div data-testid="conversation-bottom-slot">Run failed</div>,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const messageList = screen.getByTestId("chat-message-list");
|
|
82
|
+
const bottomSlot = screen.getByTestId("conversation-bottom-slot");
|
|
83
|
+
expect(
|
|
84
|
+
messageList.compareDocumentPosition(bottomSlot) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
85
|
+
).toBeTruthy();
|
|
86
|
+
});
|
|
@@ -7,12 +7,17 @@ import { ChatConversationHeaderSection } from "@/features/chat/components/conver
|
|
|
7
7
|
import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
|
|
8
8
|
import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
|
|
9
9
|
import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
|
|
10
|
-
import
|
|
10
|
+
import { viewportLayoutManager } from "@/app/managers/viewport-layout.manager";
|
|
11
|
+
import type { NcpSessionListItemView } from "@/features/chat/features/ncp/hooks/use-ncp-session-list-view";
|
|
12
|
+
import type { NcpSessionSummaryView, SessionEntryView } from "@/shared/lib/api";
|
|
11
13
|
|
|
12
14
|
const mocks = vi.hoisted(() => ({
|
|
13
15
|
deleteSession: vi.fn(),
|
|
14
16
|
openChildSessionPanel: vi.fn(),
|
|
15
17
|
openSessionCronPanel: vi.fn(),
|
|
18
|
+
selectSession: vi.fn(),
|
|
19
|
+
sessionItems: [] as NcpSessionListItemView[],
|
|
20
|
+
isSessionListLoading: false,
|
|
16
21
|
}));
|
|
17
22
|
|
|
18
23
|
vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
|
|
@@ -22,9 +27,35 @@ vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
|
|
|
22
27
|
openChildSessionPanel: mocks.openChildSessionPanel,
|
|
23
28
|
openSessionCronPanel: mocks.openSessionCronPanel,
|
|
24
29
|
},
|
|
30
|
+
chatSessionListManager: {
|
|
31
|
+
selectSession: mocks.selectSession,
|
|
32
|
+
},
|
|
25
33
|
}),
|
|
26
34
|
}));
|
|
27
35
|
|
|
36
|
+
vi.mock("@/features/chat/features/ncp/hooks/use-ncp-session-list-view", () => ({
|
|
37
|
+
useNcpSessionListView: (params?: { query?: string | null }) => {
|
|
38
|
+
const query = params?.query?.trim().toLowerCase() ?? "";
|
|
39
|
+
const items = query
|
|
40
|
+
? mocks.sessionItems.filter(({ session }) =>
|
|
41
|
+
[
|
|
42
|
+
session.key,
|
|
43
|
+
session.label ?? "",
|
|
44
|
+
session.projectRoot ?? "",
|
|
45
|
+
session.projectName ?? "",
|
|
46
|
+
]
|
|
47
|
+
.join(" ")
|
|
48
|
+
.toLowerCase()
|
|
49
|
+
.includes(query),
|
|
50
|
+
)
|
|
51
|
+
: mocks.sessionItems;
|
|
52
|
+
return {
|
|
53
|
+
isLoading: mocks.isSessionListLoading,
|
|
54
|
+
items,
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
}));
|
|
58
|
+
|
|
28
59
|
vi.mock("@/features/cron", () => ({
|
|
29
60
|
useCronJobs: () => ({ data: { jobs: [] } }),
|
|
30
61
|
}));
|
|
@@ -54,6 +85,24 @@ function createSummary(
|
|
|
54
85
|
};
|
|
55
86
|
}
|
|
56
87
|
|
|
88
|
+
function createSessionListItem(
|
|
89
|
+
overrides: Partial<SessionEntryView> & Pick<SessionEntryView, "key">,
|
|
90
|
+
): NcpSessionListItemView {
|
|
91
|
+
return {
|
|
92
|
+
session: {
|
|
93
|
+
createdAt: "2026-06-18T00:00:00.000Z",
|
|
94
|
+
updatedAt: "2026-06-18T00:00:00.000Z",
|
|
95
|
+
lastMessageAt: "2026-06-18T00:00:00.000Z",
|
|
96
|
+
readAt: "2026-06-18T00:00:00.000Z",
|
|
97
|
+
sessionType: "native",
|
|
98
|
+
sessionTypeMutable: false,
|
|
99
|
+
messageCount: 1,
|
|
100
|
+
...overrides,
|
|
101
|
+
},
|
|
102
|
+
runStatus: overrides.status === "running" ? "running" : undefined,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
57
106
|
function renderHeaderSection() {
|
|
58
107
|
const queryClient = new QueryClient({
|
|
59
108
|
defaultOptions: {
|
|
@@ -73,6 +122,9 @@ describe("ChatConversationHeaderSection", () => {
|
|
|
73
122
|
mocks.deleteSession.mockReset();
|
|
74
123
|
mocks.openChildSessionPanel.mockReset();
|
|
75
124
|
mocks.openSessionCronPanel.mockReset();
|
|
125
|
+
mocks.selectSession.mockReset();
|
|
126
|
+
mocks.isSessionListLoading = false;
|
|
127
|
+
viewportLayoutManager.resetForTests();
|
|
76
128
|
useChatSessionListStore.persist.setOptions({
|
|
77
129
|
storage: new MemoryPersistStorage(),
|
|
78
130
|
});
|
|
@@ -129,6 +181,18 @@ describe("ChatConversationHeaderSection", () => {
|
|
|
129
181
|
} as never,
|
|
130
182
|
},
|
|
131
183
|
});
|
|
184
|
+
mocks.sessionItems = [
|
|
185
|
+
createSessionListItem({
|
|
186
|
+
key: "parent-session-1",
|
|
187
|
+
label: "Parent Task",
|
|
188
|
+
}),
|
|
189
|
+
createSessionListItem({
|
|
190
|
+
key: "session:ncp-2",
|
|
191
|
+
label: "Background Task",
|
|
192
|
+
lastMessageAt: "2026-06-18T01:00:00.000Z",
|
|
193
|
+
readAt: "2026-06-18T01:00:00.000Z",
|
|
194
|
+
}),
|
|
195
|
+
];
|
|
132
196
|
});
|
|
133
197
|
|
|
134
198
|
it("shows the child-session entry from session query state before a tool card opens it", async () => {
|
|
@@ -142,4 +206,71 @@ describe("ChatConversationHeaderSection", () => {
|
|
|
142
206
|
activeChildSessionKey: "child-session-1",
|
|
143
207
|
});
|
|
144
208
|
});
|
|
209
|
+
|
|
210
|
+
it("uses the collapsed desktop title as a session switcher", async () => {
|
|
211
|
+
const user = userEvent.setup();
|
|
212
|
+
viewportLayoutManager.setSidebarCollapsed(true);
|
|
213
|
+
|
|
214
|
+
renderHeaderSection();
|
|
215
|
+
|
|
216
|
+
await user.click(screen.getByRole("button", { name: /Switch session/ }));
|
|
217
|
+
await user.click(screen.getByRole("button", { name: /Background Task/ }));
|
|
218
|
+
|
|
219
|
+
expect(mocks.selectSession).toHaveBeenCalledWith("session:ncp-2");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("keeps the desktop title plain while the sidebar is expanded", () => {
|
|
223
|
+
renderHeaderSection();
|
|
224
|
+
|
|
225
|
+
expect(screen.queryByRole("button", { name: /Switch session/ })).toBeNull();
|
|
226
|
+
expect(screen.getByText("Parent Task")).toBeTruthy();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("keeps session switching available from a collapsed new-session header", async () => {
|
|
230
|
+
const user = userEvent.setup();
|
|
231
|
+
viewportLayoutManager.setSidebarCollapsed(true);
|
|
232
|
+
useChatSessionListStore.setState({
|
|
233
|
+
snapshot: {
|
|
234
|
+
...useChatSessionListStore.getState().snapshot,
|
|
235
|
+
selectedSessionKey: null,
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
useChatThreadStore.setState({
|
|
239
|
+
snapshot: {
|
|
240
|
+
...useChatThreadStore.getState().snapshot,
|
|
241
|
+
sessionKey: null,
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
renderHeaderSection();
|
|
246
|
+
|
|
247
|
+
await user.click(screen.getByRole("button", { name: /Switch session/ }));
|
|
248
|
+
await user.click(screen.getByRole("button", { name: /Background Task/ }));
|
|
249
|
+
|
|
250
|
+
expect(mocks.selectSession).toHaveBeenCalledWith("session:ncp-2");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("filters the collapsed title switcher with a local search query", async () => {
|
|
254
|
+
const user = userEvent.setup();
|
|
255
|
+
viewportLayoutManager.setSidebarCollapsed(true);
|
|
256
|
+
|
|
257
|
+
renderHeaderSection();
|
|
258
|
+
|
|
259
|
+
await user.click(screen.getByRole("button", { name: /Switch session/ }));
|
|
260
|
+
const searchInput = screen.getByRole("textbox", {
|
|
261
|
+
name: "Search session key / label",
|
|
262
|
+
});
|
|
263
|
+
await user.type(searchInput, "Background");
|
|
264
|
+
await user.click(screen.getByRole("button", { name: /Background Task/ }));
|
|
265
|
+
|
|
266
|
+
expect(mocks.selectSession).toHaveBeenCalledWith("session:ncp-2");
|
|
267
|
+
|
|
268
|
+
await user.click(screen.getByRole("button", { name: /Switch session/ }));
|
|
269
|
+
await user.type(
|
|
270
|
+
screen.getByRole("textbox", { name: "Search session key / label" }),
|
|
271
|
+
"missing",
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
expect(screen.getByText("No matching sessions")).toBeTruthy();
|
|
275
|
+
});
|
|
145
276
|
});
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { useRef, type ReactNode } from "react";
|
|
2
2
|
import { useStickyBottomScroll } from "@nextclaw/agent-chat-ui";
|
|
3
3
|
import type { NcpMessage } from "@nextclaw/ncp";
|
|
4
|
+
import { ArrowDown } from "lucide-react";
|
|
4
5
|
import { ChatMessageListContainer } from "@/features/chat/features/message/components/chat-message-list.container";
|
|
6
|
+
import { IconActionButton } from "@/shared/components/ui/actions/icon-action-button";
|
|
7
|
+
import { t } from "@/shared/lib/i18n";
|
|
5
8
|
|
|
6
9
|
type ChatConversationContentProps = {
|
|
10
|
+
bottomSlot?: ReactNode;
|
|
7
11
|
isAwaitingAssistantOutput: boolean;
|
|
8
12
|
isHistoryLoading: boolean;
|
|
9
13
|
isSending: boolean;
|
|
@@ -14,6 +18,7 @@ type ChatConversationContentProps = {
|
|
|
14
18
|
};
|
|
15
19
|
|
|
16
20
|
export function ChatConversationContent({
|
|
21
|
+
bottomSlot,
|
|
17
22
|
isAwaitingAssistantOutput,
|
|
18
23
|
isHistoryLoading,
|
|
19
24
|
isSending,
|
|
@@ -28,7 +33,11 @@ export function ChatConversationContent({
|
|
|
28
33
|
messages.length === 0 &&
|
|
29
34
|
!isSending &&
|
|
30
35
|
!isAwaitingAssistantOutput;
|
|
31
|
-
const {
|
|
36
|
+
const {
|
|
37
|
+
isAtBottom,
|
|
38
|
+
onScroll,
|
|
39
|
+
scrollToBottom,
|
|
40
|
+
} = useStickyBottomScroll({
|
|
32
41
|
scrollRef: threadRef,
|
|
33
42
|
resetKey: sessionKey,
|
|
34
43
|
isLoading: isHistoryLoading,
|
|
@@ -36,24 +45,45 @@ export function ChatConversationContent({
|
|
|
36
45
|
contentVersion: messages[messages.length - 1] ?? null,
|
|
37
46
|
});
|
|
38
47
|
const hasMessages = messages.length > 0;
|
|
48
|
+
const showScrollToBottom = hasMessages && !showWelcome && !isAtBottom;
|
|
39
49
|
|
|
40
50
|
return (
|
|
41
|
-
<div
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
<div className="relative min-h-0 flex-1">
|
|
52
|
+
<div
|
|
53
|
+
ref={threadRef}
|
|
54
|
+
onScroll={onScroll}
|
|
55
|
+
data-chat-scroll-container="true"
|
|
56
|
+
className="h-full overflow-y-auto custom-scrollbar"
|
|
57
|
+
>
|
|
58
|
+
{showWelcome ? (
|
|
59
|
+
welcomeSlot ?? null
|
|
60
|
+
) : (
|
|
61
|
+
<>
|
|
62
|
+
{hideEmptyHint || !hasMessages ? null : (
|
|
63
|
+
<div className="mx-auto w-full max-w-[min(1120px,100%)] px-4 py-4 sm:px-6 sm:py-5">
|
|
64
|
+
<ChatMessageListContainer
|
|
65
|
+
messages={messages}
|
|
66
|
+
isSending={hasMessages && isSending && isAwaitingAssistantOutput}
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
{bottomSlot ? (
|
|
71
|
+
<div className="mx-auto w-full max-w-[min(1120px,100%)] px-4 pb-4 sm:px-6 sm:pb-5">
|
|
72
|
+
{bottomSlot}
|
|
73
|
+
</div>
|
|
74
|
+
) : null}
|
|
75
|
+
</>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
{showScrollToBottom ? (
|
|
79
|
+
<IconActionButton
|
|
80
|
+
icon={<ArrowDown className="h-4 w-4" />}
|
|
81
|
+
label={t("chatScrollToBottom")}
|
|
82
|
+
onClick={scrollToBottom}
|
|
83
|
+
tooltipSide="top"
|
|
84
|
+
className="absolute bottom-4 left-1/2 z-10 h-9 w-9 -translate-x-1/2 rounded-full border border-border bg-background/90 text-foreground shadow-lg backdrop-blur hover:bg-accent hover:text-accent-foreground"
|
|
85
|
+
/>
|
|
86
|
+
) : null}
|
|
57
87
|
</div>
|
|
58
88
|
);
|
|
59
89
|
}
|
|
@@ -2,6 +2,7 @@ import { useMemo } from "react";
|
|
|
2
2
|
import { ChatConversationHeader } from "@/features/chat/components/conversation/chat-conversation-header";
|
|
3
3
|
import { ChatSessionHeaderActions } from "@/features/chat/features/session/components/session-header/chat-session-header-actions";
|
|
4
4
|
import { ChatSessionProjectBadge } from "@/features/chat/features/session/components/session-header/chat-session-project-badge";
|
|
5
|
+
import { ChatSessionTitleSwitcher } from "@/features/chat/features/session/components/session-header/chat-session-title-switcher";
|
|
5
6
|
import { usePresenter } from "@/features/chat/components/providers/chat-presenter.provider";
|
|
6
7
|
import { SessionContextIconNode } from "@/features/chat/features/session/components/session-context-icon";
|
|
7
8
|
import { useChatConversationWorkspaceState } from "@/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state";
|
|
@@ -89,6 +90,13 @@ export function ChatConversationHeaderSection({
|
|
|
89
90
|
<ChatConversationHeader
|
|
90
91
|
layoutMode={layoutMode}
|
|
91
92
|
title={sessionHeaderTitle}
|
|
93
|
+
titleContent={
|
|
94
|
+
<ChatSessionTitleSwitcher
|
|
95
|
+
layoutMode={layoutMode}
|
|
96
|
+
selectedSessionKey={sessionKey}
|
|
97
|
+
title={sessionHeaderTitle}
|
|
98
|
+
/>
|
|
99
|
+
}
|
|
92
100
|
shouldShow={shouldShowSessionHeader}
|
|
93
101
|
onBackToList={onBackToList}
|
|
94
102
|
leading={
|
|
@@ -41,6 +41,7 @@ export function ChatConversationHeader({
|
|
|
41
41
|
sessionTypeBadge,
|
|
42
42
|
shouldShow,
|
|
43
43
|
title,
|
|
44
|
+
titleContent,
|
|
44
45
|
}: {
|
|
45
46
|
layoutMode: "desktop" | "mobile";
|
|
46
47
|
actions?: ReactNode;
|
|
@@ -50,6 +51,7 @@ export function ChatConversationHeader({
|
|
|
50
51
|
sessionTypeBadge?: ReactNode;
|
|
51
52
|
shouldShow: boolean;
|
|
52
53
|
title: string;
|
|
54
|
+
titleContent?: ReactNode;
|
|
53
55
|
}) {
|
|
54
56
|
const isMobileLayout = layoutMode === "mobile";
|
|
55
57
|
|
|
@@ -78,9 +80,11 @@ export function ChatConversationHeader({
|
|
|
78
80
|
/>
|
|
79
81
|
) : null}
|
|
80
82
|
{leading}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
{titleContent ?? (
|
|
84
|
+
<span className="text-sm font-medium text-foreground truncate">
|
|
85
|
+
{title}
|
|
86
|
+
</span>
|
|
87
|
+
)}
|
|
84
88
|
{sessionTypeBadge}
|
|
85
89
|
{projectBadge}
|
|
86
90
|
</div>
|
|
@@ -188,6 +188,13 @@ describe("ChatSidebar read state sync", () => {
|
|
|
188
188
|
it("collapses the desktop sidebar into accessible icon actions", () => {
|
|
189
189
|
const { container } = renderReadStateSidebar(createTestQueryClient());
|
|
190
190
|
|
|
191
|
+
expect(
|
|
192
|
+
screen.getByText("Sessions").parentElement?.nextElementSibling?.className,
|
|
193
|
+
).toContain("[mask-image:linear-gradient");
|
|
194
|
+
expect(
|
|
195
|
+
screen.getByRole("button", { name: "Settings menu" }).parentElement
|
|
196
|
+
?.className,
|
|
197
|
+
).not.toContain("border-t");
|
|
191
198
|
fireEvent.click(screen.getByRole("button", { name: "Collapse sidebar" }));
|
|
192
199
|
|
|
193
200
|
const persistedState = JSON.parse(
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
SIDEBAR_RAIL_PADDING_X_CLASS,
|
|
30
30
|
SIDEBAR_RAIL_STACK_CLASS,
|
|
31
31
|
SIDEBAR_RAIL_SURFACE_CLASS,
|
|
32
|
+
SIDEBAR_SCROLL_EDGE_FADE_CLASS,
|
|
32
33
|
} from "@/app/components/layout/sidebar-rail.styles";
|
|
33
34
|
|
|
34
35
|
const navItems = [
|
|
@@ -67,9 +68,7 @@ export function ChatSidebarDesktopHeader({
|
|
|
67
68
|
isCollapsed: boolean;
|
|
68
69
|
}) {
|
|
69
70
|
const isWindowsHost = isWindowsDesktopHost();
|
|
70
|
-
const shouldReserveMacWindowControls =
|
|
71
|
-
typeof window !== "undefined" &&
|
|
72
|
-
window.nextclawDesktop?.platform === "darwin";
|
|
71
|
+
const shouldReserveMacWindowControls = typeof window !== "undefined" && window.nextclawDesktop?.platform === "darwin";
|
|
73
72
|
|
|
74
73
|
return (
|
|
75
74
|
<div
|
|
@@ -170,7 +169,12 @@ export function ChatSidebarSessionArea({
|
|
|
170
169
|
/>
|
|
171
170
|
</div>
|
|
172
171
|
|
|
173
|
-
<div
|
|
172
|
+
<div
|
|
173
|
+
className={cn(
|
|
174
|
+
"custom-scrollbar min-h-0 flex-1 overflow-y-auto px-3 pb-7 pt-2",
|
|
175
|
+
SIDEBAR_SCROLL_EDGE_FADE_CLASS,
|
|
176
|
+
)}
|
|
177
|
+
>
|
|
174
178
|
<ChatSidebarSessionList
|
|
175
179
|
isLoading={isLoading}
|
|
176
180
|
isProjectFirstView={isProjectFirstView}
|
|
@@ -221,7 +225,7 @@ export function ChatSidebarDesktopFooter({
|
|
|
221
225
|
return (
|
|
222
226
|
<div
|
|
223
227
|
className={cn(
|
|
224
|
-
"
|
|
228
|
+
"py-3",
|
|
225
229
|
isCollapsed
|
|
226
230
|
? cn("flex justify-center", SIDEBAR_RAIL_PADDING_X_CLASS)
|
|
227
231
|
: "px-3",
|
|
@@ -115,7 +115,7 @@ export function ChatSidebarUtilityMenu({
|
|
|
115
115
|
<button
|
|
116
116
|
type="button"
|
|
117
117
|
aria-label={t("settingsMenu")}
|
|
118
|
-
className="flex w-full items-center gap-2.5 rounded-xl px-3 py-2 text-[13px] font-medium text-muted-foreground transition-
|
|
118
|
+
className="flex w-full items-center gap-2.5 rounded-xl px-3 py-2 text-[13px] font-medium text-muted-foreground transition-colors duration-base hover:bg-gray-200/60 hover:text-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35"
|
|
119
119
|
>
|
|
120
120
|
<Settings className="h-4 w-4 shrink-0 text-muted-foreground/70" />
|
|
121
121
|
<span className="min-w-0 flex-1 text-left">{t("settings")}</span>
|
|
@@ -134,7 +134,7 @@ export function ChatSidebarUtilityMenu({
|
|
|
134
134
|
<NavLink
|
|
135
135
|
to="/settings"
|
|
136
136
|
onClick={() => onOpenChange(false)}
|
|
137
|
-
className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-foreground transition-colors hover:bg-
|
|
137
|
+
className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-foreground transition-colors hover:bg-gray-200/60 hover:text-gray-900"
|
|
138
138
|
>
|
|
139
139
|
<Settings className="h-4 w-4 text-muted-foreground/70" />
|
|
140
140
|
<span className="flex-1 text-left">{t("settings")}</span>
|
|
@@ -142,7 +142,7 @@ export function ChatSidebarUtilityMenu({
|
|
|
142
142
|
<button
|
|
143
143
|
type="button"
|
|
144
144
|
onClick={handleOpenDocs}
|
|
145
|
-
className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-foreground transition-colors hover:bg-
|
|
145
|
+
className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-foreground transition-colors hover:bg-gray-200/60 hover:text-gray-900"
|
|
146
146
|
>
|
|
147
147
|
<BookOpen className="h-4 w-4 text-muted-foreground/70" />
|
|
148
148
|
<span className="flex-1 text-left">{t("docBrowserHelp")}</span>
|
|
@@ -150,7 +150,7 @@ export function ChatSidebarUtilityMenu({
|
|
|
150
150
|
<button
|
|
151
151
|
type="button"
|
|
152
152
|
onClick={handleOpenApps}
|
|
153
|
-
className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-foreground transition-colors hover:bg-
|
|
153
|
+
className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-foreground transition-colors hover:bg-gray-200/60 hover:text-gray-900"
|
|
154
154
|
>
|
|
155
155
|
<Boxes className="h-4 w-4 text-muted-foreground/70" />
|
|
156
156
|
<span className="flex-1 text-left">{t("appsTitle")}</span>
|
|
@@ -212,7 +212,7 @@ function ChatSidebarUtilitySelect<Value extends string>({
|
|
|
212
212
|
>
|
|
213
213
|
<SelectTrigger
|
|
214
214
|
aria-label={label}
|
|
215
|
-
className="h-auto w-full rounded-lg border-0 bg-transparent px-3 py-2 text-[13px] font-medium text-foreground shadow-none hover:bg-
|
|
215
|
+
className="h-auto w-full rounded-lg border-0 bg-transparent px-3 py-2 text-[13px] font-medium text-foreground shadow-none hover:bg-gray-200/60 hover:text-gray-900 focus:ring-0"
|
|
216
216
|
indicator={<ChevronRight className="h-4 w-4 text-muted-foreground/70" />}
|
|
217
217
|
>
|
|
218
218
|
<div className="flex min-w-0 items-center gap-2.5">
|
|
@@ -7,6 +7,7 @@ import { SessionConversationArea } from '@/features/chat/features/conversation/c
|
|
|
7
7
|
|
|
8
8
|
const mocks = vi.hoisted(() => {
|
|
9
9
|
const inputRenderSpy = vi.fn();
|
|
10
|
+
const controllerParamsSpy = vi.fn();
|
|
10
11
|
const inputActions = {
|
|
11
12
|
update: vi.fn(),
|
|
12
13
|
syncComposer: vi.fn(),
|
|
@@ -47,7 +48,14 @@ const mocks = vi.hoisted(() => {
|
|
|
47
48
|
isProviderStateResolved: true,
|
|
48
49
|
isSkillsLoading: false,
|
|
49
50
|
modelOptions: [],
|
|
50
|
-
selectedSession: null
|
|
51
|
+
selectedSession: null as null | {
|
|
52
|
+
activityPreview?: {
|
|
53
|
+
state: 'running' | 'completed' | 'failed' | 'cancelled' | 'idle';
|
|
54
|
+
statusText?: string;
|
|
55
|
+
replyText?: string;
|
|
56
|
+
};
|
|
57
|
+
status?: string;
|
|
58
|
+
},
|
|
51
59
|
selectedSessionKey: null,
|
|
52
60
|
sessionTypeState: {
|
|
53
61
|
sessionTypeOptions: [],
|
|
@@ -62,8 +70,11 @@ const mocks = vi.hoisted(() => {
|
|
|
62
70
|
};
|
|
63
71
|
const controller = {
|
|
64
72
|
canStopGeneration: true,
|
|
73
|
+
deleteQueuedInput: vi.fn(),
|
|
74
|
+
editQueuedInput: vi.fn(),
|
|
65
75
|
hasSendableDraft: true,
|
|
66
76
|
isSending: true,
|
|
77
|
+
queuedInputs: [],
|
|
67
78
|
send: vi.fn(),
|
|
68
79
|
sendDisabled: true,
|
|
69
80
|
stop: vi.fn(),
|
|
@@ -87,6 +98,7 @@ const mocks = vi.hoisted(() => {
|
|
|
87
98
|
return {
|
|
88
99
|
agent,
|
|
89
100
|
controller,
|
|
101
|
+
controllerParamsSpy,
|
|
90
102
|
inputActions,
|
|
91
103
|
inputQuery,
|
|
92
104
|
inputRenderSpy,
|
|
@@ -120,16 +132,19 @@ vi.mock('@/features/chat/components/providers/chat-presenter.provider', () => ({
|
|
|
120
132
|
|
|
121
133
|
vi.mock('@/features/chat/components/conversation/chat-conversation-content', () => ({
|
|
122
134
|
ChatConversationContent: ({
|
|
135
|
+
bottomSlot,
|
|
123
136
|
messages,
|
|
124
137
|
showWelcome,
|
|
125
138
|
welcomeSlot,
|
|
126
139
|
}: {
|
|
140
|
+
bottomSlot?: ReactNode;
|
|
127
141
|
messages: readonly unknown[];
|
|
128
142
|
showWelcome: boolean;
|
|
129
143
|
welcomeSlot?: ReactNode;
|
|
130
144
|
}) => (
|
|
131
145
|
<div data-testid="conversation-content" data-show-welcome={String(showWelcome)}>
|
|
132
146
|
{showWelcome ? welcomeSlot : <div data-testid="message-count">{messages.length}</div>}
|
|
147
|
+
{bottomSlot ? <div data-testid="conversation-bottom-slot">{bottomSlot}</div> : null}
|
|
133
148
|
</div>
|
|
134
149
|
),
|
|
135
150
|
}));
|
|
@@ -162,7 +177,10 @@ vi.mock('@/features/chat/features/conversation/hooks/use-session-conversation-in
|
|
|
162
177
|
}));
|
|
163
178
|
|
|
164
179
|
vi.mock('@/features/chat/features/conversation/hooks/use-session-conversation-controller', () => ({
|
|
165
|
-
useSessionConversationController: () =>
|
|
180
|
+
useSessionConversationController: (params: unknown) => {
|
|
181
|
+
mocks.controllerParamsSpy(params);
|
|
182
|
+
return mocks.controller;
|
|
183
|
+
},
|
|
166
184
|
}));
|
|
167
185
|
|
|
168
186
|
vi.mock('@/features/chat/stores/chat-session-list.store', () => ({
|
|
@@ -197,6 +215,21 @@ describe('SessionConversationArea input boundary', () => {
|
|
|
197
215
|
mocks.agent.isRunning = true;
|
|
198
216
|
mocks.agent.isSending = true;
|
|
199
217
|
mocks.agent.snapshot.contextWindow = null;
|
|
218
|
+
mocks.inputQuery.selectedSession = null;
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('passes running state without treating an active run as a send request in flight', () => {
|
|
222
|
+
mocks.agent.isRunning = true;
|
|
223
|
+
mocks.agent.isSending = false;
|
|
224
|
+
|
|
225
|
+
renderArea('session-1');
|
|
226
|
+
|
|
227
|
+
expect(mocks.controllerParamsSpy).toHaveBeenCalled();
|
|
228
|
+
const params = mocks.controllerParamsSpy.mock.calls[0]?.[0] as {
|
|
229
|
+
agent: { isRunning: boolean; isSending: boolean };
|
|
230
|
+
};
|
|
231
|
+
expect(params.agent.isRunning).toBe(true);
|
|
232
|
+
expect(params.agent.isSending).toBe(false);
|
|
200
233
|
});
|
|
201
234
|
|
|
202
235
|
it('keeps the composer input subtree stable when only streamed messages change', () => {
|
|
@@ -222,4 +255,35 @@ describe('SessionConversationArea input boundary', () => {
|
|
|
222
255
|
expect(screen.getByTestId('welcome')).toBeTruthy();
|
|
223
256
|
expect(screen.getByTestId('conversation-input')).toBeTruthy();
|
|
224
257
|
});
|
|
258
|
+
|
|
259
|
+
it('surfaces selected-session failure previews at the conversation bottom', () => {
|
|
260
|
+
mocks.inputQuery.selectedSession = {
|
|
261
|
+
activityPreview: {
|
|
262
|
+
state: 'failed',
|
|
263
|
+
statusText: 'Run failed: Invalid API Key',
|
|
264
|
+
},
|
|
265
|
+
status: 'idle',
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
renderArea('session-1');
|
|
269
|
+
|
|
270
|
+
expect(screen.getByTestId('conversation-bottom-slot')).toBeTruthy();
|
|
271
|
+
expect(screen.getByText(/出错了|Something went wrong/)).toBeTruthy();
|
|
272
|
+
expect(screen.getByText('Run failed: Invalid API Key')).toBeTruthy();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('does not surface user-cancelled previews as conversation errors', () => {
|
|
276
|
+
mocks.inputQuery.selectedSession = {
|
|
277
|
+
activityPreview: {
|
|
278
|
+
state: 'cancelled',
|
|
279
|
+
statusText: 'Run interrupted: User stopped the current run.',
|
|
280
|
+
},
|
|
281
|
+
status: 'idle',
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
renderArea('session-1');
|
|
285
|
+
|
|
286
|
+
expect(screen.queryByTestId('conversation-bottom-slot')).toBeNull();
|
|
287
|
+
expect(screen.queryByText(/出错了|Something went wrong/)).toBeNull();
|
|
288
|
+
});
|
|
225
289
|
});
|