@nextclaw/ui 0.23.0 → 0.24.0-beta.0
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 +14 -0
- package/dist/assets/{api-n96IfCKb.js → api-CoqPRKmF.js} +5 -5
- package/dist/assets/{appearance-settings-page-C_AqxSF_.js → appearance-settings-page-CPoFZLw8.js} +1 -1
- package/dist/assets/{book-open-DYRapW_h.js → book-open-jcUt0rr9.js} +1 -1
- package/dist/assets/{channels-list-page-uR-M4Ls8.js → channels-list-page-anMJAkfK.js} +2 -2
- package/dist/assets/{config-split-page-DJDp86Ne.js → config-split-page-AcuFnbIC.js} +1 -1
- package/dist/assets/{confirm-dialog-CHVBMpla.js → confirm-dialog-B-atCfC5.js} +1 -1
- package/dist/assets/{createLucideIcon-CgM0rnsa.js → createLucideIcon-Blz0FzrV.js} +1 -1
- package/dist/assets/{desktop-capabilities-page-phMuNq1B.js → desktop-capabilities-page-8lKF_Ob-.js} +1 -1
- package/dist/assets/desktop-update-config-DABXDpbj.js +1 -0
- package/dist/assets/{dist-BjYvUhPL.js → dist-D8kcEpTf.js} +1 -1
- package/dist/assets/{dist-N9zc5bkM.js → dist-eH-_j8yU.js} +1 -1
- package/dist/assets/doc-browser-DJEMr7Zk.js +1 -0
- package/dist/assets/{doc-browser-DwRLokCI.js → doc-browser-DzXRehlW.js} +1 -1
- package/dist/assets/{doc-browser-context-BDuypObS.js → doc-browser-context-BlbUcg2J.js} +1 -1
- package/dist/assets/{extensions-CQKXTqTI.js → extensions-mquwAJ2Z.js} +1 -1
- package/dist/assets/{form-actions-CzGmrPAv.js → form-actions-CaZDGEmB.js} +1 -1
- package/dist/assets/index-DyR5iR0O.css +1 -0
- package/dist/assets/{index-CLw9DuCJ.js → index-XxXj8WDS.js} +9 -9
- package/dist/assets/loader-circle-0qPOOYQ5.js +1 -0
- package/dist/assets/{mcp-marketplace-page-s9PTnSDT.js → mcp-marketplace-page-gFtgyueL.js} +1 -1
- package/dist/assets/mcp-marketplace-page-uanamJ6L.js +1 -0
- package/dist/assets/{model-config-page-CxnCUWRW.js → model-config-page-hFBpeRAO.js} +1 -1
- package/dist/assets/{navigation-link-Ci4Z7Phi.js → navigation-link-Hb5Z0UxZ.js} +1 -1
- package/dist/assets/plus-CYOvNvcV.js +1 -0
- package/dist/assets/privacy-settings-page-C18zo3ox.js +1 -0
- package/dist/assets/projects-C8Gm9RS8.js +1 -0
- package/dist/assets/providers-config-page-YU0-AOXJ.js +1 -0
- package/dist/assets/react-DF-kP7iN.js +8 -0
- package/dist/assets/{refresh-cw-C-3_thgr.js → refresh-cw-ZHKfE3yx.js} +1 -1
- package/dist/assets/remote-B_utkBra.js +1 -0
- package/dist/assets/{rotate-cw-DSFTq_yu.js → rotate-cw-CLhOXGwE.js} +1 -1
- package/dist/assets/runtime-config-page-Dmy1A_0Y.js +1 -0
- package/dist/assets/{save-CpPr78_S.js → save-CWuuZs77.js} +1 -1
- package/dist/assets/{search-msqk2-rd.js → search-DcmsKAn2.js} +1 -1
- package/dist/assets/{search-config-page-D3_qNXbB.js → search-config-page-CxBacx6W.js} +1 -1
- package/dist/assets/{secrets-config-page-Be4P1E8o.js → secrets-config-page-B4py8-KR.js} +2 -2
- package/dist/assets/security-config-CTP4IQWw.js +1 -0
- package/dist/assets/{select-DPKCWajZ.js → select-BEr0Iy_f.js} +1 -1
- package/dist/assets/skeleton-B2NHluWf.js +1 -0
- package/dist/assets/{tag-chip-Dc_NcCEe.js → tag-chip-ByqDkn6g.js} +1 -1
- package/dist/assets/x-CENkFyK5.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/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/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 +6 -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 +150 -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/project-work-items.test.tsx +78 -0
- package/src/features/projects/components/project-work-items.tsx +272 -0
- package/src/features/projects/hooks/use-project-observation.test.tsx +93 -0
- package/src/features/projects/hooks/use-project-observation.ts +85 -0
- package/src/features/projects/index.ts +2 -0
- package/src/features/projects/pages/project-home-page.tsx +253 -0
- package/src/features/projects/pages/projects-page.test.tsx +366 -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/projects.json +90 -0
- package/src/shared/lib/i18n/locales/zh-CN/projects.json +90 -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/desktop-update-config-C77hkbor.js +0 -1
- package/dist/assets/doc-browser-JlXVLNRe.js +0 -1
- 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
|
@@ -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();
|
|
@@ -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",
|
|
@@ -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,6 @@
|
|
|
1
|
+
# Projects UI boundary
|
|
2
|
+
|
|
3
|
+
- 本目录拥有 Projects 页面、presenter、查询状态和用户回复编排;事实来源只能是 `client.projects` 的公共快照,组件不得自行扫描文件或解析 Marker。
|
|
4
|
+
- 页面默认只读;确认/拒绝只复用现有 agent-run 消息入口,并携带可追踪、可幂等的响应元数据,不创建第二套 response bridge。
|
|
5
|
+
- Overview、列表、看板、甘特图和详情必须消费同一份 snapshot,不为视图复制业务状态或推断缺失阶段、日期与进度。
|
|
6
|
+
- 组件负责连接与展示,跨组件状态归 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
|
+
}
|