@nextclaw/ui 0.20.1 → 0.20.3
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 +30 -0
- package/dist/assets/{api-BBFMpRGl.js → api-iMkU_o5P.js} +3 -3
- package/dist/assets/{appearance-settings-page-C1Erayzl.js → appearance-settings-page-CU5k4b-M.js} +1 -1
- package/dist/assets/{book-open-BhTIpBKg.js → book-open-nD8gdMhA.js} +1 -1
- package/dist/assets/channels-list-page-Drlt62kY.js +8 -0
- package/dist/assets/{config-split-page-By7p-4X-.js → config-split-page-BtJZp1E5.js} +1 -1
- package/dist/assets/{confirm-dialog-xpl1Yl7r.js → confirm-dialog-k2-Jq_x3.js} +1 -1
- package/dist/assets/{createLucideIcon-B_8khHn8.js → createLucideIcon-DxlwKLr-.js} +1 -1
- package/dist/assets/{desktop-update-config-CGFfa7-Z.js → desktop-update-config-BiZmy7FC.js} +1 -1
- package/dist/assets/{dist-CXN62DSs.js → dist-Cy_e4kNb.js} +1 -1
- package/dist/assets/{dist-DdE-kCbi.js → dist-m08DUswB.js} +1 -1
- package/dist/assets/doc-browser-CPsPHh12.js +1 -0
- package/dist/assets/{doc-browser-context-B8NCuMv5.js → doc-browser-context-jiy-O4F8.js} +1 -1
- package/dist/assets/doc-browser-npOvWtgf.js +1 -0
- package/dist/assets/{extensions-CMctxyTf.js → extensions-CIb17__o.js} +1 -1
- package/dist/assets/{form-actions-BFgTdxqI.js → form-actions-DGf5zUg_.js} +1 -1
- package/dist/assets/index-C86hxgv5.css +1 -0
- package/dist/assets/index-CYBXR7zN.js +109 -0
- package/dist/assets/loader-circle-BlLuRFVW.js +1 -0
- package/dist/assets/mcp-marketplace-page-B12JNh7Q.js +1 -0
- package/dist/assets/mcp-marketplace-page-kLWW6kzt.js +9 -0
- package/dist/assets/model-config-page-DSeAjVT_.js +1 -0
- package/dist/assets/{navigation-link-BwWrm4k1.js → navigation-link-DAfwFCj4.js} +1 -1
- package/dist/assets/plus-D4LtWtTk.js +1 -0
- package/dist/assets/privacy-settings-page-tN0Mt15x.js +1 -0
- package/dist/assets/providers-config-page-CgPrXR1u.js +1 -0
- package/dist/assets/react-8iBFECZE.js +8 -0
- package/dist/assets/{refresh-cw-B7NmPJ_Z.js → refresh-cw-D_Eibznw.js} +1 -1
- package/dist/assets/remote-Derr_AQp.js +1 -0
- package/dist/assets/{rotate-cw-B0ajMaHu.js → rotate-cw-QQjmoWuq.js} +1 -1
- package/dist/assets/runtime-config-page-Crq96Jjq.js +1 -0
- package/dist/assets/{save-DWnYYpgQ.js → save-W2xthJJ2.js} +1 -1
- package/dist/assets/{search-D6pX7FC-.js → search-Vih1R2BD.js} +1 -1
- package/dist/assets/{search-config-page-Cx3o_675.js → search-config-page-CtBcL2uc.js} +1 -1
- package/dist/assets/{secrets-config-page-tyFl7Rvt.js → secrets-config-page-Q6XfIj0h.js} +2 -2
- package/dist/assets/security-config-CE9rkQvq.js +1 -0
- package/dist/assets/{select-qEtFTWB5.js → select-lNdozU4o.js} +1 -1
- package/dist/assets/skeleton-B__aTcly.js +1 -0
- package/dist/assets/{tag-chip-vcjAYdV_.js → tag-chip-gdaU9bE7.js} +1 -1
- package/dist/assets/x-CWdWBu_U.js +1 -0
- package/dist/index.html +21 -21
- package/package.json +7 -7
- package/src/app/components/layout/sidebar.tsx +7 -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-read-state.test.tsx +4 -3
- package/src/features/chat/components/layout/__tests__/chat-sidebar-session-area.test.tsx +27 -6
- package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +17 -17
- package/src/features/chat/components/layout/chat-page-shell.tsx +19 -2
- package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +33 -5
- package/src/features/chat/components/layout/chat-sidebar.tsx +31 -21
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +41 -1
- package/src/features/chat/features/conversation/hooks/use-session-pending-input-actions.ts +22 -4
- package/src/features/chat/features/message/utils/__tests__/chat-message-timeline-compaction.utils.test.ts +2 -2
- package/src/features/chat/features/message/utils/__tests__/chat-message-trigger-details.utils.test.ts +35 -0
- package/src/features/chat/features/message/utils/chat-message-timeline.utils.ts +2 -2
- package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime-queue.test.tsx +50 -0
- package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-session-list-view.test.tsx +35 -6
- package/src/features/chat/features/ncp/hooks/use-ncp-session-list-view.ts +43 -20
- package/src/features/chat/features/ncp/hooks/use-ncp-session-queries.ts +15 -1
- package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +26 -2
- package/src/features/chat/features/session/components/__tests__/chat-sidebar-session-item.test.tsx +55 -27
- package/src/features/chat/features/session/components/chat-sidebar-context-card.tsx +60 -0
- package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +95 -54
- package/src/features/chat/features/session/components/chat-sidebar-session-entry.tsx +9 -8
- package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +141 -95
- package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +11 -2
- package/src/features/chat/features/session/hooks/use-chat-sidebar-context-counts.ts +40 -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__/chat-session-workspace-file-preview.test.tsx +16 -9
- package/src/features/chat/features/workspace/components/__tests__/session-cron-job-content.test.tsx +78 -0
- package/src/features/chat/features/workspace/components/__tests__/workspace-markdown-outline.test.tsx +128 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +3 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +65 -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/components/workspace-markdown-outline.tsx +156 -0
- package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +5 -0
- 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/inbox/pages/inbox-page.tsx +18 -2
- package/src/features/marketplace/components/marketplace-page.tsx +7 -0
- package/src/features/panel-apps/pages/panel-app-main-page.tsx +8 -0
- package/src/features/settings/pages/__tests__/privacy-settings-page.test.tsx +14 -5
- package/src/features/settings/pages/privacy-settings-page.tsx +38 -2
- package/src/platforms/desktop/components/desktop-app-shell.tsx +10 -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/actions/icon-action-button.tsx +1 -8
- package/src/shared/components/ui/tooltip.tsx +16 -1
- package/src/shared/hooks/use-config.ts +11 -0
- 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/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 +9 -0
- package/src/shared/lib/api/utils/config.utils.ts +5 -0
- package/src/shared/lib/api/utils/ncp-session.utils.ts +3 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +15 -5
- package/src/shared/lib/i18n/locales/en-US/core.json +17 -9
- 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/zh-CN/chat.json +15 -5
- package/src/shared/lib/i18n/locales/zh-CN/core.json +17 -9
- 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/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/channels-list-page-OTCkjMOW.js +0 -8
- package/dist/assets/doc-browser-5xrHcQ2z.js +0 -1
- package/dist/assets/doc-browser-c2yO477-.js +0 -1
- package/dist/assets/index-DUCA8m2w.js +0 -109
- package/dist/assets/index-QrdUWHfm.css +0 -1
- package/dist/assets/loader-circle-CbdTrSp3.js +0 -1
- package/dist/assets/mcp-marketplace-page-DGve0muV.js +0 -9
- package/dist/assets/mcp-marketplace-page-_37VzG2q.js +0 -1
- package/dist/assets/model-config-page-DyC5H0bu.js +0 -1
- package/dist/assets/plus-DbeE4r-Q.js +0 -1
- package/dist/assets/privacy-settings-page--MOnhAZ6.js +0 -1
- package/dist/assets/providers-config-page-ChL3FUvK.js +0 -1
- package/dist/assets/react-DVMmnPWz.js +0 -8
- package/dist/assets/remote-DFoxj4ls.js +0 -1
- package/dist/assets/runtime-config-page-BQdT3nmd.js +0 -1
- package/dist/assets/security-config-CUTUm6JT.js +0 -1
- package/dist/assets/skeleton-7MYPzn8w.js +0 -1
- package/dist/assets/x-DThaS5ZL.js +0 -1
|
@@ -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,14 +14,16 @@ 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={[]}
|
|
20
|
+
projectCronJobCountByRoot={new Map()}
|
|
19
21
|
renderSessionItem={() => <></>}
|
|
20
22
|
sessionTypeOptions={[]}
|
|
21
23
|
/>,
|
|
22
24
|
);
|
|
23
25
|
|
|
24
|
-
return { onAddProject, onSelectMode };
|
|
26
|
+
return { onAddProject, onScrollNearEnd, onSelectMode };
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
describe("ChatSidebarSessionArea", () => {
|
|
@@ -59,7 +61,9 @@ describe("ChatSidebarSessionArea", () => {
|
|
|
59
61
|
|
|
60
62
|
it("uses a folder-plus icon for the add-project action", () => {
|
|
61
63
|
const { onAddProject } = renderSessionArea(true);
|
|
62
|
-
const addProjectButton = screen.getByRole("button", {
|
|
64
|
+
const addProjectButton = screen.getByRole("button", {
|
|
65
|
+
name: "Add Project",
|
|
66
|
+
});
|
|
63
67
|
const modeIndicator = screen
|
|
64
68
|
.getByRole("group", { name: "Session list view" })
|
|
65
69
|
.querySelector("span[aria-hidden='true']");
|
|
@@ -72,12 +76,29 @@ describe("ChatSidebarSessionArea", () => {
|
|
|
72
76
|
expect(addProjectButton.className).toContain("hover:bg-gray-200/60");
|
|
73
77
|
expect(modeIndicator?.className).toContain("translate-x-full");
|
|
74
78
|
expect(modeIndicator?.className).toContain("motion-reduce:transition-none");
|
|
75
|
-
expect(
|
|
76
|
-
|
|
79
|
+
expect(
|
|
80
|
+
screen
|
|
81
|
+
.getByRole("button", { name: "Project" })
|
|
82
|
+
.getAttribute("aria-pressed"),
|
|
83
|
+
).toBe("true");
|
|
84
|
+
expect(modeIndicator?.parentElement?.previousElementSibling).toBe(
|
|
85
|
+
addProjectButton,
|
|
77
86
|
);
|
|
78
|
-
expect(modeIndicator?.parentElement?.previousElementSibling).toBe(addProjectButton);
|
|
79
87
|
fireEvent.click(addProjectButton);
|
|
80
88
|
|
|
81
89
|
expect(onAddProject).toHaveBeenCalledOnce();
|
|
82
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
|
+
});
|
|
83
104
|
});
|
|
@@ -18,7 +18,6 @@ const mocks = vi.hoisted(() => ({
|
|
|
18
18
|
setQuery: vi.fn(),
|
|
19
19
|
setListMode: vi.fn(),
|
|
20
20
|
selectSession: vi.fn(),
|
|
21
|
-
openChildSessionPanel: vi.fn(),
|
|
22
21
|
docOpen: vi.fn(),
|
|
23
22
|
setLanguage: vi.fn(),
|
|
24
23
|
setTheme: vi.fn(),
|
|
@@ -49,7 +48,12 @@ vi.mock("@/shared/hooks/use-projects", () => ({
|
|
|
49
48
|
isPending: false,
|
|
50
49
|
error: null,
|
|
51
50
|
}),
|
|
52
|
-
useAddExistingProject: () => ({
|
|
51
|
+
useAddExistingProject: () => ({
|
|
52
|
+
mutateAsync: vi.fn(),
|
|
53
|
+
reset: vi.fn(),
|
|
54
|
+
isPending: false,
|
|
55
|
+
error: null,
|
|
56
|
+
}),
|
|
53
57
|
}));
|
|
54
58
|
function sidebarElement(variant?: "desktop" | "mobile") {
|
|
55
59
|
const queryClient = new QueryClient({
|
|
@@ -82,7 +86,10 @@ function expectCodexSelectedInSessionTypeMenu() {
|
|
|
82
86
|
).toBe("true");
|
|
83
87
|
}
|
|
84
88
|
function expectSessionCreated(sessionType: string, projectRoot?: string) {
|
|
85
|
-
expect(mocks.createSession).toHaveBeenCalledWith({
|
|
89
|
+
expect(mocks.createSession).toHaveBeenCalledWith({
|
|
90
|
+
projectRoot,
|
|
91
|
+
sessionType,
|
|
92
|
+
});
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
|
|
@@ -106,9 +113,7 @@ vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
|
|
|
106
113
|
.markSessionRead(sessionKey, readAt)
|
|
107
114
|
: undefined,
|
|
108
115
|
},
|
|
109
|
-
chatThreadManager: {
|
|
110
|
-
openChildSessionPanel: mocks.openChildSessionPanel,
|
|
111
|
-
},
|
|
116
|
+
chatThreadManager: {},
|
|
112
117
|
}),
|
|
113
118
|
}));
|
|
114
119
|
|
|
@@ -145,6 +150,7 @@ vi.mock(
|
|
|
145
150
|
|
|
146
151
|
vi.mock("@/features/chat/features/ncp/hooks/use-ncp-session-list-view", () => ({
|
|
147
152
|
useNcpSessionListView: () => ({
|
|
153
|
+
allItems: mocks.sessionItems,
|
|
148
154
|
isLoading: mocks.isLoading,
|
|
149
155
|
items: mocks.sessionItems,
|
|
150
156
|
}),
|
|
@@ -196,7 +202,6 @@ function resetSidebarTestState() {
|
|
|
196
202
|
mocks.setQuery.mockReset();
|
|
197
203
|
mocks.setListMode.mockReset();
|
|
198
204
|
mocks.selectSession.mockReset();
|
|
199
|
-
mocks.openChildSessionPanel.mockReset();
|
|
200
205
|
mocks.docOpen.mockReset();
|
|
201
206
|
mocks.setLanguage.mockReset();
|
|
202
207
|
mocks.setTheme.mockReset();
|
|
@@ -284,9 +289,9 @@ describe("ChatSidebar create and list basics", () => {
|
|
|
284
289
|
it("keeps the desktop brand row compact with symmetric vertical padding", () => {
|
|
285
290
|
renderSidebar();
|
|
286
291
|
|
|
287
|
-
expect(
|
|
288
|
-
|
|
289
|
-
)
|
|
292
|
+
expect(screen.getByTestId("brand-header").parentElement?.className).toMatch(
|
|
293
|
+
/(?:^|\s)py-2(?:\s|$)/,
|
|
294
|
+
);
|
|
290
295
|
});
|
|
291
296
|
|
|
292
297
|
it("shows setup required status for runtime session types that are not ready yet", () => {
|
|
@@ -857,7 +862,7 @@ describe("ChatSidebar session item interactions", () => {
|
|
|
857
862
|
expect(screen.queryByLabelText("Session has unread updates")).toBeNull();
|
|
858
863
|
});
|
|
859
864
|
|
|
860
|
-
it("
|
|
865
|
+
it("keeps child-session counts out of the parent session row", () => {
|
|
861
866
|
mocks.sessionItems = [
|
|
862
867
|
createSessionItem({
|
|
863
868
|
key: "session:parent-1",
|
|
@@ -882,11 +887,6 @@ describe("ChatSidebar session item interactions", () => {
|
|
|
882
887
|
|
|
883
888
|
renderSidebar();
|
|
884
889
|
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
expect(mocks.openChildSessionPanel).toHaveBeenCalledWith({
|
|
888
|
-
parentSessionKey: "session:parent-1",
|
|
889
|
-
activeChildSessionKey: "session:child-1",
|
|
890
|
-
});
|
|
890
|
+
expect(screen.queryByLabelText("View child sessions")).toBeNull();
|
|
891
891
|
});
|
|
892
892
|
});
|
|
@@ -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,
|
|
@@ -63,7 +64,10 @@ function ChatSidebarCollapseButton({ isCollapsed }: { isCollapsed: boolean }) {
|
|
|
63
64
|
<IconActionButton
|
|
64
65
|
icon={<Icon className={SIDEBAR_RAIL_ICON_CLASS} />}
|
|
65
66
|
label={label}
|
|
66
|
-
className={cn(
|
|
67
|
+
className={cn(
|
|
68
|
+
SIDEBAR_RAIL_SURFACE_CLASS,
|
|
69
|
+
isCollapsed && SIDEBAR_RAIL_CONTROL_CLASS,
|
|
70
|
+
)}
|
|
67
71
|
onClick={viewportLayoutManager.toggleSidebarCollapsed}
|
|
68
72
|
/>
|
|
69
73
|
);
|
|
@@ -77,7 +81,9 @@ export function ChatSidebarDesktopHeader({
|
|
|
77
81
|
isCollapsed: boolean;
|
|
78
82
|
}) {
|
|
79
83
|
const isWindowsHost = isWindowsDesktopHost();
|
|
80
|
-
const shouldReserveMacWindowControls =
|
|
84
|
+
const shouldReserveMacWindowControls =
|
|
85
|
+
typeof window !== "undefined" &&
|
|
86
|
+
window.nextclawDesktop?.platform === "darwin";
|
|
81
87
|
|
|
82
88
|
return (
|
|
83
89
|
<div
|
|
@@ -124,9 +130,13 @@ export function ChatSidebarDesktopNav({
|
|
|
124
130
|
density="compact"
|
|
125
131
|
collapsed={isCollapsed}
|
|
126
132
|
indicator={item.target === "/inbox" && unreadCount > 0}
|
|
127
|
-
trailing={
|
|
128
|
-
|
|
129
|
-
|
|
133
|
+
trailing={
|
|
134
|
+
item.target === "/inbox" && unreadCount > 0
|
|
135
|
+
? unreadCount > 99
|
|
136
|
+
? "99+"
|
|
137
|
+
: unreadCount
|
|
138
|
+
: undefined
|
|
139
|
+
}
|
|
130
140
|
className={isCollapsed ? undefined : "rounded-lg px-2.5 py-1.5"}
|
|
131
141
|
/>
|
|
132
142
|
</li>
|
|
@@ -145,8 +155,10 @@ export function ChatSidebarSessionArea({
|
|
|
145
155
|
isLoading,
|
|
146
156
|
isProjectFirstView,
|
|
147
157
|
onAddProject,
|
|
158
|
+
onScrollNearEnd = () => undefined,
|
|
148
159
|
onSelectMode,
|
|
149
160
|
projectGroups,
|
|
161
|
+
projectCronJobCountByRoot,
|
|
150
162
|
renderSessionItem,
|
|
151
163
|
sessionTypeOptions,
|
|
152
164
|
}: {
|
|
@@ -156,13 +168,20 @@ export function ChatSidebarSessionArea({
|
|
|
156
168
|
isLoading: boolean;
|
|
157
169
|
isProjectFirstView: boolean;
|
|
158
170
|
onAddProject: () => void;
|
|
171
|
+
onScrollNearEnd?: () => void;
|
|
159
172
|
onSelectMode: (mode: "time-first" | "project-first") => void;
|
|
160
173
|
projectGroups: ReturnType<typeof groupSessionsByProject>;
|
|
174
|
+
projectCronJobCountByRoot: ReadonlyMap<string, number>;
|
|
161
175
|
renderSessionItem: (item: NcpSessionListItemView) => JSX.Element;
|
|
162
176
|
sessionTypeOptions: Parameters<
|
|
163
177
|
typeof ChatSidebarSessionList
|
|
164
178
|
>[0]["sessionTypeOptions"];
|
|
165
179
|
}) {
|
|
180
|
+
const scrollRestoration = useScrollRestoration<HTMLDivElement>({
|
|
181
|
+
restorationKey: "chat-sidebar:sessions",
|
|
182
|
+
isEnabled: !isCollapsed,
|
|
183
|
+
});
|
|
184
|
+
const { onScroll: onScrollPositionSave, scrollRef } = scrollRestoration;
|
|
166
185
|
if (isCollapsed) {
|
|
167
186
|
return <div className="min-h-0 flex-1" aria-hidden="true" />;
|
|
168
187
|
}
|
|
@@ -185,6 +204,14 @@ export function ChatSidebarSessionArea({
|
|
|
185
204
|
</div>
|
|
186
205
|
|
|
187
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
|
+
}}
|
|
188
215
|
className={cn(
|
|
189
216
|
"custom-scrollbar min-h-0 flex-1 overflow-y-auto px-3 pb-4 pt-1",
|
|
190
217
|
SCROLL_BOTTOM_EDGE_FADE_CLASS,
|
|
@@ -195,6 +222,7 @@ export function ChatSidebarSessionArea({
|
|
|
195
222
|
isProjectFirstView={isProjectFirstView}
|
|
196
223
|
groups={groups}
|
|
197
224
|
projectGroups={projectGroups}
|
|
225
|
+
projectCronJobCountByRoot={projectCronJobCountByRoot}
|
|
198
226
|
defaultSessionType={defaultSessionType}
|
|
199
227
|
sessionTypeOptions={sessionTypeOptions}
|
|
200
228
|
renderSessionItem={renderSessionItem}
|
|
@@ -21,7 +21,10 @@ import {
|
|
|
21
21
|
useCreateProject,
|
|
22
22
|
useProjects,
|
|
23
23
|
} from "@/shared/hooks/use-projects";
|
|
24
|
-
import type {
|
|
24
|
+
import type {
|
|
25
|
+
ProjectAddExistingRequest,
|
|
26
|
+
ProjectCreateRequest,
|
|
27
|
+
} from "@/shared/lib/api";
|
|
25
28
|
import { normalizeSessionProjectRootValue } from "@/shared/lib/session-project";
|
|
26
29
|
import { cn } from "@/shared/lib/utils";
|
|
27
30
|
import { LANGUAGE_OPTIONS, t, type I18nLanguage } from "@/shared/lib/i18n";
|
|
@@ -50,6 +53,7 @@ import { useChatNewSessionTypePreference } from "@/features/chat/features/sessio
|
|
|
50
53
|
import { useViewportLayoutStore } from "@/app/stores/viewport-layout.store";
|
|
51
54
|
import { SIDEBAR_RAIL_WIDTH_CLASS } from "@/app/components/layout/sidebar-rail.styles";
|
|
52
55
|
import { ChatProjectAddDialog } from "@/features/chat/features/project/components/chat-project-add-dialog";
|
|
56
|
+
import { useChatSidebarContextCounts } from "@/features/chat/features/session/hooks/use-chat-sidebar-context-counts";
|
|
53
57
|
|
|
54
58
|
type ChatSidebarVariant = "desktop" | "mobile";
|
|
55
59
|
|
|
@@ -116,7 +120,9 @@ export function ChatSidebar({
|
|
|
116
120
|
const projectsQuery = useProjects();
|
|
117
121
|
const projectCreateMutation = useCreateProject();
|
|
118
122
|
const projectAddExistingMutation = useAddExistingProject();
|
|
119
|
-
const { isLoading, items } = useNcpSessionListView();
|
|
123
|
+
const { allItems, hasMore, isLoading, isLoadingMore, items, loadMore } = useNcpSessionListView();
|
|
124
|
+
const { cronJobCountByProjectRoot, cronJobCountBySessionKey } =
|
|
125
|
+
useChatSidebarContextCounts(allItems);
|
|
120
126
|
const { language, setLanguage } = useI18n();
|
|
121
127
|
const { theme, setTheme } = useTheme();
|
|
122
128
|
const currentThemeLabel = t(
|
|
@@ -160,21 +166,27 @@ export function ChatSidebar({
|
|
|
160
166
|
[items],
|
|
161
167
|
);
|
|
162
168
|
const childSessionsByParentKey = useMemo(
|
|
163
|
-
() => groupChildSessionsByParentKey(
|
|
164
|
-
[
|
|
169
|
+
() => groupChildSessionsByParentKey(allItems),
|
|
170
|
+
[allItems],
|
|
165
171
|
);
|
|
166
172
|
const groups = useMemo(
|
|
167
173
|
() => groupSessionsByDate(sortedItems, pinnedSessionKeys),
|
|
168
174
|
[pinnedSessionKeys, sortedItems],
|
|
169
175
|
);
|
|
170
176
|
const projectGroups = useMemo(
|
|
171
|
-
() =>
|
|
172
|
-
|
|
173
|
-
|
|
177
|
+
() =>
|
|
178
|
+
groupSessionsByProject(
|
|
179
|
+
sortedItems,
|
|
180
|
+
pinnedSessionKeys,
|
|
181
|
+
pinnedProjectRoots,
|
|
182
|
+
projectsQuery.data?.projects ?? [],
|
|
183
|
+
),
|
|
184
|
+
[
|
|
174
185
|
pinnedProjectRoots,
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
186
|
+
pinnedSessionKeys,
|
|
187
|
+
projectsQuery.data?.projects,
|
|
188
|
+
sortedItems,
|
|
189
|
+
],
|
|
178
190
|
);
|
|
179
191
|
const sessionTypeOptions = useMemo(
|
|
180
192
|
() => buildSessionTypeOptions(sessionTypesData?.options ?? []),
|
|
@@ -220,6 +232,7 @@ export function ChatSidebar({
|
|
|
220
232
|
optimisticReadAtBySessionKey={optimisticReadAtBySessionKey}
|
|
221
233
|
agentsById={agentsById}
|
|
222
234
|
childSessionsByParentKey={childSessionsByParentKey}
|
|
235
|
+
cronJobCount={cronJobCountBySessionKey.get(item.session.key) ?? 0}
|
|
223
236
|
editingSessionKey={editingSessionKey}
|
|
224
237
|
draftLabel={draftLabel}
|
|
225
238
|
savingSessionKey={savingSessionKey}
|
|
@@ -227,12 +240,6 @@ export function ChatSidebar({
|
|
|
227
240
|
isPinned={pinnedSessionKeys.has(item.session.key)}
|
|
228
241
|
sessionTitle={getSessionTitle}
|
|
229
242
|
onSelectSession={presenter.chatSessionListManager.selectSession}
|
|
230
|
-
onOpenChildSessions={(parentSessionKey, activeChildSessionKey) =>
|
|
231
|
-
presenter.chatThreadManager.openChildSessionPanel({
|
|
232
|
-
parentSessionKey,
|
|
233
|
-
activeChildSessionKey,
|
|
234
|
-
})
|
|
235
|
-
}
|
|
236
243
|
onStartEditingSessionLabel={startEditingSessionLabel}
|
|
237
244
|
onDraftLabelChange={setDraftLabel}
|
|
238
245
|
onSaveSessionLabel={saveSessionLabel}
|
|
@@ -256,7 +263,9 @@ export function ChatSidebar({
|
|
|
256
263
|
projectAddExistingMutation.reset();
|
|
257
264
|
setIsProjectAddOpen(true);
|
|
258
265
|
};
|
|
259
|
-
const createProjectFromSidebar = async (
|
|
266
|
+
const createProjectFromSidebar = async (
|
|
267
|
+
input: ProjectCreateRequest,
|
|
268
|
+
): Promise<void> => {
|
|
260
269
|
await projectCreateMutation.mutateAsync(input);
|
|
261
270
|
setIsProjectAddOpen(false);
|
|
262
271
|
};
|
|
@@ -274,10 +283,7 @@ export function ChatSidebar({
|
|
|
274
283
|
isMobileVariant
|
|
275
284
|
? "flex-1 overflow-hidden"
|
|
276
285
|
: shouldCollapse
|
|
277
|
-
? cn(
|
|
278
|
-
SIDEBAR_RAIL_WIDTH_CLASS,
|
|
279
|
-
"shrink-0",
|
|
280
|
-
)
|
|
286
|
+
? cn(SIDEBAR_RAIL_WIDTH_CLASS, "shrink-0")
|
|
281
287
|
: "w-[280px] shrink-0",
|
|
282
288
|
)}
|
|
283
289
|
data-sidebar-collapsed={shouldCollapse ? "true" : "false"}
|
|
@@ -337,8 +343,12 @@ export function ChatSidebar({
|
|
|
337
343
|
isLoading={isLoading}
|
|
338
344
|
isProjectFirstView={isProjectFirstView}
|
|
339
345
|
onAddProject={openProjectAdd}
|
|
346
|
+
onScrollNearEnd={() => {
|
|
347
|
+
if (hasMore && !isLoadingMore) void loadMore();
|
|
348
|
+
}}
|
|
340
349
|
onSelectMode={presenter.chatSessionListManager.setListMode}
|
|
341
350
|
projectGroups={projectGroups}
|
|
351
|
+
projectCronJobCountByRoot={cronJobCountByProjectRoot}
|
|
342
352
|
renderSessionItem={renderSessionItem}
|
|
343
353
|
sessionTypeOptions={sessionTypeOptions}
|
|
344
354
|
/>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { act, renderHook, waitFor } from '@testing-library/react';
|
|
2
2
|
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import type
|
|
3
|
+
import { NextClawClientError, type UiNcpSessionQueuedInputView } from '@nextclaw/client-sdk';
|
|
4
4
|
import type { NcpAgentSendEnvelope, NcpMessage, NcpRunHandle } from '@nextclaw/ncp';
|
|
5
5
|
|
|
6
6
|
import { useSessionConversationController } from '@/features/chat/features/conversation/hooks/use-session-conversation-controller';
|
|
@@ -448,3 +448,43 @@ describe('useSessionConversationController backend run queue', () => {
|
|
|
448
448
|
expect(params.onSessionMaterialized).not.toHaveBeenCalled();
|
|
449
449
|
});
|
|
450
450
|
});
|
|
451
|
+
|
|
452
|
+
describe('useSessionConversationController stale queue reconciliation', () => {
|
|
453
|
+
it('reconciles repeated stale queued actions without exposing their not-found errors', async () => {
|
|
454
|
+
const queuedInput = createQueuedInput();
|
|
455
|
+
const params = createControllerParams({ isRunning: true, queuedInputs: [queuedInput] });
|
|
456
|
+
params.runQueue.steerQueuedInput.mockRejectedValue(new NextClawClientError({
|
|
457
|
+
code: 'NOT_FOUND',
|
|
458
|
+
message: `queued input not found in session session-1: ${queuedInput.id}`,
|
|
459
|
+
status: 404,
|
|
460
|
+
}));
|
|
461
|
+
const { result } = renderHook(() => useSessionConversationController(params));
|
|
462
|
+
|
|
463
|
+
act(() => result.current.steerQueuedInput(queuedInput.id));
|
|
464
|
+
await waitFor(() => expect(params.runQueue.refreshPendingInputs).toHaveBeenCalledTimes(1));
|
|
465
|
+
|
|
466
|
+
act(() => result.current.steerQueuedInput(queuedInput.id));
|
|
467
|
+
await waitFor(() => expect(params.runQueue.refreshPendingInputs).toHaveBeenCalledTimes(2));
|
|
468
|
+
|
|
469
|
+
expect(params.setSendError).toHaveBeenLastCalledWith(null);
|
|
470
|
+
expect(params.setSendError).not.toHaveBeenCalledWith(expect.stringContaining('queued input not found'));
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('keeps a real steering failure visible', async () => {
|
|
474
|
+
const queuedInput = createQueuedInput();
|
|
475
|
+
const params = createControllerParams({ isRunning: true, queuedInputs: [queuedInput] });
|
|
476
|
+
params.runQueue.steerQueuedInput.mockRejectedValue(new NextClawClientError({
|
|
477
|
+
code: 'STEER_UNAVAILABLE',
|
|
478
|
+
message: 'The active runtime cannot accept this input at the next safe step.',
|
|
479
|
+
status: 409,
|
|
480
|
+
}));
|
|
481
|
+
const { result } = renderHook(() => useSessionConversationController(params));
|
|
482
|
+
|
|
483
|
+
act(() => result.current.steerQueuedInput(queuedInput.id));
|
|
484
|
+
|
|
485
|
+
await waitFor(() => expect(params.setSendError).toHaveBeenCalledWith(
|
|
486
|
+
'The active runtime cannot accept this input at the next safe step.',
|
|
487
|
+
));
|
|
488
|
+
expect(params.runQueue.refreshPendingInputs).not.toHaveBeenCalled();
|
|
489
|
+
});
|
|
490
|
+
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useCallback, useMemo, useState } from 'react';
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import {
|
|
3
|
+
NextClawClientError,
|
|
4
|
+
type UiNcpSessionPendingInputView,
|
|
5
|
+
type UiNcpSessionQueuedInputView,
|
|
5
6
|
} from '@nextclaw/client-sdk';
|
|
6
7
|
import type { NcpAgentSendEnvelope } from '@nextclaw/ncp';
|
|
7
8
|
|
|
@@ -20,10 +21,17 @@ type ComposerDraftSnapshot = Pick<
|
|
|
20
21
|
|
|
21
22
|
type SessionRunQueue = {
|
|
22
23
|
readonly inputs: readonly UiNcpSessionQueuedInputView[];
|
|
24
|
+
readonly refreshPendingInputs: () => Promise<readonly UiNcpSessionPendingInputView[]>;
|
|
23
25
|
readonly removeQueuedInput: (id: string) => Promise<UiNcpSessionQueuedInputView | null>;
|
|
24
26
|
readonly steerQueuedInput: (id: string) => Promise<UiNcpSessionPendingInputView | null>;
|
|
25
27
|
};
|
|
26
28
|
|
|
29
|
+
function isStaleQueuedInputError(error: unknown): boolean {
|
|
30
|
+
return error instanceof NextClawClientError
|
|
31
|
+
&& error.status === 404
|
|
32
|
+
&& error.code === 'NOT_FOUND';
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
export type SubmittingQueuedInput = {
|
|
28
36
|
readonly attachments: readonly SessionQueuedInputAttachmentPreview[];
|
|
29
37
|
readonly id: string;
|
|
@@ -127,7 +135,17 @@ export function useQueuedInputActions(params: {
|
|
|
127
135
|
const steerQueuedInput = useCallback((id: string) => {
|
|
128
136
|
void runQueue.steerQueuedInput(id).then((steered) => {
|
|
129
137
|
if (steered) setSendError(null);
|
|
130
|
-
}).catch((error) => {
|
|
138
|
+
}).catch(async (error) => {
|
|
139
|
+
if (isStaleQueuedInputError(error)) {
|
|
140
|
+
try {
|
|
141
|
+
await runQueue.refreshPendingInputs();
|
|
142
|
+
setSendError(null);
|
|
143
|
+
return;
|
|
144
|
+
} catch (refreshError) {
|
|
145
|
+
setSendError(refreshError instanceof Error ? refreshError.message : String(refreshError));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
131
149
|
setSendError(error instanceof Error ? error.message : String(error));
|
|
132
150
|
});
|
|
133
151
|
}, [runQueue, setSendError]);
|
|
@@ -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", () => {
|
|
@@ -109,4 +109,39 @@ describe("buildChatMessageTriggerDetails", () => {
|
|
|
109
109
|
"run-trigger-metadata",
|
|
110
110
|
]);
|
|
111
111
|
});
|
|
112
|
+
|
|
113
|
+
it("keeps a completed steering user message actionable from persisted run metadata", () => {
|
|
114
|
+
const details = buildChatMessageTriggerDetails({
|
|
115
|
+
labels,
|
|
116
|
+
message: {
|
|
117
|
+
metadata: {
|
|
118
|
+
run_spec: {
|
|
119
|
+
runId: "run-1",
|
|
120
|
+
model: "openai/gpt-5.6",
|
|
121
|
+
},
|
|
122
|
+
run_trigger: {
|
|
123
|
+
version: 1,
|
|
124
|
+
actor: "human",
|
|
125
|
+
source: "internal",
|
|
126
|
+
sourceMessageId: "user-steering",
|
|
127
|
+
targetRunId: "run-1",
|
|
128
|
+
triggeredAt: "2026-08-25T13:39:04.000Z",
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(details).toMatchObject({
|
|
135
|
+
triggerLabel: "More actions",
|
|
136
|
+
items: [{
|
|
137
|
+
key: "run-trigger-metadata",
|
|
138
|
+
dialog: {
|
|
139
|
+
rows: expect.arrayContaining([
|
|
140
|
+
{ label: "Target run", value: "run-1" },
|
|
141
|
+
{ label: "Run model", value: "openai/gpt-5.6" },
|
|
142
|
+
]),
|
|
143
|
+
},
|
|
144
|
+
}],
|
|
145
|
+
});
|
|
146
|
+
});
|
|
112
147
|
});
|
|
@@ -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,
|
|
@@ -73,4 +73,54 @@ describe('useNcpAgentRuntime backend queue submission', () => {
|
|
|
73
73
|
expect(result.current.visibleMessages).toEqual([queuedMessage]);
|
|
74
74
|
});
|
|
75
75
|
});
|
|
76
|
+
|
|
77
|
+
it('keeps a steered message in the pending projection instead of accepting it as a new run', async () => {
|
|
78
|
+
const manager = new DefaultNcpAgentConversationStateManager();
|
|
79
|
+
manager.hydrate({ sessionId: 'session-1', messages: [] });
|
|
80
|
+
await manager.dispatch({
|
|
81
|
+
type: NcpEventType.RunStarted,
|
|
82
|
+
payload: {
|
|
83
|
+
sessionId: 'session-1',
|
|
84
|
+
runId: 'run-1',
|
|
85
|
+
messageId: 'assistant-1',
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
const send = vi.fn(async (envelope: NcpAgentSendEnvelope) => ({
|
|
89
|
+
assistantMessageId: null,
|
|
90
|
+
delivery: 'steered' as const,
|
|
91
|
+
runId: 'run-1',
|
|
92
|
+
sessionId: 'session-1',
|
|
93
|
+
userMessageId: envelope.message.id,
|
|
94
|
+
}));
|
|
95
|
+
const client = {
|
|
96
|
+
abort: vi.fn(async () => undefined),
|
|
97
|
+
send,
|
|
98
|
+
stop: vi.fn(async () => undefined),
|
|
99
|
+
stream: vi.fn(async () => undefined),
|
|
100
|
+
subscribe: vi.fn(() => () => undefined),
|
|
101
|
+
} as unknown as NcpAgentClientEndpoint;
|
|
102
|
+
const { result } = renderHook(() => useNcpAgentRuntime({
|
|
103
|
+
client,
|
|
104
|
+
manager,
|
|
105
|
+
sessionId: 'session-1',
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
await act(async () => {
|
|
109
|
+
await result.current.send({
|
|
110
|
+
delivery: 'prefer-steer',
|
|
111
|
+
message: {
|
|
112
|
+
id: 'user-steering',
|
|
113
|
+
sessionId: 'session-1',
|
|
114
|
+
role: 'user',
|
|
115
|
+
status: 'final',
|
|
116
|
+
parts: [{ type: 'text', text: 'change direction' }],
|
|
117
|
+
timestamp: '2026-08-25T00:00:00.000Z',
|
|
118
|
+
},
|
|
119
|
+
sessionId: 'session-1',
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(result.current.visibleMessages).toEqual([]);
|
|
124
|
+
expect(result.current.snapshot.activeRun?.runId).toBe('run-1');
|
|
125
|
+
});
|
|
76
126
|
});
|