@nextclaw/ui 0.24.2 → 0.24.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/.vite/manifest.json +517 -517
  3. package/dist/assets/{account-panel-BxMlLXYH.js → account-panel-BRTSwNvA.js} +1 -1
  4. package/dist/assets/{api-Utt-xzrT.js → api-BnUzdltV.js} +5 -5
  5. package/dist/assets/{app-data-DYXDL6X2.js → app-data-Bjh_zM45.js} +1 -1
  6. package/dist/assets/app-presenter-provider-CBt-lyr_.js +109 -0
  7. package/dist/assets/{appearance-settings-page-BfYznAK5.js → appearance-settings-page-BrOYSgG0.js} +1 -1
  8. package/dist/assets/{book-open-DDW_3XpM.js → book-open-xqEj1OLE.js} +1 -1
  9. package/dist/assets/{boxes-DwJeuK_h.js → boxes-D6uPnkGo.js} +1 -1
  10. package/dist/assets/{card-IevY2nm-.js → card-CS8su2Zh.js} +1 -1
  11. package/dist/assets/{channels-list-page-DitnqbMR.js → channels-list-page-BE0TxB3B.js} +1 -1
  12. package/dist/assets/{chevron-down-An7Km-t4.js → chevron-down-BL-yMYcX.js} +1 -1
  13. package/dist/assets/{circle-alert-CDXqELuo.js → circle-alert-lSa3LRvS.js} +1 -1
  14. package/dist/assets/{circle-dashed-CapXEpiV.js → circle-dashed-CU3yApvP.js} +1 -1
  15. package/dist/assets/{client-B0krol7A.js → client-BtGBwLTs.js} +1 -1
  16. package/dist/assets/{config-split-page-BtnkqWVp.js → config-split-page-BxDwwAGa.js} +1 -1
  17. package/dist/assets/{confirm-dialog-DFt4jpN7.js → confirm-dialog-S5JxZcS1.js} +1 -1
  18. package/dist/assets/{createLucideIcon-BTHK0Ot9.js → createLucideIcon-BNKf0_kc.js} +1 -1
  19. package/dist/assets/{desktop-capabilities-page-CCf14WId.js → desktop-capabilities-page-BwHxJpbl.js} +1 -1
  20. package/dist/assets/{desktop-update-config-DVoVgfFi.js → desktop-update-config-Bfg7OdJ2.js} +1 -1
  21. package/dist/assets/{dialog-BFN_1DBH.js → dialog-Xe73hpQz.js} +1 -1
  22. package/dist/assets/{dist-Bc9hOVNN.js → dist-BiIcnf9j.js} +1 -1
  23. package/dist/assets/{dist-CEhIDAnr.js → dist-C5I0v8Qn.js} +1 -1
  24. package/dist/assets/{dist-hkVPP1th.js → dist-czPa8xFO.js} +1 -1
  25. package/dist/assets/doc-browser-BgRP0Wni.js +1 -0
  26. package/dist/assets/{doc-browser-BNUxmwD6.js → doc-browser-C8eqJLi_.js} +1 -1
  27. package/dist/assets/{doc-browser-context-C4yQrKrH.js → doc-browser-context-BPCDFcC-.js} +1 -1
  28. package/dist/assets/{ellipsis-BRI6ROeu.js → ellipsis-C5YXdOUx.js} +1 -1
  29. package/dist/assets/{es2015-MOCbM0Eu.js → es2015-cuwbYI-l.js} +1 -1
  30. package/dist/assets/{extensions-DxUBMtHW.js → extensions-DgKQRebe.js} +1 -1
  31. package/dist/assets/{eye-cnynUDGt.js → eye-C84inPcV.js} +1 -1
  32. package/dist/assets/{form-actions-gQ0JEUwB.js → form-actions-AIdXOoCL.js} +1 -1
  33. package/dist/assets/{i18n-B9yXA8KO.js → i18n-CKvm9zyk.js} +2 -2
  34. package/dist/assets/{i18n-provider-BYBJua91.js → i18n-provider-CPV43pMX.js} +1 -1
  35. package/dist/assets/loader-circle-K1BDdHR5.js +1 -0
  36. package/dist/assets/login-page-ClcrvN04.js +1 -0
  37. package/dist/assets/{login-page-CqrHhzaN.js → login-page-DeDQjVxv.js} +1 -1
  38. package/dist/assets/{logo-badge-Cr03YceJ.js → logo-badge-C-5-zim6.js} +1 -1
  39. package/dist/assets/main-DGqlaAaI.css +1 -0
  40. package/dist/assets/main-i18n-catalog.config-CpNF5aVc.js +1 -0
  41. package/dist/assets/main-nMVfHb5K.js +2 -0
  42. package/dist/assets/{mcp-marketplace-page-CifiVgFD.js → mcp-marketplace-page-CWjJGp_D.js} +1 -1
  43. package/dist/assets/mcp-marketplace-page-fvu2NcXT.js +1 -0
  44. package/dist/assets/{model-config-page-C6jVNbSj.js → model-config-page-Dv_zE7HN.js} +1 -1
  45. package/dist/assets/{navigation-link-kbCLRSr_.js → navigation-link-BAQjgx2F.js} +1 -1
  46. package/dist/assets/panel-app-i18n-catalog.config-BzxE3tZb.js +1 -0
  47. package/dist/assets/{panelStandalone-DMBnYyTw.js → panelStandalone-CFRBHV94.js} +2 -2
  48. package/dist/assets/plus-LA8-jpxP.js +1 -0
  49. package/dist/assets/{popover-XVDgjxcl.js → popover-NkrFlWHp.js} +1 -1
  50. package/dist/assets/{privacy-settings-page-BN27MBVA.js → privacy-settings-page-D6uc9f-A.js} +1 -1
  51. package/dist/assets/projects-BD1rG75R.js +1 -0
  52. package/dist/assets/{provider-scoped-model-input-BGZMCThn.js → provider-scoped-model-input-DUa_9uwg.js} +1 -1
  53. package/dist/assets/{providers-config-page-D5aBwbOD.js → providers-config-page-DAiqxlF_.js} +1 -1
  54. package/dist/assets/{puzzle-D5Dhacmb.js → puzzle-D6gulz7u.js} +1 -1
  55. package/dist/assets/{pwa-ClQK56Qe.js → pwa-C_Zte1xe.js} +1 -1
  56. package/dist/assets/{refresh-cw-Bw5BY7vp.js → refresh-cw-h9Xq3LY3.js} +1 -1
  57. package/dist/assets/remote-BO0QP7ua.js +1 -0
  58. package/dist/assets/{rotate-cw-BksCLAeE.js → rotate-cw-B6xm41Wn.js} +1 -1
  59. package/dist/assets/runtime-config-page-w1UAkGF_.js +1 -0
  60. package/dist/assets/{save-CVi8VWzb.js → save-fff_WpY2.js} +1 -1
  61. package/dist/assets/{search-DN-f7IIC.js → search-CUe41wjm.js} +1 -1
  62. package/dist/assets/{search-config-page-BmuZTVxs.js → search-config-page-3xbhRlW2.js} +1 -1
  63. package/dist/assets/{secrets-config-page-NPHpg9sb.js → secrets-config-page-Dn8_rdOj.js} +1 -1
  64. package/dist/assets/security-config-Y1nEGM3A.js +1 -0
  65. package/dist/assets/{select-CmDTqk2C.js → select-CG5YWnwe.js} +1 -1
  66. package/dist/assets/{service-apps-panel-BYGJ9_pO.js → service-apps-panel-CGc04aMn.js} +1 -1
  67. package/dist/assets/{settings-2-B62YtZon.js → settings-2-CE3ELQyk.js} +1 -1
  68. package/dist/assets/skeleton-BL9h32W7.js +1 -0
  69. package/dist/assets/{tabs-Bk_mG7ql.js → tabs-BuE0qKMh.js} +1 -1
  70. package/dist/assets/{tag-chip-DgqKlI5D.js → tag-chip-BDzpHEli.js} +1 -1
  71. package/dist/assets/{theme-provider-CeBY2pyu.js → theme-provider-B6FinATQ.js} +1 -1
  72. package/dist/assets/{tooltip-CgGbBdYf.js → tooltip-DYcuWhuP.js} +1 -1
  73. package/dist/assets/{trash-2-CVOev8yc.js → trash-2-BPPYt2NV.js} +1 -1
  74. package/dist/assets/{use-config-b2DN67gs.js → use-config-BPUM2lbo.js} +1 -1
  75. package/dist/assets/{use-desktop-capabilities-B3tLU1yl.js → use-desktop-capabilities-BDjArQP3.js} +1 -1
  76. package/dist/assets/{use-panel-app-scroll-restoration-BekzoX6z.js → use-panel-app-scroll-restoration-DsV1IJx-.js} +1 -1
  77. package/dist/assets/{use-viewport-layout-CUA3jQsA.js → use-viewport-layout-BS18drMT.js} +1 -1
  78. package/dist/assets/{utils-UBHTdG5Q.js → utils-CBZPzC_b.js} +1 -1
  79. package/dist/assets/x-DdXOqLnX.js +1 -0
  80. package/dist/index.html +50 -50
  81. package/dist/panel-standalone.html +15 -15
  82. package/package.json +3 -3
  83. package/src/features/chat/features/message/utils/__tests__/chat-message-timeline.utils.test.ts +10 -0
  84. package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +90 -6
  85. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +64 -2
  86. package/src/features/panel-apps/components/__tests__/panel-app-open-standalone-menu-item.test.tsx +3 -2
  87. package/src/features/panel-apps/components/panel-app-open-standalone-menu-item.tsx +4 -1
  88. package/src/features/projects/AGENTS.md +1 -2
  89. package/src/features/projects/components/project-agreement.tsx +36 -77
  90. package/src/features/projects/components/project-artifacts.test.tsx +90 -62
  91. package/src/features/projects/components/project-artifacts.tsx +81 -160
  92. package/src/features/projects/components/project-materials.test.tsx +80 -0
  93. package/src/features/projects/components/project-overview.test.tsx +50 -40
  94. package/src/features/projects/components/project-overview.tsx +5 -4
  95. package/src/features/projects/components/project-skills.tsx +24 -14
  96. package/src/features/projects/components/work/project-work-item-drawer.test.tsx +3 -2
  97. package/src/features/projects/components/work/project-work-items.test.tsx +13 -1
  98. package/src/features/projects/components/work/project-work-items.tsx +7 -7
  99. package/src/features/projects/hooks/use-project-materials.test.tsx +65 -0
  100. package/src/features/projects/hooks/use-project-materials.ts +24 -0
  101. package/src/features/projects/hooks/use-project-work.test.tsx +57 -1
  102. package/src/features/projects/hooks/use-project-work.ts +11 -5
  103. package/src/features/projects/pages/project-home-page.tsx +51 -127
  104. package/src/features/projects/pages/projects-page.test.tsx +68 -75
  105. package/src/features/projects/presenters/project-home.presenter.test.ts +1 -64
  106. package/src/features/projects/presenters/project-home.presenter.ts +17 -54
  107. package/src/features/projects/utils/project-artifact-view.utils.test.ts +0 -46
  108. package/src/features/projects/utils/project-artifact-view.utils.ts +0 -47
  109. package/src/shared/lib/i18n/locales/en-US/projects.json +8 -59
  110. package/src/shared/lib/i18n/locales/zh-CN/projects.json +8 -59
  111. package/dist/assets/app-presenter-provider-CLsFiXvV.js +0 -109
  112. package/dist/assets/doc-browser-w4eGZXLu.js +0 -1
  113. package/dist/assets/loader-circle-rWr5Yo0E.js +0 -1
  114. package/dist/assets/login-page-CfMSkOsg.js +0 -1
  115. package/dist/assets/main-8cVkMfgE.css +0 -1
  116. package/dist/assets/main-Bys58GxY.js +0 -2
  117. package/dist/assets/main-i18n-catalog.config-BSq9yiUQ.js +0 -1
  118. package/dist/assets/mcp-marketplace-page-DX5VE3mU.js +0 -1
  119. package/dist/assets/panel-app-i18n-catalog.config-C2St67SP.js +0 -1
  120. package/dist/assets/plus-BHnKwuPq.js +0 -1
  121. package/dist/assets/projects-DV3UuJMS.js +0 -1
  122. package/dist/assets/remote-Bl005fzu.js +0 -1
  123. package/dist/assets/runtime-config-page-CmktQcMW.js +0 -1
  124. package/dist/assets/security-config-Djptuyf2.js +0 -1
  125. package/dist/assets/skeleton-Eh64l4T2.js +0 -1
  126. package/dist/assets/x-C0iaOsRW.js +0 -1
  127. package/src/features/projects/components/project-requests.tsx +0 -57
  128. package/src/features/projects/components/project-runs.tsx +0 -60
  129. package/src/features/projects/hooks/use-project-observation.test.tsx +0 -93
  130. package/src/features/projects/hooks/use-project-observation.ts +0 -115
  131. package/src/features/projects/utils/project-request-response.utils.test.ts +0 -44
  132. package/src/features/projects/utils/project-request-response.utils.ts +0 -39
