@nextclaw/ui 0.20.2 → 0.21.0
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 +35 -0
- package/dist/assets/{api-DvpQWjTS.js → api-DNLQPHfQ.js} +5 -5
- package/dist/assets/{appearance-settings-page-U0m0ktu3.js → appearance-settings-page-BlAFMOGV.js} +1 -1
- package/dist/assets/{book-open-DRofSTWr.js → book-open-BryfPTTa.js} +1 -1
- package/dist/assets/{channels-list-page-BURQarD0.js → channels-list-page-Cfbgubyv.js} +2 -2
- package/dist/assets/{config-split-page-BrAQYTIl.js → config-split-page-Dd8xG0Jk.js} +1 -1
- package/dist/assets/{confirm-dialog-DDJomLnO.js → confirm-dialog-CZIWjnJ6.js} +2 -2
- package/dist/assets/{createLucideIcon-DzkNHr5Z.js → createLucideIcon-BxwVe8ZD.js} +1 -1
- package/dist/assets/desktop-capabilities-page-C6YxsHeV.js +1 -0
- package/dist/assets/desktop-update-config-BVN4QPJn.js +1 -0
- package/dist/assets/{dist-NskPB_yW.js → dist-BRloKr3S.js} +1 -1
- package/dist/assets/{dist-DXbLo2b1.js → dist-Cd8Nm3BW.js} +1 -1
- package/dist/assets/doc-browser-Bzgqmmiu.js +1 -0
- package/dist/assets/doc-browser-D0qI0Z_R.js +1 -0
- package/dist/assets/{doc-browser-context-CnKV4gJy.js → doc-browser-context-3JGufiMA.js} +1 -1
- package/dist/assets/{extensions-DvlFMpAs.js → extensions-Dedd2oDI.js} +1 -1
- package/dist/assets/{form-actions-UnPEZBkx.js → form-actions-FWCEof3V.js} +1 -1
- package/dist/assets/index-CXkHcESm.css +1 -0
- package/dist/assets/index-D25tRg1p.js +109 -0
- package/dist/assets/loader-circle-BoyCoFps.js +1 -0
- package/dist/assets/mcp-marketplace-page-Cn-9CDh3.js +1 -0
- package/dist/assets/mcp-marketplace-page-e9Rmfz-d.js +10 -0
- package/dist/assets/model-config-page-C173Ssil.js +1 -0
- package/dist/assets/{navigation-link-NjUdG6Vp.js → navigation-link-DTPOvGGL.js} +1 -1
- package/dist/assets/plus-D1R4Vipq.js +1 -0
- package/dist/assets/privacy-settings-page-CRserBVF.js +1 -0
- package/dist/assets/providers-config-page-C8jUtnZb.js +1 -0
- package/dist/assets/react-BAzO6LZS.js +8 -0
- package/dist/assets/{refresh-cw-CkeninE0.js → refresh-cw-BCj-yjde.js} +1 -1
- package/dist/assets/remote-6BHWP5zi.js +1 -0
- package/dist/assets/{rotate-cw-DP33PuXU.js → rotate-cw-BPnW4qUl.js} +1 -1
- package/dist/assets/runtime-config-page-aeQ1Dryz.js +1 -0
- package/dist/assets/{save-Cw2Xz-pB.js → save-KT0MumfJ.js} +1 -1
- package/dist/assets/{search-C2CFAVvd.js → search-Cv1Hx_nx.js} +1 -1
- package/dist/assets/{search-config-page-dqNT0Swf.js → search-config-page-b283V-fo.js} +1 -1
- package/dist/assets/{secrets-config-page-De6J3wVx.js → secrets-config-page-ss7x3BOo.js} +2 -2
- package/dist/assets/security-config-HHz62oxH.js +1 -0
- package/dist/assets/{select-DSuMhwVP.js → select-BVgD4EpG.js} +1 -1
- package/dist/assets/skeleton-ChUmcyys.js +1 -0
- package/dist/assets/{tag-chip-DFBLgsGf.js → tag-chip-DZ4ircUB.js} +1 -1
- package/dist/assets/x-BWDRfVWL.js +1 -0
- package/dist/index.html +21 -21
- package/package.json +5 -5
- package/src/app/components/layout/sidebar.tsx +12 -1
- package/src/app/configs/app-navigation.config.ts +17 -2
- package/src/app/hooks/use-feature-controls.ts +11 -0
- package/src/app/index.tsx +10 -0
- package/src/features/apps/components/app-package-card.tsx +29 -7
- package/src/features/apps/components/app-packages-panel.tsx +4 -0
- package/src/features/chat/components/conversation/chat-conversation-content.tsx +18 -2
- package/src/features/chat/components/conversation/chat-conversation-workspace-section.tsx +6 -0
- package/src/features/chat/components/layout/__tests__/chat-sidebar-session-area.test.tsx +16 -2
- package/src/features/chat/components/layout/chat-page-shell.tsx +19 -2
- package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +16 -0
- package/src/features/chat/components/layout/chat-sidebar.tsx +5 -1
- package/src/features/chat/features/message/utils/__tests__/chat-message-timeline-compaction.utils.test.ts +2 -2
- package/src/features/chat/features/message/utils/chat-message-timeline.utils.ts +2 -2
- package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-session-list-view.test.tsx +11 -3
- package/src/features/chat/features/ncp/hooks/use-ncp-session-list-view.ts +12 -5
- package/src/features/chat/features/ncp/hooks/use-ncp-session-queries.ts +15 -1
- package/src/features/chat/features/session/components/__tests__/chat-sidebar-session-item.test.tsx +12 -0
- package/src/features/chat/features/session/components/chat-sidebar-session-entry.tsx +3 -0
- package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +14 -1
- package/src/features/chat/features/session/utils/chat-session-deletion.utils.ts +25 -0
- package/src/features/chat/features/workspace/components/__tests__/chat-session-token-usage.test.tsx +4 -1
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.scroll-restoration.test.tsx +182 -0
- package/src/features/chat/features/workspace/components/__tests__/session-cron-job-content.test.tsx +78 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +47 -2
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +14 -1
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +9 -0
- package/src/features/chat/features/workspace/components/overview/chat-session-token-usage.tsx +17 -0
- package/src/features/chat/features/workspace/components/session-cron-job-content.tsx +269 -75
- package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +5 -0
- package/src/features/chat/managers/__tests__/chat-thread-deletion.manager.test.ts +125 -0
- package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +0 -37
- package/src/features/chat/managers/chat-thread.manager.ts +17 -28
- package/src/features/cron/components/__tests__/cron-config.test.tsx +16 -5
- package/src/features/cron/components/cron-config.tsx +20 -46
- package/src/features/cron/components/cron-job-row.tsx +1 -1
- package/src/features/cron/hooks/__tests__/use-cron-job-actions.test.tsx +95 -0
- package/src/features/cron/hooks/use-cron-job-actions.ts +109 -0
- package/src/features/cron/index.ts +2 -0
- package/src/features/desktop-capabilities/components/desktop-authorization-dialog.test.tsx +58 -0
- package/src/features/desktop-capabilities/components/desktop-authorization-dialog.tsx +161 -0
- package/src/features/desktop-capabilities/hooks/use-desktop-capabilities.test.ts +10 -0
- package/src/features/desktop-capabilities/hooks/use-desktop-capabilities.ts +55 -0
- package/src/features/desktop-capabilities/index.ts +1 -0
- package/src/features/desktop-capabilities/managers/desktop-capability.manager.ts +25 -0
- package/src/features/desktop-capabilities/pages/desktop-capabilities-page.test.tsx +44 -0
- package/src/features/desktop-capabilities/pages/desktop-capabilities-page.tsx +171 -0
- package/src/features/desktop-capabilities/stores/desktop-authorization.store.ts +21 -0
- package/src/features/inbox/pages/inbox-page.tsx +18 -2
- package/src/features/marketplace/components/marketplace-page.tsx +7 -0
- package/src/features/marketplace/components/mcp/__tests__/mcp-connection-dialog.test.tsx +23 -0
- package/src/features/marketplace/components/mcp/mcp-connection-dialog.tsx +35 -0
- package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +20 -2
- package/src/features/panel-apps/pages/panel-app-main-page.tsx +8 -0
- package/src/platforms/desktop/components/desktop-app-shell.tsx +10 -1
- package/src/platforms/mobile/components/mobile-settings-shell.tsx +5 -1
- package/src/shared/components/doc-browser/doc-browser.tsx +0 -1
- package/src/shared/components/doc-browser/hooks/use-doc-browser-scroll-restoration.ts +9 -113
- package/src/shared/components/ui/__tests__/confirm-dialog.test.tsx +31 -0
- package/src/shared/components/ui/confirm-dialog.tsx +11 -1
- package/src/shared/hooks/use-panel-app-scroll-restoration.test.tsx +66 -0
- package/src/shared/hooks/use-panel-app-scroll-restoration.ts +107 -0
- package/src/shared/hooks/use-scroll-restoration.test.tsx +62 -0
- package/src/shared/hooks/use-scroll-restoration.ts +61 -0
- package/src/shared/lib/api/index.ts +1 -0
- package/src/shared/lib/api/ncp-session-query-cache.utils.ts +1 -0
- package/src/shared/lib/api/ncp-session.types.ts +3 -0
- package/src/shared/lib/api/types.ts +1 -0
- package/src/shared/lib/api/utils/mcp-connection.utils.ts +10 -0
- package/src/shared/lib/api/utils/ncp-session.utils.ts +3 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +10 -5
- package/src/shared/lib/i18n/locales/en-US/core.json +43 -0
- package/src/shared/lib/i18n/locales/en-US/cron.json +4 -2
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +1 -0
- package/src/shared/lib/i18n/locales/en-US/marketplace.json +29 -1
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +10 -5
- package/src/shared/lib/i18n/locales/zh-CN/core.json +43 -0
- package/src/shared/lib/i18n/locales/zh-CN/cron.json +4 -2
- package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +1 -0
- package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +29 -1
- package/src/shared/lib/navigation-history/index.ts +1 -0
- package/src/shared/lib/navigation-history/scroll-restoration.manager.test.ts +37 -0
- package/src/shared/lib/navigation-history/scroll-restoration.manager.ts +60 -0
- package/dist/assets/desktop-update-config-BzZFhMv9.js +0 -1
- package/dist/assets/doc-browser-DB22tQKU.js +0 -1
- package/dist/assets/doc-browser-sJjw9fl5.js +0 -1
- package/dist/assets/index-Dc_ZMkmo.css +0 -1
- package/dist/assets/index-N1HnesBp.js +0 -109
- package/dist/assets/loader-circle-BlPXxgDB.js +0 -1
- package/dist/assets/mcp-marketplace-page-BIgFTbMq.js +0 -9
- package/dist/assets/mcp-marketplace-page-CZN4ovdK.js +0 -1
- package/dist/assets/model-config-page-CnSm7kfa.js +0 -1
- package/dist/assets/plus-DVXol9Q5.js +0 -1
- package/dist/assets/privacy-settings-page-DVn2HU7W.js +0 -1
- package/dist/assets/providers-config-page-CzHJS3eg.js +0 -1
- package/dist/assets/react-DCVbGroo.js +0 -8
- package/dist/assets/remote-B2lsJF1c.js +0 -1
- package/dist/assets/runtime-config-page-D69buyGp.js +0 -1
- package/dist/assets/security-config-Dcu9C4OL.js +0 -1
- package/dist/assets/skeleton-QoWydNLp.js +0 -1
- package/dist/assets/x-BXvAzpjb.js +0 -1
|
@@ -15,6 +15,7 @@ import { ChatConversationTrack } from "@/features/chat/components/conversation/c
|
|
|
15
15
|
import { IconActionButton } from "@/shared/components/ui/actions/icon-action-button";
|
|
16
16
|
import { SCROLL_BOTTOM_EDGE_FADE_CLASS } from "@/shared/components/ui/scroll-area";
|
|
17
17
|
import { t } from "@/shared/lib/i18n";
|
|
18
|
+
import { useScrollRestoration } from "@/shared/hooks/use-scroll-restoration";
|
|
18
19
|
import type { SessionMessageToolPayloadState } from "@/features/chat/features/ncp/hooks/use-ncp-session-message-history";
|
|
19
20
|
|
|
20
21
|
type ChatConversationContentProps = {
|
|
@@ -41,6 +42,13 @@ type ChatConversationContentProps = {
|
|
|
41
42
|
welcomeSlot?: ReactNode;
|
|
42
43
|
};
|
|
43
44
|
|
|
45
|
+
function createConversationScrollRestorationKey(
|
|
46
|
+
sessionKey: string | null,
|
|
47
|
+
showWelcome: boolean,
|
|
48
|
+
): string | null {
|
|
49
|
+
return sessionKey && !showWelcome ? `chat-conversation:${sessionKey}` : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
44
52
|
export function ChatConversationContent({
|
|
45
53
|
bottomSlot,
|
|
46
54
|
canContinue = false,
|
|
@@ -61,9 +69,15 @@ export function ChatConversationContent({
|
|
|
61
69
|
onEditMessage,
|
|
62
70
|
welcomeSlot,
|
|
63
71
|
}: ChatConversationContentProps) {
|
|
64
|
-
const threadRef = useRef<HTMLDivElement
|
|
65
|
-
const contentRef = useRef<HTMLDivElement
|
|
72
|
+
const threadRef = useRef<HTMLDivElement>(null);
|
|
73
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
66
74
|
const hasConversationContent = messages.length > 0 || isSending;
|
|
75
|
+
const scrollRestoration = useScrollRestoration({
|
|
76
|
+
restorationKey: createConversationScrollRestorationKey(sessionKey, showWelcome),
|
|
77
|
+
scrollRef: threadRef,
|
|
78
|
+
isEnabled: !showWelcome,
|
|
79
|
+
});
|
|
80
|
+
const { onScroll: onScrollPositionSave } = scrollRestoration;
|
|
67
81
|
const { isAtBottom, onScroll, scrollToBottom } = useStickyBottomScroll({
|
|
68
82
|
contentRef,
|
|
69
83
|
scrollRef: threadRef,
|
|
@@ -75,6 +89,7 @@ export function ChatConversationContent({
|
|
|
75
89
|
const hasMessages = messages.length > 0;
|
|
76
90
|
const handleScroll = useCallback(
|
|
77
91
|
(event: UIEvent<HTMLDivElement>) => {
|
|
92
|
+
onScrollPositionSave(event);
|
|
78
93
|
onScroll();
|
|
79
94
|
if (
|
|
80
95
|
event.currentTarget.scrollTop <= 320 &&
|
|
@@ -89,6 +104,7 @@ export function ChatConversationContent({
|
|
|
89
104
|
isLoadingPreviousMessages,
|
|
90
105
|
onLoadPreviousMessages,
|
|
91
106
|
onScroll,
|
|
107
|
+
onScrollPositionSave,
|
|
92
108
|
],
|
|
93
109
|
);
|
|
94
110
|
return (
|
|
@@ -29,6 +29,9 @@ export function ChatConversationWorkspaceSection({
|
|
|
29
29
|
activeSideChatDraft,
|
|
30
30
|
workspaceFileTabs,
|
|
31
31
|
sessionCronJobs,
|
|
32
|
+
sessionCronJobsError,
|
|
33
|
+
sessionCronJobsLoading,
|
|
34
|
+
retrySessionCronJobs,
|
|
32
35
|
showWorkspacePanel,
|
|
33
36
|
} = useChatConversationWorkspaceState(snapshot, sessionKey);
|
|
34
37
|
|
|
@@ -49,6 +52,9 @@ export function ChatConversationWorkspaceSection({
|
|
|
49
52
|
workspaceNavigationHistoryIndex={snapshot.workspaceNavigationHistoryIndex}
|
|
50
53
|
activePanelKind={snapshot.activeWorkspacePanelKind ?? null}
|
|
51
54
|
sessionCronJobs={sessionKey ? sessionCronJobs : []}
|
|
55
|
+
sessionCronJobsError={sessionKey ? sessionCronJobsError : false}
|
|
56
|
+
sessionCronJobsLoading={sessionKey ? sessionCronJobsLoading : false}
|
|
57
|
+
onRetrySessionCronJobs={retrySessionCronJobs}
|
|
52
58
|
sessionProjectRoot={selectedSession?.projectRoot ?? draftProjectRoot}
|
|
53
59
|
sessionWorkingDir={
|
|
54
60
|
selectedSession?.workingDir ??
|
|
@@ -2,7 +2,7 @@ import { fireEvent, render, screen } from "@testing-library/react";
|
|
|
2
2
|
import { describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { ChatSidebarSessionArea } from "@/features/chat/components/layout/chat-sidebar-desktop-layout";
|
|
4
4
|
|
|
5
|
-
function renderSessionArea(isProjectFirstView: boolean) {
|
|
5
|
+
function renderSessionArea(isProjectFirstView: boolean, onScrollNearEnd = vi.fn()) {
|
|
6
6
|
const onAddProject = vi.fn();
|
|
7
7
|
const onSelectMode = vi.fn();
|
|
8
8
|
|
|
@@ -14,6 +14,7 @@ function renderSessionArea(isProjectFirstView: boolean) {
|
|
|
14
14
|
isLoading={false}
|
|
15
15
|
isProjectFirstView={isProjectFirstView}
|
|
16
16
|
onAddProject={onAddProject}
|
|
17
|
+
onScrollNearEnd={onScrollNearEnd}
|
|
17
18
|
onSelectMode={onSelectMode}
|
|
18
19
|
projectGroups={[]}
|
|
19
20
|
projectCronJobCountByRoot={new Map()}
|
|
@@ -22,7 +23,7 @@ function renderSessionArea(isProjectFirstView: boolean) {
|
|
|
22
23
|
/>,
|
|
23
24
|
);
|
|
24
25
|
|
|
25
|
-
return { onAddProject, onSelectMode };
|
|
26
|
+
return { onAddProject, onScrollNearEnd, onSelectMode };
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
describe("ChatSidebarSessionArea", () => {
|
|
@@ -87,4 +88,17 @@ describe("ChatSidebarSessionArea", () => {
|
|
|
87
88
|
|
|
88
89
|
expect(onAddProject).toHaveBeenCalledOnce();
|
|
89
90
|
});
|
|
91
|
+
|
|
92
|
+
it("requests the next page before scrolling reaches the end", () => {
|
|
93
|
+
const { onScrollNearEnd } = renderSessionArea(false);
|
|
94
|
+
const scroller = document.querySelector(".overflow-y-auto") as HTMLDivElement;
|
|
95
|
+
Object.defineProperties(scroller, {
|
|
96
|
+
scrollHeight: { value: 2_000 },
|
|
97
|
+
clientHeight: { value: 500 },
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
fireEvent.scroll(scroller, { target: { scrollTop: 1_000 } });
|
|
101
|
+
|
|
102
|
+
expect(onScrollNearEnd).toHaveBeenCalledOnce();
|
|
103
|
+
});
|
|
90
104
|
});
|
|
@@ -8,6 +8,7 @@ import { useViewportLayout } from "@/app/hooks/use-viewport-layout";
|
|
|
8
8
|
import { ChatMobileShell } from "@/platforms/mobile";
|
|
9
9
|
import { InboxPage } from "@/features/inbox";
|
|
10
10
|
import { PanelAppMainPage } from "@/features/panel-apps";
|
|
11
|
+
import { useScrollRestoration } from "@/shared/hooks/use-scroll-restoration";
|
|
11
12
|
export type MainPanelView = "chat" | "cron" | "skills" | "agents" | "inbox" | "panel-app";
|
|
12
13
|
export type ChatPageProps = {
|
|
13
14
|
view: MainPanelView;
|
|
@@ -36,6 +37,14 @@ type ChatPageLayoutProps = {
|
|
|
36
37
|
};
|
|
37
38
|
export function ChatPageLayout({ view, confirmDialog }: ChatPageLayoutProps) {
|
|
38
39
|
const { isMobile } = useViewportLayout();
|
|
40
|
+
const cronScroll = useScrollRestoration<HTMLDivElement>({
|
|
41
|
+
restorationKey: "main-page:cron",
|
|
42
|
+
});
|
|
43
|
+
const agentsScroll = useScrollRestoration<HTMLDivElement>({
|
|
44
|
+
restorationKey: "main-page:agents",
|
|
45
|
+
});
|
|
46
|
+
const { onScroll: onCronScroll, scrollRef: cronScrollRef } = cronScroll;
|
|
47
|
+
const { onScroll: onAgentsScroll, scrollRef: agentsScrollRef } = agentsScroll;
|
|
39
48
|
|
|
40
49
|
return (
|
|
41
50
|
<div className="h-full flex">
|
|
@@ -52,13 +61,21 @@ export function ChatPageLayout({ view, confirmDialog }: ChatPageLayoutProps) {
|
|
|
52
61
|
) : view === "panel-app" ? (
|
|
53
62
|
<PanelAppMainPage />
|
|
54
63
|
) : view === "cron" ? (
|
|
55
|
-
<div
|
|
64
|
+
<div
|
|
65
|
+
ref={cronScrollRef}
|
|
66
|
+
onScroll={onCronScroll}
|
|
67
|
+
className="h-full overflow-auto custom-scrollbar"
|
|
68
|
+
>
|
|
56
69
|
<div className={`mx-auto w-full px-4 py-4 sm:px-6 sm:py-5 ${MANAGEMENT_PAGE_CANVAS_WIDTH_CLASS}`}>
|
|
57
70
|
<CronConfig />
|
|
58
71
|
</div>
|
|
59
72
|
</div>
|
|
60
73
|
) : view === "agents" ? (
|
|
61
|
-
<div
|
|
74
|
+
<div
|
|
75
|
+
ref={agentsScrollRef}
|
|
76
|
+
onScroll={onAgentsScroll}
|
|
77
|
+
className="h-full overflow-auto custom-scrollbar"
|
|
78
|
+
>
|
|
62
79
|
<div className={`mx-auto w-full px-4 py-4 sm:px-6 sm:py-5 ${MANAGEMENT_PAGE_CANVAS_WIDTH_CLASS}`}>
|
|
63
80
|
<AgentsPage />
|
|
64
81
|
</div>
|
|
@@ -28,6 +28,7 @@ import { ChatSidebarUtilityMenu } from "@/features/chat/components/layout/chat-s
|
|
|
28
28
|
import { PanelAppMainSidebarNav } from "@/features/panel-apps";
|
|
29
29
|
import { isWindowsDesktopHost } from "@/platforms/desktop";
|
|
30
30
|
import { viewportLayoutManager } from "@/app/managers/viewport-layout.manager";
|
|
31
|
+
import { useScrollRestoration } from "@/shared/hooks/use-scroll-restoration";
|
|
31
32
|
import {
|
|
32
33
|
SIDEBAR_RAIL_CONTROL_CLASS,
|
|
33
34
|
SIDEBAR_RAIL_ICON_CLASS,
|
|
@@ -154,6 +155,7 @@ export function ChatSidebarSessionArea({
|
|
|
154
155
|
isLoading,
|
|
155
156
|
isProjectFirstView,
|
|
156
157
|
onAddProject,
|
|
158
|
+
onScrollNearEnd = () => undefined,
|
|
157
159
|
onSelectMode,
|
|
158
160
|
projectGroups,
|
|
159
161
|
projectCronJobCountByRoot,
|
|
@@ -166,6 +168,7 @@ export function ChatSidebarSessionArea({
|
|
|
166
168
|
isLoading: boolean;
|
|
167
169
|
isProjectFirstView: boolean;
|
|
168
170
|
onAddProject: () => void;
|
|
171
|
+
onScrollNearEnd?: () => void;
|
|
169
172
|
onSelectMode: (mode: "time-first" | "project-first") => void;
|
|
170
173
|
projectGroups: ReturnType<typeof groupSessionsByProject>;
|
|
171
174
|
projectCronJobCountByRoot: ReadonlyMap<string, number>;
|
|
@@ -174,6 +177,11 @@ export function ChatSidebarSessionArea({
|
|
|
174
177
|
typeof ChatSidebarSessionList
|
|
175
178
|
>[0]["sessionTypeOptions"];
|
|
176
179
|
}) {
|
|
180
|
+
const scrollRestoration = useScrollRestoration<HTMLDivElement>({
|
|
181
|
+
restorationKey: "chat-sidebar:sessions",
|
|
182
|
+
isEnabled: !isCollapsed,
|
|
183
|
+
});
|
|
184
|
+
const { onScroll: onScrollPositionSave, scrollRef } = scrollRestoration;
|
|
177
185
|
if (isCollapsed) {
|
|
178
186
|
return <div className="min-h-0 flex-1" aria-hidden="true" />;
|
|
179
187
|
}
|
|
@@ -196,6 +204,14 @@ export function ChatSidebarSessionArea({
|
|
|
196
204
|
</div>
|
|
197
205
|
|
|
198
206
|
<div
|
|
207
|
+
ref={scrollRef}
|
|
208
|
+
onScroll={(event) => {
|
|
209
|
+
onScrollPositionSave(event);
|
|
210
|
+
const element = event.currentTarget;
|
|
211
|
+
if (element.scrollHeight - element.scrollTop - element.clientHeight < 600) {
|
|
212
|
+
onScrollNearEnd();
|
|
213
|
+
}
|
|
214
|
+
}}
|
|
199
215
|
className={cn(
|
|
200
216
|
"custom-scrollbar min-h-0 flex-1 overflow-y-auto px-3 pb-4 pt-1",
|
|
201
217
|
SCROLL_BOTTOM_EDGE_FADE_CLASS,
|
|
@@ -120,7 +120,7 @@ export function ChatSidebar({
|
|
|
120
120
|
const projectsQuery = useProjects();
|
|
121
121
|
const projectCreateMutation = useCreateProject();
|
|
122
122
|
const projectAddExistingMutation = useAddExistingProject();
|
|
123
|
-
const { allItems, isLoading, items } = useNcpSessionListView();
|
|
123
|
+
const { allItems, hasMore, isLoading, isLoadingMore, items, loadMore } = useNcpSessionListView();
|
|
124
124
|
const { cronJobCountByProjectRoot, cronJobCountBySessionKey } =
|
|
125
125
|
useChatSidebarContextCounts(allItems);
|
|
126
126
|
const { language, setLanguage } = useI18n();
|
|
@@ -247,6 +247,7 @@ export function ChatSidebar({
|
|
|
247
247
|
onTogglePinned={() =>
|
|
248
248
|
presenter.chatSessionListManager.toggleSessionPinned(item.session.key)
|
|
249
249
|
}
|
|
250
|
+
onDeleteSession={presenter.chatThreadManager.deleteSession}
|
|
250
251
|
/>
|
|
251
252
|
);
|
|
252
253
|
const createSessionAndOpenIfNeeded = (
|
|
@@ -343,6 +344,9 @@ export function ChatSidebar({
|
|
|
343
344
|
isLoading={isLoading}
|
|
344
345
|
isProjectFirstView={isProjectFirstView}
|
|
345
346
|
onAddProject={openProjectAdd}
|
|
347
|
+
onScrollNearEnd={() => {
|
|
348
|
+
if (hasMore && !isLoadingMore) void loadMore();
|
|
349
|
+
}}
|
|
346
350
|
onSelectMode={presenter.chatSessionListManager.setListMode}
|
|
347
351
|
projectGroups={projectGroups}
|
|
348
352
|
projectCronJobCountByRoot={cronJobCountByProjectRoot}
|
|
@@ -169,7 +169,7 @@ describe("mid-run compaction timeline projection", () => {
|
|
|
169
169
|
]);
|
|
170
170
|
});
|
|
171
171
|
|
|
172
|
-
it("does not
|
|
172
|
+
it("does not render a temporarily unavailable canonical part boundary as a standalone marker", () => {
|
|
173
173
|
const invalidCompaction = createCompactionMessage({
|
|
174
174
|
assistantId: visibleMessage.id,
|
|
175
175
|
coveredPartCount: 2,
|
|
@@ -183,7 +183,7 @@ describe("mid-run compaction timeline projection", () => {
|
|
|
183
183
|
expect(buildChatMessageTimelineItems({
|
|
184
184
|
rawMessages: [visibleMessage, invalidCompaction],
|
|
185
185
|
messages: [{ id: visibleMessage.id } as never],
|
|
186
|
-
}).map((item) => item.kind)).toEqual(["message"
|
|
186
|
+
}).map((item) => item.kind)).toEqual(["message"]);
|
|
187
187
|
});
|
|
188
188
|
|
|
189
189
|
it("offsets a continued assistant compaction within its canonical message", () => {
|
|
@@ -252,12 +252,12 @@ function projectVisibleChatMessageState(
|
|
|
252
252
|
.forEach((placement) => {
|
|
253
253
|
if (
|
|
254
254
|
!Number.isSafeInteger(placement.boundaryIndex) ||
|
|
255
|
-
placement.boundaryIndex < 0
|
|
256
|
-
placement.boundaryIndex > message.parts.length
|
|
255
|
+
placement.boundaryIndex < 0
|
|
257
256
|
) {
|
|
258
257
|
inlineCompactionMessageIds.delete(placement.serviceMessageId);
|
|
259
258
|
return;
|
|
260
259
|
}
|
|
260
|
+
if (placement.boundaryIndex > message.parts.length) return;
|
|
261
261
|
parts.splice(placement.boundaryIndex + insertedCount, 0, {
|
|
262
262
|
type: "extension",
|
|
263
263
|
extensionType: CONTEXT_COMPACTION_PART_EXTENSION_TYPE,
|
|
@@ -9,12 +9,20 @@ const mocks = vi.hoisted(() => ({
|
|
|
9
9
|
}));
|
|
10
10
|
|
|
11
11
|
vi.mock("@/features/chat/features/ncp/hooks/use-ncp-session-queries", () => ({
|
|
12
|
-
|
|
12
|
+
useInfiniteNcpSessions: () => ({
|
|
13
13
|
data: {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
pages: [{
|
|
15
|
+
sessions: mocks.sessions,
|
|
16
|
+
total: mocks.sessions.length,
|
|
17
|
+
page: 1,
|
|
18
|
+
pageSize: 100,
|
|
19
|
+
hasMore: false,
|
|
20
|
+
}],
|
|
16
21
|
},
|
|
17
22
|
isLoading: false,
|
|
23
|
+
hasNextPage: false,
|
|
24
|
+
isFetchingNextPage: false,
|
|
25
|
+
fetchNextPage: vi.fn(),
|
|
18
26
|
}),
|
|
19
27
|
}));
|
|
20
28
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
1
|
+
import { useDeferredValue, useMemo } from "react";
|
|
2
2
|
import type { SessionEntryView } from "@/shared/lib/api";
|
|
3
3
|
import { sessionMatchesQuery } from "@/features/chat/features/session/utils/chat-session-display.utils";
|
|
4
4
|
import { adaptNcpSessionSummaries } from "@/features/chat/features/session/utils/ncp-session-adapter.utils";
|
|
5
5
|
import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
|
|
6
|
-
import {
|
|
6
|
+
import { useInfiniteNcpSessions } from "@/features/chat/features/ncp/hooks/use-ncp-session-queries";
|
|
7
7
|
import type { SessionRunStatus } from "@/features/chat/types/session-run-status.types";
|
|
8
8
|
|
|
9
9
|
export type NcpSessionListItemView = {
|
|
@@ -30,15 +30,19 @@ export function useNcpSessionListView(
|
|
|
30
30
|
) {
|
|
31
31
|
const storedQuery = useChatSessionListStore((state) => state.snapshot.query);
|
|
32
32
|
const query = params.query ?? storedQuery;
|
|
33
|
-
const
|
|
33
|
+
const deferredQuery = useDeferredValue(query);
|
|
34
|
+
const sessionsQuery = useInfiniteNcpSessions({
|
|
35
|
+
pageSize: params.limit ?? 100,
|
|
36
|
+
query: deferredQuery,
|
|
37
|
+
});
|
|
34
38
|
|
|
35
39
|
const allItems = useMemo<NcpSessionListItemView[]>(() => {
|
|
36
|
-
const summaries = sessionsQuery.data?.sessions ?? [];
|
|
40
|
+
const summaries = sessionsQuery.data?.pages.flatMap((page) => page.sessions) ?? [];
|
|
37
41
|
return adaptNcpSessionSummaries(summaries).map((session) => ({
|
|
38
42
|
session,
|
|
39
43
|
runStatus: session.status === "running" ? "running" : undefined,
|
|
40
44
|
}));
|
|
41
|
-
}, [sessionsQuery.data?.
|
|
45
|
+
}, [sessionsQuery.data?.pages]);
|
|
42
46
|
const items = useMemo<NcpSessionListItemView[]>(() => {
|
|
43
47
|
const visibleItems = allItems.filter(({ session }) =>
|
|
44
48
|
shouldShowSessionInSidebar(session),
|
|
@@ -59,5 +63,8 @@ export function useNcpSessionListView(
|
|
|
59
63
|
allItems,
|
|
60
64
|
isLoading: sessionsQuery.isLoading,
|
|
61
65
|
items,
|
|
66
|
+
hasMore: sessionsQuery.hasNextPage,
|
|
67
|
+
isLoadingMore: sessionsQuery.isFetchingNextPage,
|
|
68
|
+
loadMore: sessionsQuery.fetchNextPage,
|
|
62
69
|
};
|
|
63
70
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
1
|
+
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
2
2
|
import {
|
|
3
3
|
fetchNcpSessionObservations,
|
|
4
4
|
fetchNcpSessionSkills,
|
|
@@ -21,6 +21,20 @@ export function useNcpSessions(params?: { limit?: number; peerId?: string }) {
|
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
export function useInfiniteNcpSessions(params: { pageSize: number; query?: string }) {
|
|
25
|
+
return useInfiniteQuery({
|
|
26
|
+
queryKey: ['ncp-session-pages', params.pageSize, params.query?.trim() || null],
|
|
27
|
+
initialPageParam: 1,
|
|
28
|
+
queryFn: ({ pageParam }) => fetchNcpSessions({
|
|
29
|
+
page: pageParam,
|
|
30
|
+
pageSize: params.pageSize,
|
|
31
|
+
...(params.query?.trim() ? { query: params.query.trim() } : {}),
|
|
32
|
+
}),
|
|
33
|
+
getNextPageParam: (lastPage) => lastPage.hasMore ? (lastPage.page ?? 1) + 1 : undefined,
|
|
34
|
+
...ncpSessionQueryDefaults,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
24
38
|
export function useNcpSessionSkills(params: {
|
|
25
39
|
sessionId: string | null;
|
|
26
40
|
projectRoot?: string | null;
|
package/src/features/chat/features/session/components/__tests__/chat-sidebar-session-item.test.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { ChatSidebarSessionItem } from "@/features/chat/features/session/compone
|
|
|
5
5
|
|
|
6
6
|
const mocks = vi.hoisted(() => ({
|
|
7
7
|
copyText: vi.fn(),
|
|
8
|
+
deleteSession: vi.fn(),
|
|
8
9
|
}));
|
|
9
10
|
|
|
10
11
|
vi.mock("@nextclaw/agent-chat-ui", () => ({
|
|
@@ -21,6 +22,7 @@ vi.mock("sonner", () => ({
|
|
|
21
22
|
beforeEach(() => {
|
|
22
23
|
mocks.copyText.mockReset();
|
|
23
24
|
mocks.copyText.mockResolvedValue(true);
|
|
25
|
+
mocks.deleteSession.mockReset();
|
|
24
26
|
render(
|
|
25
27
|
<ChatSidebarSessionItem
|
|
26
28
|
sessionKey="session:current"
|
|
@@ -50,6 +52,7 @@ beforeEach(() => {
|
|
|
50
52
|
onSave={vi.fn()}
|
|
51
53
|
onCancel={vi.fn()}
|
|
52
54
|
onTogglePinned={vi.fn()}
|
|
55
|
+
onDelete={mocks.deleteSession}
|
|
53
56
|
/>,
|
|
54
57
|
);
|
|
55
58
|
});
|
|
@@ -87,6 +90,15 @@ it("copies the sidebar session ID from the more-actions menu", async () => {
|
|
|
87
90
|
expect(mocks.copyText).toHaveBeenCalledWith("session:current");
|
|
88
91
|
});
|
|
89
92
|
|
|
93
|
+
it("deletes the sidebar session from the more-actions menu", async () => {
|
|
94
|
+
const user = userEvent.setup();
|
|
95
|
+
|
|
96
|
+
await user.click(screen.getByRole("button", { name: "More actions" }));
|
|
97
|
+
await user.click(screen.getByRole("button", { name: "Delete Session" }));
|
|
98
|
+
|
|
99
|
+
expect(mocks.deleteSession).toHaveBeenCalledOnce();
|
|
100
|
+
});
|
|
101
|
+
|
|
90
102
|
it("sizes the runtime icon to the session title text", () => {
|
|
91
103
|
const runtimeIcon = screen.getByRole("img", { name: "Codex logo" });
|
|
92
104
|
|
|
@@ -32,6 +32,7 @@ export function ChatSidebarSessionEntry(props: {
|
|
|
32
32
|
onSaveSessionLabel: (session: SessionEntryView) => void;
|
|
33
33
|
onCancelEditingSessionLabel: () => void;
|
|
34
34
|
onTogglePinned: () => void;
|
|
35
|
+
onDeleteSession: (sessionKey: string) => void;
|
|
35
36
|
}) {
|
|
36
37
|
const {
|
|
37
38
|
item,
|
|
@@ -52,6 +53,7 @@ export function ChatSidebarSessionEntry(props: {
|
|
|
52
53
|
onSaveSessionLabel,
|
|
53
54
|
onCancelEditingSessionLabel,
|
|
54
55
|
onTogglePinned,
|
|
56
|
+
onDeleteSession,
|
|
55
57
|
} = props;
|
|
56
58
|
const { session, runStatus } = item;
|
|
57
59
|
const active = selectedSessionKey === session.key;
|
|
@@ -111,6 +113,7 @@ export function ChatSidebarSessionEntry(props: {
|
|
|
111
113
|
onSave={() => onSaveSessionLabel(session)}
|
|
112
114
|
onCancel={onCancelEditingSessionLabel}
|
|
113
115
|
onTogglePinned={onTogglePinned}
|
|
116
|
+
onDelete={() => onDeleteSession(session.key)}
|
|
114
117
|
/>
|
|
115
118
|
);
|
|
116
119
|
}
|
|
@@ -15,9 +15,11 @@ import {
|
|
|
15
15
|
GitBranch,
|
|
16
16
|
Pencil,
|
|
17
17
|
Pin,
|
|
18
|
+
Trash2,
|
|
18
19
|
X,
|
|
19
20
|
} from "lucide-react";
|
|
20
21
|
import { ChatSidebarContextCard } from "@/features/chat/features/session/components/chat-sidebar-context-card";
|
|
22
|
+
import { ChatSessionHeaderMenuItem } from "@/features/chat/features/session/components/session-header/chat-session-header-menu-item";
|
|
21
23
|
|
|
22
24
|
type ChatSidebarSessionItemProps = {
|
|
23
25
|
sessionKey: string;
|
|
@@ -44,6 +46,7 @@ type ChatSidebarSessionItemProps = {
|
|
|
44
46
|
onSave: () => void | Promise<void>;
|
|
45
47
|
onCancel: () => void;
|
|
46
48
|
onTogglePinned: () => void;
|
|
49
|
+
onDelete: () => void;
|
|
47
50
|
};
|
|
48
51
|
|
|
49
52
|
type ChatSidebarSessionEditingViewProps = Pick<
|
|
@@ -139,6 +142,7 @@ function ChatSidebarSessionDisplayView({
|
|
|
139
142
|
onSelect,
|
|
140
143
|
onStartEditing,
|
|
141
144
|
onTogglePinned,
|
|
145
|
+
onDelete,
|
|
142
146
|
}: ChatSidebarSessionDisplayViewProps) {
|
|
143
147
|
return (
|
|
144
148
|
<div className="group/session relative">
|
|
@@ -262,7 +266,14 @@ function ChatSidebarSessionDisplayView({
|
|
|
262
266
|
sessionKey={sessionKey}
|
|
263
267
|
triggerSize="sm"
|
|
264
268
|
triggerTone="strong"
|
|
265
|
-
|
|
269
|
+
>
|
|
270
|
+
<ChatSessionHeaderMenuItem
|
|
271
|
+
icon={Trash2}
|
|
272
|
+
label={t("chatDeleteSession")}
|
|
273
|
+
onClick={onDelete}
|
|
274
|
+
destructive
|
|
275
|
+
/>
|
|
276
|
+
</ChatSessionMoreActionsMenu>
|
|
266
277
|
</div>
|
|
267
278
|
</div>
|
|
268
279
|
);
|
|
@@ -293,6 +304,7 @@ export function ChatSidebarSessionItem({
|
|
|
293
304
|
onSave,
|
|
294
305
|
onCancel,
|
|
295
306
|
onTogglePinned,
|
|
307
|
+
onDelete,
|
|
296
308
|
}: ChatSidebarSessionItemProps) {
|
|
297
309
|
return (
|
|
298
310
|
<div
|
|
@@ -332,6 +344,7 @@ export function ChatSidebarSessionItem({
|
|
|
332
344
|
projectName={projectName}
|
|
333
345
|
onStartEditing={onStartEditing}
|
|
334
346
|
onTogglePinned={onTogglePinned}
|
|
347
|
+
onDelete={onDelete}
|
|
335
348
|
/>
|
|
336
349
|
)}
|
|
337
350
|
</div>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ChatThreadSnapshot } from "@/features/chat/stores/chat-thread.store";
|
|
2
|
+
|
|
3
|
+
export function createDeletedChatThreadStatePatch(): Partial<ChatThreadSnapshot> {
|
|
4
|
+
return {
|
|
5
|
+
sessionKey: null,
|
|
6
|
+
sessionTypeLabel: null,
|
|
7
|
+
agentId: null,
|
|
8
|
+
sessionDisplayName: undefined,
|
|
9
|
+
draftProjectRoot: null,
|
|
10
|
+
sessionProjectName: null,
|
|
11
|
+
canDeleteSession: false,
|
|
12
|
+
parentSessionKey: null,
|
|
13
|
+
parentSessionLabel: null,
|
|
14
|
+
workspacePanelParentKey: null,
|
|
15
|
+
activeWorkspacePanelKind: null,
|
|
16
|
+
childSessionTabs: [],
|
|
17
|
+
activeChildSessionKey: null,
|
|
18
|
+
activeSideChatDraft: null,
|
|
19
|
+
workspaceFileTabs: [],
|
|
20
|
+
activeWorkspaceFileKey: null,
|
|
21
|
+
closedWorkspaceTabEntries: [],
|
|
22
|
+
workspaceNavigationHistory: [],
|
|
23
|
+
workspaceNavigationHistoryIndex: 0,
|
|
24
|
+
};
|
|
25
|
+
}
|
package/src/features/chat/features/workspace/components/__tests__/chat-session-token-usage.test.tsx
CHANGED
|
@@ -66,12 +66,15 @@ beforeEach(() => {
|
|
|
66
66
|
mocks.query.isError = false;
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
it('shows totals, cache
|
|
69
|
+
it('shows totals, model calls, cache read ratio, and usage grouped by model', () => {
|
|
70
70
|
render(<ChatSessionTokenUsage sessionKey="parent-1" />);
|
|
71
71
|
|
|
72
72
|
expect(screen.getByText('Token usage')).toBeTruthy();
|
|
73
73
|
expect(screen.getByText('1,490')).toBeTruthy();
|
|
74
74
|
expect(screen.getAllByText('40%').length).toBeGreaterThan(0);
|
|
75
|
+
expect(screen.getByText('Model calls')).toBeTruthy();
|
|
76
|
+
expect(screen.getByText('Reported calls')).toBeTruthy();
|
|
77
|
+
expect(screen.getByText('2 agent rounds · 3 model calls · Cache read ratio: 40%')).toBeTruthy();
|
|
75
78
|
expect(screen.getByText('openai/gpt-5')).toBeTruthy();
|
|
76
79
|
expect(screen.getByText('anthropic/claude-sonnet-4')).toBeTruthy();
|
|
77
80
|
expect(screen.getAllByText('Partial usage')).toHaveLength(2);
|