@nextclaw/ui 0.20.2 → 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 +12 -0
- package/dist/assets/{api-DvpQWjTS.js → api-iMkU_o5P.js} +3 -3
- package/dist/assets/{appearance-settings-page-U0m0ktu3.js → appearance-settings-page-CU5k4b-M.js} +1 -1
- package/dist/assets/{book-open-DRofSTWr.js → book-open-nD8gdMhA.js} +1 -1
- package/dist/assets/{channels-list-page-BURQarD0.js → channels-list-page-Drlt62kY.js} +1 -1
- package/dist/assets/{config-split-page-BrAQYTIl.js → config-split-page-BtJZp1E5.js} +1 -1
- package/dist/assets/{confirm-dialog-DDJomLnO.js → confirm-dialog-k2-Jq_x3.js} +1 -1
- package/dist/assets/{createLucideIcon-DzkNHr5Z.js → createLucideIcon-DxlwKLr-.js} +1 -1
- package/dist/assets/{desktop-update-config-BzZFhMv9.js → desktop-update-config-BiZmy7FC.js} +1 -1
- package/dist/assets/{dist-DXbLo2b1.js → dist-Cy_e4kNb.js} +1 -1
- package/dist/assets/{dist-NskPB_yW.js → dist-m08DUswB.js} +1 -1
- package/dist/assets/doc-browser-CPsPHh12.js +1 -0
- package/dist/assets/{doc-browser-context-CnKV4gJy.js → doc-browser-context-jiy-O4F8.js} +1 -1
- package/dist/assets/doc-browser-npOvWtgf.js +1 -0
- package/dist/assets/{extensions-DvlFMpAs.js → extensions-CIb17__o.js} +1 -1
- package/dist/assets/{form-actions-UnPEZBkx.js → form-actions-DGf5zUg_.js} +1 -1
- package/dist/assets/{index-Dc_ZMkmo.css → index-C86hxgv5.css} +1 -1
- 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-BIgFTbMq.js → mcp-marketplace-page-kLWW6kzt.js} +1 -1
- package/dist/assets/{model-config-page-CnSm7kfa.js → model-config-page-DSeAjVT_.js} +1 -1
- package/dist/assets/{navigation-link-NjUdG6Vp.js → navigation-link-DAfwFCj4.js} +1 -1
- package/dist/assets/plus-D4LtWtTk.js +1 -0
- package/dist/assets/{privacy-settings-page-DVn2HU7W.js → privacy-settings-page-tN0Mt15x.js} +1 -1
- package/dist/assets/{providers-config-page-CzHJS3eg.js → providers-config-page-CgPrXR1u.js} +1 -1
- package/dist/assets/react-8iBFECZE.js +8 -0
- package/dist/assets/{refresh-cw-CkeninE0.js → refresh-cw-D_Eibznw.js} +1 -1
- package/dist/assets/remote-Derr_AQp.js +1 -0
- package/dist/assets/{rotate-cw-DP33PuXU.js → rotate-cw-QQjmoWuq.js} +1 -1
- package/dist/assets/{runtime-config-page-D69buyGp.js → runtime-config-page-Crq96Jjq.js} +1 -1
- package/dist/assets/{save-Cw2Xz-pB.js → save-W2xthJJ2.js} +1 -1
- package/dist/assets/{search-C2CFAVvd.js → search-Vih1R2BD.js} +1 -1
- package/dist/assets/{search-config-page-dqNT0Swf.js → search-config-page-CtBcL2uc.js} +1 -1
- package/dist/assets/{secrets-config-page-De6J3wVx.js → secrets-config-page-Q6XfIj0h.js} +1 -1
- package/dist/assets/security-config-CE9rkQvq.js +1 -0
- package/dist/assets/{select-DSuMhwVP.js → select-lNdozU4o.js} +1 -1
- package/dist/assets/skeleton-B__aTcly.js +1 -0
- package/dist/assets/{tag-chip-DFBLgsGf.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 +8 -8
- 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-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 +4 -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/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/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/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/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 +1 -0
- package/src/shared/lib/api/utils/ncp-session.utils.ts +3 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +8 -5
- 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 +8 -5
- 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/doc-browser-DB22tQKU.js +0 -1
- package/dist/assets/doc-browser-sJjw9fl5.js +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-CZN4ovdK.js +0 -1
- package/dist/assets/plus-DVXol9Q5.js +0 -1
- package/dist/assets/react-DCVbGroo.js +0 -8
- package/dist/assets/remote-B2lsJF1c.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
|
@@ -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();
|
|
@@ -343,6 +343,9 @@ export function ChatSidebar({
|
|
|
343
343
|
isLoading={isLoading}
|
|
344
344
|
isProjectFirstView={isProjectFirstView}
|
|
345
345
|
onAddProject={openProjectAdd}
|
|
346
|
+
onScrollNearEnd={() => {
|
|
347
|
+
if (hasMore && !isLoadingMore) void loadMore();
|
|
348
|
+
}}
|
|
346
349
|
onSelectMode={presenter.chatSessionListManager.setListMode}
|
|
347
350
|
projectGroups={projectGroups}
|
|
348
351
|
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/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);
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { ChatSessionWorkspaceFilePreview } from "@/features/chat/features/workspace/components/chat-session-workspace-file-preview";
|
|
5
|
+
import { scrollRestorationManager } from "@/shared/lib/navigation-history";
|
|
6
|
+
|
|
7
|
+
const serverPathReadMock = vi.fn();
|
|
8
|
+
const serverPathBrowseMock = vi.fn();
|
|
9
|
+
|
|
10
|
+
vi.mock("@/shared/hooks/use-server-path-read", () => ({
|
|
11
|
+
useServerPathRead: (...args: unknown[]) => serverPathReadMock(...args),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock("@/shared/hooks/use-server-path-browse", () => ({
|
|
15
|
+
useServerPathBrowse: (...args: unknown[]) => serverPathBrowseMock(...args),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock("@nextclaw/agent-chat-ui", () => ({
|
|
19
|
+
ChatMessageMarkdown: ({ text }: { text: string }) => <div data-testid="markdown-preview">{text}</div>,
|
|
20
|
+
FileOperationCodeSurface: () => null,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
vi.mock(
|
|
24
|
+
"@/features/chat/features/workspace/components/workspace-text-selection-menu",
|
|
25
|
+
() => ({
|
|
26
|
+
WorkspaceTextSelectionMenu: ({ children }: { children: ReactNode }) => children,
|
|
27
|
+
}),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
function renderMarkdownPreview(file: {
|
|
31
|
+
key: string;
|
|
32
|
+
path: string;
|
|
33
|
+
previewViewer?: "rendered" | "source";
|
|
34
|
+
} = {
|
|
35
|
+
key: "markdown-tab",
|
|
36
|
+
path: "/tmp/README.md",
|
|
37
|
+
previewViewer: "rendered",
|
|
38
|
+
}) {
|
|
39
|
+
return render(
|
|
40
|
+
<ChatSessionWorkspaceFilePreview
|
|
41
|
+
file={{
|
|
42
|
+
key: file.key,
|
|
43
|
+
parentSessionKey: "session-1",
|
|
44
|
+
path: file.path,
|
|
45
|
+
viewMode: "preview",
|
|
46
|
+
previewViewer: file.previewViewer ?? "rendered",
|
|
47
|
+
}}
|
|
48
|
+
sessionProjectRoot="/tmp"
|
|
49
|
+
sessionWorkingDir="/tmp"
|
|
50
|
+
showBreadcrumbs={false}
|
|
51
|
+
onFileOpen={vi.fn()}
|
|
52
|
+
/>,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe("ChatSessionWorkspaceFilePreview Markdown scroll restoration", () => {
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
scrollRestorationManager.clear();
|
|
59
|
+
serverPathReadMock.mockImplementation(({ path }: { path: string }) => ({
|
|
60
|
+
isLoading: false,
|
|
61
|
+
error: null,
|
|
62
|
+
data: {
|
|
63
|
+
kind: "markdown",
|
|
64
|
+
resolvedPath: path,
|
|
65
|
+
startLine: 1,
|
|
66
|
+
text: `# ${path}`,
|
|
67
|
+
truncated: false,
|
|
68
|
+
},
|
|
69
|
+
}));
|
|
70
|
+
serverPathBrowseMock.mockReturnValue({
|
|
71
|
+
isLoading: false,
|
|
72
|
+
error: new Error("not a directory"),
|
|
73
|
+
data: null,
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("restores the rendered Markdown position when returning to the same file tab", () => {
|
|
78
|
+
const firstView = renderMarkdownPreview();
|
|
79
|
+
const firstScrollContainer = screen.getByTestId("markdown-preview").parentElement as HTMLDivElement;
|
|
80
|
+
firstScrollContainer.scrollTop = 144;
|
|
81
|
+
fireEvent.scroll(firstScrollContainer);
|
|
82
|
+
firstView.unmount();
|
|
83
|
+
|
|
84
|
+
renderMarkdownPreview();
|
|
85
|
+
|
|
86
|
+
expect((screen.getByTestId("markdown-preview").parentElement as HTMLDivElement).scrollTop).toBe(144);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("keeps each Markdown tab's position when switching files in one preview instance", () => {
|
|
90
|
+
const firstFile = { key: "markdown-a", path: "/tmp/A.md" };
|
|
91
|
+
const secondFile = { key: "markdown-b", path: "/tmp/B.md" };
|
|
92
|
+
const resolvedPaths = new Map([
|
|
93
|
+
[firstFile.path, "/tmp/first-location/A.md"],
|
|
94
|
+
[secondFile.path, "/tmp/B.md"],
|
|
95
|
+
]);
|
|
96
|
+
serverPathReadMock.mockImplementation(({ path }: { path: string }) => ({
|
|
97
|
+
isLoading: false,
|
|
98
|
+
error: null,
|
|
99
|
+
data: {
|
|
100
|
+
kind: "markdown",
|
|
101
|
+
resolvedPath: resolvedPaths.get(path) ?? path,
|
|
102
|
+
startLine: 1,
|
|
103
|
+
text: `# ${path}`,
|
|
104
|
+
truncated: false,
|
|
105
|
+
},
|
|
106
|
+
}));
|
|
107
|
+
const view = renderMarkdownPreview(firstFile);
|
|
108
|
+
const firstScrollContainer = screen.getByTestId("markdown-preview").parentElement as HTMLDivElement;
|
|
109
|
+
firstScrollContainer.scrollTop = 144;
|
|
110
|
+
fireEvent.scroll(firstScrollContainer);
|
|
111
|
+
|
|
112
|
+
view.rerender(
|
|
113
|
+
<ChatSessionWorkspaceFilePreview
|
|
114
|
+
file={{ ...secondFile, parentSessionKey: "session-1", viewMode: "preview" }}
|
|
115
|
+
sessionProjectRoot="/tmp"
|
|
116
|
+
sessionWorkingDir="/tmp"
|
|
117
|
+
showBreadcrumbs={false}
|
|
118
|
+
onFileOpen={vi.fn()}
|
|
119
|
+
/>,
|
|
120
|
+
);
|
|
121
|
+
const secondScrollContainer = screen.getByTestId("markdown-preview").parentElement as HTMLDivElement;
|
|
122
|
+
secondScrollContainer.scrollTop = 72;
|
|
123
|
+
fireEvent.scroll(secondScrollContainer);
|
|
124
|
+
|
|
125
|
+
// Server-path data can be stale while the query swaps back to the first
|
|
126
|
+
// tab. Its resolved path is display data, not tab identity.
|
|
127
|
+
resolvedPaths.set(firstFile.path, "/tmp/returned-location/A.md");
|
|
128
|
+
|
|
129
|
+
view.rerender(
|
|
130
|
+
<ChatSessionWorkspaceFilePreview
|
|
131
|
+
file={{ ...firstFile, parentSessionKey: "session-1", viewMode: "preview" }}
|
|
132
|
+
sessionProjectRoot="/tmp"
|
|
133
|
+
sessionWorkingDir="/tmp"
|
|
134
|
+
showBreadcrumbs={false}
|
|
135
|
+
onFileOpen={vi.fn()}
|
|
136
|
+
/>,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
expect((screen.getByTestId("markdown-preview").parentElement as HTMLDivElement).scrollTop).toBe(144);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("restores Markdown after returning from its source-view tab", () => {
|
|
143
|
+
const renderedFile = {
|
|
144
|
+
key: "markdown-rendered",
|
|
145
|
+
path: "/tmp/README.md",
|
|
146
|
+
previewViewer: "rendered" as const,
|
|
147
|
+
};
|
|
148
|
+
const sourceFile = {
|
|
149
|
+
key: "markdown-source",
|
|
150
|
+
path: "/tmp/README.md",
|
|
151
|
+
previewViewer: "source" as const,
|
|
152
|
+
};
|
|
153
|
+
const view = renderMarkdownPreview(renderedFile);
|
|
154
|
+
const scrollContainer = screen.getByTestId("markdown-preview").parentElement as HTMLDivElement;
|
|
155
|
+
scrollContainer.scrollTop = 144;
|
|
156
|
+
fireEvent.scroll(scrollContainer);
|
|
157
|
+
|
|
158
|
+
// The source-view switch replaces the rendered scroll surface before the
|
|
159
|
+
// rendered effect cleanup executes.
|
|
160
|
+
scrollContainer.scrollTop = 0;
|
|
161
|
+
view.rerender(
|
|
162
|
+
<ChatSessionWorkspaceFilePreview
|
|
163
|
+
file={{ ...sourceFile, parentSessionKey: "session-1", viewMode: "preview" }}
|
|
164
|
+
sessionProjectRoot="/tmp"
|
|
165
|
+
sessionWorkingDir="/tmp"
|
|
166
|
+
showBreadcrumbs={false}
|
|
167
|
+
onFileOpen={vi.fn()}
|
|
168
|
+
/>,
|
|
169
|
+
);
|
|
170
|
+
view.rerender(
|
|
171
|
+
<ChatSessionWorkspaceFilePreview
|
|
172
|
+
file={{ ...renderedFile, parentSessionKey: "session-1", viewMode: "preview" }}
|
|
173
|
+
sessionProjectRoot="/tmp"
|
|
174
|
+
sessionWorkingDir="/tmp"
|
|
175
|
+
showBreadcrumbs={false}
|
|
176
|
+
onFileOpen={vi.fn()}
|
|
177
|
+
/>,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
expect((screen.getByTestId("markdown-preview").parentElement as HTMLDivElement).scrollTop).toBe(144);
|
|
181
|
+
});
|
|
182
|
+
});
|
package/src/features/chat/features/workspace/components/__tests__/session-cron-job-content.test.tsx
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { createElement } from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
import type { CronJobView } from "@/shared/lib/api";
|
|
7
|
+
import { setLanguage } from "@/shared/lib/i18n";
|
|
8
|
+
import { SessionCronJobContent } from "@/features/chat/features/workspace/components/session-cron-job-content";
|
|
9
|
+
|
|
10
|
+
const mocks = vi.hoisted(() => ({
|
|
11
|
+
deleteJob: vi.fn(),
|
|
12
|
+
runJob: vi.fn(),
|
|
13
|
+
toggleJob: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock("@/features/cron", () => ({
|
|
17
|
+
CronJobDetailDialog: ({ job, open }: { job: CronJobView | null; open: boolean }) => (
|
|
18
|
+
open ? createElement("div", { role: "dialog" }, job?.payload.message) : null
|
|
19
|
+
),
|
|
20
|
+
useCronJobActions: () => ({
|
|
21
|
+
ConfirmDialog: () => createElement("div", { "data-testid": "cron-confirm" }),
|
|
22
|
+
deleteJob: mocks.deleteJob,
|
|
23
|
+
isPending: () => false,
|
|
24
|
+
runJob: mocks.runJob,
|
|
25
|
+
toggleJob: mocks.toggleJob,
|
|
26
|
+
}),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
function createJob(): CronJobView {
|
|
30
|
+
return {
|
|
31
|
+
id: "paint-daily",
|
|
32
|
+
name: "每日绘画",
|
|
33
|
+
enabled: true,
|
|
34
|
+
schedule: { kind: "cron", expr: "0 9 * * *", tz: "Asia/Shanghai" },
|
|
35
|
+
payload: {
|
|
36
|
+
kind: "agent_turn",
|
|
37
|
+
message: "请生成一幅拥有柔和晨光、细腻建筑纹理和远处山景的城市水彩画,并将结果保存到项目图库。",
|
|
38
|
+
agentId: "main",
|
|
39
|
+
sessionId: "session:paint",
|
|
40
|
+
},
|
|
41
|
+
state: {
|
|
42
|
+
nextRunAt: "2026-08-27T01:00:00.000Z",
|
|
43
|
+
lastRunAt: "2026-08-26T01:00:00.000Z",
|
|
44
|
+
lastStatus: "ok",
|
|
45
|
+
lastError: null,
|
|
46
|
+
},
|
|
47
|
+
createdAt: "2026-08-26T01:00:00.000Z",
|
|
48
|
+
updatedAt: "2026-08-26T01:00:00.000Z",
|
|
49
|
+
deleteAfterRun: false,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe("SessionCronJobContent", () => {
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
setLanguage("zh");
|
|
56
|
+
vi.clearAllMocks();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("keeps the task prompt as a two-line summary and exposes task controls", async () => {
|
|
60
|
+
const user = userEvent.setup();
|
|
61
|
+
const job = createJob();
|
|
62
|
+
render(createElement(SessionCronJobContent, { jobs: [job] }));
|
|
63
|
+
|
|
64
|
+
const summary = screen.getByText(job.payload.message);
|
|
65
|
+
expect(summary.className.split(" ")).toContain("line-clamp-2");
|
|
66
|
+
expect(screen.getAllByTestId("cron-confirm").length).toBe(1);
|
|
67
|
+
|
|
68
|
+
await user.click(screen.getByRole("switch", { name: "暂停" }));
|
|
69
|
+
expect(mocks.toggleJob).toHaveBeenCalledWith(job, false);
|
|
70
|
+
|
|
71
|
+
await user.click(screen.getByRole("button", { name: "更多操作 每日绘画" }));
|
|
72
|
+
await user.click(screen.getByRole("button", { name: "立即执行" }));
|
|
73
|
+
expect(mocks.runJob).toHaveBeenCalledWith(job);
|
|
74
|
+
|
|
75
|
+
await user.click(screen.getByRole("button", { name: "查看任务详情" }));
|
|
76
|
+
expect(screen.getByRole("dialog").textContent).toContain(job.payload.message);
|
|
77
|
+
});
|
|
78
|
+
});
|
package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo, useRef, type ReactNode, type RefObject } from 'react';
|
|
1
|
+
import { useMemo, useRef, type ReactNode, type RefObject, type UIEvent } from 'react';
|
|
2
2
|
import type { ChatFileOpenActionViewModel, ChatFileOperationBlockViewModel } from '@nextclaw/agent-chat-ui';
|
|
3
3
|
import { NextClawClientError } from '@nextclaw/client-sdk';
|
|
4
4
|
import { ChatMessageMarkdown, FileOperationCodeSurface } from '@nextclaw/agent-chat-ui';
|
|
@@ -24,6 +24,7 @@ import { buildWorkspaceFileBreadcrumb, resolveWorkspaceRelativePath } from '@/sh
|
|
|
24
24
|
import { cn } from '@/shared/lib/utils';
|
|
25
25
|
import { resolveWorkspaceFileViewer } from '@/features/chat/features/workspace/utils/chat-workspace-file-viewer.utils';
|
|
26
26
|
import { WorkspaceMarkdownOutline } from './workspace-markdown-outline';
|
|
27
|
+
import { useScrollRestoration } from '@/shared/hooks/use-scroll-restoration';
|
|
27
28
|
|
|
28
29
|
function inferPreviewKind(params: {
|
|
29
30
|
path: string;
|
|
@@ -42,6 +43,31 @@ function appendPreviewRefreshVersion(url: string, refreshVersion: number): strin
|
|
|
42
43
|
return `${url}${url.includes('?') ? '&' : '?'}refresh=${refreshVersion}`;
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
function createMarkdownScrollRestorationKey(params: {
|
|
47
|
+
contentUrl: string | null;
|
|
48
|
+
fileKey: string;
|
|
49
|
+
previewKind: 'text' | 'markdown' | 'binary';
|
|
50
|
+
previewText: string | null;
|
|
51
|
+
previewViewer: 'source' | 'rendered' | null;
|
|
52
|
+
refreshVersion: number;
|
|
53
|
+
}): string | null {
|
|
54
|
+
const {
|
|
55
|
+
contentUrl,
|
|
56
|
+
fileKey,
|
|
57
|
+
previewKind,
|
|
58
|
+
previewText,
|
|
59
|
+
previewViewer,
|
|
60
|
+
refreshVersion,
|
|
61
|
+
} = params;
|
|
62
|
+
if (previewKind !== 'markdown' || previewViewer === 'source' || !previewText || contentUrl) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
// `fileKey` is the workspace tab's stable identity. Do not include resolvedPath:
|
|
66
|
+
// React Query may temporarily expose a previous result while a different file
|
|
67
|
+
// loads, which would turn one tab into several restoration records.
|
|
68
|
+
return `workspace-preview:markdown:${fileKey}:${refreshVersion}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
45
71
|
function buildPreviewBlock(params: {
|
|
46
72
|
path: string;
|
|
47
73
|
text: string;
|
|
@@ -159,6 +185,7 @@ function WorkspacePreviewBody({
|
|
|
159
185
|
onFileOpen,
|
|
160
186
|
onHtmlContentHeightChange,
|
|
161
187
|
markdownScrollRef,
|
|
188
|
+
onMarkdownScroll,
|
|
162
189
|
previewBlock,
|
|
163
190
|
previewKind,
|
|
164
191
|
previewViewer,
|
|
@@ -176,6 +203,7 @@ function WorkspacePreviewBody({
|
|
|
176
203
|
onFileOpen: (action: ChatFileOpenActionViewModel) => void;
|
|
177
204
|
onHtmlContentHeightChange?: (height: number) => void;
|
|
178
205
|
markdownScrollRef: RefObject<HTMLDivElement>;
|
|
206
|
+
onMarkdownScroll: (event: UIEvent<HTMLDivElement>) => void;
|
|
179
207
|
previewBlock: ChatFileOperationBlockViewModel | null;
|
|
180
208
|
previewKind: 'text' | 'markdown' | 'binary';
|
|
181
209
|
previewViewer: 'source' | 'rendered' | null;
|
|
@@ -224,7 +252,11 @@ function WorkspacePreviewBody({
|
|
|
224
252
|
|
|
225
253
|
if (previewKind === 'markdown' && previewViewer !== 'source' && previewText) {
|
|
226
254
|
return (
|
|
227
|
-
<div
|
|
255
|
+
<div
|
|
256
|
+
ref={markdownScrollRef}
|
|
257
|
+
onScroll={onMarkdownScroll}
|
|
258
|
+
className="h-full overflow-auto custom-scrollbar px-5 py-4"
|
|
259
|
+
>
|
|
228
260
|
<ChatMessageMarkdown
|
|
229
261
|
text={previewText}
|
|
230
262
|
role="assistant"
|
|
@@ -338,6 +370,18 @@ export function ChatSessionWorkspaceFilePreview({
|
|
|
338
370
|
previewText,
|
|
339
371
|
]);
|
|
340
372
|
const isTextPreviewTruncated = !contentUrl && Boolean(previewQuery?.data?.truncated);
|
|
373
|
+
const markdownScrollRestoration = useScrollRestoration({
|
|
374
|
+
restorationKey: createMarkdownScrollRestorationKey({
|
|
375
|
+
contentUrl,
|
|
376
|
+
fileKey: file.key,
|
|
377
|
+
previewKind,
|
|
378
|
+
previewText,
|
|
379
|
+
previewViewer,
|
|
380
|
+
refreshVersion,
|
|
381
|
+
}),
|
|
382
|
+
scrollRef: markdownScrollRef,
|
|
383
|
+
});
|
|
384
|
+
const { onScroll: onMarkdownScroll } = markdownScrollRestoration;
|
|
341
385
|
const breadcrumbBasePath = sessionProjectRoot ?? sessionWorkingDir;
|
|
342
386
|
const excerptPath =
|
|
343
387
|
resolveWorkspaceRelativePath({
|
|
@@ -365,6 +409,7 @@ export function ChatSessionWorkspaceFilePreview({
|
|
|
365
409
|
onFileOpen={onFileOpen}
|
|
366
410
|
onHtmlContentHeightChange={onHtmlContentHeightChange}
|
|
367
411
|
markdownScrollRef={markdownScrollRef}
|
|
412
|
+
onMarkdownScroll={onMarkdownScroll}
|
|
368
413
|
previewBlock={previewBlock}
|
|
369
414
|
previewKind={previewKind}
|
|
370
415
|
previewViewer={previewViewer}
|
package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx
CHANGED
|
@@ -39,8 +39,11 @@ type ChatSessionWorkspacePanelContentProps = {
|
|
|
39
39
|
filePreviewRefreshVersion: number;
|
|
40
40
|
sessionKey: string | null;
|
|
41
41
|
sessionCronJobs: readonly CronJobView[];
|
|
42
|
+
sessionCronJobsError?: boolean;
|
|
43
|
+
sessionCronJobsLoading?: boolean;
|
|
42
44
|
sessionProjectRoot: string | null;
|
|
43
45
|
sessionWorkingDir: string | null;
|
|
46
|
+
onRetrySessionCronJobs?: () => void;
|
|
44
47
|
};
|
|
45
48
|
|
|
46
49
|
function WorkspaceOverviewEntry({
|
|
@@ -220,8 +223,11 @@ function WorkspaceSelectedContent({
|
|
|
220
223
|
filePreviewRefreshVersion,
|
|
221
224
|
sessionKey,
|
|
222
225
|
sessionCronJobs,
|
|
226
|
+
sessionCronJobsError = false,
|
|
227
|
+
sessionCronJobsLoading = false,
|
|
223
228
|
sessionProjectRoot,
|
|
224
229
|
sessionWorkingDir,
|
|
230
|
+
onRetrySessionCronJobs,
|
|
225
231
|
explorerControl,
|
|
226
232
|
}: ChatSessionWorkspacePanelContentProps & {
|
|
227
233
|
explorerControl?: { open: boolean; onToggle: () => void };
|
|
@@ -309,7 +315,14 @@ function WorkspaceSelectedContent({
|
|
|
309
315
|
);
|
|
310
316
|
}
|
|
311
317
|
|
|
312
|
-
return
|
|
318
|
+
return (
|
|
319
|
+
<SessionCronJobContent
|
|
320
|
+
jobs={sessionCronJobs}
|
|
321
|
+
isError={sessionCronJobsError}
|
|
322
|
+
isLoading={sessionCronJobsLoading}
|
|
323
|
+
onRetry={onRetrySessionCronJobs}
|
|
324
|
+
/>
|
|
325
|
+
);
|
|
313
326
|
}
|
|
314
327
|
|
|
315
328
|
export function ChatSessionWorkspacePanelContent({
|