@nextclaw/ui 0.12.35-beta.3 → 0.12.35-beta.4
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 +63 -0
- package/dist/assets/api-BTZui7nj.js +15 -0
- package/dist/assets/{book-open--T9dJHnp.js → book-open-qc0UVn6m.js} +1 -1
- package/dist/assets/channels-list-page-DBtU_M1Y.js +8 -0
- package/dist/assets/{chat-page-DOKLoiqI.js → chat-page-BcoJYo91.js} +2 -2
- package/dist/assets/config-split-page-DW8tP9Bc.js +1 -0
- package/dist/assets/createLucideIcon-CA0sgoMJ.js +1 -0
- package/dist/assets/desktop-update-config-DARSwQG8.js +1 -0
- package/dist/assets/dist-D5zsy7pX.js +1 -0
- package/dist/assets/dist-DNxIa3pe.js +41 -0
- package/dist/assets/doc-browser-B9skxzVo.js +1 -0
- package/dist/assets/doc-browser-CwyITRXx.js +1 -0
- package/dist/assets/doc-browser-context-Do2qSrB8.js +1 -0
- package/dist/assets/{external-link-CZ9bQY0L.js → external-link-C9fbyi6k.js} +1 -1
- package/dist/assets/index-BYRP_BMj.css +1 -0
- package/dist/assets/index-DckPGTdM.js +103 -0
- package/dist/assets/mcp-marketplace-page-BiExW5wq.js +40 -0
- package/dist/assets/mcp-marketplace-page-Cj6MDSkE.js +1 -0
- package/dist/assets/model-config-DbdY7fF1.js +1 -0
- package/dist/assets/notice-card-CyiVcu_Z.js +5 -0
- package/dist/assets/play-D-s0C4MN.js +1 -0
- package/dist/assets/plus-DOwjM_kF.js +1 -0
- package/dist/assets/provider-scoped-model-input-Bk8jlpRm.js +1 -0
- package/dist/assets/providers-list-CJjNStDV.js +1 -0
- package/dist/assets/react-BaRotFd6.js +1 -0
- package/dist/assets/remote-gl8QckNc.js +1 -0
- package/dist/assets/runtime-config-page-BMFc1yzf.js +1 -0
- package/dist/assets/{save-B6wbBLkU.js → save-CG4Hb1u8.js} +1 -1
- package/dist/assets/{search-config-BFmG4_F7.js → search-config-DUOR26if.js} +1 -1
- package/dist/assets/{search-Czito3Dk.js → search-lWpOIHEn.js} +1 -1
- package/dist/assets/secrets-config-ZbLzjIRx.js +3 -0
- package/dist/assets/setting-row-CNfTKDjc.js +1 -0
- package/dist/assets/tabs-custom-BbZoysAl.js +1 -0
- package/dist/assets/tag-chip-Dl8JbhC7.js +1 -0
- package/dist/assets/x-D6GwT_8k.js +1 -0
- package/dist/index.html +15 -17
- package/package.json +8 -9
- package/src/app/components/app-presenter-provider.tsx +21 -0
- package/src/app/components/layout/sidebar.layout.test.tsx +2 -2
- package/src/app/components/layout/sidebar.tsx +3 -3
- package/src/app/index.tsx +5 -3
- package/src/app/presenters/app.presenter.ts +24 -0
- package/src/features/account/components/account-panel.tsx +11 -11
- package/src/features/account/index.ts +1 -4
- package/src/features/account/managers/account.manager.ts +52 -27
- package/src/features/account/stores/account.store.ts +0 -15
- package/src/features/apps/components/apps-panel.tsx +43 -0
- package/src/features/apps/index.ts +1 -0
- package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +8 -8
- package/src/features/chat/components/layout/chat-sidebar.tsx +3 -2
- package/src/features/chat/managers/chat-session-list.manager.test.ts +58 -0
- package/src/features/chat/stores/chat-session-list.store.ts +58 -7
- package/src/features/panel-apps/components/panel-app-toolbar.tsx +4 -4
- package/src/features/panel-apps/components/panel-apps-list.tsx +6 -2
- package/src/features/panel-apps/index.ts +3 -1
- package/src/features/panel-apps/managers/panel-app-bridge.manager.test.ts +111 -0
- package/src/features/panel-apps/managers/panel-app-bridge.manager.ts +247 -0
- package/src/features/panel-apps/utils/panel-app-doc-browser.utils.test.ts +23 -0
- package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +42 -16
- package/src/features/remote/components/remote-access-page.test.tsx +2 -2
- package/src/features/remote/components/remote-access-page.tsx +9 -9
- package/src/features/remote/managers/remote-access.manager.ts +24 -41
- package/src/features/service-apps/components/service-action-authorization-dialog.tsx +78 -0
- package/src/features/service-apps/components/service-apps-panel.tsx +290 -0
- package/src/features/service-apps/hooks/use-service-apps.ts +66 -0
- package/src/features/service-apps/index.ts +3 -0
- package/src/features/service-apps/managers/service-action-authorization.manager.ts +10 -0
- package/src/features/service-apps/stores/service-action-authorization.store.ts +53 -0
- package/src/shared/components/doc-browser/doc-browser-context.test.tsx +150 -1
- package/src/shared/components/doc-browser/doc-browser-context.tsx +34 -443
- package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +7 -0
- package/src/shared/components/doc-browser/doc-browser.tsx +16 -1
- package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +272 -0
- package/src/shared/components/doc-browser/stores/doc-browser.store.ts +140 -0
- package/src/shared/components/doc-browser/types/doc-browser.types.ts +47 -0
- package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +47 -0
- package/src/shared/components/doc-browser/utils/doc-browser-url.utils.ts +95 -0
- package/src/shared/lib/api/managers/client.manager.ts +2 -1
- package/src/shared/lib/i18n/runtime/doc-browser-labels.utils.ts +31 -0
- package/src/shared/lib/transport/local-transport.service.ts +18 -2
- package/src/shared/lib/transport/remote-transport.service.ts +19 -3
- package/src/shared/lib/transport/request-raw-api-response.utils.ts +1 -1
- package/dist/assets/api-D3uoaOR8.js +0 -15
- package/dist/assets/channels-list-page-DHmz9iHg.js +0 -8
- package/dist/assets/config-split-page-Dutq3R5o.js +0 -1
- package/dist/assets/createLucideIcon-Cwr2o6Zq.js +0 -1
- package/dist/assets/desktop-update-config-ClbDsIOs.js +0 -1
- package/dist/assets/dist-e5WATjIW.js +0 -1
- package/dist/assets/doc-browser-ChY6hMn8.js +0 -1
- package/dist/assets/doc-browser-cR1vWRs1.js +0 -1
- package/dist/assets/doc-browser-context-CLczyX2Z.js +0 -1
- package/dist/assets/es2015-BJw-hrex.js +0 -41
- package/dist/assets/i18n-tWKGEGxB.js +0 -1
- package/dist/assets/index-DPz9DRZN.js +0 -103
- package/dist/assets/index-HvJDzfSU.css +0 -1
- package/dist/assets/key-round-B8tTb3ZJ.js +0 -1
- package/dist/assets/mcp-marketplace-page-BYPxLdUR.js +0 -40
- package/dist/assets/mcp-marketplace-page-DXCvk7wJ.js +0 -1
- package/dist/assets/model-config-DcnC_fSr.js +0 -1
- package/dist/assets/notice-card-DUDbOvqx.js +0 -5
- package/dist/assets/play-CV_kwi9X.js +0 -1
- package/dist/assets/plus-BqfNq7Tu.js +0 -1
- package/dist/assets/provider-scoped-model-input-eO4XY0iY.js +0 -1
- package/dist/assets/providers-list-BgvjRVmp.js +0 -1
- package/dist/assets/remote-BCuZ1JJe.js +0 -1
- package/dist/assets/rotate-cw-BKEhWuWp.js +0 -1
- package/dist/assets/runtime-config-page-B2XPXHrk.js +0 -1
- package/dist/assets/secrets-config-4uY5sk-V.js +0 -3
- package/dist/assets/select-CmmDgG8o.js +0 -1
- package/dist/assets/setting-row-BUO6FeN1.js +0 -1
- package/dist/assets/tabs-custom-D3IZL9WX.js +0 -1
- package/dist/assets/tag-chip-Dm_pOtE3.js +0 -1
- package/dist/assets/x-DBP3WiC9.js +0 -1
- package/src/app/components/app-manager-provider.tsx +0 -20
- package/src/app/managers/app.manager.ts +0 -12
- /package/dist/assets/{config-hints-DGfwbumO.js → config-hints-CTqBnCDp.js} +0 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { AppWindow, Server } from 'lucide-react';
|
|
2
|
+
import type { PanelAppEntryView } from '@/shared/lib/api';
|
|
3
|
+
import { PanelAppsList } from '@/features/panel-apps';
|
|
4
|
+
import { ServiceAppsPanel } from '@/features/service-apps';
|
|
5
|
+
import { Tabs, TabsList, TabsTrigger } from '@/shared/components/ui/tabs';
|
|
6
|
+
import { t } from '@/shared/lib/i18n';
|
|
7
|
+
|
|
8
|
+
export type AppsPanelTab = 'panel-apps' | 'service-apps';
|
|
9
|
+
|
|
10
|
+
export function AppsPanel({
|
|
11
|
+
activeTab,
|
|
12
|
+
onActiveTabChange,
|
|
13
|
+
onOpenPanelApp,
|
|
14
|
+
}: {
|
|
15
|
+
activeTab: AppsPanelTab;
|
|
16
|
+
onActiveTabChange: (tab: AppsPanelTab) => void;
|
|
17
|
+
onOpenPanelApp: (entry: PanelAppEntryView) => void;
|
|
18
|
+
}) {
|
|
19
|
+
const headerTabs = (
|
|
20
|
+
<Tabs value={activeTab} onValueChange={(value) => onActiveTabChange(value as AppsPanelTab)}>
|
|
21
|
+
<TabsList className="grid h-auto w-[min(340px,100%)] grid-cols-2 rounded-lg bg-gray-100/70 p-0.5">
|
|
22
|
+
<TabsTrigger value="panel-apps" className="gap-1.5 px-2 py-1.5 text-xs">
|
|
23
|
+
<AppWindow className="h-3.5 w-3.5" />
|
|
24
|
+
{t('panelAppsTitle')}
|
|
25
|
+
</TabsTrigger>
|
|
26
|
+
<TabsTrigger value="service-apps" className="gap-1.5 px-2 py-1.5 text-xs">
|
|
27
|
+
<Server className="h-3.5 w-3.5" />
|
|
28
|
+
{t('serviceAppsTitle')}
|
|
29
|
+
</TabsTrigger>
|
|
30
|
+
</TabsList>
|
|
31
|
+
</Tabs>
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="h-full min-h-0 bg-white">
|
|
36
|
+
{activeTab === 'panel-apps' ? (
|
|
37
|
+
<PanelAppsList headerContent={headerTabs} onOpenPanelApp={onOpenPanelApp} />
|
|
38
|
+
) : (
|
|
39
|
+
<ServiceAppsPanel headerContent={headerTabs} />
|
|
40
|
+
)}
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AppsPanel, type AppsPanelTab } from './components/apps-panel';
|
|
@@ -6,7 +6,7 @@ import type { UiTheme } from '@/shared/lib/theme';
|
|
|
6
6
|
import {
|
|
7
7
|
BookOpen,
|
|
8
8
|
ChevronRight,
|
|
9
|
-
|
|
9
|
+
Boxes,
|
|
10
10
|
Languages,
|
|
11
11
|
Palette,
|
|
12
12
|
Settings,
|
|
@@ -30,7 +30,7 @@ type ChatSidebarUtilityMenuProps = {
|
|
|
30
30
|
languageOptions: ChatSidebarUtilityOption<I18nLanguage>[];
|
|
31
31
|
onSelectLanguage: (language: I18nLanguage) => void;
|
|
32
32
|
onOpenDocs: () => void;
|
|
33
|
-
|
|
33
|
+
onOpenApps: () => void;
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
export function ChatSidebarUtilityMenu({
|
|
@@ -45,15 +45,15 @@ export function ChatSidebarUtilityMenu({
|
|
|
45
45
|
languageOptions,
|
|
46
46
|
onSelectLanguage,
|
|
47
47
|
onOpenDocs,
|
|
48
|
-
|
|
48
|
+
onOpenApps,
|
|
49
49
|
}: ChatSidebarUtilityMenuProps) {
|
|
50
50
|
const handleOpenDocs = () => {
|
|
51
51
|
onOpenDocs();
|
|
52
52
|
onOpenChange(false);
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
-
const
|
|
56
|
-
|
|
55
|
+
const handleOpenApps = () => {
|
|
56
|
+
onOpenApps();
|
|
57
57
|
onOpenChange(false);
|
|
58
58
|
};
|
|
59
59
|
|
|
@@ -92,11 +92,11 @@ export function ChatSidebarUtilityMenu({
|
|
|
92
92
|
</button>
|
|
93
93
|
<button
|
|
94
94
|
type="button"
|
|
95
|
-
onClick={
|
|
95
|
+
onClick={handleOpenApps}
|
|
96
96
|
className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-gray-700 transition-colors hover:bg-gray-100"
|
|
97
97
|
>
|
|
98
|
-
<
|
|
99
|
-
<span className="flex-1 text-left">{t('
|
|
98
|
+
<Boxes className="h-4 w-4 text-gray-400" />
|
|
99
|
+
<span className="flex-1 text-left">{t('appsTitle')}</span>
|
|
100
100
|
</button>
|
|
101
101
|
</div>
|
|
102
102
|
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
ChatSidebarMobileToolbar,
|
|
31
31
|
} from '@/features/chat/components/layout/chat-sidebar-toolbar';
|
|
32
32
|
import { ChatSidebarUtilityMenu } from '@/features/chat/components/layout/chat-sidebar-utility-menu';
|
|
33
|
-
import {
|
|
33
|
+
import { openApps } from '@/features/panel-apps';
|
|
34
34
|
import { isWindowsDesktopHost } from '@/platforms/desktop';
|
|
35
35
|
|
|
36
36
|
type DateGroup = {
|
|
@@ -379,7 +379,8 @@ export function ChatSidebar({
|
|
|
379
379
|
currentLanguageLabel={currentLanguageLabel}
|
|
380
380
|
languageOptions={utilityLanguageOptions}
|
|
381
381
|
onSelectLanguage={handleLanguageSwitch}
|
|
382
|
-
onOpenDocs={() => docBrowser.open(undefined, { kind: 'docs', newTab: true, title: 'Docs' })}
|
|
382
|
+
onOpenDocs={() => docBrowser.open(undefined, { kind: 'docs', newTab: true, title: 'Docs' })}
|
|
383
|
+
onOpenApps={() => openApps(docBrowser)}
|
|
383
384
|
/>
|
|
384
385
|
</div>
|
|
385
386
|
) : null}
|
|
@@ -5,6 +5,16 @@ import { useChatInputStore } from '@/features/chat/stores/chat-input.store';
|
|
|
5
5
|
import { useChatSessionListStore } from '@/features/chat/stores/chat-session-list.store';
|
|
6
6
|
import { useChatThreadStore } from '@/features/chat/stores/chat-thread.store';
|
|
7
7
|
|
|
8
|
+
const chatSessionListModeStorageKey = 'nextclaw.chat.session-list.mode';
|
|
9
|
+
|
|
10
|
+
function createLocalStoragePersistStorage() {
|
|
11
|
+
return {
|
|
12
|
+
getItem: (name: string) => JSON.parse(window.localStorage.getItem(name) ?? 'null'),
|
|
13
|
+
setItem: (name: string, value: unknown) => window.localStorage.setItem(name, JSON.stringify(value)),
|
|
14
|
+
removeItem: (name: string) => window.localStorage.removeItem(name)
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
8
18
|
const mocks = vi.hoisted(() => ({
|
|
9
19
|
updateNcpSession: vi.fn(),
|
|
10
20
|
}));
|
|
@@ -19,6 +29,8 @@ vi.mock('@/shared/lib/api', async (importOriginal) => {
|
|
|
19
29
|
|
|
20
30
|
describe('ChatSessionListManager', () => {
|
|
21
31
|
beforeEach(() => {
|
|
32
|
+
window.localStorage.clear();
|
|
33
|
+
useChatSessionListStore.persist.setOptions({ storage: createLocalStoragePersistStorage() });
|
|
22
34
|
mocks.updateNcpSession.mockReset();
|
|
23
35
|
mocks.updateNcpSession.mockResolvedValue({});
|
|
24
36
|
useChatInputStore.setState({
|
|
@@ -193,6 +205,13 @@ describe('ChatSessionListManager', () => {
|
|
|
193
205
|
|
|
194
206
|
expect(useChatSessionListStore.getState().snapshot.listMode).toBe('project-first');
|
|
195
207
|
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBe('session-1');
|
|
208
|
+
expect(JSON.parse(window.localStorage.getItem(chatSessionListModeStorageKey) ?? '{}')).toMatchObject({
|
|
209
|
+
state: {
|
|
210
|
+
snapshot: {
|
|
211
|
+
listMode: 'project-first'
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
});
|
|
196
215
|
});
|
|
197
216
|
|
|
198
217
|
it('marks a session as read through the session list owner boundary', () => {
|
|
@@ -252,3 +271,42 @@ describe('ChatSessionListManager', () => {
|
|
|
252
271
|
expect(uiManager.goToSession).toHaveBeenCalledWith('ncp-materialized-session', { replace: true });
|
|
253
272
|
});
|
|
254
273
|
});
|
|
274
|
+
|
|
275
|
+
describe('ChatSessionListStore persistence', () => {
|
|
276
|
+
beforeEach(() => {
|
|
277
|
+
window.localStorage.clear();
|
|
278
|
+
useChatSessionListStore.persist.setOptions({ storage: createLocalStoragePersistStorage() });
|
|
279
|
+
useChatSessionListStore.setState({
|
|
280
|
+
snapshot: {
|
|
281
|
+
...useChatSessionListStore.getState().snapshot,
|
|
282
|
+
listMode: 'time-first'
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('falls back to time-first when the persisted sidebar list mode is invalid', () => {
|
|
288
|
+
useChatSessionListStore.persist.setOptions({
|
|
289
|
+
storage: {
|
|
290
|
+
getItem: () => ({ state: { snapshot: { listMode: 'sideways' } } }),
|
|
291
|
+
setItem: vi.fn(),
|
|
292
|
+
removeItem: vi.fn()
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
useChatSessionListStore.persist.rehydrate();
|
|
297
|
+
|
|
298
|
+
expect(useChatSessionListStore.getState().snapshot.listMode).toBe('time-first');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('hydrates the sidebar list mode from persisted preference on store initialization', async () => {
|
|
302
|
+
window.localStorage.setItem(
|
|
303
|
+
chatSessionListModeStorageKey,
|
|
304
|
+
JSON.stringify({ state: { snapshot: { listMode: 'project-first' } }, version: 0 })
|
|
305
|
+
);
|
|
306
|
+
vi.resetModules();
|
|
307
|
+
|
|
308
|
+
const hydratedStoreModule = await import('@/features/chat/stores/chat-session-list.store');
|
|
309
|
+
|
|
310
|
+
expect(hydratedStoreModule.useChatSessionListStore.getState().snapshot.listMode).toBe('project-first');
|
|
311
|
+
});
|
|
312
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { create, type StateCreator } from 'zustand';
|
|
2
|
+
import { createJSONStorage, persist } from 'zustand/middleware';
|
|
2
3
|
import type { SessionRunStatus } from '@/features/chat/types/session-run-status.types';
|
|
3
4
|
export type ChatSessionListMode = 'time-first' | 'project-first';
|
|
4
5
|
export type ChatSessionListSnapshot = {
|
|
@@ -8,6 +9,30 @@ export type ChatSessionListSnapshot = {
|
|
|
8
9
|
listMode: ChatSessionListMode;
|
|
9
10
|
};
|
|
10
11
|
|
|
12
|
+
const CHAT_SESSION_LIST_MODE_STORAGE_KEY = 'nextclaw.chat.session-list.mode';
|
|
13
|
+
const CHAT_SESSION_LIST_DEFAULT_MODE: ChatSessionListMode = 'time-first';
|
|
14
|
+
|
|
15
|
+
type PersistedChatSessionListStore = {
|
|
16
|
+
snapshot?: {
|
|
17
|
+
listMode?: unknown;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function isChatSessionListMode(value: unknown): value is ChatSessionListMode {
|
|
22
|
+
return value === 'time-first' || value === 'project-first';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolvePersistedChatSessionListMode(persistedState: unknown): ChatSessionListMode | null {
|
|
26
|
+
if (!persistedState || typeof persistedState !== 'object') {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const { snapshot } = persistedState as PersistedChatSessionListStore;
|
|
30
|
+
if (!snapshot || typeof snapshot !== 'object') {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return isChatSessionListMode(snapshot.listMode) ? snapshot.listMode : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
11
36
|
export function hasUnreadSessionUpdate(
|
|
12
37
|
lastMessageAt: string | null | undefined,
|
|
13
38
|
readAt: string | undefined,
|
|
@@ -50,7 +75,7 @@ const initialSnapshot: ChatSessionListSnapshot = {
|
|
|
50
75
|
selectedSessionKey: null,
|
|
51
76
|
selectedAgentId: 'main',
|
|
52
77
|
query: '',
|
|
53
|
-
listMode:
|
|
78
|
+
listMode: CHAT_SESSION_LIST_DEFAULT_MODE
|
|
54
79
|
};
|
|
55
80
|
|
|
56
81
|
function createSetSnapshotAction(set: ChatSessionListStoreSet) {
|
|
@@ -85,9 +110,35 @@ function createMarkSessionReadAction(set: ChatSessionListStoreSet) {
|
|
|
85
110
|
});
|
|
86
111
|
}
|
|
87
112
|
|
|
88
|
-
export const useChatSessionListStore = create<ChatSessionListStore>(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
113
|
+
export const useChatSessionListStore = create<ChatSessionListStore>()(
|
|
114
|
+
persist(
|
|
115
|
+
(set) => ({
|
|
116
|
+
snapshot: initialSnapshot,
|
|
117
|
+
optimisticReadAtBySessionKey: {},
|
|
118
|
+
setSnapshot: createSetSnapshotAction(set),
|
|
119
|
+
markSessionRead: createMarkSessionReadAction(set)
|
|
120
|
+
}),
|
|
121
|
+
{
|
|
122
|
+
name: CHAT_SESSION_LIST_MODE_STORAGE_KEY,
|
|
123
|
+
storage: createJSONStorage(() => window.localStorage),
|
|
124
|
+
partialize: (state): PersistedChatSessionListStore => ({
|
|
125
|
+
snapshot: {
|
|
126
|
+
listMode: state.snapshot.listMode
|
|
127
|
+
}
|
|
128
|
+
}),
|
|
129
|
+
merge: (persistedState, currentState) => {
|
|
130
|
+
const listMode = resolvePersistedChatSessionListMode(persistedState);
|
|
131
|
+
if (!listMode) {
|
|
132
|
+
return currentState;
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
...currentState,
|
|
136
|
+
snapshot: {
|
|
137
|
+
...currentState.snapshot,
|
|
138
|
+
listMode
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
);
|
|
@@ -2,23 +2,23 @@ import { ArrowLeft, RefreshCw } from 'lucide-react';
|
|
|
2
2
|
import { t } from '@/shared/lib/i18n';
|
|
3
3
|
|
|
4
4
|
type PanelAppToolbarProps = {
|
|
5
|
-
|
|
5
|
+
onOpenApps: () => void;
|
|
6
6
|
onRefresh: () => void;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
export function PanelAppToolbar({
|
|
10
|
-
|
|
10
|
+
onOpenApps,
|
|
11
11
|
onRefresh,
|
|
12
12
|
}: PanelAppToolbarProps) {
|
|
13
13
|
return (
|
|
14
14
|
<div className="flex items-center justify-between gap-2 px-3.5 py-2 bg-white border-b border-gray-100 shrink-0">
|
|
15
15
|
<button
|
|
16
16
|
type="button"
|
|
17
|
-
onClick={
|
|
17
|
+
onClick={onOpenApps}
|
|
18
18
|
className="inline-flex items-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900"
|
|
19
19
|
>
|
|
20
20
|
<ArrowLeft className="h-3.5 w-3.5" />
|
|
21
|
-
{t('
|
|
21
|
+
{t('appsTitle')}
|
|
22
22
|
</button>
|
|
23
23
|
<button
|
|
24
24
|
type="button"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo, useState } from 'react';
|
|
1
|
+
import { useMemo, useState, type ReactNode } from 'react';
|
|
2
2
|
import { HelpCircle, RefreshCw } from 'lucide-react';
|
|
3
3
|
import { PanelAppListItem } from '@/features/panel-apps/components/panel-app-list-item';
|
|
4
4
|
import { usePanelApps, useRecordPanelAppOpened, useUpdatePanelAppPreferences } from '@/features/panel-apps/hooks/use-panel-apps';
|
|
@@ -10,8 +10,10 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/shar
|
|
|
10
10
|
import { t } from '@/shared/lib/i18n';
|
|
11
11
|
|
|
12
12
|
export function PanelAppsList({
|
|
13
|
+
headerContent,
|
|
13
14
|
onOpenPanelApp,
|
|
14
15
|
}: {
|
|
16
|
+
headerContent?: ReactNode;
|
|
15
17
|
onOpenPanelApp: (entry: PanelAppEntryView) => void;
|
|
16
18
|
}) {
|
|
17
19
|
const panelApps = usePanelApps();
|
|
@@ -54,7 +56,9 @@ export function PanelAppsList({
|
|
|
54
56
|
<div className="flex h-full min-h-0 flex-col bg-white">
|
|
55
57
|
<div className="flex items-center justify-between gap-2 border-b border-gray-100 px-4 py-3">
|
|
56
58
|
<div className="flex min-w-0 items-center gap-1.5">
|
|
57
|
-
|
|
59
|
+
{headerContent ?? (
|
|
60
|
+
<div className="truncate text-sm font-semibold text-gray-900">{t('panelAppsTitle')}</div>
|
|
61
|
+
)}
|
|
58
62
|
{panelApps.data?.panelsPath ? (
|
|
59
63
|
<TooltipProvider delayDuration={250}>
|
|
60
64
|
<Tooltip>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { NextClawClientError } from '@nextclaw/client-sdk';
|
|
2
|
+
import { waitFor } from '@testing-library/react';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { PanelAppBridgeManager } from '@/features/panel-apps/managers/panel-app-bridge.manager';
|
|
5
|
+
import type { ServiceActionAuthorizationManager } from '@/features/service-apps';
|
|
6
|
+
|
|
7
|
+
const mocks = vi.hoisted(() => ({
|
|
8
|
+
createBridgeSession: vi.fn(),
|
|
9
|
+
grantServiceAction: vi.fn(),
|
|
10
|
+
invokeServiceAction: vi.fn(),
|
|
11
|
+
listServiceActions: vi.fn(),
|
|
12
|
+
requestAuthorization: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock('@/shared/lib/api', () => ({
|
|
16
|
+
nextclawClient: {
|
|
17
|
+
panelApps: {
|
|
18
|
+
createBridgeSession: mocks.createBridgeSession,
|
|
19
|
+
},
|
|
20
|
+
serviceApps: {
|
|
21
|
+
grantServiceAction: mocks.grantServiceAction,
|
|
22
|
+
invokeServiceAction: mocks.invokeServiceAction,
|
|
23
|
+
listServiceActions: mocks.listServiceActions,
|
|
24
|
+
revokeServiceAction: vi.fn(),
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
describe('PanelAppBridgeManager', () => {
|
|
30
|
+
it('opens the authorization flow before retrying a protected service action', async () => {
|
|
31
|
+
const manager = new PanelAppBridgeManager({
|
|
32
|
+
requestAuthorization: mocks.requestAuthorization,
|
|
33
|
+
} as unknown as ServiceActionAuthorizationManager);
|
|
34
|
+
const postMessage = vi.fn();
|
|
35
|
+
const contentWindow = { postMessage } as unknown as Window;
|
|
36
|
+
const iframe = { contentWindow } as HTMLIFrameElement;
|
|
37
|
+
const actionId = 'mood-tracker.saveMood';
|
|
38
|
+
mocks.createBridgeSession.mockResolvedValue({
|
|
39
|
+
expiresAt: '2026-05-28T00:00:00.000Z',
|
|
40
|
+
id: 'session-1',
|
|
41
|
+
panelAppId: 'mood-calendar',
|
|
42
|
+
tabId: 'tab-1',
|
|
43
|
+
token: 'token-1',
|
|
44
|
+
});
|
|
45
|
+
mocks.invokeServiceAction
|
|
46
|
+
.mockRejectedValueOnce(new NextClawClientError({
|
|
47
|
+
code: 'AUTHORIZATION_REQUIRED',
|
|
48
|
+
message: 'authorization required',
|
|
49
|
+
}))
|
|
50
|
+
.mockResolvedValueOnce({ actionId, result: { saved: true } });
|
|
51
|
+
mocks.listServiceActions.mockResolvedValue({
|
|
52
|
+
actions: [{
|
|
53
|
+
appId: 'mood-tracker',
|
|
54
|
+
description: 'Save daily mood entries',
|
|
55
|
+
id: actionId,
|
|
56
|
+
name: 'saveMood',
|
|
57
|
+
risk: 'write',
|
|
58
|
+
title: 'Save mood',
|
|
59
|
+
}],
|
|
60
|
+
});
|
|
61
|
+
mocks.requestAuthorization.mockResolvedValue(true);
|
|
62
|
+
mocks.grantServiceAction.mockResolvedValue({
|
|
63
|
+
actionId,
|
|
64
|
+
caller: { surface: 'panel-app', appId: 'mood-calendar' },
|
|
65
|
+
grantedAt: '2026-05-28T00:00:00.000Z',
|
|
66
|
+
risk: 'write',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
manager.handleIframeMessage({
|
|
70
|
+
event: {
|
|
71
|
+
data: {
|
|
72
|
+
method: 'invoke',
|
|
73
|
+
payload: { actionId, input: { mood: 'happy' } },
|
|
74
|
+
requestId: 'request-1',
|
|
75
|
+
type: 'nextclaw:panel-app-service-actions:request',
|
|
76
|
+
},
|
|
77
|
+
source: contentWindow,
|
|
78
|
+
} as MessageEvent,
|
|
79
|
+
iframe,
|
|
80
|
+
tab: {
|
|
81
|
+
currentUrl: '/api/panel-apps/mood-calendar/content',
|
|
82
|
+
history: [],
|
|
83
|
+
historyIndex: 0,
|
|
84
|
+
id: 'tab-1',
|
|
85
|
+
kind: 'content',
|
|
86
|
+
navVersion: 0,
|
|
87
|
+
title: 'Mood Calendar',
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await waitFor(() => expect(postMessage).toHaveBeenCalled());
|
|
92
|
+
|
|
93
|
+
expect(mocks.requestAuthorization).toHaveBeenCalledWith(expect.objectContaining({
|
|
94
|
+
actionId,
|
|
95
|
+
actionTitle: 'Save mood',
|
|
96
|
+
inputPreview: expect.stringContaining('happy'),
|
|
97
|
+
panelAppId: 'mood-calendar',
|
|
98
|
+
risk: 'write',
|
|
99
|
+
}));
|
|
100
|
+
expect(mocks.grantServiceAction).toHaveBeenCalledWith(actionId, {
|
|
101
|
+
bridgeSessionToken: 'token-1',
|
|
102
|
+
});
|
|
103
|
+
expect(mocks.invokeServiceAction).toHaveBeenCalledTimes(2);
|
|
104
|
+
expect(postMessage).toHaveBeenCalledWith({
|
|
105
|
+
data: { actionId, result: { saved: true } },
|
|
106
|
+
ok: true,
|
|
107
|
+
requestId: 'request-1',
|
|
108
|
+
type: 'nextclaw:panel-app-service-actions:response',
|
|
109
|
+
}, '*');
|
|
110
|
+
});
|
|
111
|
+
});
|