@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
|
@@ -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
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
2
3
|
import type { ReactNode } from "react";
|
|
3
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
5
|
import type {
|
|
@@ -123,16 +124,21 @@ function renderWorkspaceFilePreview({
|
|
|
123
124
|
sessionWorkingDir = "/tmp",
|
|
124
125
|
showBreadcrumbs,
|
|
125
126
|
}: RenderWorkspaceFilePreviewOptions = {}) {
|
|
127
|
+
const queryClient = new QueryClient({
|
|
128
|
+
defaultOptions: { queries: { retry: false } },
|
|
129
|
+
});
|
|
126
130
|
const view = render(
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
<QueryClientProvider client={queryClient}>
|
|
132
|
+
<ChatSessionWorkspaceFilePreview
|
|
133
|
+
file={buildWorkspaceFile(file ?? {})}
|
|
134
|
+
sessionProjectRoot={sessionProjectRoot}
|
|
135
|
+
sessionWorkingDir={sessionWorkingDir}
|
|
136
|
+
refreshVersion={refreshVersion}
|
|
137
|
+
showBreadcrumbs={showBreadcrumbs}
|
|
138
|
+
onHtmlContentHeightChange={onHtmlContentHeightChange}
|
|
139
|
+
onFileOpen={onFileOpen}
|
|
140
|
+
/>
|
|
141
|
+
</QueryClientProvider>,
|
|
136
142
|
);
|
|
137
143
|
return { ...view, onFileOpen };
|
|
138
144
|
}
|
|
@@ -580,6 +586,7 @@ describe("ChatSessionWorkspaceFilePreview text rendering", () => {
|
|
|
580
586
|
|
|
581
587
|
expect(screen.queryByTestId("markdown-preview")).toBeNull();
|
|
582
588
|
expect(screen.getByTestId("file-code-surface")).toBeTruthy();
|
|
589
|
+
expect(screen.queryByRole("button", { name: t("chatWorkspaceMarkdownOutline") })).toBeNull();
|
|
583
590
|
});
|
|
584
591
|
|
|
585
592
|
it("keeps HTML files in the source preview when preview viewer is automatic", () => {
|
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
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import { createRef } from "react";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { WorkspaceMarkdownOutline } from "@/features/chat/features/workspace/components/workspace-markdown-outline";
|
|
5
|
+
import { t } from "@/shared/lib/i18n";
|
|
6
|
+
|
|
7
|
+
describe("WorkspaceMarkdownOutline", () => {
|
|
8
|
+
it("does not render an outline action when the document has no headings", async () => {
|
|
9
|
+
const scrollContainerRef = createRef<HTMLDivElement>();
|
|
10
|
+
render(
|
|
11
|
+
<>
|
|
12
|
+
<WorkspaceMarkdownOutline
|
|
13
|
+
contentKey="plain fixture"
|
|
14
|
+
documentKey="notes.md"
|
|
15
|
+
scrollContainerRef={scrollContainerRef}
|
|
16
|
+
/>
|
|
17
|
+
<div ref={scrollContainerRef}>Plain paragraph</div>
|
|
18
|
+
</>,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
await waitFor(() => {
|
|
22
|
+
expect(
|
|
23
|
+
screen.queryByRole("button", {
|
|
24
|
+
name: t("chatWorkspaceMarkdownOutline"),
|
|
25
|
+
}),
|
|
26
|
+
).toBeNull();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("preserves outline scroll state and instantly jumps to the selected rendered heading", async () => {
|
|
31
|
+
const scrollContainerRef = createRef<HTMLDivElement>();
|
|
32
|
+
const scrollTo = vi.fn();
|
|
33
|
+
const { rerender } = render(
|
|
34
|
+
<>
|
|
35
|
+
<WorkspaceMarkdownOutline
|
|
36
|
+
contentKey="outline fixture"
|
|
37
|
+
documentKey="README.md"
|
|
38
|
+
scrollContainerRef={scrollContainerRef}
|
|
39
|
+
/>
|
|
40
|
+
<div ref={scrollContainerRef}>
|
|
41
|
+
<h1>Overview</h1>
|
|
42
|
+
<h3>
|
|
43
|
+
Install <em>quickly</em>
|
|
44
|
+
</h3>
|
|
45
|
+
<h2>Overview</h2>
|
|
46
|
+
</div>
|
|
47
|
+
</>,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const outlineTrigger = await screen.findByRole("button", {
|
|
51
|
+
name: t("chatWorkspaceMarkdownOutline"),
|
|
52
|
+
});
|
|
53
|
+
const scrollContainer = scrollContainerRef.current!;
|
|
54
|
+
const headings = screen.getAllByRole("heading");
|
|
55
|
+
Object.defineProperty(scrollContainer, "scrollTop", {
|
|
56
|
+
configurable: true,
|
|
57
|
+
value: 40,
|
|
58
|
+
writable: true,
|
|
59
|
+
});
|
|
60
|
+
Object.defineProperty(scrollContainer, "scrollTo", {
|
|
61
|
+
configurable: true,
|
|
62
|
+
value: scrollTo,
|
|
63
|
+
});
|
|
64
|
+
vi.spyOn(scrollContainer, "getBoundingClientRect").mockReturnValue({
|
|
65
|
+
top: 100,
|
|
66
|
+
} as DOMRect);
|
|
67
|
+
vi.spyOn(headings[1]!, "getBoundingClientRect").mockReturnValue({
|
|
68
|
+
top: 420,
|
|
69
|
+
} as DOMRect);
|
|
70
|
+
|
|
71
|
+
fireEvent.click(outlineTrigger);
|
|
72
|
+
|
|
73
|
+
expect(
|
|
74
|
+
screen.getByTestId("workspace-markdown-outline-popover").className,
|
|
75
|
+
).toContain("w-[min(20rem,calc(100vw-1.5rem))]");
|
|
76
|
+
const outlineNavigation = screen.getByRole("navigation");
|
|
77
|
+
expect(outlineNavigation.className).toContain("max-h-80");
|
|
78
|
+
expect(screen.getAllByRole("button", { name: "Overview" })).toHaveLength(2);
|
|
79
|
+
const nestedHeading = screen.getByRole("button", {
|
|
80
|
+
name: "Install quickly",
|
|
81
|
+
});
|
|
82
|
+
expect(nestedHeading.style.paddingInlineStart).toBe("1.25rem");
|
|
83
|
+
|
|
84
|
+
outlineNavigation.scrollTop = 176;
|
|
85
|
+
fireEvent.click(outlineTrigger);
|
|
86
|
+
await waitFor(() => {
|
|
87
|
+
expect(
|
|
88
|
+
screen.queryByTestId("workspace-markdown-outline-popover"),
|
|
89
|
+
).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
fireEvent.click(outlineTrigger);
|
|
92
|
+
expect((await screen.findByRole("navigation")).scrollTop).toBe(176);
|
|
93
|
+
fireEvent.click(outlineTrigger);
|
|
94
|
+
rerender(
|
|
95
|
+
<>
|
|
96
|
+
<WorkspaceMarkdownOutline
|
|
97
|
+
contentKey="outline fixture"
|
|
98
|
+
documentKey="docs/other.md"
|
|
99
|
+
scrollContainerRef={scrollContainerRef}
|
|
100
|
+
/>
|
|
101
|
+
<div ref={scrollContainerRef}>
|
|
102
|
+
<h1>Overview</h1>
|
|
103
|
+
<h3>
|
|
104
|
+
Install <em>quickly</em>
|
|
105
|
+
</h3>
|
|
106
|
+
<h2>Overview</h2>
|
|
107
|
+
</div>
|
|
108
|
+
</>,
|
|
109
|
+
);
|
|
110
|
+
fireEvent.click(outlineTrigger);
|
|
111
|
+
expect((await screen.findByRole("navigation")).scrollTop).toBe(0);
|
|
112
|
+
|
|
113
|
+
fireEvent.click(screen.getByRole("button", { name: "Install quickly" }));
|
|
114
|
+
|
|
115
|
+
expect(scrollTo).toHaveBeenCalledWith({ top: 344 });
|
|
116
|
+
await waitFor(() => {
|
|
117
|
+
expect(
|
|
118
|
+
screen.queryByTestId("workspace-markdown-outline-popover"),
|
|
119
|
+
).toBeNull();
|
|
120
|
+
});
|
|
121
|
+
expect(document.activeElement).toBe(outlineTrigger);
|
|
122
|
+
expect(
|
|
123
|
+
document.querySelector(
|
|
124
|
+
'[role="tooltip"][data-state="delayed-open"], [role="tooltip"][data-state="instant-open"]',
|
|
125
|
+
),
|
|
126
|
+
).toBeNull();
|
|
127
|
+
});
|
|
128
|
+
});
|
package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx
CHANGED
|
@@ -78,10 +78,12 @@ export function ChatSessionWorkspaceFileBreadcrumbs({
|
|
|
78
78
|
breadcrumb,
|
|
79
79
|
leading,
|
|
80
80
|
onFileOpen,
|
|
81
|
+
trailing,
|
|
81
82
|
}: {
|
|
82
83
|
breadcrumb: WorkspaceFileBreadcrumbViewModel;
|
|
83
84
|
leading?: ReactNode;
|
|
84
85
|
onFileOpen: (action: ChatFileOpenActionViewModel) => void;
|
|
86
|
+
trailing?: ReactNode;
|
|
85
87
|
}) {
|
|
86
88
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
87
89
|
|
|
@@ -138,6 +140,7 @@ export function ChatSessionWorkspaceFileBreadcrumbs({
|
|
|
138
140
|
</div>
|
|
139
141
|
) : null}
|
|
140
142
|
</div>
|
|
143
|
+
{trailing ? <div className="flex shrink-0 items-center py-1.5 pr-2 pl-0.5">{trailing}</div> : null}
|
|
141
144
|
</div>
|
|
142
145
|
);
|
|
143
146
|
}
|
package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo, type ReactNode } 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';
|
|
@@ -23,6 +23,8 @@ import { t } from '@/shared/lib/i18n';
|
|
|
23
23
|
import { buildWorkspaceFileBreadcrumb, resolveWorkspaceRelativePath } from '@/shared/lib/session-project';
|
|
24
24
|
import { cn } from '@/shared/lib/utils';
|
|
25
25
|
import { resolveWorkspaceFileViewer } from '@/features/chat/features/workspace/utils/chat-workspace-file-viewer.utils';
|
|
26
|
+
import { WorkspaceMarkdownOutline } from './workspace-markdown-outline';
|
|
27
|
+
import { useScrollRestoration } from '@/shared/hooks/use-scroll-restoration';
|
|
26
28
|
|
|
27
29
|
function inferPreviewKind(params: {
|
|
28
30
|
path: string;
|
|
@@ -41,6 +43,31 @@ function appendPreviewRefreshVersion(url: string, refreshVersion: number): strin
|
|
|
41
43
|
return `${url}${url.includes('?') ? '&' : '?'}refresh=${refreshVersion}`;
|
|
42
44
|
}
|
|
43
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
|
+
|
|
44
71
|
function buildPreviewBlock(params: {
|
|
45
72
|
path: string;
|
|
46
73
|
text: string;
|
|
@@ -157,6 +184,8 @@ function WorkspacePreviewBody({
|
|
|
157
184
|
fileBasePath,
|
|
158
185
|
onFileOpen,
|
|
159
186
|
onHtmlContentHeightChange,
|
|
187
|
+
markdownScrollRef,
|
|
188
|
+
onMarkdownScroll,
|
|
160
189
|
previewBlock,
|
|
161
190
|
previewKind,
|
|
162
191
|
previewViewer,
|
|
@@ -173,6 +202,8 @@ function WorkspacePreviewBody({
|
|
|
173
202
|
fileBasePath: string | null;
|
|
174
203
|
onFileOpen: (action: ChatFileOpenActionViewModel) => void;
|
|
175
204
|
onHtmlContentHeightChange?: (height: number) => void;
|
|
205
|
+
markdownScrollRef: RefObject<HTMLDivElement>;
|
|
206
|
+
onMarkdownScroll: (event: UIEvent<HTMLDivElement>) => void;
|
|
176
207
|
previewBlock: ChatFileOperationBlockViewModel | null;
|
|
177
208
|
previewKind: 'text' | 'markdown' | 'binary';
|
|
178
209
|
previewViewer: 'source' | 'rendered' | null;
|
|
@@ -221,7 +252,11 @@ function WorkspacePreviewBody({
|
|
|
221
252
|
|
|
222
253
|
if (previewKind === 'markdown' && previewViewer !== 'source' && previewText) {
|
|
223
254
|
return (
|
|
224
|
-
<div
|
|
255
|
+
<div
|
|
256
|
+
ref={markdownScrollRef}
|
|
257
|
+
onScroll={onMarkdownScroll}
|
|
258
|
+
className="h-full overflow-auto custom-scrollbar px-5 py-4"
|
|
259
|
+
>
|
|
225
260
|
<ChatMessageMarkdown
|
|
226
261
|
text={previewText}
|
|
227
262
|
role="assistant"
|
|
@@ -266,6 +301,7 @@ export function ChatSessionWorkspaceFilePreview({
|
|
|
266
301
|
onFileOpen,
|
|
267
302
|
onTextExcerptAdd,
|
|
268
303
|
}: ChatSessionWorkspaceFilePreviewProps) {
|
|
304
|
+
const markdownScrollRef = useRef<HTMLDivElement>(null);
|
|
269
305
|
const isPreviewMode = file.viewMode === 'preview';
|
|
270
306
|
const suppliedContentUrl = file.contentUrl?.trim() || null;
|
|
271
307
|
const usesServerPath = isPreviewMode && !suppliedContentUrl;
|
|
@@ -334,6 +370,18 @@ export function ChatSessionWorkspaceFilePreview({
|
|
|
334
370
|
previewText,
|
|
335
371
|
]);
|
|
336
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;
|
|
337
385
|
const breadcrumbBasePath = sessionProjectRoot ?? sessionWorkingDir;
|
|
338
386
|
const excerptPath =
|
|
339
387
|
resolveWorkspaceRelativePath({
|
|
@@ -360,6 +408,8 @@ export function ChatSessionWorkspaceFilePreview({
|
|
|
360
408
|
fileBasePath={sessionWorkingDir}
|
|
361
409
|
onFileOpen={onFileOpen}
|
|
362
410
|
onHtmlContentHeightChange={onHtmlContentHeightChange}
|
|
411
|
+
markdownScrollRef={markdownScrollRef}
|
|
412
|
+
onMarkdownScroll={onMarkdownScroll}
|
|
363
413
|
previewBlock={previewBlock}
|
|
364
414
|
previewKind={previewKind}
|
|
365
415
|
previewViewer={previewViewer}
|
|
@@ -377,6 +427,19 @@ export function ChatSessionWorkspaceFilePreview({
|
|
|
377
427
|
breadcrumb={breadcrumb}
|
|
378
428
|
leading={breadcrumbLeading}
|
|
379
429
|
onFileOpen={onFileOpen}
|
|
430
|
+
trailing={
|
|
431
|
+
isPreviewMode &&
|
|
432
|
+
previewKind === 'markdown' &&
|
|
433
|
+
previewViewer !== 'source' &&
|
|
434
|
+
previewText &&
|
|
435
|
+
!contentUrl ? (
|
|
436
|
+
<WorkspaceMarkdownOutline
|
|
437
|
+
contentKey={previewText}
|
|
438
|
+
documentKey={resolvedPath}
|
|
439
|
+
scrollContainerRef={markdownScrollRef}
|
|
440
|
+
/>
|
|
441
|
+
) : undefined
|
|
442
|
+
}
|
|
380
443
|
/>
|
|
381
444
|
) : null}
|
|
382
445
|
|
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({
|
|
@@ -44,6 +44,9 @@ type ChatSessionWorkspacePanelProps = {
|
|
|
44
44
|
workspaceNavigationHistoryIndex?: number;
|
|
45
45
|
activePanelKind?: ChatWorkspacePanelKind | null;
|
|
46
46
|
sessionCronJobs?: readonly CronJobView[];
|
|
47
|
+
sessionCronJobsError?: boolean;
|
|
48
|
+
sessionCronJobsLoading?: boolean;
|
|
49
|
+
onRetrySessionCronJobs?: () => void;
|
|
47
50
|
sessionProjectRoot: string | null;
|
|
48
51
|
sessionWorkingDir: string | null;
|
|
49
52
|
workspacePanelWidth?: number;
|
|
@@ -62,6 +65,9 @@ export function ChatSessionWorkspacePanel({
|
|
|
62
65
|
workspaceNavigationHistoryIndex = 0,
|
|
63
66
|
activePanelKind,
|
|
64
67
|
sessionCronJobs = [],
|
|
68
|
+
sessionCronJobsError = false,
|
|
69
|
+
sessionCronJobsLoading = false,
|
|
70
|
+
onRetrySessionCronJobs,
|
|
65
71
|
sessionProjectRoot,
|
|
66
72
|
sessionWorkingDir,
|
|
67
73
|
workspacePanelWidth = CHAT_WORKSPACE_PANEL_DEFAULT_WIDTH,
|
|
@@ -218,6 +224,9 @@ export function ChatSessionWorkspacePanel({
|
|
|
218
224
|
filePreviewRefreshVersion={filePreviewRefreshVersion}
|
|
219
225
|
sessionKey={sessionKey}
|
|
220
226
|
sessionCronJobs={sessionCronJobs}
|
|
227
|
+
sessionCronJobsError={sessionCronJobsError}
|
|
228
|
+
sessionCronJobsLoading={sessionCronJobsLoading}
|
|
229
|
+
onRetrySessionCronJobs={onRetrySessionCronJobs}
|
|
221
230
|
sessionProjectRoot={sessionProjectRoot}
|
|
222
231
|
sessionWorkingDir={sessionWorkingDir}
|
|
223
232
|
/>
|