@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.
Files changed (123) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/assets/api-BqVIq0lF.js +13 -0
  3. package/dist/assets/{appearance-settings-page-C_AqxSF_.js → appearance-settings-page-CTWn1eAT.js} +1 -1
  4. package/dist/assets/{book-open-DYRapW_h.js → book-open-o2x7OLiO.js} +1 -1
  5. package/dist/assets/{channels-list-page-uR-M4Ls8.js → channels-list-page-Bzn-pzyJ.js} +2 -2
  6. package/dist/assets/{config-split-page-DJDp86Ne.js → config-split-page-CnRbXXlh.js} +1 -1
  7. package/dist/assets/{confirm-dialog-CHVBMpla.js → confirm-dialog-B_Hbcra2.js} +1 -1
  8. package/dist/assets/{createLucideIcon-CgM0rnsa.js → createLucideIcon-rwwpqyks.js} +1 -1
  9. package/dist/assets/{desktop-capabilities-page-phMuNq1B.js → desktop-capabilities-page-C2ayYQTr.js} +1 -1
  10. package/dist/assets/desktop-update-config-C13czpyL.js +1 -0
  11. package/dist/assets/{dist-N9zc5bkM.js → dist-BsF3hfOi.js} +1 -1
  12. package/dist/assets/{dist-BjYvUhPL.js → dist-aNN3iQxV.js} +1 -1
  13. package/dist/assets/{doc-browser-DwRLokCI.js → doc-browser-D8dIOo9E.js} +1 -1
  14. package/dist/assets/doc-browser-DcuOOtpP.js +1 -0
  15. package/dist/assets/{doc-browser-context-BDuypObS.js → doc-browser-context-Dhsit2Ma.js} +1 -1
  16. package/dist/assets/{extensions-CQKXTqTI.js → extensions-CD6xvKF_.js} +1 -1
  17. package/dist/assets/{form-actions-CzGmrPAv.js → form-actions-CC4Wz_lL.js} +1 -1
  18. package/dist/assets/index-BTsbdtv6.js +109 -0
  19. package/dist/assets/index-Bcfy2kTh.css +1 -0
  20. package/dist/assets/loader-circle-B6aFbB-8.js +1 -0
  21. package/dist/assets/mcp-marketplace-page-BtUAypA7.js +1 -0
  22. package/dist/assets/{mcp-marketplace-page-s9PTnSDT.js → mcp-marketplace-page-r2MmD9xO.js} +1 -1
  23. package/dist/assets/{model-config-page-CxnCUWRW.js → model-config-page-DboxoT-I.js} +1 -1
  24. package/dist/assets/{navigation-link-Ci4Z7Phi.js → navigation-link-C_6oj3zz.js} +1 -1
  25. package/dist/assets/plus-DRsq9Kvs.js +1 -0
  26. package/dist/assets/privacy-settings-page-C2sDBqhb.js +1 -0
  27. package/dist/assets/projects-BcPcx-uD.js +1 -0
  28. package/dist/assets/providers-config-page-CmUFl9RF.js +1 -0
  29. package/dist/assets/react-BjO9I4WZ.js +8 -0
  30. package/dist/assets/{refresh-cw-C-3_thgr.js → refresh-cw-DsT2kvXi.js} +1 -1
  31. package/dist/assets/remote-Ci6x91ES.js +1 -0
  32. package/dist/assets/{rotate-cw-DSFTq_yu.js → rotate-cw-BCAs6c3R.js} +1 -1
  33. package/dist/assets/runtime-config-page-cQKNeAjL.js +1 -0
  34. package/dist/assets/{save-CpPr78_S.js → save-CadRMTMQ.js} +1 -1
  35. package/dist/assets/{search-msqk2-rd.js → search-B6tB4fzB.js} +1 -1
  36. package/dist/assets/{search-config-page-D3_qNXbB.js → search-config-page-C8lOIkxn.js} +1 -1
  37. package/dist/assets/{secrets-config-page-Be4P1E8o.js → secrets-config-page-8clzAag2.js} +2 -2
  38. package/dist/assets/security-config-DPbM6IVT.js +1 -0
  39. package/dist/assets/{select-DPKCWajZ.js → select-CO9S_C2C.js} +1 -1
  40. package/dist/assets/skeleton-IHILNhIW.js +1 -0
  41. package/dist/assets/{tag-chip-Dc_NcCEe.js → tag-chip-BZKk0UkK.js} +1 -1
  42. package/dist/assets/x-BuvHIb1d.js +1 -0
  43. package/dist/index.html +21 -21
  44. package/package.json +6 -6
  45. package/src/app/configs/__tests__/app-navigation.config.test.ts +20 -0
  46. package/src/app/configs/app-navigation.config.ts +10 -0
  47. package/src/app/index.tsx +8 -1
  48. package/src/features/apps/components/__tests__/app-packages-panel.test.tsx +362 -241
  49. package/src/features/apps/components/app-document-access-section.tsx +187 -0
  50. package/src/features/apps/components/app-package-card.tsx +150 -275
  51. package/src/features/apps/hooks/use-app-packages.ts +91 -35
  52. package/src/features/chat/components/conversation/__tests__/chat-conversation-workspace-section.test.tsx +29 -0
  53. package/src/features/chat/components/conversation/chat-conversation-workspace-section.tsx +4 -2
  54. package/src/features/chat/components/layout/__tests__/chat-page-shell.test.tsx +21 -0
  55. package/src/features/chat/components/layout/chat-page-shell.tsx +19 -4
  56. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +1 -1
  57. package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +0 -1
  58. package/src/features/chat/features/conversation/utils/__tests__/session-message-composer.utils.test.ts +11 -4
  59. package/src/features/chat/features/conversation/utils/__tests__/session-queued-input.utils.test.ts +0 -1
  60. package/src/features/chat/features/conversation/utils/session-message-composer.utils.ts +10 -11
  61. package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +1 -0
  62. package/src/features/chat/features/message/hooks/use-chat-message-actions.tsx +0 -2
  63. package/src/features/chat/features/runtime/utils/chat-runtime.utils.ts +0 -14
  64. package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +70 -22
  65. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +79 -32
  66. package/src/features/chat/features/session/utils/__tests__/chat-sidebar-session-groups.utils.test.ts +2 -0
  67. package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +0 -6
  68. package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +2 -0
  69. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +1 -0
  70. package/src/features/chat/index.ts +5 -1
  71. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +27 -0
  72. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +2 -14
  73. package/src/features/chat/types/chat-run.types.ts +0 -1
  74. package/src/features/projects/AGENTS.md +7 -0
  75. package/src/features/projects/components/project-agreement.tsx +92 -0
  76. package/src/features/projects/components/project-artifacts.test.tsx +77 -0
  77. package/src/features/projects/components/project-artifacts.tsx +188 -0
  78. package/src/features/projects/components/project-overview.tsx +81 -0
  79. package/src/features/projects/components/project-requests.tsx +57 -0
  80. package/src/features/projects/components/project-runs.tsx +60 -0
  81. package/src/features/projects/components/project-section.tsx +60 -0
  82. package/src/features/projects/components/project-skills.tsx +53 -0
  83. package/src/features/projects/components/work/project-work-item-drawer.test.tsx +65 -0
  84. package/src/features/projects/components/work/project-work-item-drawer.tsx +367 -0
  85. package/src/features/projects/components/work/project-work-items.test.tsx +98 -0
  86. package/src/features/projects/components/work/project-work-items.tsx +199 -0
  87. package/src/features/projects/components/work/project-work-state-settings.tsx +285 -0
  88. package/src/features/projects/hooks/use-project-observation.test.tsx +93 -0
  89. package/src/features/projects/hooks/use-project-observation.ts +115 -0
  90. package/src/features/projects/hooks/use-project-work.test.tsx +58 -0
  91. package/src/features/projects/hooks/use-project-work.ts +215 -0
  92. package/src/features/projects/index.ts +2 -0
  93. package/src/features/projects/pages/project-home-page.tsx +234 -0
  94. package/src/features/projects/pages/projects-page.test.tsx +137 -0
  95. package/src/features/projects/presenters/project-home.presenter.test.ts +74 -0
  96. package/src/features/projects/presenters/project-home.presenter.ts +55 -0
  97. package/src/features/projects/utils/project-artifact-view.utils.test.ts +63 -0
  98. package/src/features/projects/utils/project-artifact-view.utils.ts +62 -0
  99. package/src/features/projects/utils/project-request-response.utils.test.ts +44 -0
  100. package/src/features/projects/utils/project-request-response.utils.ts +39 -0
  101. package/src/shared/lib/i18n/index.ts +6 -2
  102. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +16 -1
  103. package/src/shared/lib/i18n/locales/en-US/projects.json +149 -0
  104. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +16 -1
  105. package/src/shared/lib/i18n/locales/zh-CN/projects.json +149 -0
  106. package/src/shared/lib/ui-document-title/__tests__/ui-document-title.test.ts +6 -0
  107. package/src/shared/lib/ui-document-title/index.ts +1 -0
  108. package/dist/assets/api-n96IfCKb.js +0 -13
  109. package/dist/assets/desktop-update-config-C77hkbor.js +0 -1
  110. package/dist/assets/doc-browser-JlXVLNRe.js +0 -1
  111. package/dist/assets/index-CLw9DuCJ.js +0 -109
  112. package/dist/assets/index-CwCTiMM9.css +0 -1
  113. package/dist/assets/loader-circle-D9etMT9f.js +0 -1
  114. package/dist/assets/mcp-marketplace-page-CbWOamyW.js +0 -1
  115. package/dist/assets/plus-DKGD04AO.js +0 -1
  116. package/dist/assets/privacy-settings-page-D8HZHHjL.js +0 -1
  117. package/dist/assets/providers-config-page-C0GIoGxm.js +0 -1
  118. package/dist/assets/react-CVFx225w.js +0 -8
  119. package/dist/assets/remote-DvTCSAxQ.js +0 -1
  120. package/dist/assets/runtime-config-page-VNJZui5f.js +0 -1
  121. package/dist/assets/security-config-BtcpKIhQ.js +0 -1
  122. package/dist/assets/skeleton-BsR5rwB7.js +0 -1
  123. 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 className="group/project relative h-8 rounded-lg px-2 text-muted-foreground transition-colors hover:bg-gray-200/60 hover:text-gray-900">
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
- <button
104
- type="button"
105
- aria-expanded={!isCollapsed}
106
- aria-label={t(
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
- <Folder className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
119
- <span
120
- className="truncate text-[11px] font-medium uppercase tracking-wider"
121
- title={group.projectRoot}
122
- >
123
- {group.projectName}
124
- </span>
125
- {isCollapsed ? (
126
- <ChevronRight
127
- className="h-3.5 w-3.5 shrink-0 opacity-0 transition-opacity group-hover/project:opacity-100 group-focus-within/project:opacity-100"
128
- aria-hidden="true"
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
- <ChevronDown
132
- className="h-3.5 w-3.5 shrink-0 opacity-0 transition-opacity group-hover/project:opacity-100 group-focus-within/project:opacity-100"
133
- aria-hidden="true"
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
- </button>
175
+ </div>
137
176
  </ChatSidebarContextCard>
138
- <div 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">
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}
@@ -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 { buildSessionPath, CHAT_DRAFT_SESSION_PATH } from "./features/session/utils/chat-session-route.utils";
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('sends requested skill refs instead of legacy requested skill names', () => {
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
- })).toMatchObject({
208
- requested_skill_refs: ['workspace:/tmp/workspace/skills/review'],
196
+ })).toEqual({
209
197
  ui_inline_tokens: {
210
198
  schemaVersion: 2,
211
199
  items: [{
@@ -16,7 +16,6 @@ export type SendMessageParams = {
16
16
  model?: string;
17
17
  thinkingLevel?: ThinkingLevel;
18
18
  projectRoot?: string | null;
19
- requestedSkills?: string[];
20
19
  attachments?: NcpDraftAttachment[];
21
20
  parts?: NcpMessagePart[];
22
21
  composerNodes?: ChatComposerNode[];
@@ -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
+ });