@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,4 +1,5 @@
|
|
|
1
1
|
import { useMemo, useState, type ReactNode } from "react";
|
|
2
|
+
import { Link, useLocation } from "react-router-dom";
|
|
2
3
|
import {
|
|
3
4
|
AlarmClock,
|
|
4
5
|
ChevronDown,
|
|
@@ -19,6 +20,8 @@ import type { ChatSidebarProjectGroup } from "@/features/chat/features/session/u
|
|
|
19
20
|
import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
|
|
20
21
|
import { t } from "@/shared/lib/i18n";
|
|
21
22
|
import { ChatSidebarContextCard } from "@/features/chat/features/session/components/chat-sidebar-context-card";
|
|
23
|
+
import { SessionRunBadge } from "@/features/chat/features/session/components/session-run-badge";
|
|
24
|
+
import { cn } from "@/shared/lib/utils";
|
|
22
25
|
|
|
23
26
|
export type { ChatSidebarProjectGroup };
|
|
24
27
|
|
|
@@ -53,6 +56,7 @@ export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
|
|
|
53
56
|
projectCronJobCountByRoot,
|
|
54
57
|
} = props;
|
|
55
58
|
const presenter = usePresenter();
|
|
59
|
+
const location = useLocation();
|
|
56
60
|
const collapsedProjectRoots = useChatSessionListStore(
|
|
57
61
|
(state) => state.snapshot.collapsedProjectRoots,
|
|
58
62
|
);
|
|
@@ -72,13 +76,23 @@ export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
|
|
|
72
76
|
{groups.map((group) => {
|
|
73
77
|
const actionLabel = `${t("chatSidebarNewTask")} · ${group.projectName}`;
|
|
74
78
|
const isCollapsed = collapsedProjectRoots.includes(group.projectRoot);
|
|
79
|
+
const hasRunningSession = isCollapsed && group.items.some(
|
|
80
|
+
(item) => item.runStatus === "running",
|
|
81
|
+
);
|
|
82
|
+
const isSelected = Boolean(group.projectId) &&
|
|
83
|
+
location.pathname.startsWith(`/projects/${group.projectId}/`);
|
|
75
84
|
const pinLabel = t(
|
|
76
85
|
group.isPinned ? "chatSidebarUnpinProject" : "chatSidebarPinProject",
|
|
77
86
|
);
|
|
78
87
|
|
|
79
88
|
return (
|
|
80
89
|
<div key={group.projectRoot}>
|
|
81
|
-
<div
|
|
90
|
+
<div
|
|
91
|
+
className={cn(
|
|
92
|
+
"group/project relative h-8 rounded-lg px-1 text-muted-foreground transition-colors hover:bg-gray-200/60 hover:text-gray-900",
|
|
93
|
+
isSelected && "bg-gray-200/60 text-gray-900",
|
|
94
|
+
)}
|
|
95
|
+
>
|
|
82
96
|
<ChatSidebarContextCard
|
|
83
97
|
title={group.projectName}
|
|
84
98
|
metrics={[
|
|
@@ -100,42 +114,75 @@ export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
|
|
|
100
114
|
},
|
|
101
115
|
]}
|
|
102
116
|
>
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
isCollapsed
|
|
108
|
-
? "chatSidebarExpandProject"
|
|
109
|
-
: "chatSidebarCollapseProject",
|
|
117
|
+
<div
|
|
118
|
+
className={cn(
|
|
119
|
+
"flex h-full w-full min-w-0 items-center group-hover/project:pr-14 group-has-[[data-project-actions]:focus-within]/project:pr-14",
|
|
120
|
+
hasRunningSession && "pr-7",
|
|
110
121
|
)}
|
|
111
|
-
className="flex h-full w-full min-w-0 items-center gap-1.5 pr-14 text-left focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
|
|
112
|
-
onClick={() =>
|
|
113
|
-
presenter.chatSessionListManager.toggleProjectCollapsed(
|
|
114
|
-
group.projectRoot,
|
|
115
|
-
)
|
|
116
|
-
}
|
|
117
122
|
>
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
123
|
+
<IconActionButton
|
|
124
|
+
icon={
|
|
125
|
+
isCollapsed ? (
|
|
126
|
+
<ChevronRight className="h-3.5 w-3.5" />
|
|
127
|
+
) : (
|
|
128
|
+
<ChevronDown className="h-3.5 w-3.5" />
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
label={t(
|
|
132
|
+
isCollapsed
|
|
133
|
+
? "chatSidebarExpandProject"
|
|
134
|
+
: "chatSidebarCollapseProject",
|
|
135
|
+
)}
|
|
136
|
+
aria-expanded={!isCollapsed}
|
|
137
|
+
size="sm"
|
|
138
|
+
tone="surface"
|
|
139
|
+
tooltip={false}
|
|
140
|
+
onClick={() =>
|
|
141
|
+
presenter.chatSessionListManager.toggleProjectCollapsed(
|
|
142
|
+
group.projectRoot,
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
/>
|
|
146
|
+
{group.projectId ? (
|
|
147
|
+
<Link
|
|
148
|
+
to={`/projects/${encodeURIComponent(group.projectId)}/overview`}
|
|
149
|
+
className="flex h-full min-w-0 flex-1 items-center gap-1.5 rounded-md px-1 text-left focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
|
|
150
|
+
aria-current={isSelected ? "page" : undefined}
|
|
151
|
+
title={group.projectRoot}
|
|
152
|
+
>
|
|
153
|
+
<Folder
|
|
154
|
+
className="h-3.5 w-3.5 shrink-0"
|
|
155
|
+
aria-hidden="true"
|
|
156
|
+
/>
|
|
157
|
+
<span className="truncate text-[13px] font-medium">
|
|
158
|
+
{group.projectName}
|
|
159
|
+
</span>
|
|
160
|
+
</Link>
|
|
130
161
|
) : (
|
|
131
|
-
<
|
|
132
|
-
className="h-
|
|
133
|
-
|
|
134
|
-
|
|
162
|
+
<span
|
|
163
|
+
className="flex h-full min-w-0 flex-1 items-center gap-1.5 rounded-md px-1 text-left"
|
|
164
|
+
title={group.projectRoot}
|
|
165
|
+
>
|
|
166
|
+
<Folder
|
|
167
|
+
className="h-3.5 w-3.5 shrink-0"
|
|
168
|
+
aria-hidden="true"
|
|
169
|
+
/>
|
|
170
|
+
<span className="truncate text-[13px] font-medium">
|
|
171
|
+
{group.projectName}
|
|
172
|
+
</span>
|
|
173
|
+
</span>
|
|
135
174
|
)}
|
|
136
|
-
</
|
|
175
|
+
</div>
|
|
137
176
|
</ChatSidebarContextCard>
|
|
138
|
-
|
|
177
|
+
{hasRunningSession ? (
|
|
178
|
+
<span className="absolute right-2 top-1/2 inline-flex -translate-y-1/2 transition-opacity group-hover/project:opacity-0 group-has-[[data-project-actions]:focus-within]/project:opacity-0">
|
|
179
|
+
<SessionRunBadge status="running" />
|
|
180
|
+
</span>
|
|
181
|
+
) : null}
|
|
182
|
+
<div
|
|
183
|
+
data-project-actions
|
|
184
|
+
className="pointer-events-none absolute right-2 top-1/2 flex -translate-y-1/2 items-center opacity-0 transition-opacity group-hover/project:pointer-events-auto group-hover/project:opacity-100 focus-within:pointer-events-auto focus-within:opacity-100"
|
|
185
|
+
>
|
|
139
186
|
{supportsSessionTypeChoice ? (
|
|
140
187
|
<Popover
|
|
141
188
|
open={openProjectRoot === group.projectRoot}
|
package/src/features/chat/features/session/utils/__tests__/chat-sidebar-session-groups.utils.test.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { groupSessionsByProject } from "@/features/chat/features/session/utils/c
|
|
|
4
4
|
describe("groupSessionsByProject", () => {
|
|
5
5
|
it("uses registered projects as the project-first baseline", () => {
|
|
6
6
|
const groups = groupSessionsByProject([], new Set(), new Set(), [{
|
|
7
|
+
id: "project-knowledge",
|
|
7
8
|
name: "Knowledge",
|
|
8
9
|
rootPath: "/tmp/knowledge",
|
|
9
10
|
template: "knowledge-base",
|
|
@@ -12,6 +13,7 @@ describe("groupSessionsByProject", () => {
|
|
|
12
13
|
}]);
|
|
13
14
|
|
|
14
15
|
expect(groups).toEqual([{
|
|
16
|
+
projectId: "project-knowledge",
|
|
15
17
|
projectRoot: "/tmp/knowledge",
|
|
16
18
|
projectName: "Knowledge",
|
|
17
19
|
items: [],
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
createInlineTokensMetadata,
|
|
9
9
|
type ChatSkillReferenceSnapshot,
|
|
10
10
|
} from "@/features/chat/features/input/utils/chat-inline-token.utils";
|
|
11
|
-
import { normalizeRequestedSkills } from "@/features/chat/features/runtime/utils/chat-runtime.utils";
|
|
12
11
|
import { normalizeSessionProjectRootValue } from "@/shared/lib/session-project";
|
|
13
12
|
|
|
14
13
|
function createMetadataFields(
|
|
@@ -24,7 +23,6 @@ export function buildChatRunMetadata(payload: {
|
|
|
24
23
|
thinkingLevel?: string;
|
|
25
24
|
sessionType?: string;
|
|
26
25
|
projectRoot?: string | null;
|
|
27
|
-
requestedSkills?: string[];
|
|
28
26
|
skillRecords?: readonly ChatSkillReferenceSnapshot[];
|
|
29
27
|
composerNodes?: Parameters<typeof buildInlineTokensFromComposer>[0];
|
|
30
28
|
sessionMaterialization?: AgentRunSessionMaterializationMetadata | null;
|
|
@@ -37,10 +35,6 @@ export function buildChatRunMetadata(payload: {
|
|
|
37
35
|
...createMetadataFields(payload.agentId?.trim(), ["agentId", "agent_id"]),
|
|
38
36
|
...createMetadataFields(projectRoot ?? undefined, ["projectRoot", "project_root"]),
|
|
39
37
|
};
|
|
40
|
-
const requestedSkills = normalizeRequestedSkills(payload.requestedSkills);
|
|
41
|
-
if (requestedSkills.length > 0) {
|
|
42
|
-
metadata.requested_skill_refs = requestedSkills;
|
|
43
|
-
}
|
|
44
38
|
const inlineTokens = payload.composerNodes
|
|
45
39
|
? buildInlineTokensFromComposer(payload.composerNodes, payload.skillRecords)
|
|
46
40
|
: [];
|
|
@@ -9,6 +9,7 @@ export type ChatSidebarDateGroup = {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
export type ChatSidebarProjectGroup = {
|
|
12
|
+
projectId?: string;
|
|
12
13
|
projectRoot: string;
|
|
13
14
|
projectName: string;
|
|
14
15
|
items: NcpSessionListItemView[];
|
|
@@ -102,6 +103,7 @@ export function groupSessionsByProject(
|
|
|
102
103
|
|
|
103
104
|
for (const project of projects) {
|
|
104
105
|
grouped.set(project.rootPath, {
|
|
106
|
+
projectId: project.id,
|
|
105
107
|
projectRoot: project.rootPath,
|
|
106
108
|
projectName: project.name,
|
|
107
109
|
items: [],
|
|
@@ -60,6 +60,7 @@ describe("buildChatWelcomeProjectOptions", () => {
|
|
|
60
60
|
const options = buildChatWelcomeProjectOptions({
|
|
61
61
|
defaultProjectRoot: "/Users/demo/.nextclaw/workspace",
|
|
62
62
|
projects: [{
|
|
63
|
+
id: "project-knowledge",
|
|
63
64
|
name: "Knowledge",
|
|
64
65
|
rootPath: "/tmp/knowledge",
|
|
65
66
|
template: "knowledge-base",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { ChatConversationPanel } from "./components/conversation/chat-conversation-panel";
|
|
2
|
+
export { ChatConversationWorkspaceSection } from "./components/conversation/chat-conversation-workspace-section";
|
|
2
3
|
export { ChatSidebar } from "./components/layout/chat-sidebar";
|
|
3
4
|
export { ChatPresenterProvider } from "./components/providers/chat-presenter.provider";
|
|
4
5
|
export { usePresenter } from "./components/providers/chat-presenter.provider";
|
|
@@ -6,7 +7,10 @@ export { ChatPresenter } from "./presenters/chat.presenter";
|
|
|
6
7
|
export { ChatDraftIntentManager } from "./managers/chat-draft-intent.manager";
|
|
7
8
|
export { ChatComposerIntentManager } from "./managers/chat-composer-intent.manager";
|
|
8
9
|
export { ChatCompletionNotificationManager } from "./managers/chat-completion-notification.manager";
|
|
9
|
-
export {
|
|
10
|
+
export {
|
|
11
|
+
buildSessionPath,
|
|
12
|
+
CHAT_DRAFT_SESSION_PATH,
|
|
13
|
+
} from "./features/session/utils/chat-session-route.utils";
|
|
10
14
|
export { useChatSessionListStore } from "./stores/chat-session-list.store";
|
|
11
15
|
export { useChatThreadStore } from "./stores/chat-thread.store";
|
|
12
16
|
export { useChatMessageLayoutStore } from "./stores/chat-message-layout.store";
|
|
@@ -230,6 +230,33 @@ describe("ChatSessionListManager draft and selection flow", () => {
|
|
|
230
230
|
});
|
|
231
231
|
});
|
|
232
232
|
|
|
233
|
+
it("keeps a project-bound initial prompt as an editable unsent draft", () => {
|
|
234
|
+
const uiManager = {
|
|
235
|
+
goToChatRoot: vi.fn(),
|
|
236
|
+
navigateTo: vi.fn(),
|
|
237
|
+
goToSession: vi.fn(),
|
|
238
|
+
isAtChatRoot: vi.fn(() => true),
|
|
239
|
+
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[0];
|
|
240
|
+
|
|
241
|
+
const manager = new ChatSessionListManager(uiManager);
|
|
242
|
+
manager.createSession({
|
|
243
|
+
projectRoot: "/tmp/project-alpha",
|
|
244
|
+
prompt: "Use the project observation capability.",
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
expect(uiManager.navigateTo).toHaveBeenCalledWith("/chat/draft", {
|
|
248
|
+
replace: true,
|
|
249
|
+
state: {
|
|
250
|
+
chatDraft: {
|
|
251
|
+
sessionType: "native",
|
|
252
|
+
projectRoot: "/tmp/project-alpha",
|
|
253
|
+
prompt: "Use the project observation capability.",
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
expect(useChatThreadStore.getState().snapshot.hasSubmittedDraftMessage).toBe(false);
|
|
258
|
+
});
|
|
259
|
+
|
|
233
260
|
it("does not eagerly replace the old selected session before the route finishes switching", () => {
|
|
234
261
|
const uiManager = {
|
|
235
262
|
goToChatRoot: vi.fn(),
|
|
@@ -180,19 +180,8 @@ describe('buildChatRunMetadata', () => {
|
|
|
180
180
|
).not.toHaveProperty('project_root');
|
|
181
181
|
});
|
|
182
182
|
|
|
183
|
-
it('
|
|
184
|
-
expect(
|
|
185
|
-
buildChatRunMetadata({
|
|
186
|
-
requestedSkills: ['project:/tmp/project-alpha/.agents/skills/review'],
|
|
187
|
-
}),
|
|
188
|
-
).toMatchObject({
|
|
189
|
-
requested_skill_refs: ['project:/tmp/project-alpha/.agents/skills/review'],
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it('snapshots explicit skill references while keeping internal refs out of message text', () => {
|
|
183
|
+
it('snapshots skill token presentation without creating runtime selection metadata', () => {
|
|
194
184
|
expect(buildChatRunMetadata({
|
|
195
|
-
requestedSkills: ['workspace:/tmp/workspace/skills/review'],
|
|
196
185
|
skillRecords: [{
|
|
197
186
|
ref: 'workspace:/tmp/workspace/skills/review',
|
|
198
187
|
name: 'review',
|
|
@@ -204,8 +193,7 @@ describe('buildChatRunMetadata', () => {
|
|
|
204
193
|
tokenKey: 'workspace:/tmp/workspace/skills/review',
|
|
205
194
|
label: 'review',
|
|
206
195
|
})],
|
|
207
|
-
})).
|
|
208
|
-
requested_skill_refs: ['workspace:/tmp/workspace/skills/review'],
|
|
196
|
+
})).toEqual({
|
|
209
197
|
ui_inline_tokens: {
|
|
210
198
|
schemaVersion: 2,
|
|
211
199
|
items: [{
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Projects UI boundary
|
|
2
|
+
|
|
3
|
+
- 本目录拥有 Projects 页面、presenter、查询状态和用户回复编排;项目观测事实来自 `client.projects` 公共快照,工作项来自 Project Work API,组件不得自行扫描文件或解析 Marker。
|
|
4
|
+
- 页面默认只读;确认/拒绝只复用现有 agent-run 消息入口,并携带可追踪、可幂等的响应元数据,不创建第二套 response bridge。
|
|
5
|
+
- Overview、列表和看板消费同一份工作项查询状态;所有工作项入口统一打开右侧详情抽屉,不得在页面内追加平铺详情。
|
|
6
|
+
- `project.work.changed` 只用于失效查询;UI 不重放事件历史,也不以轮询或目录扫描代替业务存储。
|
|
7
|
+
- 组件负责连接与展示,跨组件状态归 hook/presenter;用户文案必须走 Projects i18n 资源。
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ProjectObservationSnapshot } from "@nextclaw/client-sdk";
|
|
2
|
+
import { t } from "@/shared/lib/i18n";
|
|
3
|
+
import {
|
|
4
|
+
ProjectEmptyState,
|
|
5
|
+
ProjectFilePreviewButton,
|
|
6
|
+
ProjectSection,
|
|
7
|
+
} from "./project-section";
|
|
8
|
+
|
|
9
|
+
export function ProjectAgreement({
|
|
10
|
+
snapshot,
|
|
11
|
+
onOpenFile,
|
|
12
|
+
}: {
|
|
13
|
+
snapshot: ProjectObservationSnapshot;
|
|
14
|
+
onOpenFile: (path: string, label: string) => void;
|
|
15
|
+
}) {
|
|
16
|
+
return (
|
|
17
|
+
<div className="grid min-w-0 gap-4 lg:grid-cols-2">
|
|
18
|
+
<ProjectSection title={t("projectsVisionAndContext")}>
|
|
19
|
+
{snapshot.project.summary ? (
|
|
20
|
+
<p className="mb-4 text-sm">{snapshot.project.summary}</p>
|
|
21
|
+
) : null}
|
|
22
|
+
{snapshot.project.context.length ? (
|
|
23
|
+
<div className="divide-y divide-border/60">
|
|
24
|
+
{snapshot.project.context.map((context) => (
|
|
25
|
+
<ProjectContextReference
|
|
26
|
+
key={context.id}
|
|
27
|
+
context={context}
|
|
28
|
+
onOpenFile={onOpenFile}
|
|
29
|
+
/>
|
|
30
|
+
))}
|
|
31
|
+
</div>
|
|
32
|
+
) : (
|
|
33
|
+
<ProjectEmptyState>{t("projectsNoContext")}</ProjectEmptyState>
|
|
34
|
+
)}
|
|
35
|
+
</ProjectSection>
|
|
36
|
+
<ProjectSection title={t("projectsSourceHealth")}>
|
|
37
|
+
<div className="space-y-2">
|
|
38
|
+
{snapshot.sources.map((source) => (
|
|
39
|
+
<div
|
|
40
|
+
key={source.id}
|
|
41
|
+
className="flex items-center justify-between gap-3 rounded-xl bg-muted/45 p-3 text-sm"
|
|
42
|
+
>
|
|
43
|
+
<span>{source.label}</span>
|
|
44
|
+
<span className="text-muted-foreground">
|
|
45
|
+
{t(`projectsSource_${source.status}`)} · {source.itemCount}
|
|
46
|
+
</span>
|
|
47
|
+
</div>
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
{snapshot.diagnostics.length ? (
|
|
51
|
+
<div className="mt-4 space-y-2 border-t border-border/60 pt-4">
|
|
52
|
+
{snapshot.diagnostics.map((diagnostic) => (
|
|
53
|
+
<div
|
|
54
|
+
key={diagnostic.id}
|
|
55
|
+
className="rounded-xl bg-muted/35 p-3 text-sm"
|
|
56
|
+
>
|
|
57
|
+
<p className="font-medium">{diagnostic.code}</p>
|
|
58
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
59
|
+
{diagnostic.message}
|
|
60
|
+
</p>
|
|
61
|
+
</div>
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
) : null}
|
|
65
|
+
</ProjectSection>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function ProjectContextReference({
|
|
71
|
+
context,
|
|
72
|
+
onOpenFile,
|
|
73
|
+
}: {
|
|
74
|
+
context: ProjectObservationSnapshot["project"]["context"][number];
|
|
75
|
+
onOpenFile: (path: string, label: string) => void;
|
|
76
|
+
}) {
|
|
77
|
+
const { accessible, role, source } = context;
|
|
78
|
+
return (
|
|
79
|
+
<ProjectFilePreviewButton
|
|
80
|
+
available={accessible}
|
|
81
|
+
path={source}
|
|
82
|
+
label={role}
|
|
83
|
+
onOpen={onOpenFile}
|
|
84
|
+
className="block w-full py-3 text-left first:pt-0 enabled:hover:text-foreground enabled:focus-visible:outline-none enabled:focus-visible:ring-1 enabled:focus-visible:ring-border"
|
|
85
|
+
>
|
|
86
|
+
<span className="block text-sm font-medium">{role}</span>
|
|
87
|
+
<span className="mt-1 block truncate text-xs text-muted-foreground">
|
|
88
|
+
{source}{!accessible ? ` · ${t("projectsUnavailable")}` : ""}
|
|
89
|
+
</span>
|
|
90
|
+
</ProjectFilePreviewButton>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import type { ProjectObservationSnapshot } from "@nextclaw/client-sdk";
|
|
4
|
+
import { ProjectArtifacts } from "./project-artifacts";
|
|
5
|
+
|
|
6
|
+
const observedAt = "2026-08-30T00:00:00.000Z";
|
|
7
|
+
const designs = Array.from({ length: 11 }, (_, index) => ({
|
|
8
|
+
id: `design-${index + 1}`,
|
|
9
|
+
path: `docs/designs/${index + 1}.md`,
|
|
10
|
+
categoryId: "designs",
|
|
11
|
+
categoryLabel: "Designs",
|
|
12
|
+
exists: true,
|
|
13
|
+
fileUpdatedAt: `2026-08-${String(index + 1).padStart(2, "0")}T00:00:00.000Z`,
|
|
14
|
+
references: [],
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
const snapshot: ProjectObservationSnapshot = {
|
|
18
|
+
asOf: observedAt,
|
|
19
|
+
project: { name: "Research", rootPath: "/tmp/research", context: [] },
|
|
20
|
+
sources: [],
|
|
21
|
+
workflows: [],
|
|
22
|
+
runs: [],
|
|
23
|
+
workItems: [],
|
|
24
|
+
artifactCategories: [
|
|
25
|
+
{ id: "plans", label: "Plans" },
|
|
26
|
+
{ id: "designs", label: "Designs" },
|
|
27
|
+
],
|
|
28
|
+
artifacts: [
|
|
29
|
+
...designs,
|
|
30
|
+
{
|
|
31
|
+
id: "plan-1",
|
|
32
|
+
path: "docs/plans/delivery.md",
|
|
33
|
+
categoryId: "plans",
|
|
34
|
+
categoryLabel: "Plans",
|
|
35
|
+
exists: true,
|
|
36
|
+
fileUpdatedAt: "2026-08-30T00:00:00.000Z",
|
|
37
|
+
references: [],
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
signals: [],
|
|
41
|
+
requests: [],
|
|
42
|
+
activity: [],
|
|
43
|
+
skills: [],
|
|
44
|
+
diagnostics: [],
|
|
45
|
+
dataQuality: "complete",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
describe("ProjectArtifacts", () => {
|
|
49
|
+
it("groups in configuration order, expands incrementally, filters, and opens the selected file", () => {
|
|
50
|
+
const onOpenFile = vi.fn();
|
|
51
|
+
render(<ProjectArtifacts snapshot={snapshot} onOpenFile={onOpenFile} />);
|
|
52
|
+
|
|
53
|
+
const groupHeaders = screen.getAllByRole("button", { name: /Plans 1|Designs 11/ });
|
|
54
|
+
expect(groupHeaders.map((header) => header.getAttribute("aria-label"))).toEqual([
|
|
55
|
+
"Plans 1",
|
|
56
|
+
"Designs 11",
|
|
57
|
+
]);
|
|
58
|
+
expect(screen.getByTitle("docs/designs/11.md")).toBeTruthy();
|
|
59
|
+
expect(screen.queryByTitle("docs/designs/1.md")).toBeNull();
|
|
60
|
+
|
|
61
|
+
fireEvent.click(screen.getByRole("button", { name: /Show more \(1\)/ }));
|
|
62
|
+
expect(screen.getByTitle("docs/designs/1.md")).toBeTruthy();
|
|
63
|
+
|
|
64
|
+
fireEvent.change(screen.getByPlaceholderText("Search artifacts"), {
|
|
65
|
+
target: { value: "delivery" },
|
|
66
|
+
});
|
|
67
|
+
expect(screen.queryByRole("button", { name: /Designs 11/ })).toBeNull();
|
|
68
|
+
expect(screen.getByTitle("docs/plans/delivery.md")).toBeTruthy();
|
|
69
|
+
fireEvent.click(screen.getByTitle("docs/plans/delivery.md"));
|
|
70
|
+
expect(onOpenFile).toHaveBeenCalledWith("docs/plans/delivery.md", "docs/plans/delivery.md");
|
|
71
|
+
|
|
72
|
+
const plansHeader = screen.getByRole("button", { name: /Plans 1/ });
|
|
73
|
+
fireEvent.click(plansHeader);
|
|
74
|
+
expect(plansHeader.getAttribute("aria-expanded")).toBe("false");
|
|
75
|
+
expect(screen.queryByTitle("docs/plans/delivery.md")).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
});
|