@nextclaw/ui 0.24.0-beta.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 (80) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/assets/api-BqVIq0lF.js +13 -0
  3. package/dist/assets/{appearance-settings-page-CPoFZLw8.js → appearance-settings-page-CTWn1eAT.js} +1 -1
  4. package/dist/assets/{book-open-jcUt0rr9.js → book-open-o2x7OLiO.js} +1 -1
  5. package/dist/assets/{channels-list-page-anMJAkfK.js → channels-list-page-Bzn-pzyJ.js} +1 -1
  6. package/dist/assets/{config-split-page-AcuFnbIC.js → config-split-page-CnRbXXlh.js} +1 -1
  7. package/dist/assets/{confirm-dialog-B-atCfC5.js → confirm-dialog-B_Hbcra2.js} +1 -1
  8. package/dist/assets/{createLucideIcon-Blz0FzrV.js → createLucideIcon-rwwpqyks.js} +1 -1
  9. package/dist/assets/{desktop-capabilities-page-8lKF_Ob-.js → desktop-capabilities-page-C2ayYQTr.js} +1 -1
  10. package/dist/assets/{desktop-update-config-DABXDpbj.js → desktop-update-config-C13czpyL.js} +1 -1
  11. package/dist/assets/{dist-eH-_j8yU.js → dist-BsF3hfOi.js} +1 -1
  12. package/dist/assets/{dist-D8kcEpTf.js → dist-aNN3iQxV.js} +1 -1
  13. package/dist/assets/{doc-browser-DzXRehlW.js → doc-browser-D8dIOo9E.js} +1 -1
  14. package/dist/assets/doc-browser-DcuOOtpP.js +1 -0
  15. package/dist/assets/{doc-browser-context-BlbUcg2J.js → doc-browser-context-Dhsit2Ma.js} +1 -1
  16. package/dist/assets/{extensions-mquwAJ2Z.js → extensions-CD6xvKF_.js} +1 -1
  17. package/dist/assets/{form-actions-CaZDGEmB.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-gFtgyueL.js → mcp-marketplace-page-r2MmD9xO.js} +1 -1
  23. package/dist/assets/{model-config-page-hFBpeRAO.js → model-config-page-DboxoT-I.js} +1 -1
  24. package/dist/assets/{navigation-link-Hb5Z0UxZ.js → navigation-link-C_6oj3zz.js} +1 -1
  25. package/dist/assets/plus-DRsq9Kvs.js +1 -0
  26. package/dist/assets/{privacy-settings-page-C18zo3ox.js → privacy-settings-page-C2sDBqhb.js} +1 -1
  27. package/dist/assets/projects-BcPcx-uD.js +1 -0
  28. package/dist/assets/{providers-config-page-YU0-AOXJ.js → providers-config-page-CmUFl9RF.js} +1 -1
  29. package/dist/assets/react-BjO9I4WZ.js +8 -0
  30. package/dist/assets/{refresh-cw-ZHKfE3yx.js → refresh-cw-DsT2kvXi.js} +1 -1
  31. package/dist/assets/remote-Ci6x91ES.js +1 -0
  32. package/dist/assets/{rotate-cw-CLhOXGwE.js → rotate-cw-BCAs6c3R.js} +1 -1
  33. package/dist/assets/{runtime-config-page-Dmy1A_0Y.js → runtime-config-page-cQKNeAjL.js} +1 -1
  34. package/dist/assets/{save-CWuuZs77.js → save-CadRMTMQ.js} +1 -1
  35. package/dist/assets/{search-DcmsKAn2.js → search-B6tB4fzB.js} +1 -1
  36. package/dist/assets/{search-config-page-CxBacx6W.js → search-config-page-C8lOIkxn.js} +1 -1
  37. package/dist/assets/{secrets-config-page-B4py8-KR.js → secrets-config-page-8clzAag2.js} +1 -1
  38. package/dist/assets/security-config-DPbM6IVT.js +1 -0
  39. package/dist/assets/{select-BEr0Iy_f.js → select-CO9S_C2C.js} +1 -1
  40. package/dist/assets/skeleton-IHILNhIW.js +1 -0
  41. package/dist/assets/{tag-chip-ByqDkn6g.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 +5 -5
  45. package/src/features/apps/components/__tests__/app-packages-panel.test.tsx +362 -241
  46. package/src/features/apps/components/app-document-access-section.tsx +187 -0
  47. package/src/features/apps/components/app-package-card.tsx +150 -275
  48. package/src/features/apps/hooks/use-app-packages.ts +91 -35
  49. package/src/features/chat/index.ts +5 -1
  50. package/src/features/projects/AGENTS.md +3 -2
  51. package/src/features/projects/components/project-overview.tsx +70 -139
  52. package/src/features/projects/components/work/project-work-item-drawer.test.tsx +65 -0
  53. package/src/features/projects/components/work/project-work-item-drawer.tsx +367 -0
  54. package/src/features/projects/components/work/project-work-items.test.tsx +98 -0
  55. package/src/features/projects/components/work/project-work-items.tsx +199 -0
  56. package/src/features/projects/components/work/project-work-state-settings.tsx +285 -0
  57. package/src/features/projects/hooks/use-project-observation.ts +56 -26
  58. package/src/features/projects/hooks/use-project-work.test.tsx +58 -0
  59. package/src/features/projects/hooks/use-project-work.ts +215 -0
  60. package/src/features/projects/pages/project-home-page.tsx +119 -138
  61. package/src/features/projects/pages/projects-page.test.tsx +79 -308
  62. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +16 -1
  63. package/src/shared/lib/i18n/locales/en-US/projects.json +60 -1
  64. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +16 -1
  65. package/src/shared/lib/i18n/locales/zh-CN/projects.json +60 -1
  66. package/dist/assets/api-CoqPRKmF.js +0 -13
  67. package/dist/assets/doc-browser-DJEMr7Zk.js +0 -1
  68. package/dist/assets/index-DyR5iR0O.css +0 -1
  69. package/dist/assets/index-XxXj8WDS.js +0 -109
  70. package/dist/assets/loader-circle-0qPOOYQ5.js +0 -1
  71. package/dist/assets/mcp-marketplace-page-uanamJ6L.js +0 -1
  72. package/dist/assets/plus-CYOvNvcV.js +0 -1
  73. package/dist/assets/projects-C8Gm9RS8.js +0 -1
  74. package/dist/assets/react-DF-kP7iN.js +0 -8
  75. package/dist/assets/remote-B_utkBra.js +0 -1
  76. package/dist/assets/security-config-CTP4IQWw.js +0 -1
  77. package/dist/assets/skeleton-B2NHluWf.js +0 -1
  78. package/dist/assets/x-CENkFyK5.js +0 -1
  79. package/src/features/projects/components/project-work-items.test.tsx +0 -78
  80. package/src/features/projects/components/project-work-items.tsx +0 -272
@@ -1,17 +1,24 @@
1
- import { act, render, screen, waitFor } from '@testing-library/react';
2
- import userEvent from '@testing-library/user-event';
3
- import type { AppPackageOperationView } from '@nextclaw/client-sdk';
4
- import { beforeEach, describe, expect, it, vi } from 'vitest';
5
- import { AppPackagesPanel } from '@/features/apps/components/app-packages-panel';
1
+ import { act, render, screen, waitFor } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import type { AppPackageOperationView } from "@nextclaw/client-sdk";
4
+ import { beforeEach, describe, expect, it, vi } from "vitest";
5
+ import { AppPackagesPanel } from "@/features/apps/components/app-packages-panel";
6
6
 
7
7
  const mocks = vi.hoisted(() => ({
8
8
  appDataError: false,
9
9
  enabled: false,
10
10
  includePackageStorageUsage: true,
11
- readinessStatus: 'ready' as 'ready' | 'needs-capability' | 'needs-configuration',
11
+ readinessStatus: "ready" as
12
+ | "ready"
13
+ | "needs-capability"
14
+ | "needs-configuration",
12
15
  grantClient: vi.fn(),
13
16
  lifecycleMutate: vi.fn(),
14
17
  lifecycleReset: vi.fn(),
18
+ documentAccessMutate: vi.fn(),
19
+ documentAccessMutateAsync: vi.fn(),
20
+ documentAccessReset: vi.fn(),
21
+ documentAccessScopes: [] as Array<Record<string, unknown>>,
15
22
  onOpen: vi.fn(),
16
23
  operations: [] as AppPackageOperationView[],
17
24
  recordOpened: vi.fn(),
@@ -25,10 +32,10 @@ const mocks = vi.hoisted(() => ({
25
32
  requestAuthorization: vi.fn(async () => true),
26
33
  }));
27
34
 
28
- vi.mock('@/features/app-data/hooks/use-app-data', () => ({
35
+ vi.mock("@/features/app-data/hooks/use-app-data", () => ({
29
36
  useAppData: () => ({
30
37
  data: { entries: mocks.retainedEntries, diagnostics: [] },
31
- error: mocks.appDataError ? new Error('App data unavailable') : null,
38
+ error: mocks.appDataError ? new Error("App data unavailable") : null,
32
39
  isError: mocks.appDataError,
33
40
  isLoading: false,
34
41
  refetch: mocks.refetchAppData,
@@ -42,7 +49,7 @@ vi.mock('@/features/app-data/hooks/use-app-data', () => ({
42
49
  }),
43
50
  }));
44
51
 
45
- vi.mock('@/app/components/app-presenter-provider', () => ({
52
+ vi.mock("@/app/components/app-presenter-provider", () => ({
46
53
  useAppPresenter: () => ({
47
54
  appPackageOperationSettlementManager: { settle: vi.fn() },
48
55
  serviceActionAuthorizationManager: {
@@ -51,15 +58,34 @@ vi.mock('@/app/components/app-presenter-provider', () => ({
51
58
  }),
52
59
  }));
53
60
 
54
- vi.mock('@/features/apps/hooks/use-app-packages', () => ({
55
- isAppPackageOperationActive: (status: string) => [
56
- 'queued',
57
- 'resolving',
58
- 'downloading',
59
- 'verifying',
60
- 'installing',
61
- 'finalizing',
62
- ].includes(status),
61
+ vi.mock("@/shared/components/path-picker/server-path-picker-dialog", () => ({
62
+ ServerPathPickerDialog: ({
63
+ open,
64
+ onConfirm,
65
+ }: {
66
+ open: boolean;
67
+ onConfirm: (path: string) => Promise<void>;
68
+ }) =>
69
+ open ? (
70
+ <button
71
+ type="button"
72
+ onClick={() => void onConfirm("/tmp/authorized-documents")}
73
+ >
74
+ Confirm test folder
75
+ </button>
76
+ ) : null,
77
+ }));
78
+
79
+ vi.mock("@/features/apps/hooks/use-app-packages", () => ({
80
+ isAppPackageOperationActive: (status: string) =>
81
+ [
82
+ "queued",
83
+ "resolving",
84
+ "downloading",
85
+ "verifying",
86
+ "installing",
87
+ "finalizing",
88
+ ].includes(status),
63
89
  useAppPackageMutation: () => ({
64
90
  error: null,
65
91
  isError: false,
@@ -68,6 +94,13 @@ vi.mock('@/features/apps/hooks/use-app-packages', () => ({
68
94
  reset: mocks.lifecycleReset,
69
95
  variables: undefined,
70
96
  }),
97
+ useAppDocumentAccessMutation: () => ({
98
+ error: null,
99
+ isPending: false,
100
+ mutate: mocks.documentAccessMutate,
101
+ mutateAsync: mocks.documentAccessMutateAsync,
102
+ reset: mocks.documentAccessReset,
103
+ }),
71
104
  useAppPackageOperationSettlement: () => undefined,
72
105
  useAppPackageOperations: () => ({
73
106
  data: { entries: mocks.operations },
@@ -75,66 +108,77 @@ vi.mock('@/features/apps/hooks/use-app-packages', () => ({
75
108
  }),
76
109
  useAppPackages: () => ({
77
110
  data: {
78
- entries: [{
79
- activeVersion: '0.1.0',
80
- builtIn: true,
81
- components: [
82
- {
83
- dataDirectory: '/tmp/data',
84
- description: 'Clear personal todos.',
85
- id: 'nextclaw-personal-organizer-todos',
86
- icon: '✓',
87
- kind: 'panel',
88
- manifestPath: '/tmp/app/panel-app.json',
89
- packageId: 'nextclaw.personal-organizer',
90
- packageVersion: '0.1.0',
91
- sourcePath: '/tmp/app/todos.panel',
92
- instanceId: 'default',
93
- isolation: 'full-user',
94
- runtimeProfile: 'native-process',
95
- storage: createStorageFixture(),
96
- title: 'Todos',
97
- },
98
- {
99
- dataDirectory: '/tmp/data',
100
- id: 'nextclaw-personal-organizer-data',
101
- kind: 'service',
102
- manifestPath: '/tmp/app/service-app.json',
103
- packageId: 'nextclaw.personal-organizer',
104
- packageVersion: '0.1.0',
105
- sourcePath: '/tmp/app/data',
106
- instanceId: 'default',
107
- isolation: 'full-user',
108
- runtimeProfile: 'native-process',
109
- storage: createStorageFixture(),
110
- title: 'Personal data',
111
- },
112
- ],
113
- dataDirectory: '/tmp/data',
114
- description: 'Todos, notes, favorites, and calendar.',
115
- enabled: mocks.enabled,
116
- id: 'nextclaw.personal-organizer',
117
- installedVersions: ['0.1.0'],
118
- instanceId: 'default',
119
- isolation: 'full-user',
120
- name: 'Personal Space',
121
- primaryPanelId: 'nextclaw-personal-organizer-todos',
122
- readiness: mocks.readinessStatus === 'ready'
123
- ? { status: 'ready' as const, requirements: [] }
124
- : {
125
- status: mocks.readinessStatus,
126
- requirements: [{
127
- componentId: 'nextclaw-personal-organizer-data',
128
- kind: mocks.readinessStatus === 'needs-capability' ? 'capability' as const : 'configuration' as const,
129
- id: 'shared-cache',
130
- title: 'Shared workspace connection',
131
- description: 'Keeps your team data in sync.',
132
- }],
111
+ entries: [
112
+ {
113
+ activeVersion: "0.1.0",
114
+ builtIn: true,
115
+ components: [
116
+ {
117
+ dataDirectory: "/tmp/data",
118
+ description: "Clear personal todos.",
119
+ id: "nextclaw-personal-organizer-todos",
120
+ icon: "✓",
121
+ kind: "panel",
122
+ manifestPath: "/tmp/app/panel-app.json",
123
+ packageId: "nextclaw.personal-organizer",
124
+ packageVersion: "0.1.0",
125
+ sourcePath: "/tmp/app/todos.panel",
126
+ instanceId: "default",
127
+ isolation: "full-user",
128
+ runtimeProfile: "native-process",
129
+ storage: createStorageFixture(),
130
+ title: "Todos",
133
131
  },
134
- runtimeProfile: 'native-process',
135
- storage: createStorageFixture(),
136
- storageUsage: mocks.includePackageStorageUsage ? createUsageFixture() : undefined,
137
- }],
132
+ {
133
+ dataDirectory: "/tmp/data",
134
+ id: "nextclaw-personal-organizer-data",
135
+ kind: "service",
136
+ manifestPath: "/tmp/app/service-app.json",
137
+ packageId: "nextclaw.personal-organizer",
138
+ packageVersion: "0.1.0",
139
+ sourcePath: "/tmp/app/data",
140
+ instanceId: "default",
141
+ isolation: "full-user",
142
+ runtimeProfile: "native-process",
143
+ storage: createStorageFixture(),
144
+ title: "Personal data",
145
+ },
146
+ ],
147
+ dataDirectory: "/tmp/data",
148
+ description: "Todos, notes, favorites, and calendar.",
149
+ documentAccess: mocks.documentAccessScopes,
150
+ enabled: mocks.enabled,
151
+ id: "nextclaw.personal-organizer",
152
+ installedVersions: ["0.1.0"],
153
+ instanceId: "default",
154
+ isolation: "full-user",
155
+ name: "Personal Space",
156
+ primaryPanelId: "nextclaw-personal-organizer-todos",
157
+ readiness:
158
+ mocks.readinessStatus === "ready"
159
+ ? { status: "ready" as const, requirements: [] }
160
+ : {
161
+ status: mocks.readinessStatus,
162
+ requirements: [
163
+ {
164
+ componentId: "nextclaw-personal-organizer-data",
165
+ kind:
166
+ mocks.readinessStatus === "needs-capability"
167
+ ? ("capability" as const)
168
+ : ("configuration" as const),
169
+ id: "shared-cache",
170
+ title: "Shared workspace connection",
171
+ description: "Keeps your team data in sync.",
172
+ },
173
+ ],
174
+ },
175
+ runtimeProfile: "native-process",
176
+ storage: createStorageFixture(),
177
+ storageUsage: mocks.includePackageStorageUsage
178
+ ? createUsageFixture()
179
+ : undefined,
180
+ },
181
+ ],
138
182
  },
139
183
  error: null,
140
184
  isError: false,
@@ -145,16 +189,16 @@ vi.mock('@/features/apps/hooks/use-app-packages', () => ({
145
189
 
146
190
  function createStorageFixture() {
147
191
  return {
148
- layout: 'instance-v1' as const,
192
+ layout: "instance-v1" as const,
149
193
  layoutVersion: 1 as const,
150
- instanceId: 'default',
151
- instanceDirectory: '/tmp/instance',
152
- dataDirectory: '/tmp/data',
153
- configDirectory: '/tmp/config',
154
- stateDirectory: '/tmp/state',
155
- cacheDirectory: '/tmp/cache',
156
- temporaryDirectory: '/tmp/tmp',
157
- logsDirectory: '/tmp/logs',
194
+ instanceId: "default",
195
+ instanceDirectory: "/tmp/instance",
196
+ dataDirectory: "/tmp/data",
197
+ configDirectory: "/tmp/config",
198
+ stateDirectory: "/tmp/state",
199
+ cacheDirectory: "/tmp/cache",
200
+ temporaryDirectory: "/tmp/tmp",
201
+ logsDirectory: "/tmp/logs",
158
202
  };
159
203
  }
160
204
 
@@ -170,46 +214,46 @@ function createUsageFixture() {
170
214
  };
171
215
  }
172
216
 
173
- vi.mock('@/features/apps/hooks/use-app-marketplace', () => ({
217
+ vi.mock("@/features/apps/hooks/use-app-marketplace", () => ({
174
218
  useAppMarketplace: () => ({
175
219
  data: {
176
220
  total: 2,
177
221
  items: [
178
222
  {
179
- id: 'app-personal-organizer',
180
- slug: 'personal-organizer',
181
- appId: 'nextclaw.personal-organizer',
182
- name: '个人空间',
183
- summary: 'A calm personal space.',
184
- summaryI18n: { en: 'A calm personal space.' },
185
- tags: ['personal'],
186
- latestVersion: '0.1.0',
223
+ id: "app-personal-organizer",
224
+ slug: "personal-organizer",
225
+ appId: "nextclaw.personal-organizer",
226
+ name: "个人空间",
227
+ summary: "A calm personal space.",
228
+ summaryI18n: { en: "A calm personal space." },
229
+ tags: ["personal"],
230
+ latestVersion: "0.1.0",
187
231
  featured: true,
188
- publisher: { id: 'nextclaw', name: 'NextClaw' },
232
+ publisher: { id: "nextclaw", name: "NextClaw" },
189
233
  install: {
190
- kind: 'registry',
191
- spec: 'nextclaw.personal-organizer',
192
- registry: 'https://apps-registry.nextclaw.io/api/v1/apps/registry/',
234
+ kind: "registry",
235
+ spec: "nextclaw.personal-organizer",
236
+ registry: "https://apps-registry.nextclaw.io/api/v1/apps/registry/",
193
237
  },
194
- webUrl: 'https://apps.nextclaw.io/apps/personal-organizer',
238
+ webUrl: "https://apps.nextclaw.io/apps/personal-organizer",
195
239
  },
196
240
  {
197
- id: 'app-workspace-glance',
198
- slug: 'workspace-glance',
199
- appId: 'nextclaw.workspace-glance',
200
- name: 'Workspace Glance',
201
- summary: 'A local workspace overview.',
202
- summaryI18n: { en: 'A local workspace overview.' },
203
- tags: ['workspace', 'local'],
204
- latestVersion: '0.1.0',
241
+ id: "app-workspace-glance",
242
+ slug: "workspace-glance",
243
+ appId: "nextclaw.workspace-glance",
244
+ name: "Workspace Glance",
245
+ summary: "A local workspace overview.",
246
+ summaryI18n: { en: "A local workspace overview." },
247
+ tags: ["workspace", "local"],
248
+ latestVersion: "0.1.0",
205
249
  featured: false,
206
- publisher: { id: 'nextclaw', name: 'NextClaw' },
250
+ publisher: { id: "nextclaw", name: "NextClaw" },
207
251
  install: {
208
- kind: 'registry',
209
- spec: 'nextclaw.workspace-glance',
210
- registry: 'https://apps-registry.nextclaw.io/api/v1/apps/registry/',
252
+ kind: "registry",
253
+ spec: "nextclaw.workspace-glance",
254
+ registry: "https://apps-registry.nextclaw.io/api/v1/apps/registry/",
211
255
  },
212
- webUrl: 'https://apps.nextclaw.io/apps/workspace-glance',
256
+ webUrl: "https://apps.nextclaw.io/apps/workspace-glance",
213
257
  },
214
258
  ],
215
259
  },
@@ -224,28 +268,32 @@ vi.mock('@/features/apps/hooks/use-app-marketplace', () => ({
224
268
  }),
225
269
  }));
226
270
 
227
- vi.mock('@/features/panel-apps/hooks/use-panel-apps', () => ({
271
+ vi.mock("@/features/panel-apps/hooks/use-panel-apps", () => ({
228
272
  useGrantPanelAppClient: () => ({ mutateAsync: mocks.grantClient }),
229
273
  usePanelApps: () => ({
230
274
  data: {
231
- entries: mocks.enabled ? [{
232
- appId: 'nextclaw-personal-organizer-todos',
233
- clientDeclared: false,
234
- clientGranted: false,
235
- contentPath: '/api/panel-apps/todos/content',
236
- createdAt: '2026-08-12T00:00:00.000Z',
237
- favorite: false,
238
- fileName: 'nextclaw-personal-organizer-todos.panel',
239
- id: 'todos',
240
- kind: 'folder',
241
- openCount: 0,
242
- packageId: 'nextclaw.personal-organizer',
243
- packageVersion: '0.1.0',
244
- sizeBytes: 1,
245
- sourceKind: 'package',
246
- title: 'Todos',
247
- updatedAt: '2026-08-12T00:00:00.000Z',
248
- }] : [],
275
+ entries: mocks.enabled
276
+ ? [
277
+ {
278
+ appId: "nextclaw-personal-organizer-todos",
279
+ clientDeclared: false,
280
+ clientGranted: false,
281
+ contentPath: "/api/panel-apps/todos/content",
282
+ createdAt: "2026-08-12T00:00:00.000Z",
283
+ favorite: false,
284
+ fileName: "nextclaw-personal-organizer-todos.panel",
285
+ id: "todos",
286
+ kind: "folder",
287
+ openCount: 0,
288
+ packageId: "nextclaw.personal-organizer",
289
+ packageVersion: "0.1.0",
290
+ sizeBytes: 1,
291
+ sourceKind: "package",
292
+ title: "Todos",
293
+ updatedAt: "2026-08-12T00:00:00.000Z",
294
+ },
295
+ ]
296
+ : [],
249
297
  },
250
298
  error: null,
251
299
  isError: false,
@@ -255,15 +303,20 @@ vi.mock('@/features/panel-apps/hooks/use-panel-apps', () => ({
255
303
  useRecordPanelAppOpened: () => ({ mutateAsync: mocks.recordOpened }),
256
304
  }));
257
305
 
258
- describe('AppPackagesPanel', () => {
306
+ describe("AppPackagesPanel", () => {
259
307
  beforeEach(() => {
260
308
  mocks.appDataError = false;
261
309
  mocks.enabled = false;
262
310
  mocks.includePackageStorageUsage = true;
263
- mocks.readinessStatus = 'ready';
311
+ mocks.readinessStatus = "ready";
264
312
  mocks.grantClient.mockReset();
265
313
  mocks.lifecycleMutate.mockReset();
266
314
  mocks.lifecycleReset.mockReset();
315
+ mocks.documentAccessMutate.mockReset();
316
+ mocks.documentAccessMutateAsync.mockReset();
317
+ mocks.documentAccessMutateAsync.mockResolvedValue({});
318
+ mocks.documentAccessReset.mockReset();
319
+ mocks.documentAccessScopes = [];
267
320
  mocks.onOpen.mockReset();
268
321
  mocks.operations = [];
269
322
  mocks.recordOpened.mockReset();
@@ -278,35 +331,70 @@ describe('AppPackagesPanel', () => {
278
331
  mocks.requestAuthorization.mockResolvedValue(true);
279
332
  });
280
333
 
281
- it('shows a built-in package and enables it from the primary action', async () => {
334
+ it("shows a built-in package and enables it from the primary action", async () => {
282
335
  const user = userEvent.setup();
283
336
 
284
337
  render(<AppPackagesPanel onOpenPanelApp={mocks.onOpen} />);
285
338
 
286
- expect(screen.getByText('Personal Space')).toBeTruthy();
287
- expect(screen.getByText('Available')).toBeTruthy();
288
- expect(screen.getByText('Local data service')).toBeTruthy();
339
+ expect(screen.getByText("Personal Space")).toBeTruthy();
340
+ expect(screen.getByText("Available")).toBeTruthy();
341
+ expect(screen.getByText("Local data service")).toBeTruthy();
289
342
 
290
- await user.click(screen.getByRole('button', { name: 'Enable' }));
343
+ await user.click(screen.getByRole("button", { name: "Enable" }));
291
344
 
292
345
  expect(mocks.lifecycleMutate).toHaveBeenCalledWith(
293
- { action: 'enable', appId: 'nextclaw.personal-organizer' },
346
+ { action: "enable", appId: "nextclaw.personal-organizer" },
294
347
  { onSuccess: undefined },
295
348
  );
296
349
  });
297
350
 
298
- it('explains missing external setup and does not offer an unusable enable action', () => {
299
- mocks.readinessStatus = 'needs-configuration';
351
+ it("chooses a runtime-host folder and grants the selected document mode", async () => {
352
+ mocks.documentAccessScopes = [
353
+ {
354
+ id: "workspace",
355
+ mode: "read-write",
356
+ description: "Read and update the chosen workspace.",
357
+ granted: false,
358
+ status: "ungranted",
359
+ availableActions: ["grant"],
360
+ },
361
+ ];
362
+ const user = userEvent.setup();
363
+ render(<AppPackagesPanel onOpenPanelApp={mocks.onOpen} />);
364
+
365
+ await user.selectOptions(
366
+ screen.getByLabelText("Access mode"),
367
+ "read-write",
368
+ );
369
+ await user.click(screen.getByRole("button", { name: "Choose folder" }));
370
+ await user.click(
371
+ screen.getByRole("button", { name: "Confirm test folder" }),
372
+ );
373
+
374
+ expect(mocks.documentAccessMutateAsync).toHaveBeenCalledWith({
375
+ action: "grant",
376
+ appId: "nextclaw.personal-organizer",
377
+ scopeId: "workspace",
378
+ directoryPath: "/tmp/authorized-documents",
379
+ mode: "read-write",
380
+ });
381
+ });
382
+
383
+ it("explains missing external setup and does not offer an unusable enable action", () => {
384
+ mocks.readinessStatus = "needs-configuration";
300
385
 
301
386
  render(<AppPackagesPanel onOpenPanelApp={mocks.onOpen} />);
302
387
 
303
- expect(screen.getByText('Needs setup')).toBeTruthy();
388
+ expect(screen.getByText("Needs setup")).toBeTruthy();
304
389
  expect(screen.getByText(/Shared workspace connection/)).toBeTruthy();
305
390
  expect(screen.getByText(/Keeps your team data in sync/)).toBeTruthy();
306
- expect((screen.getByRole('button', { name: 'Enable' }) as HTMLButtonElement).disabled).toBe(true);
391
+ expect(
392
+ (screen.getByRole("button", { name: "Enable" }) as HTMLButtonElement)
393
+ .disabled,
394
+ ).toBe(true);
307
395
  });
308
396
 
309
- it('scrolls and transfers focus to a package selected from Service Apps', async () => {
397
+ it("scrolls and transfers focus to a package selected from Service Apps", async () => {
310
398
  render(
311
399
  <AppPackagesPanel
312
400
  focusedPackageId="nextclaw.personal-organizer"
@@ -314,188 +402,221 @@ describe('AppPackagesPanel', () => {
314
402
  />,
315
403
  );
316
404
 
317
- const packageCard = screen.getByRole('region', { name: 'Personal Space' });
405
+ const packageCard = screen.getByRole("region", { name: "Personal Space" });
318
406
  await waitFor(() => expect(document.activeElement).toBe(packageCard));
319
- expect(packageCard.className).toContain('ring-2');
407
+ expect(packageCard.className).toContain("ring-2");
320
408
  });
321
409
 
322
- it('distinguishes an untouched app from an App Data query failure', () => {
410
+ it("distinguishes an untouched app from an App Data query failure", () => {
323
411
  mocks.includePackageStorageUsage = false;
324
412
  const view = render(<AppPackagesPanel onOpenPanelApp={mocks.onOpen} />);
325
- expect(screen.getByTitle('/tmp/data').textContent).toContain('No data yet');
413
+ expect(screen.getByTitle("/tmp/data").textContent).toContain("No data yet");
326
414
 
327
415
  mocks.appDataError = true;
328
416
  view.rerender(<AppPackagesPanel onOpenPanelApp={mocks.onOpen} />);
329
417
 
330
- expect(screen.getByTitle('/tmp/data').textContent).toContain('Size unavailable');
418
+ expect(screen.getByTitle("/tmp/data").textContent).toContain(
419
+ "Size unavailable",
420
+ );
331
421
  });
332
422
 
333
- it('opens an enabled package panel through the existing panel app chain', async () => {
423
+ it("opens an enabled package panel through the existing panel app chain", async () => {
334
424
  const user = userEvent.setup();
335
425
  mocks.enabled = true;
336
426
  const openedEntry = {
337
- appId: 'nextclaw-personal-organizer-todos',
338
- id: 'todos',
339
- title: 'Todos',
427
+ appId: "nextclaw-personal-organizer-todos",
428
+ id: "todos",
429
+ title: "Todos",
340
430
  };
341
431
  mocks.recordOpened.mockResolvedValue(openedEntry);
342
432
 
343
433
  render(<AppPackagesPanel onOpenPanelApp={mocks.onOpen} />);
344
434
 
345
- await user.click(screen.getByRole('button', { name: /Todos/ }));
435
+ await user.click(screen.getByRole("button", { name: /Todos/ }));
346
436
 
347
- expect(mocks.recordOpened).toHaveBeenCalledWith('todos');
437
+ expect(mocks.recordOpened).toHaveBeenCalledWith("todos");
348
438
  expect(mocks.onOpen).toHaveBeenCalledWith(openedEntry);
349
439
  });
350
440
 
351
- it('discovers marketplace apps and installs one through its registry spec', async () => {
441
+ it("discovers marketplace apps and installs one through its registry spec", async () => {
352
442
  const user = userEvent.setup();
353
443
 
354
444
  render(<AppPackagesPanel onOpenPanelApp={mocks.onOpen} />);
355
445
 
356
- await user.click(screen.getByRole('button', { name: 'Add apps' }));
446
+ await user.click(screen.getByRole("button", { name: "Add apps" }));
357
447
 
358
- expect(screen.getByRole('heading', { name: 'Add apps' })).toBeTruthy();
359
- expect(screen.getByText('Workspace Glance')).toBeTruthy();
448
+ expect(screen.getByRole("heading", { name: "Add apps" })).toBeTruthy();
449
+ expect(screen.getByText("Workspace Glance")).toBeTruthy();
360
450
  expect(
361
- (screen.getByRole('button', { name: 'Installed' }) as HTMLButtonElement).disabled,
451
+ (screen.getByRole("button", { name: "Installed" }) as HTMLButtonElement)
452
+ .disabled,
362
453
  ).toBe(true);
363
454
 
364
- const installButtons = screen.getAllByRole('button', { name: 'Install app' });
455
+ const installButtons = screen.getAllByRole("button", {
456
+ name: "Install app",
457
+ });
365
458
  await user.click(installButtons.at(-1)!);
366
459
 
367
460
  expect(mocks.lifecycleMutate).toHaveBeenCalledWith(
368
461
  {
369
- action: 'install',
370
- source: 'nextclaw.workspace-glance',
371
- registryUrl: 'https://apps-registry.nextclaw.io/api/v1/apps/registry/',
462
+ action: "install",
463
+ source: "nextclaw.workspace-glance",
464
+ registryUrl: "https://apps-registry.nextclaw.io/api/v1/apps/registry/",
372
465
  },
373
466
  { onSuccess: undefined },
374
467
  );
375
468
  });
376
469
 
377
- it('allows a built-in app to be uninstalled while keeping its data by default', async () => {
470
+ it("allows a built-in app to be uninstalled while keeping its data by default", async () => {
378
471
  const user = userEvent.setup();
379
472
 
380
473
  render(<AppPackagesPanel onOpenPanelApp={mocks.onOpen} />);
381
474
 
382
- await user.click(screen.getByRole('button', { name: 'More app actions' }));
383
- await user.click(screen.getByRole('button', { name: 'Uninstall' }));
384
- expect(screen.getByRole('heading', { name: 'Uninstall this app?' })).toBeTruthy();
475
+ await user.click(screen.getByRole("button", { name: "More app actions" }));
476
+ await user.click(screen.getByRole("button", { name: "Uninstall" }));
477
+ expect(
478
+ screen.getByRole("heading", { name: "Uninstall this app?" }),
479
+ ).toBeTruthy();
385
480
  await waitFor(() => {
386
- expect(document.activeElement).toBe(screen.getByRole('button', { name: 'Cancel' }));
481
+ expect(document.activeElement).toBe(
482
+ screen.getByRole("button", { name: "Cancel" }),
483
+ );
387
484
  });
388
- expect(screen.getByRole('button', { name: /Keep personal data/ }).getAttribute('aria-pressed'))
389
- .toBe('true');
390
- expect(screen.getByText('Total app data')).toBeTruthy();
391
- expect(screen.getByText('Managed instance path')).toBeTruthy();
392
-
393
- await user.click(screen.getByRole('button', { name: 'Uninstall' }));
485
+ expect(
486
+ screen
487
+ .getByRole("button", { name: /Keep personal data/ })
488
+ .getAttribute("aria-pressed"),
489
+ ).toBe("true");
490
+ expect(screen.getByText("Total app data")).toBeTruthy();
491
+ expect(screen.getByText("Managed instance path")).toBeTruthy();
492
+
493
+ await user.click(screen.getByRole("button", { name: "Uninstall" }));
394
494
  expect(mocks.lifecycleMutate).toHaveBeenCalledWith(
395
- { action: 'uninstall', appId: 'nextclaw.personal-organizer', purgeData: false },
495
+ {
496
+ action: "uninstall",
497
+ appId: "nextclaw.personal-organizer",
498
+ purgeData: false,
499
+ },
396
500
  { onSuccess: undefined },
397
501
  );
398
502
  });
399
503
 
400
- it('shows retained data and requires a dedicated permanent-delete confirmation', async () => {
504
+ it("shows retained data and requires a dedicated permanent-delete confirmation", async () => {
401
505
  const user = userEvent.setup();
402
- mocks.retainedEntries = [{
403
- id: 'ad1.encoded',
404
- appId: 'example.notes',
405
- instanceId: 'default',
406
- displayName: 'Notes',
407
- source: 'package',
408
- lifecycle: 'retained',
409
- storage: createStorageFixture(),
410
- usage: {
411
- dataBytes: 128,
412
- configBytes: 3,
413
- stateBytes: 2,
414
- cacheBytes: 1,
415
- temporaryBytes: 0,
416
- logsBytes: 4,
417
- totalBytes: 138,
506
+ mocks.retainedEntries = [
507
+ {
508
+ id: "ad1.encoded",
509
+ appId: "example.notes",
510
+ instanceId: "default",
511
+ displayName: "Notes",
512
+ source: "package",
513
+ lifecycle: "retained",
514
+ storage: createStorageFixture(),
515
+ usage: {
516
+ dataBytes: 128,
517
+ configBytes: 3,
518
+ stateBytes: 2,
519
+ cacheBytes: 1,
520
+ temporaryBytes: 0,
521
+ logsBytes: 4,
522
+ totalBytes: 138,
523
+ },
524
+ createdAt: "2026-08-14T00:00:00.000Z",
525
+ actions: { deleteRetainedData: true },
418
526
  },
419
- createdAt: '2026-08-14T00:00:00.000Z',
420
- actions: { deleteRetainedData: true },
421
- }];
527
+ ];
422
528
  const view = render(<AppPackagesPanel onOpenPanelApp={mocks.onOpen} />);
423
529
 
424
- expect(screen.getByRole('heading', { name: 'Saved app data' })).toBeTruthy();
425
- await user.click(screen.getByRole('button', { name: 'Delete data' }));
426
- expect(screen.getByRole('heading', { name: 'Permanently delete this app data?' })).toBeTruthy();
530
+ expect(
531
+ screen.getByRole("heading", { name: "Saved app data" }),
532
+ ).toBeTruthy();
533
+ await user.click(screen.getByRole("button", { name: "Delete data" }));
534
+ expect(
535
+ screen.getByRole("heading", {
536
+ name: "Permanently delete this app data?",
537
+ }),
538
+ ).toBeTruthy();
427
539
 
428
- await user.click(screen.getByRole('button', { name: 'Permanently delete data' }));
540
+ await user.click(
541
+ screen.getByRole("button", { name: "Permanently delete data" }),
542
+ );
429
543
  expect(mocks.deleteRetained).toHaveBeenCalledWith(
430
- { dataId: 'ad1.encoded', confirmAppId: 'example.notes' },
544
+ { dataId: "ad1.encoded", confirmAppId: "example.notes" },
431
545
  { onSuccess: expect.any(Function) },
432
546
  );
433
- const onSuccess = mocks.deleteRetained.mock.calls[0]?.[1].onSuccess as () => void;
547
+ const onSuccess = mocks.deleteRetained.mock.calls[0]?.[1]
548
+ .onSuccess as () => void;
434
549
  mocks.retainedEntries = [];
435
550
  view.rerender(<AppPackagesPanel onOpenPanelApp={mocks.onOpen} />);
436
551
  await act(async () => onSuccess());
437
552
  await waitFor(() => {
438
- expect(document.activeElement).toBe(screen.getByRole('heading', { name: 'Your apps' }));
553
+ expect(document.activeElement).toBe(
554
+ screen.getByRole("heading", { name: "Your apps" }),
555
+ );
439
556
  });
440
557
  });
441
558
 
442
- it('keeps an accepted install visible in the library and marketplace while it runs', async () => {
559
+ it("keeps an accepted install visible in the library and marketplace while it runs", async () => {
443
560
  const user = userEvent.setup();
444
- mocks.operations = [{
445
- id: 'operation-1',
446
- action: 'install',
447
- appId: 'nextclaw.workspace-glance',
448
- source: 'nextclaw.workspace-glance',
449
- status: 'downloading',
450
- completedSteps: 2,
451
- totalSteps: 5,
452
- createdAt: '2026-08-12T00:00:00.000Z',
453
- updatedAt: '2026-08-12T00:00:01.000Z',
454
- }];
561
+ mocks.operations = [
562
+ {
563
+ id: "operation-1",
564
+ action: "install",
565
+ appId: "nextclaw.workspace-glance",
566
+ source: "nextclaw.workspace-glance",
567
+ status: "downloading",
568
+ completedSteps: 2,
569
+ totalSteps: 5,
570
+ createdAt: "2026-08-12T00:00:00.000Z",
571
+ updatedAt: "2026-08-12T00:00:01.000Z",
572
+ },
573
+ ];
455
574
 
456
575
  render(<AppPackagesPanel onOpenPanelApp={mocks.onOpen} />);
457
576
 
458
- expect(screen.getByRole('status').textContent).toContain('Downloading');
459
- expect(screen.getByText('nextclaw.workspace-glance')).toBeTruthy();
577
+ expect(screen.getByRole("status").textContent).toContain("Downloading");
578
+ expect(screen.getByText("nextclaw.workspace-glance")).toBeTruthy();
460
579
 
461
- await user.click(screen.getByRole('button', { name: 'Add apps' }));
580
+ await user.click(screen.getByRole("button", { name: "Add apps" }));
462
581
 
463
- expect(screen.getByRole('heading', { name: 'Add apps' })).toBeTruthy();
464
- expect(screen.getAllByText('Downloading')).not.toHaveLength(0);
465
- expect((screen.getByRole('button', { name: 'Downloading' }) as HTMLButtonElement).disabled)
466
- .toBe(true);
582
+ expect(screen.getByRole("heading", { name: "Add apps" })).toBeTruthy();
583
+ expect(screen.getAllByText("Downloading")).not.toHaveLength(0);
584
+ expect(
585
+ (screen.getByRole("button", { name: "Downloading" }) as HTMLButtonElement)
586
+ .disabled,
587
+ ).toBe(true);
467
588
  });
468
589
 
469
- it('clears an earlier failure after a newer operation succeeds', () => {
590
+ it("clears an earlier failure after a newer operation succeeds", () => {
470
591
  mocks.operations = [
471
592
  {
472
- id: 'operation-succeeded',
473
- action: 'update',
474
- appId: 'nextclaw.personal-organizer',
475
- status: 'succeeded',
593
+ id: "operation-succeeded",
594
+ action: "update",
595
+ appId: "nextclaw.personal-organizer",
596
+ status: "succeeded",
476
597
  completedSteps: 5,
477
598
  totalSteps: 5,
478
- createdAt: '2026-08-13T11:27:49.090Z',
479
- updatedAt: '2026-08-13T11:27:50.627Z',
480
- completedAt: '2026-08-13T11:27:50.627Z',
599
+ createdAt: "2026-08-13T11:27:49.090Z",
600
+ updatedAt: "2026-08-13T11:27:50.627Z",
601
+ completedAt: "2026-08-13T11:27:50.627Z",
481
602
  },
482
603
  {
483
- id: 'operation-failed',
484
- action: 'update',
485
- appId: 'nextclaw.personal-organizer',
486
- status: 'failed',
604
+ id: "operation-failed",
605
+ action: "update",
606
+ appId: "nextclaw.personal-organizer",
607
+ status: "failed",
487
608
  completedSteps: 1,
488
609
  totalSteps: 5,
489
- createdAt: '2026-08-13T11:08:08.080Z',
490
- updatedAt: '2026-08-13T11:08:10.316Z',
491
- completedAt: '2026-08-13T11:08:10.316Z',
492
- error: 'name 必须是非空字符串。',
610
+ createdAt: "2026-08-13T11:08:08.080Z",
611
+ updatedAt: "2026-08-13T11:08:10.316Z",
612
+ completedAt: "2026-08-13T11:08:10.316Z",
613
+ error: "name 必须是非空字符串。",
493
614
  },
494
615
  ];
495
616
 
496
617
  render(<AppPackagesPanel onOpenPanelApp={mocks.onOpen} />);
497
618
 
498
- expect(screen.queryByRole('alert')).toBeNull();
499
- expect(screen.queryByText('name 必须是非空字符串。')).toBeNull();
619
+ expect(screen.queryByRole("alert")).toBeNull();
620
+ expect(screen.queryByText("name 必须是非空字符串。")).toBeNull();
500
621
  });
501
622
  });