@@ -1,13 +1,8 @@
1
1
  import { useState } from "react";
2
2
  import { useNavigate, useParams } from "react-router-dom";
3
- import { useMutation, useQueryClient } from "@tanstack/react-query";
4
- import type { ObservedRequest } from "@nextclaw/client-sdk";
5
- import { toast } from "sonner";
6
- import { useProjects, useRemoveProject } from "@/shared/hooks/use-projects";
3
+ import { useProjects } from "@/shared/hooks/use-projects";
7
4
  import { useViewportLayout } from "@/app/hooks/use-viewport-layout";
8
5
  import { t } from "@/shared/lib/i18n";
9
- import { Button } from "@/shared/components/ui/button";
10
- import { useConfirmDialog } from "@/shared/hooks/use-confirm-dialog";
11
6
  import {
12
7
  Tabs,
13
8
  TabsContent,
@@ -17,7 +12,6 @@ import {
17
12
  import { ProjectAgreement } from "@/features/projects/components/project-agreement";
18
13
  import { ProjectArtifacts } from "@/features/projects/components/project-artifacts";
19
14
  import { ProjectOverview } from "@/features/projects/components/project-overview";
20
- import { ProjectRequests } from "@/features/projects/components/project-requests";
21
15
  import { ProjectSkills } from "@/features/projects/components/project-skills";
22
16
  import { ProjectWorkItemDrawer } from "@/features/projects/components/work/project-work-item-drawer";
23
17
  import { ProjectWorkItems } from "@/features/projects/components/work/project-work-items";
@@ -26,18 +20,13 @@ import {
26
20
  usePresenter,
27
21
  } from "@/features/chat";
28
22
  import {
29
- projectObservationQueryKey,
30
- useProjectObservation,
31
- } from "@/features/projects/hooks/use-project-observation";
23
+ useProjectAgreement,
24
+ useProjectSkills,
25
+ } from "@/features/projects/hooks/use-project-materials";
32
26
  import {
33
- getOpenProjectRequests,
34
27
  isProjectHomeTab,
35
28
  type ProjectHomeTab,
36
29
  } from "@/features/projects/presenters/project-home.presenter";
37
- import {
38
- sendProjectRequestResponse,
39
- type ProjectRequestDecision,
40
- } from "@/features/projects/utils/project-request-response.utils";
41
30
  import { joinProjectPath } from "@/features/projects/utils/project-artifact-view.utils";
42
31
 
43
32
  const PROJECT_TABS: ProjectHomeTab[] = [
@@ -55,73 +44,23 @@ export function ProjectsPage() {
55
44
  }>();
56
45
  const navigate = useNavigate();
57
46
  const presenter = usePresenter();
58
- const queryClient = useQueryClient();
59
47
  const { isMobile } = useViewportLayout();
60
48
  const projects = useProjects();
61
- const projectRemoval = useRemoveProject();
62
- const { confirm, ConfirmDialog } = useConfirmDialog();
63
49
  const [selectedWorkItemId, setSelectedWorkItemId] = useState<string | null>(
64
50
  null,
65
51
  );
66
- const [pendingRequestId, setPendingRequestId] = useState<string | null>(null);
67
52
  const registered = projects.data?.projects ?? [];
68
53
  const selectedProject =
69
54
  registered.find((project) => project.id === projectId) ?? null;
70
55
  const tab: ProjectHomeTab | null = isProjectHomeTab(tabParam)
71
56
  ? tabParam
72
57
  : null;
73
- const needsObservation =
74
- tab === "artifacts" || tab === "skills" || tab === "agreement";
75
- const observation = useProjectObservation(
76
- selectedProject?.id ?? null,
77
- selectedProject?.rootPath ?? null,
78
- needsObservation,
58
+ const agreement = useProjectAgreement(
59
+ tab === "agreement" ? (selectedProject?.id ?? null) : null,
60
+ );
61
+ const skills = useProjectSkills(
62
+ tab === "skills" ? (selectedProject?.id ?? null) : null,
79
63
  );
80
- const response = useMutation({
81
- mutationFn: sendProjectRequestResponse,
82
- onMutate: (input) => setPendingRequestId(input.requestId),
83
- onSuccess: async () => {
84
- if (selectedProject)
85
- await queryClient.invalidateQueries({
86
- queryKey: projectObservationQueryKey(selectedProject.id),
87
- });
88
- toast.success(t("projectsResponseSent"));
89
- },
90
- onError: (error) =>
91
- toast.error(
92
- `${t("projectsResponseFailed")}: ${error instanceof Error ? error.message : String(error)}`,
93
- ),
94
- onSettled: () => setPendingRequestId(null),
95
- });
96
- const reply = (
97
- request: ObservedRequest,
98
- decision: ProjectRequestDecision,
99
- ) => {
100
- if (!request.reference.sessionId) return;
101
- response.mutate({
102
- requestId: request.id,
103
- sessionId: request.reference.sessionId,
104
- decision,
105
- prompt: request.prompt,
106
- });
107
- };
108
- const removeSelectedProject = async () => {
109
- if (!selectedProject) return;
110
- const confirmed = await confirm({
111
- title: `${t("projectsRemoveConfirmTitle")} “${selectedProject.name}”`,
112
- description: t("projectsRemoveConfirmDescription"),
113
- confirmLabel: t("projectsRemoveConfirmAction"),
114
- variant: "destructive",
115
- });
116
- if (!confirmed) return;
117
- try {
118
- await projectRemoval.mutateAsync(selectedProject.id);
119
- navigate("/projects");
120
- } catch {
121
- // The mutation owns the visible error and the current project stays open.
122
- }
123
- };
124
-
125
64
  if (projects.isLoading)
126
65
  return (
127
66
  <main className="flex h-full items-center justify-center p-6 text-sm text-muted-foreground">
@@ -154,35 +93,16 @@ export function ProjectsPage() {
154
93
  viewMode: "preview",
155
94
  previewViewer: "rendered",
156
95
  });
157
- const snapshot = observation.data;
158
96
  return (
159
97
  <>
160
98
  <main className="h-full min-w-0 flex-1 overflow-y-auto p-3 sm:p-4 md:p-6">
161
99
  <div className="mx-auto max-w-6xl space-y-4">
162
- <header className="flex flex-col gap-3 rounded-2xl border border-border/60 bg-card p-3 sm:flex-row sm:items-start sm:justify-between sm:p-4">
163
- <div className="min-w-0">
164
- <h1 className="text-lg font-semibold">{selectedProject.name}</h1>
165
- <p className="mt-1 break-all text-xs text-muted-foreground">
166
- {selectedProject.rootPath}
167
- </p>
168
- </div>
169
- <Button
170
- type="button"
171
- variant="destructive"
172
- size="sm"
173
- disabled={projectRemoval.isPending}
174
- onClick={() => void removeSelectedProject()}
175
- >
176
- {t("projectsRemoveFromList")}
177
- </Button>
100
+ <header className="rounded-2xl border border-border/60 bg-card p-3 sm:p-4">
101
+ <h1 className="text-lg font-semibold">{selectedProject.name}</h1>
102
+ <p className="mt-1 break-all text-xs text-muted-foreground">
103
+ {selectedProject.rootPath}
104
+ </p>
178
105
  </header>
179
- {snapshot ? (
180
- <ProjectRequests
181
- requests={getOpenProjectRequests(snapshot)}
182
- pendingRequestId={pendingRequestId}
183
- onReply={reply}
184
- />
185
- ) : null}
186
106
  <Tabs
187
107
  value={tab}
188
108
  onValueChange={(value) =>
@@ -218,38 +138,43 @@ export function ProjectsPage() {
218
138
  onOpenWorkItem={setSelectedWorkItemId}
219
139
  />
220
140
  </TabsContent>
221
- {needsObservation && observation.isLoading ? (
222
- <div className="mt-4 rounded-xl border border-border/60 p-5 text-sm text-muted-foreground">
223
- {t("projectsLoading")}
224
- </div>
225
- ) : null}
226
- {needsObservation && observation.isError ? (
227
- <div className="mt-4 rounded-xl border border-destructive/40 p-5 text-sm text-destructive">
228
- {t("projectsLoadFailed")}: {observation.error.message}
229
- </div>
230
- ) : null}
231
- {snapshot ? (
232
- <>
233
- <TabsContent value="artifacts" className="mt-4">
234
- <ProjectArtifacts
235
- snapshot={snapshot}
236
- onOpenFile={openProjectFile}
237
- />
238
- </TabsContent>
239
- <TabsContent value="skills" className="mt-4">
240
- <ProjectSkills
241
- snapshot={snapshot}
242
- onOpen={(skill) => openProjectFile(skill.path, skill.name)}
243
- />
244
- </TabsContent>
245
- <TabsContent value="agreement" className="mt-4">
246
- <ProjectAgreement
247
- snapshot={snapshot}
248
- onOpenFile={openProjectFile}
249
- />
250
- </TabsContent>
251
- </>
252
- ) : null}
141
+ <TabsContent value="artifacts" className="mt-4">
142
+ <ProjectArtifacts
143
+ projectId={selectedProject.id}
144
+ onOpenFile={(path, label) =>
145
+ openProjectFile(
146
+ joinProjectPath(selectedProject.rootPath, path),
147
+ label,
148
+ )
149
+ }
150
+ />
151
+ </TabsContent>
152
+ <TabsContent value="skills" className="mt-4">
153
+ <ProjectSkills
154
+ skills={skills.data ?? []}
155
+ isLoading={skills.isLoading}
156
+ isError={skills.isError}
157
+ onOpen={(skill) =>
158
+ openProjectFile(
159
+ joinProjectPath(selectedProject.rootPath, skill.path),
160
+ skill.name,
161
+ )
162
+ }
163
+ />
164
+ </TabsContent>
165
+ <TabsContent value="agreement" className="mt-4">
166
+ <ProjectAgreement
167
+ agreement={agreement.data}
168
+ isLoading={agreement.isLoading}
169
+ isError={agreement.isError}
170
+ onOpenFile={(path, label) =>
171
+ openProjectFile(
172
+ joinProjectPath(selectedProject.rootPath, path),
173
+ label,
174
+ )
175
+ }
176
+ />
177
+ </TabsContent>
253
178
  </Tabs>
254
179
  </div>
255
180
  </main>
@@ -267,7 +192,6 @@ export function ProjectsPage() {
267
192
  sessionKey={null}
268
193
  projectRoot={selectedProject.rootPath}
269
194
  />
270
- <ConfirmDialog />
271
195
  </>
272
196
  );
273
197
  }
@@ -5,32 +5,21 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
5
5
  import { ProjectsPage } from "./project-home-page";
6
6
 
7
7
  const mocks = vi.hoisted(() => ({
8
- confirm: vi.fn(),
8
+ agreementProps: vi.fn(),
9
+ artifactsProps: vi.fn(),
9
10
  drawerProps: vi.fn(),
10
- removeProject: vi.fn(),
11
- useProjectObservation: vi.fn(),
11
+ skillsProps: vi.fn(),
12
+ useProjectAgreement: vi.fn(),
13
+ useProjectSkills: vi.fn(),
12
14
  useProjects: vi.fn(),
13
15
  }));
14
16
 
15
17
  vi.mock("@/shared/hooks/use-projects", () => ({
16
18
  useProjects: mocks.useProjects,
17
- useRemoveProject: () => ({
18
- mutateAsync: mocks.removeProject,
19
- isPending: false,
20
- }),
21
19
  }));
22
- vi.mock("@/shared/hooks/use-confirm-dialog", () => ({
23
- useConfirmDialog: () => ({
24
- confirm: mocks.confirm,
25
- ConfirmDialog: () => <div data-testid="confirm-dialog" />,
26
- }),
27
- }));
28
- vi.mock("@/features/projects/hooks/use-project-observation", () => ({
29
- projectObservationQueryKey: (projectId: string) => [
30
- "project-observation",
31
- projectId,
32
- ],
33
- useProjectObservation: mocks.useProjectObservation,
20
+ vi.mock("@/features/projects/hooks/use-project-materials", () => ({
21
+ useProjectAgreement: mocks.useProjectAgreement,
22
+ useProjectSkills: mocks.useProjectSkills,
34
23
  }));
35
24
  vi.mock("@/features/chat", () => ({
36
25
  ChatConversationWorkspaceSection: () => <div data-testid="workspace" />,
@@ -64,18 +53,23 @@ vi.mock("@/features/projects/components/work/project-work-item-drawer", () => ({
64
53
  },
65
54
  }));
66
55
  vi.mock("@/features/projects/components/project-artifacts", () => ({
67
- ProjectArtifacts: () => <div>Artifact content</div>,
56
+ ProjectArtifacts: (props: { projectId: string }) => {
57
+ mocks.artifactsProps(props);
58
+ return <div>Artifact content</div>;
59
+ },
68
60
  }));
69
61
  vi.mock("@/features/projects/components/project-skills", () => ({
70
- ProjectSkills: () => <div>Skills content</div>,
62
+ ProjectSkills: (props: { skills: unknown[] }) => {
63
+ mocks.skillsProps(props);
64
+ return <div>Skills content</div>;
65
+ },
71
66
  }));
72
67
  vi.mock("@/features/projects/components/project-agreement", () => ({
73
- ProjectAgreement: () => <div>Agreement content</div>,
74
- }));
75
- vi.mock("@/features/projects/components/project-requests", () => ({
76
- ProjectRequests: () => null,
68
+ ProjectAgreement: (props: { agreement?: unknown }) => {
69
+ mocks.agreementProps(props);
70
+ return <div>Agreement content</div>;
71
+ },
77
72
  }));
78
-
79
73
  function renderPage(path: string) {
80
74
  return render(
81
75
  <QueryClientProvider client={new QueryClient()}>
@@ -92,10 +86,9 @@ function renderPage(path: string) {
92
86
  describe("ProjectsPage", () => {
93
87
  beforeEach(() => {
94
88
  mocks.drawerProps.mockReset();
95
- mocks.confirm.mockReset();
96
- mocks.confirm.mockResolvedValue(true);
97
- mocks.removeProject.mockReset();
98
- mocks.removeProject.mockResolvedValue(undefined);
89
+ mocks.agreementProps.mockReset();
90
+ mocks.artifactsProps.mockReset();
91
+ mocks.skillsProps.mockReset();
99
92
  mocks.useProjects.mockReturnValue({
100
93
  data: {
101
94
  projects: [
@@ -105,38 +98,35 @@ describe("ProjectsPage", () => {
105
98
  isLoading: false,
106
99
  isError: false,
107
100
  });
108
- mocks.useProjectObservation.mockReturnValue({
101
+ mocks.useProjectAgreement.mockReturnValue({
102
+ data: undefined,
103
+ isLoading: false,
104
+ isError: false,
105
+ });
106
+ mocks.useProjectSkills.mockReturnValue({
109
107
  data: undefined,
110
108
  isLoading: false,
111
109
  isError: false,
112
110
  });
113
111
  });
114
112
 
115
- it("keeps overview scan-free and opens its work item in the unified drawer", () => {
113
+ it("keeps overview material-free and opens its work item in the unified drawer", () => {
116
114
  renderPage("/projects/project-1/overview");
117
115
 
118
- expect(mocks.useProjectObservation).toHaveBeenCalledWith(
119
- "project-1",
120
- "/tmp/research",
121
- false,
122
- );
116
+ expect(mocks.useProjectAgreement).toHaveBeenCalledWith(null);
117
+ expect(mocks.useProjectSkills).toHaveBeenCalledWith(null);
123
118
  expect(screen.getAllByRole("tab")).toHaveLength(5);
124
119
  fireEvent.click(screen.getByRole("button", { name: "Overview work" }));
125
120
  expect(screen.getByTestId("work-drawer").textContent).toBe("work-overview");
126
121
  });
127
122
 
128
- it("enables legacy observation only for preserved artifact, skill, and agreement surfaces", () => {
129
- mocks.useProjectObservation.mockReturnValue({
130
- data: { requests: [] },
131
- isLoading: false,
132
- isError: false,
133
- });
123
+ it("loads artifacts from project work without material observation", () => {
134
124
  renderPage("/projects/project-1/artifacts");
135
125
 
136
- expect(mocks.useProjectObservation).toHaveBeenCalledWith(
137
- "project-1",
138
- "/tmp/research",
139
- true,
126
+ expect(mocks.useProjectAgreement).toHaveBeenCalledWith(null);
127
+ expect(mocks.useProjectSkills).toHaveBeenCalledWith(null);
128
+ expect(mocks.artifactsProps).toHaveBeenCalledWith(
129
+ expect.objectContaining({ projectId: "project-1" }),
140
130
  );
141
131
  expect(screen.getByText("Artifact content")).toBeTruthy();
142
132
  expect(screen.getByRole("tab", { name: /Skills/ })).toBeTruthy();
@@ -147,47 +137,50 @@ describe("ProjectsPage", () => {
147
137
 
148
138
  it("asks the user to choose from the sidebar when no project route is selected", () => {
149
139
  renderPage("/projects");
150
- expect(mocks.useProjectObservation).toHaveBeenCalledWith(null, null, false);
140
+ expect(mocks.useProjectAgreement).toHaveBeenCalledWith(null);
141
+ expect(mocks.useProjectSkills).toHaveBeenCalledWith(null);
151
142
  expect(screen.getByText(/Choose a project|选择项目/)).toBeTruthy();
152
143
  });
153
144
 
154
- it("explains the impact before removing a project and returns to project selection", async () => {
155
- renderPage("/projects/project-1/overview");
156
-
157
- fireEvent.click(
158
- screen.getByRole("button", {
159
- name: /Remove from project list|从项目列表移除/,
145
+ it("loads each fixed project material only on its own tab", () => {
146
+ mocks.useProjectAgreement.mockReturnValue({
147
+ data: { path: "AGENTS.md", available: true },
148
+ isLoading: false,
149
+ isError: false,
150
+ });
151
+ const { unmount } = renderPage("/projects/project-1/agreement");
152
+ expect(mocks.useProjectAgreement).toHaveBeenCalledWith("project-1");
153
+ expect(mocks.useProjectSkills).toHaveBeenCalledWith(null);
154
+ expect(mocks.agreementProps).toHaveBeenCalledWith(
155
+ expect.objectContaining({
156
+ agreement: { path: "AGENTS.md", available: true },
160
157
  }),
161
158
  );
159
+ unmount();
162
160
 
163
- await vi.waitFor(() =>
164
- expect(mocks.confirm).toHaveBeenCalledWith(
165
- expect.objectContaining({
166
- description: expect.stringMatching(/local folder|本地目录/),
167
- variant: "destructive",
168
- }),
169
- ),
170
- );
171
- await vi.waitFor(() =>
172
- expect(mocks.removeProject).toHaveBeenCalledWith("project-1"),
173
- );
174
- await vi.waitFor(() =>
175
- expect(screen.getByText(/Choose a project|选择项目/)).toBeTruthy(),
161
+ mocks.useProjectSkills.mockReturnValue({
162
+ data: [{ ref: "project:alpha", name: "alpha" }],
163
+ isLoading: false,
164
+ isError: false,
165
+ });
166
+ renderPage("/projects/project-1/skills");
167
+ expect(mocks.useProjectAgreement).toHaveBeenLastCalledWith(null);
168
+ expect(mocks.useProjectSkills).toHaveBeenLastCalledWith("project-1");
169
+ expect(mocks.skillsProps).toHaveBeenCalledWith(
170
+ expect.objectContaining({
171
+ skills: [{ ref: "project:alpha", name: "alpha" }],
172
+ }),
176
173
  );
177
174
  });
178
175
 
179
- it("keeps the project when removal is cancelled", async () => {
180
- mocks.confirm.mockResolvedValue(false);
176
+ it("does not expose project removal as a persistent detail-page action", () => {
181
177
  renderPage("/projects/project-1/overview");
182
178
 
183
- fireEvent.click(
184
- screen.getByRole("button", {
179
+ expect(
180
+ screen.queryByRole("button", {
185
181
  name: /Remove from project list|从项目列表移除/,
186
182
  }),
187
- );
188
-
189
- await vi.waitFor(() => expect(mocks.confirm).toHaveBeenCalledOnce());
190
- expect(mocks.removeProject).not.toHaveBeenCalled();
183
+ ).toBeNull();
191
184
  expect(screen.getByText("Research")).toBeTruthy();
192
185
  });
193
186
  });
@@ -1,17 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import type { ObservedRequest, ObservedWorkItem, ObservedWorkflow } from "@nextclaw/client-sdk";
3
- import {
4
- canReplyToProjectRequest,
5
- createProjectBoardColumns,
6
- getScheduledWorkItems,
7
- isProjectHomeTab,
8
- } from "./project-home.presenter";
9
-
10
- const reference = {
11
- kind: "ai-report" as const,
12
- label: "AI report",
13
- observedAt: "2026-08-30T00:00:00.000Z",
14
- };
2
+ import { isProjectHomeTab } from "./project-home.presenter";
15
3
 
16
4
  describe("project home presenter", () => {
17
5
  it("accepts only the project home tabs represented in the route", () => {
@@ -20,55 +8,4 @@ describe("project home presenter", () => {
20
8
  expect(isProjectHomeTab("missing")).toBe(false);
21
9
  expect(isProjectHomeTab(undefined)).toBe(false);
22
10
  });
23
-
24
- it("projects the same work items into board and scheduled views without inventing facts", () => {
25
- const workflows: ObservedWorkflow[] = [{
26
- id: "research",
27
- label: "Research",
28
- stages: [{ id: "draft", label: "Draft" }],
29
- reference,
30
- }];
31
- const workItems: ObservedWorkItem[] = [
32
- {
33
- id: "with-schedule",
34
- name: "Scheduled",
35
- status: "active",
36
- workflowId: "research",
37
- stageId: "draft",
38
- schedule: { start: "2026-08-30", end: "2026-09-02", milestone: false, dependsOn: [] },
39
- updatedAt: reference.observedAt,
40
- reference,
41
- },
42
- {
43
- id: "without-schedule",
44
- name: "Unscheduled",
45
- status: "blocked",
46
- updatedAt: reference.observedAt,
47
- reference,
48
- },
49
- ];
50
-
51
- expect(createProjectBoardColumns(workflows, workItems)[0]?.items.map((item) => item.id))
52
- .toEqual(["with-schedule"]);
53
- expect(getScheduledWorkItems(workItems).map((item) => item.id))
54
- .toEqual(["with-schedule"]);
55
- });
56
-
57
- it("offers direct replies only for open confirm-reject requests with a source session", () => {
58
- const request: ObservedRequest = {
59
- id: "approval",
60
- status: "open",
61
- response: "confirm-reject",
62
- prompt: "Ship?",
63
- updatedAt: reference.observedAt,
64
- reference: { ...reference, sessionId: "session-1" },
65
- };
66
- expect(canReplyToProjectRequest(request)).toBe(true);
67
- expect(canReplyToProjectRequest({ ...request, reply: {
68
- decision: "confirmed",
69
- sentAt: reference.observedAt,
70
- messageId: "response-1",
71
- } })).toBe(false);
72
- expect(canReplyToProjectRequest({ ...request, reference })).toBe(false);
73
- });
74
11
  });
@@ -1,55 +1,18 @@
1
- import type {
2
- ObservedRequest,
3
- ObservedWorkItem,
4
- ObservedWorkflow,
5
- ProjectObservationSnapshot,
6
- } from "@nextclaw/client-sdk";
7
-
8
- export type ProjectHomeTab = "overview" | "work" | "artifacts" | "skills" | "agreement";
9
- export type ProjectWorkView = "list" | "board" | "gantt";
10
-
11
- export type ProjectBoardColumn = {
12
- id: string;
13
- label: string;
14
- items: ObservedWorkItem[];
15
- };
16
-
17
- export function isProjectHomeTab(value: string | undefined): value is ProjectHomeTab {
18
- return value === "overview" || value === "work" || value === "artifacts" || value === "skills" || value === "agreement";
19
- }
20
-
21
- export function createProjectBoardColumns(
22
- workflows: readonly ObservedWorkflow[],
23
- workItems: readonly ObservedWorkItem[],
24
- ): ProjectBoardColumn[] {
25
- return workflows.flatMap((workflow) => workflow.stages.map((stage) => ({
26
- id: `${workflow.id}:${stage.id}`,
27
- label: stage.label,
28
- items: workItems.filter((item) => (
29
- item.workflowId === workflow.id && item.stageId === stage.id
30
- )),
31
- })));
32
- }
33
-
34
- export function getUnstagedWorkItems(
35
- workItems: readonly ObservedWorkItem[],
36
- ): ObservedWorkItem[] {
37
- return workItems.filter((item) => !item.workflowId || !item.stageId);
38
- }
39
-
40
- export function getScheduledWorkItems(
41
- workItems: readonly ObservedWorkItem[],
42
- ): ObservedWorkItem[] {
43
- return workItems.filter((item) => item.schedule?.start || item.schedule?.end);
44
- }
45
-
46
- export function canReplyToProjectRequest(request: ObservedRequest): boolean {
47
- return request.status === "open"
48
- && request.response === "confirm-reject"
49
- && Boolean(request.reference.sessionId)
50
- && !request.reply;
51
- }
52
-
53
- export function getOpenProjectRequests(snapshot: ProjectObservationSnapshot): ObservedRequest[] {
54
- return snapshot.requests.filter((request) => request.status === "open");
1
+ export type ProjectHomeTab =
2
+ | "overview"
3
+ | "work"
4
+ | "artifacts"
5
+ | "skills"
6
+ | "agreement";
7
+
8
+ export function isProjectHomeTab(
9
+ value: string | undefined,
10
+ ): value is ProjectHomeTab {
11
+ return (
12
+ value === "overview" ||
13
+ value === "work" ||
14
+ value === "artifacts" ||
15
+ value === "skills" ||
16
+ value === "agreement"
17
+ );
55
18
  }
@@ -2,55 +2,9 @@ import { describe, expect, it } from "vitest";
2
2
  import {
3
3
  formatProjectRelativeTime,
4
4
  joinProjectPath,
5
- sortProjectArtifacts,
6
5
  } from "./project-artifact-view.utils";
7
6
 
8
- const artifacts = [
9
- {
10
- id: "older-update",
11
- path: "docs/designs/older.md",
12
- categoryId: "designs",
13
- categoryLabel: "Designs",
14
- exists: true,
15
- fileCreatedAt: "2026-08-01T00:00:00.000Z",
16
- fileUpdatedAt: "2026-08-28T00:00:00.000Z",
17
- references: [],
18
- },
19
- {
20
- id: "newer-update",
21
- path: "docs/designs/newer.md",
22
- categoryId: "designs",
23
- categoryLabel: "Designs",
24
- exists: true,
25
- fileCreatedAt: "2026-08-29T00:00:00.000Z",
26
- fileUpdatedAt: "2026-08-30T00:00:00.000Z",
27
- references: [],
28
- },
29
- {
30
- id: "untimed",
31
- path: "docs/designs/alpha.md",
32
- categoryId: "designs",
33
- categoryLabel: "Designs",
34
- exists: false,
35
- references: [],
36
- },
37
- ];
38
-
39
7
  describe("project artifact view", () => {
40
- it("prioritizes recent updates by default and supports alternate sort orders", () => {
41
- expect(
42
- sortProjectArtifacts(artifacts).map((artifact) => artifact.id),
43
- ).toEqual(["newer-update", "older-update", "untimed"]);
44
- expect(
45
- sortProjectArtifacts(artifacts, "created-desc").map(
46
- (artifact) => artifact.id,
47
- ),
48
- ).toEqual(["newer-update", "older-update", "untimed"]);
49
- expect(
50
- sortProjectArtifacts(artifacts, "name").map((artifact) => artifact.id),
51
- ).toEqual(["untimed", "newer-update", "older-update"]);
52
- });
53
-
54
8
  it("renders recent timestamps in people-friendly language", () => {
55
9
  const now = new Date("2026-08-30T12:00:00.000Z");
56
10
  expect(formatProjectRelativeTime("2026-08-30T11:57:00.000Z", now)).toBe(