@nextclaw/ui 0.23.0 → 0.24.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/dist/assets/api-BqVIq0lF.js +13 -0
- package/dist/assets/{appearance-settings-page-C_AqxSF_.js → appearance-settings-page-CTWn1eAT.js} +1 -1
- package/dist/assets/{book-open-DYRapW_h.js → book-open-o2x7OLiO.js} +1 -1
- package/dist/assets/{channels-list-page-uR-M4Ls8.js → channels-list-page-Bzn-pzyJ.js} +2 -2
- package/dist/assets/{config-split-page-DJDp86Ne.js → config-split-page-CnRbXXlh.js} +1 -1
- package/dist/assets/{confirm-dialog-CHVBMpla.js → confirm-dialog-B_Hbcra2.js} +1 -1
- package/dist/assets/{createLucideIcon-CgM0rnsa.js → createLucideIcon-rwwpqyks.js} +1 -1
- package/dist/assets/{desktop-capabilities-page-phMuNq1B.js → desktop-capabilities-page-C2ayYQTr.js} +1 -1
- package/dist/assets/desktop-update-config-C13czpyL.js +1 -0
- package/dist/assets/{dist-N9zc5bkM.js → dist-BsF3hfOi.js} +1 -1
- package/dist/assets/{dist-BjYvUhPL.js → dist-aNN3iQxV.js} +1 -1
- package/dist/assets/{doc-browser-DwRLokCI.js → doc-browser-D8dIOo9E.js} +1 -1
- package/dist/assets/doc-browser-DcuOOtpP.js +1 -0
- package/dist/assets/{doc-browser-context-BDuypObS.js → doc-browser-context-Dhsit2Ma.js} +1 -1
- package/dist/assets/{extensions-CQKXTqTI.js → extensions-CD6xvKF_.js} +1 -1
- package/dist/assets/{form-actions-CzGmrPAv.js → form-actions-CC4Wz_lL.js} +1 -1
- package/dist/assets/index-BTsbdtv6.js +109 -0
- package/dist/assets/index-Bcfy2kTh.css +1 -0
- package/dist/assets/loader-circle-B6aFbB-8.js +1 -0
- package/dist/assets/mcp-marketplace-page-BtUAypA7.js +1 -0
- package/dist/assets/{mcp-marketplace-page-s9PTnSDT.js → mcp-marketplace-page-r2MmD9xO.js} +1 -1
- package/dist/assets/{model-config-page-CxnCUWRW.js → model-config-page-DboxoT-I.js} +1 -1
- package/dist/assets/{navigation-link-Ci4Z7Phi.js → navigation-link-C_6oj3zz.js} +1 -1
- package/dist/assets/plus-DRsq9Kvs.js +1 -0
- package/dist/assets/privacy-settings-page-C2sDBqhb.js +1 -0
- package/dist/assets/projects-BcPcx-uD.js +1 -0
- package/dist/assets/providers-config-page-CmUFl9RF.js +1 -0
- package/dist/assets/react-BjO9I4WZ.js +8 -0
- package/dist/assets/{refresh-cw-C-3_thgr.js → refresh-cw-DsT2kvXi.js} +1 -1
- package/dist/assets/remote-Ci6x91ES.js +1 -0
- package/dist/assets/{rotate-cw-DSFTq_yu.js → rotate-cw-BCAs6c3R.js} +1 -1
- package/dist/assets/runtime-config-page-cQKNeAjL.js +1 -0
- package/dist/assets/{save-CpPr78_S.js → save-CadRMTMQ.js} +1 -1
- package/dist/assets/{search-msqk2-rd.js → search-B6tB4fzB.js} +1 -1
- package/dist/assets/{search-config-page-D3_qNXbB.js → search-config-page-C8lOIkxn.js} +1 -1
- package/dist/assets/{secrets-config-page-Be4P1E8o.js → secrets-config-page-8clzAag2.js} +2 -2
- package/dist/assets/security-config-DPbM6IVT.js +1 -0
- package/dist/assets/{select-DPKCWajZ.js → select-CO9S_C2C.js} +1 -1
- package/dist/assets/skeleton-IHILNhIW.js +1 -0
- package/dist/assets/{tag-chip-Dc_NcCEe.js → tag-chip-BZKk0UkK.js} +1 -1
- package/dist/assets/x-BuvHIb1d.js +1 -0
- package/dist/index.html +21 -21
- package/package.json +6 -6
- package/src/app/configs/__tests__/app-navigation.config.test.ts +20 -0
- package/src/app/configs/app-navigation.config.ts +10 -0
- package/src/app/index.tsx +8 -1
- package/src/features/apps/components/__tests__/app-packages-panel.test.tsx +362 -241
- package/src/features/apps/components/app-document-access-section.tsx +187 -0
- package/src/features/apps/components/app-package-card.tsx +150 -275
- package/src/features/apps/hooks/use-app-packages.ts +91 -35
- package/src/features/chat/components/conversation/__tests__/chat-conversation-workspace-section.test.tsx +29 -0
- package/src/features/chat/components/conversation/chat-conversation-workspace-section.tsx +4 -2
- package/src/features/chat/components/layout/__tests__/chat-page-shell.test.tsx +21 -0
- package/src/features/chat/components/layout/chat-page-shell.tsx +19 -4
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +1 -1
- package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +0 -1
- package/src/features/chat/features/conversation/utils/__tests__/session-message-composer.utils.test.ts +11 -4
- package/src/features/chat/features/conversation/utils/__tests__/session-queued-input.utils.test.ts +0 -1
- package/src/features/chat/features/conversation/utils/session-message-composer.utils.ts +10 -11
- package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +1 -0
- package/src/features/chat/features/message/hooks/use-chat-message-actions.tsx +0 -2
- package/src/features/chat/features/runtime/utils/chat-runtime.utils.ts +0 -14
- package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +70 -22
- package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +79 -32
- package/src/features/chat/features/session/utils/__tests__/chat-sidebar-session-groups.utils.test.ts +2 -0
- package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +0 -6
- package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +2 -0
- package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +1 -0
- package/src/features/chat/index.ts +5 -1
- package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +27 -0
- package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +2 -14
- package/src/features/chat/types/chat-run.types.ts +0 -1
- package/src/features/projects/AGENTS.md +7 -0
- package/src/features/projects/components/project-agreement.tsx +92 -0
- package/src/features/projects/components/project-artifacts.test.tsx +77 -0
- package/src/features/projects/components/project-artifacts.tsx +188 -0
- package/src/features/projects/components/project-overview.tsx +81 -0
- package/src/features/projects/components/project-requests.tsx +57 -0
- package/src/features/projects/components/project-runs.tsx +60 -0
- package/src/features/projects/components/project-section.tsx +60 -0
- package/src/features/projects/components/project-skills.tsx +53 -0
- package/src/features/projects/components/work/project-work-item-drawer.test.tsx +65 -0
- package/src/features/projects/components/work/project-work-item-drawer.tsx +367 -0
- package/src/features/projects/components/work/project-work-items.test.tsx +98 -0
- package/src/features/projects/components/work/project-work-items.tsx +199 -0
- package/src/features/projects/components/work/project-work-state-settings.tsx +285 -0
- package/src/features/projects/hooks/use-project-observation.test.tsx +93 -0
- package/src/features/projects/hooks/use-project-observation.ts +115 -0
- package/src/features/projects/hooks/use-project-work.test.tsx +58 -0
- package/src/features/projects/hooks/use-project-work.ts +215 -0
- package/src/features/projects/index.ts +2 -0
- package/src/features/projects/pages/project-home-page.tsx +234 -0
- package/src/features/projects/pages/projects-page.test.tsx +137 -0
- package/src/features/projects/presenters/project-home.presenter.test.ts +74 -0
- package/src/features/projects/presenters/project-home.presenter.ts +55 -0
- package/src/features/projects/utils/project-artifact-view.utils.test.ts +63 -0
- package/src/features/projects/utils/project-artifact-view.utils.ts +62 -0
- package/src/features/projects/utils/project-request-response.utils.test.ts +44 -0
- package/src/features/projects/utils/project-request-response.utils.ts +39 -0
- package/src/shared/lib/i18n/index.ts +6 -2
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +16 -1
- package/src/shared/lib/i18n/locales/en-US/projects.json +149 -0
- package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +16 -1
- package/src/shared/lib/i18n/locales/zh-CN/projects.json +149 -0
- package/src/shared/lib/ui-document-title/__tests__/ui-document-title.test.ts +6 -0
- package/src/shared/lib/ui-document-title/index.ts +1 -0
- package/dist/assets/api-n96IfCKb.js +0 -13
- package/dist/assets/desktop-update-config-C77hkbor.js +0 -1
- package/dist/assets/doc-browser-JlXVLNRe.js +0 -1
- package/dist/assets/index-CLw9DuCJ.js +0 -109
- package/dist/assets/index-CwCTiMM9.css +0 -1
- package/dist/assets/loader-circle-D9etMT9f.js +0 -1
- package/dist/assets/mcp-marketplace-page-CbWOamyW.js +0 -1
- package/dist/assets/plus-DKGD04AO.js +0 -1
- package/dist/assets/privacy-settings-page-D8HZHHjL.js +0 -1
- package/dist/assets/providers-config-page-C0GIoGxm.js +0 -1
- package/dist/assets/react-CVFx225w.js +0 -8
- package/dist/assets/remote-DvTCSAxQ.js +0 -1
- package/dist/assets/runtime-config-page-VNJZui5f.js +0 -1
- package/dist/assets/security-config-BtcpKIhQ.js +0 -1
- package/dist/assets/skeleton-BsR5rwB7.js +0 -1
- package/dist/assets/x-Dp5GAFmP.js +0 -1
|
@@ -1,37 +1,51 @@
|
|
|
1
|
-
import { useEffect } from
|
|
2
|
-
import { useMutation, useQuery, useQueryClient } from
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import type {
|
|
4
|
+
AppPackageOperationStatus,
|
|
5
|
+
AppPackageOperationView,
|
|
6
|
+
} from "@nextclaw/client-sdk";
|
|
7
|
+
import { nextclawClient } from "@/shared/lib/api";
|
|
8
|
+
import type { AppPackageOperationSettlementManager } from "@/features/apps/managers/app-package-operation-settlement.manager";
|
|
9
|
+
import { APP_DATA_QUERY_KEY } from "@/features/app-data";
|
|
7
10
|
|
|
8
|
-
const APP_PACKAGES_QUERY_KEY = [
|
|
9
|
-
const PANEL_APPS_QUERY_KEY = [
|
|
10
|
-
const SERVICE_APPS_QUERY_KEY = [
|
|
11
|
-
const SERVICE_ACTIONS_QUERY_KEY = [
|
|
12
|
-
const APP_PACKAGE_OPERATIONS_QUERY_KEY = [
|
|
11
|
+
const APP_PACKAGES_QUERY_KEY = ["app-packages"] as const;
|
|
12
|
+
const PANEL_APPS_QUERY_KEY = ["panel-apps"] as const;
|
|
13
|
+
const SERVICE_APPS_QUERY_KEY = ["service-apps"] as const;
|
|
14
|
+
const SERVICE_ACTIONS_QUERY_KEY = ["service-actions"] as const;
|
|
15
|
+
const APP_PACKAGE_OPERATIONS_QUERY_KEY = ["app-package-operations"] as const;
|
|
13
16
|
|
|
14
17
|
const ACTIVE_OPERATION_STATUSES = new Set<AppPackageOperationStatus>([
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
"queued",
|
|
19
|
+
"resolving",
|
|
20
|
+
"downloading",
|
|
21
|
+
"verifying",
|
|
22
|
+
"installing",
|
|
23
|
+
"finalizing",
|
|
21
24
|
]);
|
|
22
25
|
|
|
23
26
|
export type AppPackageMutationInput =
|
|
24
|
-
| { action:
|
|
25
|
-
| { action:
|
|
26
|
-
| { action:
|
|
27
|
-
| { action:
|
|
28
|
-
| { action:
|
|
29
|
-
| { action:
|
|
27
|
+
| { action: "enable"; appId: string }
|
|
28
|
+
| { action: "disable"; appId: string }
|
|
29
|
+
| { action: "install"; source: string; registryUrl?: string }
|
|
30
|
+
| { action: "update"; appId: string }
|
|
31
|
+
| { action: "rollback"; appId: string; version: string }
|
|
32
|
+
| { action: "uninstall"; appId: string; purgeData?: boolean };
|
|
33
|
+
|
|
34
|
+
export type AppDocumentAccessMutationInput =
|
|
35
|
+
| {
|
|
36
|
+
action: "grant";
|
|
37
|
+
appId: string;
|
|
38
|
+
scopeId: string;
|
|
39
|
+
directoryPath: string;
|
|
40
|
+
mode: "read" | "read-write";
|
|
41
|
+
}
|
|
42
|
+
| { action: "revoke"; appId: string; scopeId: string };
|
|
30
43
|
|
|
31
44
|
export function useAppPackages() {
|
|
32
45
|
return useQuery({
|
|
33
46
|
queryKey: APP_PACKAGES_QUERY_KEY,
|
|
34
|
-
queryFn: () =>
|
|
47
|
+
queryFn: () =>
|
|
48
|
+
nextclawClient.appPackages.list({ includeStorageUsage: false }),
|
|
35
49
|
staleTime: 0,
|
|
36
50
|
});
|
|
37
51
|
}
|
|
@@ -42,13 +56,17 @@ export function useAppPackageOperations() {
|
|
|
42
56
|
queryFn: () => nextclawClient.appPackages.listOperations(),
|
|
43
57
|
refetchInterval: (query) => {
|
|
44
58
|
const entries = query.state.data?.entries ?? [];
|
|
45
|
-
return entries.some((entry) => isAppPackageOperationActive(entry.status))
|
|
59
|
+
return entries.some((entry) => isAppPackageOperationActive(entry.status))
|
|
60
|
+
? 500
|
|
61
|
+
: false;
|
|
46
62
|
},
|
|
47
63
|
staleTime: 0,
|
|
48
64
|
});
|
|
49
65
|
}
|
|
50
66
|
|
|
51
|
-
export function isAppPackageOperationActive(
|
|
67
|
+
export function isAppPackageOperationActive(
|
|
68
|
+
status: AppPackageOperationStatus,
|
|
69
|
+
): boolean {
|
|
52
70
|
return ACTIVE_OPERATION_STATUSES.has(status);
|
|
53
71
|
}
|
|
54
72
|
|
|
@@ -77,7 +95,14 @@ export function useAppPackageOperationSettlement({
|
|
|
77
95
|
refetchPanels,
|
|
78
96
|
refetchAppData,
|
|
79
97
|
});
|
|
80
|
-
}, [
|
|
98
|
+
}, [
|
|
99
|
+
isLoaded,
|
|
100
|
+
operations,
|
|
101
|
+
refetchPackages,
|
|
102
|
+
refetchPanels,
|
|
103
|
+
refetchAppData,
|
|
104
|
+
settlementManager,
|
|
105
|
+
]);
|
|
81
106
|
}
|
|
82
107
|
|
|
83
108
|
export function useAppPackageMutation() {
|
|
@@ -85,20 +110,23 @@ export function useAppPackageMutation() {
|
|
|
85
110
|
return useMutation({
|
|
86
111
|
mutationFn: async (input: AppPackageMutationInput) => {
|
|
87
112
|
switch (input.action) {
|
|
88
|
-
case
|
|
113
|
+
case "enable":
|
|
89
114
|
return await nextclawClient.appPackages.enable(input.appId);
|
|
90
|
-
case
|
|
115
|
+
case "disable":
|
|
91
116
|
return await nextclawClient.appPackages.disable(input.appId);
|
|
92
|
-
case
|
|
117
|
+
case "install":
|
|
93
118
|
return await nextclawClient.appPackages.startInstall({
|
|
94
119
|
source: input.source,
|
|
95
120
|
registryUrl: input.registryUrl,
|
|
96
121
|
});
|
|
97
|
-
case
|
|
122
|
+
case "update":
|
|
98
123
|
return await nextclawClient.appPackages.startUpdate(input.appId);
|
|
99
|
-
case
|
|
100
|
-
return await nextclawClient.appPackages.startRollback(
|
|
101
|
-
|
|
124
|
+
case "rollback":
|
|
125
|
+
return await nextclawClient.appPackages.startRollback(
|
|
126
|
+
input.appId,
|
|
127
|
+
input.version,
|
|
128
|
+
);
|
|
129
|
+
case "uninstall":
|
|
102
130
|
return await nextclawClient.appPackages.startUninstall(
|
|
103
131
|
input.appId,
|
|
104
132
|
input.purgeData ?? false,
|
|
@@ -111,9 +139,37 @@ export function useAppPackageMutation() {
|
|
|
111
139
|
queryClient.invalidateQueries({ queryKey: PANEL_APPS_QUERY_KEY }),
|
|
112
140
|
queryClient.invalidateQueries({ queryKey: SERVICE_APPS_QUERY_KEY }),
|
|
113
141
|
queryClient.invalidateQueries({ queryKey: SERVICE_ACTIONS_QUERY_KEY }),
|
|
114
|
-
queryClient.invalidateQueries({
|
|
142
|
+
queryClient.invalidateQueries({
|
|
143
|
+
queryKey: APP_PACKAGE_OPERATIONS_QUERY_KEY,
|
|
144
|
+
}),
|
|
115
145
|
queryClient.invalidateQueries({ queryKey: APP_DATA_QUERY_KEY }),
|
|
116
146
|
]);
|
|
117
147
|
},
|
|
118
148
|
});
|
|
119
149
|
}
|
|
150
|
+
|
|
151
|
+
export function useAppDocumentAccessMutation() {
|
|
152
|
+
const queryClient = useQueryClient();
|
|
153
|
+
return useMutation({
|
|
154
|
+
mutationFn: async (input: AppDocumentAccessMutationInput) => {
|
|
155
|
+
if (input.action === "revoke") {
|
|
156
|
+
return await nextclawClient.appPackages.revokeDocumentAccess(
|
|
157
|
+
input.appId,
|
|
158
|
+
input.scopeId,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return await nextclawClient.appPackages.grantDocumentAccess(input.appId, {
|
|
162
|
+
scopeId: input.scopeId,
|
|
163
|
+
directoryPath: input.directoryPath,
|
|
164
|
+
mode: input.mode,
|
|
165
|
+
});
|
|
166
|
+
},
|
|
167
|
+
onSuccess: async () => {
|
|
168
|
+
await Promise.all([
|
|
169
|
+
queryClient.invalidateQueries({ queryKey: APP_PACKAGES_QUERY_KEY }),
|
|
170
|
+
queryClient.invalidateQueries({ queryKey: SERVICE_APPS_QUERY_KEY }),
|
|
171
|
+
queryClient.invalidateQueries({ queryKey: SERVICE_ACTIONS_QUERY_KEY }),
|
|
172
|
+
]);
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
}
|
|
@@ -183,6 +183,35 @@ describe("ChatConversationWorkspaceSection", () => {
|
|
|
183
183
|
);
|
|
184
184
|
});
|
|
185
185
|
|
|
186
|
+
it("uses an explicit project root for a project-owned file preview", () => {
|
|
187
|
+
useChatThreadStore.getState().setSnapshot({
|
|
188
|
+
workspacePanelParentKey: null,
|
|
189
|
+
activeWorkspacePanelKind: "file",
|
|
190
|
+
workspaceFileTabs: [{
|
|
191
|
+
key: "project-skill",
|
|
192
|
+
parentSessionKey: null,
|
|
193
|
+
path: "/Users/peiwang/Projects/nextbot/.agents/skills/project/SKILL.md",
|
|
194
|
+
viewMode: "preview",
|
|
195
|
+
}],
|
|
196
|
+
activeWorkspaceFileKey: "project-skill",
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
render(
|
|
200
|
+
<ChatConversationWorkspaceSection
|
|
201
|
+
layoutMode="desktop"
|
|
202
|
+
sessionKey={null}
|
|
203
|
+
projectRoot="/Users/peiwang/Projects/nextbot"
|
|
204
|
+
/>,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
expect(mocks.panelProps).toHaveBeenCalledWith(
|
|
208
|
+
expect.objectContaining({
|
|
209
|
+
sessionProjectRoot: "/Users/peiwang/Projects/nextbot",
|
|
210
|
+
sessionWorkingDir: "/Users/peiwang/Projects/nextbot",
|
|
211
|
+
}),
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
|
|
186
215
|
it("keeps the workspace panel mounted while a root draft materializes", () => {
|
|
187
216
|
useChatSessionListStore.getState().setSnapshot({ selectedSessionKey: null });
|
|
188
217
|
useChatThreadStore.getState().setSnapshot({
|
|
@@ -8,11 +8,13 @@ import { normalizeSessionProjectRootValue } from "@/shared/lib/session-project";
|
|
|
8
8
|
type ChatConversationWorkspaceSectionProps = {
|
|
9
9
|
layoutMode: "desktop" | "mobile";
|
|
10
10
|
sessionKey: string | null;
|
|
11
|
+
projectRoot?: string | null;
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
export function ChatConversationWorkspaceSection({
|
|
14
15
|
layoutMode,
|
|
15
16
|
sessionKey,
|
|
17
|
+
projectRoot,
|
|
16
18
|
}: ChatConversationWorkspaceSectionProps) {
|
|
17
19
|
const snapshot = useChatThreadStore((state) => state.snapshot);
|
|
18
20
|
const defaultWorkspacePath = useChatQueryStore((state) =>
|
|
@@ -21,9 +23,9 @@ export function ChatConversationWorkspaceSection({
|
|
|
21
23
|
),
|
|
22
24
|
);
|
|
23
25
|
const selectedSession = useNcpChatSelectedSession(sessionKey);
|
|
24
|
-
const draftProjectRoot = sessionKey
|
|
26
|
+
const draftProjectRoot = projectRoot ?? (sessionKey
|
|
25
27
|
? null
|
|
26
|
-
: snapshot.draftProjectRoot ?? defaultWorkspacePath;
|
|
28
|
+
: snapshot.draftProjectRoot ?? defaultWorkspacePath);
|
|
27
29
|
const {
|
|
28
30
|
childSessionTabs,
|
|
29
31
|
activeSideChatDraft,
|
|
@@ -38,6 +38,10 @@ vi.mock("@/features/marketplace", () => ({
|
|
|
38
38
|
MarketplacePage: () => <div>Marketplace</div>,
|
|
39
39
|
}));
|
|
40
40
|
|
|
41
|
+
vi.mock("@/features/projects", () => ({
|
|
42
|
+
ProjectsPage: () => <div>Project Home</div>,
|
|
43
|
+
}));
|
|
44
|
+
|
|
41
45
|
describe("ChatPageLayout", () => {
|
|
42
46
|
it("uses the same canvas width for management pages", () => {
|
|
43
47
|
const agentsView = render(
|
|
@@ -81,4 +85,21 @@ describe("ChatPageLayout", () => {
|
|
|
81
85
|
expect(screen.queryByTestId("chat-conversation-panel")).toBeNull();
|
|
82
86
|
expect(screen.getByTestId("confirm-dialog")).toBeTruthy();
|
|
83
87
|
});
|
|
88
|
+
|
|
89
|
+
it("keeps the chat sidebar while rendering the project home in the workspace", async () => {
|
|
90
|
+
useViewportLayoutMock.mockReturnValue({
|
|
91
|
+
mode: "desktop",
|
|
92
|
+
isMobile: false,
|
|
93
|
+
isDesktop: true,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
render(<ChatPageLayout view="projects" confirmDialog={<div />} />);
|
|
97
|
+
|
|
98
|
+
expect(screen.getByTestId("desktop-chat-sidebar")).toBeTruthy();
|
|
99
|
+
const projectHome = await screen.findByText("Project Home");
|
|
100
|
+
expect(projectHome).toBeTruthy();
|
|
101
|
+
expect(projectHome.parentElement?.className).toContain("flex");
|
|
102
|
+
expect(projectHome.parentElement?.className).toContain("overflow-hidden");
|
|
103
|
+
expect(screen.queryByTestId("chat-conversation-panel")).toBeNull();
|
|
104
|
+
});
|
|
84
105
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useLayoutEffect } from "react";
|
|
1
|
+
import { lazy, Suspense, useLayoutEffect } from "react";
|
|
2
2
|
import { ChatSidebar } from "@/features/chat/components/layout/chat-sidebar";
|
|
3
3
|
import { ChatConversationPanel } from "@/features/chat/components/conversation/chat-conversation-panel";
|
|
4
4
|
import { AgentsPage } from "@/features/agents";
|
|
@@ -9,11 +9,22 @@ import { ChatMobileShell } from "@/platforms/mobile";
|
|
|
9
9
|
import { InboxPage } from "@/features/inbox";
|
|
10
10
|
import { PanelAppMainPage } from "@/features/panel-apps";
|
|
11
11
|
import { useScrollRestoration } from "@/shared/hooks/use-scroll-restoration";
|
|
12
|
-
|
|
12
|
+
const ProjectsPage = lazy(async () => ({
|
|
13
|
+
default: (await import("@/features/projects")).ProjectsPage,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
export type MainPanelView = "chat" | "cron" | "skills" | "agents" | "inbox" | "panel-app" | "projects";
|
|
13
17
|
export type ChatPageProps = {
|
|
14
18
|
view: MainPanelView;
|
|
15
19
|
};
|
|
16
20
|
const MANAGEMENT_PAGE_CANVAS_WIDTH_CLASS = "max-w-[min(1180px,100%)]";
|
|
21
|
+
|
|
22
|
+
function resolveManagementWorkspaceClass(view: MainPanelView): string {
|
|
23
|
+
return view === "projects"
|
|
24
|
+
? "relative flex flex-1 min-h-0 overflow-hidden bg-background"
|
|
25
|
+
: "flex-1 min-h-0 overflow-hidden bg-background";
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
type UseChatSessionSyncParams = {
|
|
18
29
|
routeSessionKey: string | null;
|
|
19
30
|
syncRouteSessionSelection: (routeSessionKey: string | null) => void;
|
|
@@ -55,9 +66,13 @@ export function ChatPageLayout({ view, confirmDialog }: ChatPageLayoutProps) {
|
|
|
55
66
|
) : (
|
|
56
67
|
<section
|
|
57
68
|
data-theme-surface="workspace"
|
|
58
|
-
className=
|
|
69
|
+
className={resolveManagementWorkspaceClass(view)}
|
|
59
70
|
>
|
|
60
|
-
{view === "
|
|
71
|
+
{view === "projects" ? (
|
|
72
|
+
<Suspense fallback={<div className="h-full animate-pulse bg-card/30" />}>
|
|
73
|
+
<ProjectsPage />
|
|
74
|
+
</Suspense>
|
|
75
|
+
) : view === "inbox" ? (
|
|
61
76
|
<div className={`mx-auto flex h-full min-h-0 w-full flex-col py-4 sm:px-6 sm:py-5 ${MANAGEMENT_PAGE_CANVAS_WIDTH_CLASS}`}>
|
|
62
77
|
<InboxPage />
|
|
63
78
|
</div>
|
|
@@ -240,7 +240,7 @@ describe('useSessionConversationController backend run queue', () => {
|
|
|
240
240
|
parts: [{ type: 'text', text: 'Update this session title' }],
|
|
241
241
|
}),
|
|
242
242
|
});
|
|
243
|
-
expect(envelope?.metadata).not.toHaveProperty('
|
|
243
|
+
expect(envelope?.metadata).not.toHaveProperty('ui_inline_tokens');
|
|
244
244
|
expect(params.resetComposer).not.toHaveBeenCalled();
|
|
245
245
|
expect(params.restoreComposer).not.toHaveBeenCalled();
|
|
246
246
|
});
|
package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts
CHANGED
|
@@ -175,7 +175,6 @@ function buildSubmissionDraft(params: BuildSubmissionDraftParams): SubmissionDra
|
|
|
175
175
|
: sessionKey
|
|
176
176
|
? inputQuery.selectedSession?.projectRoot ?? null
|
|
177
177
|
: inputSnapshot.pendingProjectRoot,
|
|
178
|
-
requestedSkills: [...composerSnapshot.selectedSkills],
|
|
179
178
|
skillRecords: inputQuery.skillRecords.filter((record) =>
|
|
180
179
|
composerSnapshot.selectedSkills.includes(record.ref)
|
|
181
180
|
),
|
|
@@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest';
|
|
|
3
3
|
import { buildSessionMessageComposerSnapshot } from '@/features/chat/features/conversation/utils/session-message-composer.utils';
|
|
4
4
|
|
|
5
5
|
describe('buildSessionMessageComposerSnapshot', () => {
|
|
6
|
-
it('round-trips text, attachment and
|
|
6
|
+
it('round-trips text, attachment and skill-token state into the shared composer', () => {
|
|
7
7
|
const snapshot = buildSessionMessageComposerSnapshot({
|
|
8
8
|
attachmentIdPrefix: 'edit-user-1',
|
|
9
9
|
availableSkills: [{ ref: 'skill://review', name: 'Review' }],
|
|
@@ -13,15 +13,22 @@ describe('buildSessionMessageComposerSnapshot', () => {
|
|
|
13
13
|
role: 'user',
|
|
14
14
|
status: 'final',
|
|
15
15
|
timestamp: '2026-08-08T10:00:00.000Z',
|
|
16
|
-
metadata: {
|
|
16
|
+
metadata: {
|
|
17
|
+
ui_inline_tokens: [{
|
|
18
|
+
kind: 'skill',
|
|
19
|
+
key: 'skill://review',
|
|
20
|
+
label: 'Review',
|
|
21
|
+
rawText: '$Review',
|
|
22
|
+
}],
|
|
23
|
+
},
|
|
17
24
|
parts: [
|
|
18
|
-
{ type: 'text', text: 'check this' },
|
|
25
|
+
{ type: 'text', text: 'check $Review this' },
|
|
19
26
|
{ type: 'file', name: 'notes.txt', mimeType: 'text/plain', sizeBytes: 12 },
|
|
20
27
|
],
|
|
21
28
|
},
|
|
22
29
|
});
|
|
23
30
|
|
|
24
|
-
expect(snapshot.text).
|
|
31
|
+
expect(snapshot.text).toBe('check this');
|
|
25
32
|
expect(snapshot.selectedSkills).toEqual(['skill://review']);
|
|
26
33
|
expect(snapshot.skillRecords).toEqual([{ ref: 'skill://review', name: 'Review' }]);
|
|
27
34
|
expect(snapshot.attachments).toEqual([
|
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
resolveInlineTokensForText,
|
|
15
15
|
type ChatInlineTokenSource,
|
|
16
16
|
} from '@/features/chat/features/input/utils/chat-inline-token.utils';
|
|
17
|
-
import { normalizeRequestedSkills } from '@/features/chat/features/runtime/utils/chat-runtime.utils';
|
|
18
17
|
import type { SessionConversationComposerState } from '@/features/chat/features/conversation/hooks/use-session-conversation-input-state';
|
|
19
18
|
|
|
20
19
|
export type SessionMessageComposerSnapshot = SessionConversationComposerState & {
|
|
@@ -55,15 +54,6 @@ function buildTextNodes(
|
|
|
55
54
|
return nodes;
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
function readRequestedSkills(metadata: Record<string, unknown>): string[] {
|
|
59
|
-
const raw = metadata.requested_skill_refs;
|
|
60
|
-
return normalizeRequestedSkills(
|
|
61
|
-
Array.isArray(raw)
|
|
62
|
-
? raw.filter((value): value is string => typeof value === 'string')
|
|
63
|
-
: undefined,
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
57
|
export function buildSessionMessageComposerSnapshot(params: {
|
|
68
58
|
readonly attachmentIdPrefix: string;
|
|
69
59
|
readonly availableSkills: readonly { ref: string; name: string }[];
|
|
@@ -107,8 +97,17 @@ export function buildSessionMessageComposerSnapshot(params: {
|
|
|
107
97
|
});
|
|
108
98
|
|
|
109
99
|
const normalizedNodes = normalizeChatComposerNodes(nodes);
|
|
110
|
-
const
|
|
100
|
+
const skillTokens = metadataTokens.filter(
|
|
101
|
+
(token): token is Extract<ChatInlineTokenSource, { kind: 'skill' }> =>
|
|
102
|
+
token.kind === 'skill',
|
|
103
|
+
);
|
|
104
|
+
const selectedSkills = [...new Set(skillTokens.map((token) => token.ref))];
|
|
111
105
|
const skillNameByRef = new Map(availableSkills.map(({ ref, name }) => [ref, name]));
|
|
106
|
+
for (const token of skillTokens) {
|
|
107
|
+
if (!skillNameByRef.has(token.ref)) {
|
|
108
|
+
skillNameByRef.set(token.ref, token.name);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
112
111
|
return {
|
|
113
112
|
text: deriveChatComposerDraft(normalizedNodes),
|
|
114
113
|
nodes: normalizedNodes,
|
|
@@ -41,7 +41,6 @@ function buildEditedMessageMetadata(
|
|
|
41
41
|
snapshot: SessionMessageComposerSnapshot,
|
|
42
42
|
): Record<string, unknown> | undefined {
|
|
43
43
|
const metadata = { ...message.metadata };
|
|
44
|
-
delete metadata.requested_skill_refs;
|
|
45
44
|
delete metadata[CHAT_INLINE_TOKENS_METADATA_KEY];
|
|
46
45
|
const skillRecords = readInlineTokensFromMetadata(message.metadata).flatMap(
|
|
47
46
|
(token) =>
|
|
@@ -59,7 +58,6 @@ function buildEditedMessageMetadata(
|
|
|
59
58
|
);
|
|
60
59
|
Object.assign(metadata, buildChatRunMetadata({
|
|
61
60
|
composerNodes: [...snapshot.nodes],
|
|
62
|
-
requestedSkills: [...snapshot.selectedSkills],
|
|
63
61
|
skillRecords,
|
|
64
62
|
}));
|
|
65
63
|
return Object.keys(metadata).length > 0 ? metadata : undefined;
|
|
@@ -216,20 +216,6 @@ class HistoryMessageBuilder {
|
|
|
216
216
|
};
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
export function normalizeRequestedSkills(value: string[] | undefined): string[] {
|
|
220
|
-
if (!Array.isArray(value)) {
|
|
221
|
-
return [];
|
|
222
|
-
}
|
|
223
|
-
const deduped = new Set<string>();
|
|
224
|
-
for (const item of value) {
|
|
225
|
-
const trimmed = item.trim();
|
|
226
|
-
if (trimmed) {
|
|
227
|
-
deduped.add(trimmed);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return [...deduped];
|
|
231
|
-
}
|
|
232
|
-
|
|
233
219
|
export function buildUiMessagesFromHistoryMessages(messages: SessionMessageView[]): UIMessage[] {
|
|
234
220
|
return new HistoryMessageBuilder().build(messages);
|
|
235
221
|
}
|
package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { fireEvent, render, screen, within } from "@testing-library/react";
|
|
2
2
|
import userEvent from "@testing-library/user-event";
|
|
3
3
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
-
import {
|
|
4
|
+
import { MemoryRouter, useLocation } from "react-router-dom";
|
|
5
|
+
import {
|
|
6
|
+
ChatSidebarProjectGroups,
|
|
7
|
+
type ChatSidebarProjectGroup,
|
|
8
|
+
} from "@/features/chat/features/session/components/chat-sidebar-project-groups";
|
|
5
9
|
import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
|
|
6
10
|
|
|
7
11
|
const mocks = vi.hoisted(() => ({
|
|
@@ -16,7 +20,8 @@ vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
|
|
|
16
20
|
}),
|
|
17
21
|
}));
|
|
18
22
|
|
|
19
|
-
const projectGroup = {
|
|
23
|
+
const projectGroup: ChatSidebarProjectGroup = {
|
|
24
|
+
projectId: "project-analysis",
|
|
20
25
|
projectRoot: "/tmp/analysis-project",
|
|
21
26
|
projectName: "analysis-project",
|
|
22
27
|
items: [
|
|
@@ -35,17 +40,30 @@ const projectGroup = {
|
|
|
35
40
|
isPinned: false,
|
|
36
41
|
};
|
|
37
42
|
|
|
38
|
-
function
|
|
43
|
+
function LocationProbe() {
|
|
44
|
+
const location = useLocation();
|
|
45
|
+
return (
|
|
46
|
+
<output data-testid="location">{`${location.pathname}${location.search}`}</output>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function renderProjectGroups(
|
|
51
|
+
isPinned = false,
|
|
52
|
+
group: ChatSidebarProjectGroup = projectGroup,
|
|
53
|
+
) {
|
|
39
54
|
return render(
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
{
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
<MemoryRouter initialEntries={["/chat"]}>
|
|
56
|
+
<ChatSidebarProjectGroups
|
|
57
|
+
groups={[{ ...group, isPinned }]}
|
|
58
|
+
projectCronJobCountByRoot={new Map([[projectGroup.projectRoot, 2]])}
|
|
59
|
+
defaultSessionType="native"
|
|
60
|
+
sessionTypeOptions={[
|
|
61
|
+
{ value: "native", label: "Native", icon: null, ready: true },
|
|
62
|
+
]}
|
|
63
|
+
renderSessionItem={() => <div>Project session</div>}
|
|
64
|
+
/>
|
|
65
|
+
<LocationProbe />
|
|
66
|
+
</MemoryRouter>,
|
|
49
67
|
);
|
|
50
68
|
}
|
|
51
69
|
|
|
@@ -72,18 +90,23 @@ describe("ChatSidebarProjectGroups", () => {
|
|
|
72
90
|
const { container } = renderProjectGroups();
|
|
73
91
|
|
|
74
92
|
const header = screen.getByLabelText("Collapse project");
|
|
93
|
+
const projectLink = screen.getByRole("link", { name: "analysis-project" });
|
|
94
|
+
const projectRow = projectLink.parentElement?.parentElement;
|
|
75
95
|
expect(container.firstElementChild?.className).toBe("space-y-0.5");
|
|
76
|
-
expect(
|
|
77
|
-
expect(
|
|
78
|
-
|
|
79
|
-
expect(
|
|
80
|
-
|
|
81
|
-
).
|
|
96
|
+
expect(projectRow?.className).toContain("h-8");
|
|
97
|
+
expect(projectRow?.className).toContain("hover:bg-gray-200/60");
|
|
98
|
+
const projectContent = projectLink.parentElement;
|
|
99
|
+
expect(projectContent?.classList.contains("pr-14")).toBe(false);
|
|
100
|
+
expect(projectContent?.className).toContain("group-hover/project:pr-14");
|
|
101
|
+
expect(projectContent?.className).toContain(
|
|
102
|
+
"group-has-[[data-project-actions]:focus-within]/project:pr-14",
|
|
103
|
+
);
|
|
104
|
+
expect(projectLink.querySelector("span")?.className).not.toContain("uppercase");
|
|
105
|
+
expect(projectLink.querySelector("span")?.className).toContain("text-[13px]");
|
|
82
106
|
expect(
|
|
83
|
-
|
|
84
|
-
.
|
|
85
|
-
|
|
86
|
-
).toContain("opacity-0");
|
|
107
|
+
header.compareDocumentPosition(projectLink) &
|
|
108
|
+
Node.DOCUMENT_POSITION_FOLLOWING,
|
|
109
|
+
).toBeTruthy();
|
|
87
110
|
expect(screen.queryByText("Sessions")).toBeNull();
|
|
88
111
|
|
|
89
112
|
fireEvent.click(header);
|
|
@@ -97,6 +120,31 @@ describe("ChatSidebarProjectGroups", () => {
|
|
|
97
120
|
expect(screen.queryByText("Project session")).toBeNull();
|
|
98
121
|
});
|
|
99
122
|
|
|
123
|
+
it("opens the project home from the project name without changing expansion", () => {
|
|
124
|
+
renderProjectGroups();
|
|
125
|
+
|
|
126
|
+
fireEvent.click(screen.getByRole("link", { name: "analysis-project" }));
|
|
127
|
+
|
|
128
|
+
expect(mocks.toggleProjectCollapsed).not.toHaveBeenCalled();
|
|
129
|
+
expect(screen.getByTestId("location").textContent).toBe(
|
|
130
|
+
"/projects/project-analysis/overview",
|
|
131
|
+
);
|
|
132
|
+
expect(screen.getByText("Project session")).toBeTruthy();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("surfaces a running child session while its project is collapsed", () => {
|
|
136
|
+
renderProjectGroups(false, {
|
|
137
|
+
...projectGroup,
|
|
138
|
+
items: [{ ...projectGroup.items[0], runStatus: "running" as const }],
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(screen.queryByLabelText("Running")).toBeNull();
|
|
142
|
+
|
|
143
|
+
fireEvent.click(screen.getByLabelText("Collapse project"));
|
|
144
|
+
|
|
145
|
+
expect(screen.getByLabelText("Running")).toBeTruthy();
|
|
146
|
+
});
|
|
147
|
+
|
|
100
148
|
it("shows project context from the row on hover", async () => {
|
|
101
149
|
const user = userEvent.setup();
|
|
102
150
|
renderProjectGroups();
|