@nextclaw/ui 0.15.26 → 0.15.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/assets/{api-CK3Fro7z.js → api-a_GaOFc7.js} +4 -4
- package/dist/assets/{appearance-settings-page-zqwWO8Dy.js → appearance-settings-page-DlGXLgff.js} +1 -1
- package/dist/assets/{channels-list-page-CVCZBlCL.js → channels-list-page-BEEk4Uc9.js} +2 -2
- package/dist/assets/{chat-page-D4wehRTz.js → chat-page-DUQXXqQm.js} +1 -1
- package/dist/assets/{config-split-page-CceNFvlY.js → config-split-page-ps1Aod3g.js} +1 -1
- package/dist/assets/{confirm-dialog-CWeNYWZY.js → confirm-dialog-DJSUNG33.js} +2 -2
- package/dist/assets/desktop-update-config--zagjHic.js +1 -0
- package/dist/assets/{dist-D9xtGgdL.js → dist-BkTYNzm_.js} +1 -1
- package/dist/assets/{dist-AYxkFx2B.js → dist-DkR7_Dpl.js} +1 -1
- package/dist/assets/doc-browser-DahzJEWj.js +1 -0
- package/dist/assets/doc-browser-Q67TOGVS.js +1 -0
- package/dist/assets/{doc-browser-context-Cb7Z0j90.js → doc-browser-context-BQim8SqD.js} +1 -1
- package/dist/assets/form-actions-B2-bpFie.js +1 -0
- package/dist/assets/index-Ba_o5fu9.js +109 -0
- package/dist/assets/index-DSLK7zgR.css +1 -0
- package/dist/assets/loader-circle-DyXZU92l.js +1 -0
- package/dist/assets/mcp-marketplace-page-B0dBE5CV.js +1 -0
- package/dist/assets/mcp-marketplace-page-w8panjZG.js +9 -0
- package/dist/assets/{model-config-page-DfOLyQqR.js → model-config-page-rrhFaque.js} +1 -1
- package/dist/assets/{navigation-link-MiiyoIjH.js → navigation-link-mpHFXwRr.js} +2 -2
- package/dist/assets/plus-Dhb4KuMH.js +1 -0
- package/dist/assets/{provider-scoped-model-input-DXzjr5l0.js → provider-scoped-model-input-CFsfWEt9.js} +1 -1
- package/dist/assets/{providers-config-page-CaO8WfZH.js → providers-config-page-D-TYFZsF.js} +1 -1
- package/dist/assets/react-BlXwvOLo.js +8 -0
- package/dist/assets/{refresh-cw-a2VVeeAF.js → refresh-cw-JFuyDj_P.js} +1 -1
- package/dist/assets/remote-BwVv_Qq-.js +1 -0
- package/dist/assets/rotate-cw-DjbbiIW_.js +1 -0
- package/dist/assets/{runtime-config-page-BuA4A02z.js → runtime-config-page-Br9j9Vxe.js} +1 -1
- package/dist/assets/{save-CGm23Yww.js → save-BL8P8CLX.js} +1 -1
- package/dist/assets/{search-CH2t--sH.js → search-bZL75Pgc.js} +1 -1
- package/dist/assets/{search-config-page-BAmYqoAz.js → search-config-page-B3fvzMd1.js} +1 -1
- package/dist/assets/{secrets-config-page-BlPOuql_.js → secrets-config-page-ClwTxtXd.js} +2 -2
- package/dist/assets/security-config-DmJw9QZA.js +1 -0
- package/dist/assets/{select-DnxxDpcV.js → select-rUZ8nIZu.js} +2 -2
- package/dist/assets/{settings-2-agnMYc8a.js → settings-2-BI4SCkxQ.js} +1 -1
- package/dist/assets/skeleton-CVmEPG43.js +1 -0
- package/dist/assets/{tag-chip-CPOc8b2N.js → tag-chip-BdI_yIOA.js} +1 -1
- package/dist/assets/x-BcEayhV6.js +1 -0
- package/dist/index.html +19 -19
- package/package.json +7 -7
- package/src/app/components/layout/app-layout.tsx +38 -2
- package/src/app/hooks/__tests__/use-app-event-consumers.test.ts +20 -0
- package/src/app/hooks/use-app-event-consumers.ts +21 -0
- package/src/app/presenters/app.presenter.ts +6 -0
- package/src/features/apps/components/__tests__/app-packages-panel.test.tsx +92 -14
- package/src/features/apps/components/__tests__/apps-panel.test.tsx +58 -0
- package/src/features/apps/components/app-artwork.tsx +49 -0
- package/src/features/apps/components/app-marketplace-catalog.tsx +332 -0
- package/src/features/apps/components/app-marketplace-cover.tsx +64 -0
- package/src/features/apps/components/app-marketplace-detail.tsx +161 -0
- package/src/features/apps/components/app-marketplace-dialog.tsx +107 -217
- package/src/features/apps/components/app-marketplace-operation.tsx +84 -0
- package/src/features/apps/components/app-package-card.tsx +201 -52
- package/src/features/apps/components/app-packages-panel.tsx +98 -26
- package/src/features/apps/components/apps-panel.tsx +28 -27
- package/src/features/apps/hooks/use-app-marketplace.test.ts +144 -0
- package/src/features/apps/hooks/use-app-marketplace.ts +55 -85
- package/src/features/apps/hooks/use-app-packages.ts +59 -4
- package/src/features/apps/index.ts +1 -0
- package/src/features/apps/managers/__tests__/app-package-operation-settlement.manager.test.ts +81 -0
- package/src/features/apps/managers/app-package-operation-settlement.manager.ts +80 -0
- package/src/features/apps/services/app-marketplace.service.ts +223 -0
- package/src/features/apps/types/app-marketplace.types.ts +75 -0
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +64 -3
- package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +24 -14
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +53 -0
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-run-queue.test.tsx +18 -0
- package/src/features/chat/features/conversation/hooks/use-chat-composer-reference-intent.ts +4 -1
- package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +126 -32
- package/src/features/chat/features/conversation/hooks/use-session-run-queue.ts +12 -2
- package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +31 -0
- package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +5 -0
- package/src/features/chat/features/input/utils/chat-inline-token-composer.utils.ts +14 -0
- package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +25 -0
- package/src/features/chat/features/message/hooks/use-chat-inline-token-actions.ts +8 -1
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +77 -20
- package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +56 -50
- package/src/features/chat/features/workspace/components/project-files/workspace-directory-tree.tsx +33 -11
- package/src/features/chat/features/workspace/components/project-files/workspace-project-tree-view.tsx +93 -0
- package/src/features/chat/features/workspace/stores/__tests__/workspace-project-tree.store.test.ts +38 -0
- package/src/features/chat/features/workspace/stores/workspace-project-tree.store.ts +160 -0
- package/src/features/chat/index.ts +1 -0
- package/src/features/chat/managers/__tests__/chat-composer-intent.manager.test.ts +29 -0
- package/src/features/chat/managers/chat-composer-intent.manager.ts +29 -2
- package/src/features/chat/presenters/chat.presenter.ts +2 -2
- package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +1 -1
- package/src/features/panel-apps/components/panel-apps-list.tsx +1 -6
- package/src/features/right-panel-resources/utils/__tests__/right-panel-resource-uri.utils.test.ts +34 -0
- package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.ts +29 -0
- package/src/features/service-apps/components/service-apps-panel.tsx +3 -17
- package/src/platforms/desktop/components/desktop-app-shell.tsx +5 -0
- package/src/platforms/mobile/components/mobile-app-shell.tsx +9 -1
- package/src/shared/components/actions/__tests__/navigation-link.test.tsx +12 -0
- package/src/shared/components/actions/navigation-link.tsx +6 -3
- package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +34 -0
- package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +5 -0
- package/src/shared/components/doc-browser/doc-browser.tsx +11 -1
- package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +13 -0
- package/src/shared/components/ui/tabs.tsx +24 -1
- package/src/shared/hooks/use-server-path-watch.ts +42 -0
- package/src/shared/lib/api/server-path/server-path.types.ts +10 -0
- package/src/shared/lib/api/types.ts +3 -0
- package/src/shared/lib/api/utils/server-path.utils.ts +10 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +1 -0
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +49 -7
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +1 -0
- package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +49 -7
- package/vite.config.ts +32 -1
- package/dist/assets/desktop-update-config-Dirc3bpt.js +0 -1
- package/dist/assets/doc-browser-4Sud6dWw.js +0 -1
- package/dist/assets/doc-browser-D4x_yH5F.js +0 -1
- package/dist/assets/form-actions-B2q4jgjE.js +0 -1
- package/dist/assets/index-CdyTsSBu.css +0 -1
- package/dist/assets/index-Das_bw2h.js +0 -107
- package/dist/assets/loader-circle-CpmBdaon.js +0 -1
- package/dist/assets/mcp-marketplace-page-CdOY3Sop.js +0 -1
- package/dist/assets/mcp-marketplace-page-DvjrfqEt.js +0 -9
- package/dist/assets/plus-C8FkjpSZ.js +0 -1
- package/dist/assets/react-C8XFFvuz.js +0 -8
- package/dist/assets/remote-BJHXa4mB.js +0 -1
- package/dist/assets/rotate-cw-DJ7UF3F2.js +0 -1
- package/dist/assets/security-config-CbDiY2xr.js +0 -1
- package/dist/assets/skeleton-XpqeWL62.js +0 -1
- package/dist/assets/x-odTHAtLZ.js +0 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { QueryClient } from '@tanstack/react-query';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { invalidateChangedServerPath } from '@/app/hooks/use-app-event-consumers';
|
|
4
|
+
|
|
5
|
+
describe('server path realtime invalidation', () => {
|
|
6
|
+
it('invalidates only the browse query for the changed directory', () => {
|
|
7
|
+
const invalidateQueries = vi.fn();
|
|
8
|
+
invalidateChangedServerPath(
|
|
9
|
+
{ invalidateQueries } as unknown as QueryClient,
|
|
10
|
+
'/Users/peiwang/Projects/nextbot/src',
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const request = invalidateQueries.mock.calls[0]?.[0] as {
|
|
14
|
+
predicate: (query: { queryKey: readonly unknown[] }) => boolean;
|
|
15
|
+
};
|
|
16
|
+
expect(request.predicate({ queryKey: ['server-path-browse', '/Users/peiwang/Projects/nextbot/src', '', true] })).toBe(true);
|
|
17
|
+
expect(request.predicate({ queryKey: ['server-path-browse', '/Users/peiwang/Projects/nextbot', '', true] })).toBe(false);
|
|
18
|
+
expect(request.predicate({ queryKey: ['server-path-read', '/Users/peiwang/Projects/nextbot/src', ''] })).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -30,6 +30,22 @@ function handleConfigUpdatedEvent(queryClient: QueryClient | undefined, path: st
|
|
|
30
30
|
invalidateMarketplaceQueries(queryClient, path);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function normalizeServerPath(path: string): string {
|
|
34
|
+
const normalized = path.trim().replace(/\\/g, '/').replace(/\/+$/g, '');
|
|
35
|
+
return /^[a-z]:/i.test(normalized) ? normalized.toLowerCase() : normalized;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function invalidateChangedServerPath(queryClient: QueryClient | undefined, directoryPath: string): void {
|
|
39
|
+
if (!queryClient) return;
|
|
40
|
+
const changedDirectory = normalizeServerPath(directoryPath);
|
|
41
|
+
void queryClient.invalidateQueries({
|
|
42
|
+
predicate: (query) =>
|
|
43
|
+
query.queryKey[0] === 'server-path-browse' &&
|
|
44
|
+
typeof query.queryKey[1] === 'string' &&
|
|
45
|
+
normalizeServerPath(query.queryKey[1]) === changedDirectory,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
33
49
|
export function useAppEventConsumers(queryClient?: QueryClient) {
|
|
34
50
|
const shouldResyncSessionsRef = useRef(false);
|
|
35
51
|
|
|
@@ -53,6 +69,10 @@ export function useAppEventConsumers(queryClient?: QueryClient) {
|
|
|
53
69
|
const unsubscribeConfigUpdated = nextclawClient.eventBus.on(eventKeys.configUpdated, ({ path }) => {
|
|
54
70
|
handleConfigUpdatedEvent(queryClient, path);
|
|
55
71
|
});
|
|
72
|
+
const unsubscribeServerPathChanged = nextclawClient.eventBus.on(
|
|
73
|
+
eventKeys.serverPathChanged,
|
|
74
|
+
({ directoryPath }) => invalidateChangedServerPath(queryClient, directoryPath),
|
|
75
|
+
);
|
|
56
76
|
const unsubscribeRuntimeUpdate = nextclawClient.eventBus.on(eventKeys.runtimeUpdateSnapshot, (snapshot) => {
|
|
57
77
|
runtimeUpdateManager.reportSnapshot(snapshot);
|
|
58
78
|
});
|
|
@@ -74,6 +94,7 @@ export function useAppEventConsumers(queryClient?: QueryClient) {
|
|
|
74
94
|
unsubscribeConnectionClose();
|
|
75
95
|
unsubscribeConnectionError();
|
|
76
96
|
unsubscribeConfigUpdated();
|
|
97
|
+
unsubscribeServerPathChanged();
|
|
77
98
|
unsubscribeRuntimeUpdate();
|
|
78
99
|
unsubscribeSessionRunStatus();
|
|
79
100
|
unsubscribeSessionSummaryUpsert();
|
|
@@ -2,12 +2,14 @@ import { AccountManager } from '@/features/account';
|
|
|
2
2
|
import { viewportLayoutManager } from '@/app/managers/viewport-layout.manager';
|
|
3
3
|
import {
|
|
4
4
|
ChatCompletionNotificationManager,
|
|
5
|
+
ChatComposerIntentManager,
|
|
5
6
|
ChatDraftIntentManager,
|
|
6
7
|
useChatThreadStore,
|
|
7
8
|
} from '@/features/chat';
|
|
8
9
|
import { AppNotificationManager } from '@/features/notifications';
|
|
9
10
|
import { PanelAppBridgeManager } from '@/features/panel-apps';
|
|
10
11
|
import { InboxManager } from '@/features/inbox';
|
|
12
|
+
import { AppPackageOperationSettlementManager } from '@/features/apps';
|
|
11
13
|
import { RightPanelResourceRouteResolver } from '@/features/right-panel-resources';
|
|
12
14
|
import { RemoteAccessManager } from '@/features/remote';
|
|
13
15
|
import { ServiceActionAuthorizationManager } from '@/features/service-apps';
|
|
@@ -42,7 +44,11 @@ export class AppPresenter {
|
|
|
42
44
|
this.rightPanelResourceRouteResolver,
|
|
43
45
|
this.notifyRightPanelOpened,
|
|
44
46
|
);
|
|
47
|
+
appPackageOperationSettlementManager = new AppPackageOperationSettlementManager(
|
|
48
|
+
this.docBrowserManager,
|
|
49
|
+
);
|
|
45
50
|
sideDockManager = new SideDockManager(this.docBrowserManager);
|
|
51
|
+
chatComposerIntentManager = new ChatComposerIntentManager();
|
|
46
52
|
chatDraftIntentManager = new ChatDraftIntentManager();
|
|
47
53
|
serviceActionAuthorizationManager = new ServiceActionAuthorizationManager();
|
|
48
54
|
panelAppBridgeManager = new PanelAppBridgeManager(this.serviceActionAuthorizationManager);
|
|
@@ -9,7 +9,19 @@ const mocks = vi.hoisted(() => ({
|
|
|
9
9
|
lifecycleMutate: vi.fn(),
|
|
10
10
|
lifecycleReset: vi.fn(),
|
|
11
11
|
onOpen: vi.fn(),
|
|
12
|
+
operations: [] as Array<{
|
|
13
|
+
id: string;
|
|
14
|
+
action: 'install';
|
|
15
|
+
appId: string;
|
|
16
|
+
source: string;
|
|
17
|
+
status: 'downloading';
|
|
18
|
+
completedSteps: number;
|
|
19
|
+
totalSteps: number;
|
|
20
|
+
createdAt: string;
|
|
21
|
+
updatedAt: string;
|
|
22
|
+
}>,
|
|
12
23
|
recordOpened: vi.fn(),
|
|
24
|
+
refetchOperations: vi.fn(),
|
|
13
25
|
refetchPackages: vi.fn(),
|
|
14
26
|
refetchPanels: vi.fn(),
|
|
15
27
|
requestAuthorization: vi.fn(async () => true),
|
|
@@ -17,6 +29,7 @@ const mocks = vi.hoisted(() => ({
|
|
|
17
29
|
|
|
18
30
|
vi.mock('@/app/components/app-presenter-provider', () => ({
|
|
19
31
|
useAppPresenter: () => ({
|
|
32
|
+
appPackageOperationSettlementManager: { settle: vi.fn() },
|
|
20
33
|
serviceActionAuthorizationManager: {
|
|
21
34
|
requestAuthorization: mocks.requestAuthorization,
|
|
22
35
|
},
|
|
@@ -24,6 +37,14 @@ vi.mock('@/app/components/app-presenter-provider', () => ({
|
|
|
24
37
|
}));
|
|
25
38
|
|
|
26
39
|
vi.mock('@/features/apps/hooks/use-app-packages', () => ({
|
|
40
|
+
isAppPackageOperationActive: (status: string) => [
|
|
41
|
+
'queued',
|
|
42
|
+
'resolving',
|
|
43
|
+
'downloading',
|
|
44
|
+
'verifying',
|
|
45
|
+
'installing',
|
|
46
|
+
'finalizing',
|
|
47
|
+
].includes(status),
|
|
27
48
|
useAppPackageMutation: () => ({
|
|
28
49
|
error: null,
|
|
29
50
|
isError: false,
|
|
@@ -32,6 +53,11 @@ vi.mock('@/features/apps/hooks/use-app-packages', () => ({
|
|
|
32
53
|
reset: mocks.lifecycleReset,
|
|
33
54
|
variables: undefined,
|
|
34
55
|
}),
|
|
56
|
+
useAppPackageOperationSettlement: () => undefined,
|
|
57
|
+
useAppPackageOperations: () => ({
|
|
58
|
+
data: { entries: mocks.operations },
|
|
59
|
+
refetch: mocks.refetchOperations,
|
|
60
|
+
}),
|
|
35
61
|
useAppPackages: () => ({
|
|
36
62
|
data: {
|
|
37
63
|
entries: [{
|
|
@@ -101,22 +127,22 @@ vi.mock('@/features/apps/hooks/use-app-marketplace', () => ({
|
|
|
101
127
|
webUrl: 'https://apps.nextclaw.io/apps/personal-organizer',
|
|
102
128
|
},
|
|
103
129
|
{
|
|
104
|
-
id: 'app-
|
|
105
|
-
slug: '
|
|
106
|
-
appId: 'nextclaw.
|
|
107
|
-
name: '
|
|
108
|
-
summary: 'A
|
|
109
|
-
summaryI18n: { en: 'A
|
|
110
|
-
tags: ['
|
|
130
|
+
id: 'app-workspace-glance',
|
|
131
|
+
slug: 'workspace-glance',
|
|
132
|
+
appId: 'nextclaw.workspace-glance',
|
|
133
|
+
name: 'Workspace Glance',
|
|
134
|
+
summary: 'A local workspace overview.',
|
|
135
|
+
summaryI18n: { en: 'A local workspace overview.' },
|
|
136
|
+
tags: ['workspace', 'local'],
|
|
111
137
|
latestVersion: '0.1.0',
|
|
112
138
|
featured: false,
|
|
113
139
|
publisher: { id: 'nextclaw', name: 'NextClaw' },
|
|
114
140
|
install: {
|
|
115
141
|
kind: 'registry',
|
|
116
|
-
spec: 'nextclaw.
|
|
142
|
+
spec: 'nextclaw.workspace-glance',
|
|
117
143
|
registry: 'https://apps-registry.nextclaw.io/api/v1/apps/registry/',
|
|
118
144
|
},
|
|
119
|
-
webUrl: 'https://apps.nextclaw.io/apps/
|
|
145
|
+
webUrl: 'https://apps.nextclaw.io/apps/workspace-glance',
|
|
120
146
|
},
|
|
121
147
|
],
|
|
122
148
|
},
|
|
@@ -124,6 +150,11 @@ vi.mock('@/features/apps/hooks/use-app-marketplace', () => ({
|
|
|
124
150
|
isError: false,
|
|
125
151
|
isLoading: false,
|
|
126
152
|
}),
|
|
153
|
+
useAppMarketplaceDetail: () => ({
|
|
154
|
+
data: undefined,
|
|
155
|
+
error: null,
|
|
156
|
+
isLoading: false,
|
|
157
|
+
}),
|
|
127
158
|
}));
|
|
128
159
|
|
|
129
160
|
vi.mock('@/features/panel-apps/hooks/use-panel-apps', () => ({
|
|
@@ -164,8 +195,10 @@ describe('AppPackagesPanel', () => {
|
|
|
164
195
|
mocks.lifecycleMutate.mockReset();
|
|
165
196
|
mocks.lifecycleReset.mockReset();
|
|
166
197
|
mocks.onOpen.mockReset();
|
|
198
|
+
mocks.operations = [];
|
|
167
199
|
mocks.recordOpened.mockReset();
|
|
168
200
|
mocks.refetchPackages.mockReset();
|
|
201
|
+
mocks.refetchOperations.mockReset();
|
|
169
202
|
mocks.refetchPanels.mockReset();
|
|
170
203
|
mocks.requestAuthorization.mockReset();
|
|
171
204
|
mocks.requestAuthorization.mockResolvedValue(true);
|
|
@@ -211,10 +244,10 @@ describe('AppPackagesPanel', () => {
|
|
|
211
244
|
|
|
212
245
|
render(<AppPackagesPanel onOpenPanelApp={mocks.onOpen} />);
|
|
213
246
|
|
|
214
|
-
await user.click(screen.getByRole('button', { name: '
|
|
247
|
+
await user.click(screen.getByRole('button', { name: 'Add apps' }));
|
|
215
248
|
|
|
216
|
-
expect(screen.getByRole('heading', { name: '
|
|
217
|
-
expect(screen.getByText('
|
|
249
|
+
expect(screen.getByRole('heading', { name: 'Add apps' })).toBeTruthy();
|
|
250
|
+
expect(screen.getByText('Workspace Glance')).toBeTruthy();
|
|
218
251
|
expect(
|
|
219
252
|
(screen.getByRole('button', { name: 'Installed' }) as HTMLButtonElement).disabled,
|
|
220
253
|
).toBe(true);
|
|
@@ -225,10 +258,55 @@ describe('AppPackagesPanel', () => {
|
|
|
225
258
|
expect(mocks.lifecycleMutate).toHaveBeenCalledWith(
|
|
226
259
|
{
|
|
227
260
|
action: 'install',
|
|
228
|
-
source: 'nextclaw.
|
|
261
|
+
source: 'nextclaw.workspace-glance',
|
|
229
262
|
registryUrl: 'https://apps-registry.nextclaw.io/api/v1/apps/registry/',
|
|
230
263
|
},
|
|
231
|
-
{ onSuccess:
|
|
264
|
+
{ onSuccess: undefined },
|
|
232
265
|
);
|
|
233
266
|
});
|
|
267
|
+
|
|
268
|
+
it('allows a built-in app to be uninstalled while keeping its data by default', async () => {
|
|
269
|
+
const user = userEvent.setup();
|
|
270
|
+
|
|
271
|
+
render(<AppPackagesPanel onOpenPanelApp={mocks.onOpen} />);
|
|
272
|
+
|
|
273
|
+
await user.click(screen.getByRole('button', { name: 'More app actions' }));
|
|
274
|
+
await user.click(screen.getByRole('button', { name: 'Uninstall' }));
|
|
275
|
+
expect(screen.getByRole('heading', { name: 'Uninstall this app?' })).toBeTruthy();
|
|
276
|
+
expect(screen.getByRole('button', { name: /Keep personal data/ }).getAttribute('aria-pressed'))
|
|
277
|
+
.toBe('true');
|
|
278
|
+
|
|
279
|
+
await user.click(screen.getByRole('button', { name: 'Uninstall' }));
|
|
280
|
+
expect(mocks.lifecycleMutate).toHaveBeenCalledWith(
|
|
281
|
+
{ action: 'uninstall', appId: 'nextclaw.personal-organizer', purgeData: false },
|
|
282
|
+
{ onSuccess: undefined },
|
|
283
|
+
);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('keeps an accepted install visible in the library and marketplace while it runs', async () => {
|
|
287
|
+
const user = userEvent.setup();
|
|
288
|
+
mocks.operations = [{
|
|
289
|
+
id: 'operation-1',
|
|
290
|
+
action: 'install',
|
|
291
|
+
appId: 'nextclaw.workspace-glance',
|
|
292
|
+
source: 'nextclaw.workspace-glance',
|
|
293
|
+
status: 'downloading',
|
|
294
|
+
completedSteps: 2,
|
|
295
|
+
totalSteps: 5,
|
|
296
|
+
createdAt: '2026-08-12T00:00:00.000Z',
|
|
297
|
+
updatedAt: '2026-08-12T00:00:01.000Z',
|
|
298
|
+
}];
|
|
299
|
+
|
|
300
|
+
render(<AppPackagesPanel onOpenPanelApp={mocks.onOpen} />);
|
|
301
|
+
|
|
302
|
+
expect(screen.getByRole('status').textContent).toContain('Downloading');
|
|
303
|
+
expect(screen.getByText('nextclaw.workspace-glance')).toBeTruthy();
|
|
304
|
+
|
|
305
|
+
await user.click(screen.getByRole('button', { name: 'Add apps' }));
|
|
306
|
+
|
|
307
|
+
expect(screen.getByRole('heading', { name: 'Add apps' })).toBeTruthy();
|
|
308
|
+
expect(screen.getAllByText('Downloading')).not.toHaveLength(0);
|
|
309
|
+
expect((screen.getByRole('button', { name: 'Downloading' }) as HTMLButtonElement).disabled)
|
|
310
|
+
.toBe(true);
|
|
311
|
+
});
|
|
234
312
|
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { AppsPanel, type AppsPanelTab } from '@/features/apps/components/apps-panel';
|
|
6
|
+
|
|
7
|
+
vi.mock('@/shared/lib/i18n', () => ({ t: (key: string) => key }));
|
|
8
|
+
vi.mock('@/features/apps/components/app-packages-panel', () => ({
|
|
9
|
+
AppPackagesPanel: () => <div>packages-content</div>,
|
|
10
|
+
}));
|
|
11
|
+
vi.mock('@/features/panel-apps', () => ({
|
|
12
|
+
PanelAppsList: () => <div>panel-apps-content</div>,
|
|
13
|
+
}));
|
|
14
|
+
vi.mock('@/features/service-apps', () => ({
|
|
15
|
+
ServiceAppsPanel: () => <div>service-apps-content</div>,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
function AppsPanelHarness() {
|
|
19
|
+
const [activeTab, setActiveTab] = useState<AppsPanelTab>('apps');
|
|
20
|
+
return (
|
|
21
|
+
<AppsPanel
|
|
22
|
+
activeTab={activeTab}
|
|
23
|
+
onActiveTabChange={setActiveTab}
|
|
24
|
+
onOpenPanelApp={vi.fn()}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('AppsPanel', () => {
|
|
30
|
+
it('keeps one fixed-width navigation instance while switching app types', async () => {
|
|
31
|
+
const user = userEvent.setup();
|
|
32
|
+
render(<AppsPanelHarness />);
|
|
33
|
+
|
|
34
|
+
const navigation = screen.getByRole('tablist');
|
|
35
|
+
expect(navigation.className).toContain('w-full');
|
|
36
|
+
expect(navigation.className).toContain('max-w-[390px]');
|
|
37
|
+
|
|
38
|
+
await user.click(screen.getByRole('tab', { name: 'panelAppsTitle' }));
|
|
39
|
+
expect(screen.getByText('panel-apps-content')).toBeTruthy();
|
|
40
|
+
expect(screen.getByRole('tablist')).toBe(navigation);
|
|
41
|
+
|
|
42
|
+
await user.click(screen.getByRole('tab', { name: 'serviceAppsTitle' }));
|
|
43
|
+
expect(screen.getByText('service-apps-content')).toBeTruthy();
|
|
44
|
+
expect(screen.getByRole('tablist')).toBe(navigation);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('supports arrow-key navigation through the shared tab primitive', async () => {
|
|
48
|
+
const user = userEvent.setup();
|
|
49
|
+
render(<AppsPanelHarness />);
|
|
50
|
+
|
|
51
|
+
const firstTab = screen.getByRole('tab', { name: 'appsTitle' });
|
|
52
|
+
firstTab.focus();
|
|
53
|
+
await user.keyboard('{ArrowRight}');
|
|
54
|
+
|
|
55
|
+
expect(document.activeElement).toBe(screen.getByRole('tab', { name: 'panelAppsTitle' }));
|
|
56
|
+
expect(screen.getByText('panel-apps-content')).toBeTruthy();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { cn } from '@/shared/lib/utils';
|
|
3
|
+
|
|
4
|
+
export function AppArtwork({
|
|
5
|
+
className,
|
|
6
|
+
icon,
|
|
7
|
+
name,
|
|
8
|
+
}: {
|
|
9
|
+
className?: string;
|
|
10
|
+
icon?: string;
|
|
11
|
+
name: string;
|
|
12
|
+
}) {
|
|
13
|
+
const [failedIcon, setFailedIcon] = useState<string>();
|
|
14
|
+
const imageIcon = isImageIcon(icon);
|
|
15
|
+
const imageFailed = failedIcon === icon;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<span className={cn(
|
|
19
|
+
'relative flex h-11 w-11 shrink-0 items-center justify-center overflow-hidden rounded-[13px] bg-gradient-to-br from-violet-500/15 via-primary/10 to-emerald-500/15 text-sm font-semibold text-foreground ring-1 ring-black/[0.04] dark:ring-white/[0.06]',
|
|
20
|
+
className,
|
|
21
|
+
)}>
|
|
22
|
+
{imageIcon && !imageFailed ? (
|
|
23
|
+
<img
|
|
24
|
+
src={icon}
|
|
25
|
+
alt=""
|
|
26
|
+
className="h-full w-full object-cover"
|
|
27
|
+
onError={() => setFailedIcon(icon)}
|
|
28
|
+
/>
|
|
29
|
+
) : icon && !imageIcon ? (
|
|
30
|
+
<span className="text-base leading-none" aria-hidden="true">{icon}</span>
|
|
31
|
+
) : (
|
|
32
|
+
<span aria-hidden="true">{readInitial(name)}</span>
|
|
33
|
+
)}
|
|
34
|
+
</span>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isImageIcon(icon: string | undefined): icon is string {
|
|
39
|
+
return Boolean(icon) && (
|
|
40
|
+
icon!.startsWith('data:') ||
|
|
41
|
+
icon!.startsWith('/') ||
|
|
42
|
+
icon!.startsWith('http://') ||
|
|
43
|
+
icon!.startsWith('https://')
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readInitial(name: string): string {
|
|
48
|
+
return [...name.trim()][0]?.toLocaleUpperCase() ?? 'A';
|
|
49
|
+
}
|