@nextclaw/ui 0.13.4 → 0.13.6
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 +128 -0
- package/dist/assets/api-BCDb13Uv.js +8 -0
- package/dist/assets/app-presenter-provider-C-QMjK7m.js +3 -0
- package/dist/assets/arrow-left-rIXFA2Zy.js +1 -0
- package/dist/assets/channels-list-page-DnqwsKGe.js +8 -0
- package/dist/assets/chat-page-CsQx20xL.js +105 -0
- package/dist/assets/config-split-page-BwhGqmUS.js +1 -0
- package/dist/assets/confirm-dialog-DBkKcQ1q.js +5 -0
- package/dist/assets/desktop-update-config-BE9KjiXi.js +1 -0
- package/dist/assets/{dist-CpwDZDtv.js → dist-BI0bX8jv.js} +1 -1
- package/dist/assets/dist-C8JRt-tf.js +1 -0
- package/dist/assets/doc-browser-context-VHUOIUXL.js +1 -0
- package/dist/assets/doc-browser-eqdNEerh.js +1 -0
- package/dist/assets/doc-browser-lpF_pyx4.js +1 -0
- package/dist/assets/ellipsis-C9VB2CxT.js +1 -0
- package/dist/assets/es2015-BCw3Q7nY.js +41 -0
- package/dist/assets/external-link-Bp7_4s9t.js +1 -0
- package/dist/assets/index-BCq7kbC8.css +1 -0
- package/dist/assets/index-DTdPDqto.js +101 -0
- package/dist/assets/key-round-Ccy6oND3.js +1 -0
- package/dist/assets/loader-circle-OpH4EErP.js +1 -0
- package/dist/assets/{mcp-marketplace-page-Dvi8mCBd.js → mcp-marketplace-page-B-GxKOqS.js} +2 -2
- package/dist/assets/mcp-marketplace-page-Dm-0-w51.js +1 -0
- package/dist/assets/model-config-BXPnV_7n.js +1 -0
- package/dist/assets/notice-card-Dsbkl7u7.js +1 -0
- package/dist/assets/play-CvgYhNbo.js +1 -0
- package/dist/assets/plus-DHcP-K1U.js +1 -0
- package/dist/assets/popover-BTLZE5Iz.js +1 -0
- package/dist/assets/provider-models-C_yOh6DE.js +1 -0
- package/dist/assets/provider-scoped-model-input-DKI9KN7N.js +1 -0
- package/dist/assets/providers-list-Bko6B7dE.js +1 -0
- package/dist/assets/react-BqS8oMyM.js +8 -0
- package/dist/assets/refresh-cw-BIaj7txb.js +1 -0
- package/dist/assets/remote-DHn_b8QU.js +1 -0
- package/dist/assets/rotate-cw-DMqSkRJQ.js +1 -0
- package/dist/assets/runtime-config-page-DX_wSK1J.js +1 -0
- package/dist/assets/save-BEoORten.js +1 -0
- package/dist/assets/search-COFh_SFI.js +1 -0
- package/dist/assets/{search-config-G2O1fcRb.js → search-config-DA5By-tE.js} +1 -1
- package/dist/assets/{secrets-config-TenzxYbE.js → secrets-config-eDxG7iN4.js} +2 -2
- package/dist/assets/{status-dot-BSMG7ZaC.js → status-dot-oWBjTovo.js} +1 -1
- package/dist/assets/{tabs-D1CjpWNX.js → tabs-CGaY0BVO.js} +1 -1
- package/dist/assets/{tabs-custom-CRSN1ekg.js → tabs-custom-D2fPYb4G.js} +1 -1
- package/dist/assets/{tag-chip-DEashiw6.js → tag-chip-TVkgofS1.js} +1 -1
- package/dist/assets/tooltip-BwHPKBv9.js +1 -0
- package/dist/assets/trash-2-D3psAGrz.js +1 -0
- package/dist/assets/x-CHAxzyp2.js +1 -0
- package/dist/index.html +25 -23
- package/dist/logos/mimo.svg +9 -0
- package/package.json +8 -8
- package/public/logos/mimo.svg +9 -0
- package/src/app/components/layout/app-layout.tsx +10 -2
- package/src/features/agents/components/agents-page.tsx +6 -4
- package/src/features/chat/components/chat-welcome.test.tsx +15 -0
- package/src/features/chat/components/chat-welcome.tsx +4 -7
- package/src/features/chat/hooks/use-ncp-chat-page-data.ts +17 -10
- package/src/features/marketplace/components/marketplace-list-card.tsx +22 -1
- package/src/features/marketplace/components/marketplace-page.tsx +3 -1
- package/src/features/marketplace/hooks/use-marketplace.ts +3 -1
- package/src/features/marketplace/utils/marketplace-installed-cache.utils.ts +8 -0
- package/src/features/right-panel-resources/configs/right-panel-resource-routes.config.ts +70 -6
- package/src/features/right-panel-resources/utils/right-panel-resource-route-resolver.utils.test.ts +25 -1
- package/src/features/right-panel-resources/utils/right-panel-resource-route-resolver.utils.ts +1 -1
- package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.test.ts +42 -0
- package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.ts +22 -0
- package/src/features/service-apps/components/service-apps-panel.test.tsx +88 -0
- package/src/features/service-apps/components/service-apps-panel.tsx +246 -56
- package/src/features/service-apps/hooks/use-service-apps.ts +14 -0
- package/src/features/side-dock/components/side-dock.test.tsx +58 -3
- package/src/features/side-dock/components/side-dock.tsx +77 -24
- package/src/features/side-dock/configs/side-dock-built-in-items.config.ts +37 -34
- package/src/features/side-dock/index.ts +2 -0
- package/src/features/side-dock/managers/side-dock.manager.test.ts +84 -3
- package/src/features/side-dock/managers/side-dock.manager.ts +135 -12
- package/src/features/side-dock/types/side-dock.types.ts +11 -4
- package/src/features/side-dock/utils/side-dock-item.utils.test.ts +8 -8
- package/src/features/side-dock/utils/side-dock-item.utils.ts +13 -5
- package/src/platforms/desktop/components/desktop-app-shell.tsx +5 -2
- package/src/platforms/mobile/components/mobile-app-shell.tsx +4 -1
- package/src/shared/components/config/provider-form-support.test.ts +21 -0
- package/src/shared/components/config/provider-form-support.ts +38 -78
- package/src/shared/components/config/provider-form.tsx +54 -67
- package/src/shared/components/config/providers-list.tsx +211 -73
- package/src/shared/components/doc-browser/doc-browser-context.test.tsx +73 -0
- package/src/shared/components/doc-browser/doc-browser-context.tsx +3 -0
- package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +184 -98
- package/src/shared/components/doc-browser/doc-browser.test.tsx +168 -43
- package/src/shared/components/doc-browser/doc-browser.tsx +38 -7
- package/src/shared/components/doc-browser/index.ts +1 -0
- package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +30 -6
- package/src/shared/components/doc-browser/stores/doc-browser.store.ts +29 -0
- package/src/shared/components/doc-browser/types/doc-browser.types.ts +23 -0
- package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +7 -0
- package/src/shared/components/model-config.tsx +5 -4
- package/src/shared/components/provider-auth-section.tsx +2 -2
- package/src/shared/components/ui/switch.tsx +5 -2
- package/src/shared/hooks/use-config.ts +27 -6
- package/src/shared/lib/api/marketplace.types.ts +193 -0
- package/src/shared/lib/api/types.ts +23 -200
- package/src/shared/lib/api/utils/config.utils.ts +17 -7
- package/src/shared/lib/i18n/index.ts +10 -12
- package/src/shared/lib/i18n/{marketplace.ts → marketplace-labels.utils.ts} +3 -0
- package/src/shared/lib/i18n/runtime/doc-browser-labels.utils.ts +46 -2
- package/src/shared/lib/provider-models/index.test.ts +93 -15
- package/src/shared/lib/provider-models/index.ts +35 -20
- package/src/shared/lib/transport/request-raw-api-response.utils.ts +1 -1
- package/dist/assets/api-DruAcori.js +0 -15
- package/dist/assets/app-presenter-provider-DIcnNB9s.js +0 -3
- package/dist/assets/book-open-CRotBOF2.js +0 -1
- package/dist/assets/boxes-BvA-I63l.js +0 -1
- package/dist/assets/channels-list-page-wK3l7p5R.js +0 -8
- package/dist/assets/chat-page-DgehvdJT.js +0 -105
- package/dist/assets/config-split-page-BnTO1mAM.js +0 -1
- package/dist/assets/confirm-dialog-CGrKHbAW.js +0 -5
- package/dist/assets/createLucideIcon-Br1EFMEr.js +0 -1
- package/dist/assets/desktop-update-config-CehWxSSg.js +0 -1
- package/dist/assets/dist-rzakR_bu.js +0 -41
- package/dist/assets/doc-browser-BgxmVsTx.js +0 -1
- package/dist/assets/doc-browser-CZn5-Qbu.js +0 -1
- package/dist/assets/doc-browser-context-B8gk7HMr.js +0 -1
- package/dist/assets/external-link-DHjFdCpy.js +0 -1
- package/dist/assets/index-DORmh0Cy.css +0 -1
- package/dist/assets/index-DcFpvnge.js +0 -101
- package/dist/assets/key-round-D41Oe35x.js +0 -1
- package/dist/assets/mcp-marketplace-page-DBLhJWOJ.js +0 -1
- package/dist/assets/model-config-B-EJXGtO.js +0 -1
- package/dist/assets/play-C-0vo7hi.js +0 -1
- package/dist/assets/plus-Cr_akLp2.js +0 -1
- package/dist/assets/popover-YieElO8c.js +0 -1
- package/dist/assets/provider-scoped-model-input-BPRNGqYn.js +0 -1
- package/dist/assets/providers-list-CtBpL1ZY.js +0 -1
- package/dist/assets/react-B2X_ph0a.js +0 -1
- package/dist/assets/remote-BGHbHoHf.js +0 -1
- package/dist/assets/rotate-cw-jueVCNJQ.js +0 -1
- package/dist/assets/runtime-config-page-C1Hv7mad.js +0 -1
- package/dist/assets/save-aYdSdK97.js +0 -1
- package/dist/assets/search-CdhgFNwf.js +0 -1
- package/dist/assets/setting-row-BNu0qhOM.js +0 -1
- package/dist/assets/trash-2-DSWTsARW.js +0 -1
- package/dist/assets/x-D1v_vb54.js +0 -1
- package/src/shared/components/provider-enabled-field.tsx +0 -23
- /package/dist/assets/{config-hints-A7G_jMTU.js → config-hints-Ceiol9x4.js} +0 -0
|
@@ -4,40 +4,43 @@ import {
|
|
|
4
4
|
RIGHT_PANEL_SERVICE_APPS_URL,
|
|
5
5
|
} from '@/features/right-panel-resources';
|
|
6
6
|
import type { SideDockItem } from '@/features/side-dock/types/side-dock.types';
|
|
7
|
+
import { t } from '@/shared/lib/i18n';
|
|
7
8
|
|
|
8
9
|
export const SIDE_DOCK_DOCS_URL = 'nextclaw://docs';
|
|
9
10
|
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
11
|
+
export function getSideDockBuiltInItems(): SideDockItem[] {
|
|
12
|
+
return [
|
|
13
|
+
{
|
|
14
|
+
builtIn: true,
|
|
15
|
+
icon: { type: 'builtin', name: 'apps' },
|
|
16
|
+
id: 'apps',
|
|
17
|
+
label: t('appsTitle'),
|
|
18
|
+
removable: false,
|
|
19
|
+
target: { type: 'right-panel-resource', uri: RIGHT_PANEL_APPS_URL },
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
builtIn: true,
|
|
23
|
+
icon: { type: 'builtin', name: 'service-apps' },
|
|
24
|
+
id: 'service-apps',
|
|
25
|
+
label: t('serviceAppsTitle'),
|
|
26
|
+
removable: false,
|
|
27
|
+
target: { type: 'right-panel-resource', uri: RIGHT_PANEL_SERVICE_APPS_URL },
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
builtIn: true,
|
|
31
|
+
icon: { type: 'builtin', name: 'docs' },
|
|
32
|
+
id: 'docs',
|
|
33
|
+
label: t('docBrowserHelp'),
|
|
34
|
+
removable: false,
|
|
35
|
+
target: { type: 'right-panel-resource', uri: SIDE_DOCK_DOCS_URL },
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
builtIn: true,
|
|
39
|
+
icon: { type: 'builtin', name: 'new-tab' },
|
|
40
|
+
id: 'new-tab',
|
|
41
|
+
label: t('docBrowserHomeTitle'),
|
|
42
|
+
removable: false,
|
|
43
|
+
target: { type: 'right-panel-resource', uri: RIGHT_PANEL_HOME_URL },
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { SideDock } from '@/features/side-dock/components/side-dock';
|
|
2
2
|
export { SideDockManager } from '@/features/side-dock/managers/side-dock.manager';
|
|
3
|
+
export { useSideDockStore } from '@/features/side-dock/stores/side-dock.store';
|
|
3
4
|
export type {
|
|
4
5
|
SideDockIconName,
|
|
5
6
|
SideDockItem,
|
|
@@ -7,4 +8,5 @@ export type {
|
|
|
7
8
|
SideDockItemId,
|
|
8
9
|
SideDockItemTarget,
|
|
9
10
|
SideDockPinnedItem,
|
|
11
|
+
SideDockResourceDockState,
|
|
10
12
|
} from '@/features/side-dock/types/side-dock.types';
|
|
@@ -8,7 +8,7 @@ const customItem: SideDockItem = {
|
|
|
8
8
|
builtIn: false,
|
|
9
9
|
icon: { type: 'builtin', name: 'docs' },
|
|
10
10
|
id: 'custom-docs',
|
|
11
|
-
|
|
11
|
+
label: 'Help Docs',
|
|
12
12
|
removable: true,
|
|
13
13
|
target: { type: 'right-panel-resource', uri: 'nextclaw://docs/custom' },
|
|
14
14
|
};
|
|
@@ -25,7 +25,10 @@ describe('SideDockManager', () => {
|
|
|
25
25
|
|
|
26
26
|
manager.openItem(customItem);
|
|
27
27
|
|
|
28
|
-
expect(open).toHaveBeenCalledWith('nextclaw://docs/custom'
|
|
28
|
+
expect(open).toHaveBeenCalledWith('nextclaw://docs/custom', {
|
|
29
|
+
dockIcon: { type: 'builtin', name: 'docs' },
|
|
30
|
+
title: 'Help Docs',
|
|
31
|
+
});
|
|
29
32
|
});
|
|
30
33
|
|
|
31
34
|
it('pins removable items and keeps built-in items immutable', () => {
|
|
@@ -39,12 +42,90 @@ describe('SideDockManager', () => {
|
|
|
39
42
|
createdAt: '2026-06-02T00:00:00.000Z',
|
|
40
43
|
icon: { type: 'builtin', name: 'docs' },
|
|
41
44
|
id: 'custom-docs',
|
|
42
|
-
|
|
45
|
+
label: 'Help Docs',
|
|
43
46
|
target: { type: 'right-panel-resource', uri: 'nextclaw://docs/custom' },
|
|
44
47
|
},
|
|
45
48
|
]);
|
|
46
49
|
});
|
|
47
50
|
|
|
51
|
+
it('pins and unpins the current doc browser tab by resource URI', () => {
|
|
52
|
+
const manager = new SideDockManager({ open: vi.fn() } as unknown as DocBrowserManager);
|
|
53
|
+
|
|
54
|
+
manager.pinTab({
|
|
55
|
+
currentUrl: 'https://docs.nextclaw.ai/guide/getting-started',
|
|
56
|
+
history: ['https://docs.nextclaw.ai/guide/getting-started'],
|
|
57
|
+
historyIndex: 0,
|
|
58
|
+
id: 'tab-1',
|
|
59
|
+
kind: 'docs',
|
|
60
|
+
navVersion: 0,
|
|
61
|
+
resourceUri: 'nextclaw://docs/guide/getting-started',
|
|
62
|
+
title: 'Getting Started',
|
|
63
|
+
}, '2026-06-02T00:00:00.000Z');
|
|
64
|
+
|
|
65
|
+
expect(useSideDockStore.getState().pinnedItems).toMatchObject([
|
|
66
|
+
{
|
|
67
|
+
createdAt: '2026-06-02T00:00:00.000Z',
|
|
68
|
+
label: 'Getting Started',
|
|
69
|
+
target: { type: 'right-panel-resource', uri: 'nextclaw://docs/guide/getting-started' },
|
|
70
|
+
},
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
manager.unpinTab({
|
|
74
|
+
currentUrl: 'https://docs.nextclaw.ai/guide/getting-started',
|
|
75
|
+
history: ['https://docs.nextclaw.ai/guide/getting-started'],
|
|
76
|
+
historyIndex: 0,
|
|
77
|
+
id: 'tab-1',
|
|
78
|
+
kind: 'docs',
|
|
79
|
+
navVersion: 0,
|
|
80
|
+
resourceUri: 'nextclaw://docs/guide/getting-started',
|
|
81
|
+
title: 'Getting Started',
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(useSideDockStore.getState().pinnedItems).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('pins panel app tabs with their own dock icon metadata', () => {
|
|
88
|
+
const manager = new SideDockManager({ open: vi.fn() } as unknown as DocBrowserManager);
|
|
89
|
+
|
|
90
|
+
manager.pinTab({
|
|
91
|
+
currentUrl: '/api/panel-apps/demo/content',
|
|
92
|
+
dockIcon: { type: 'url', url: '/api/panel-apps/demo/assets/icon.png' },
|
|
93
|
+
history: ['/api/panel-apps/demo/content'],
|
|
94
|
+
historyIndex: 0,
|
|
95
|
+
id: 'tab-1',
|
|
96
|
+
kind: 'panel-app',
|
|
97
|
+
navVersion: 0,
|
|
98
|
+
resourceUri: 'nextclaw://panel-app/demo',
|
|
99
|
+
title: 'Demo App',
|
|
100
|
+
}, '2026-06-02T00:00:00.000Z');
|
|
101
|
+
|
|
102
|
+
expect(useSideDockStore.getState().pinnedItems).toMatchObject([
|
|
103
|
+
{
|
|
104
|
+
icon: { type: 'url', url: '/api/panel-apps/demo/assets/icon.png' },
|
|
105
|
+
label: 'Demo App',
|
|
106
|
+
target: { type: 'right-panel-resource', uri: 'nextclaw://panel-app/demo' },
|
|
107
|
+
},
|
|
108
|
+
]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('pins panel app text icons instead of falling back to a generic icon', () => {
|
|
112
|
+
const manager = new SideDockManager({ open: vi.fn() } as unknown as DocBrowserManager);
|
|
113
|
+
|
|
114
|
+
manager.pinTab({
|
|
115
|
+
currentUrl: '/api/panel-apps/compass/content',
|
|
116
|
+
dockIcon: { type: 'text', value: 'C' },
|
|
117
|
+
history: ['/api/panel-apps/compass/content'],
|
|
118
|
+
historyIndex: 0,
|
|
119
|
+
id: 'tab-1',
|
|
120
|
+
kind: 'panel-app',
|
|
121
|
+
navVersion: 0,
|
|
122
|
+
resourceUri: 'nextclaw://panel-app/compass',
|
|
123
|
+
title: 'Compass',
|
|
124
|
+
}, '2026-06-02T00:00:00.000Z');
|
|
125
|
+
|
|
126
|
+
expect(useSideDockStore.getState().pinnedItems[0]?.icon).toEqual({ type: 'text', value: 'C' });
|
|
127
|
+
});
|
|
128
|
+
|
|
48
129
|
it('unpins and reorders pinned items', () => {
|
|
49
130
|
const manager = new SideDockManager({ open: vi.fn() } as unknown as DocBrowserManager);
|
|
50
131
|
|
|
@@ -1,43 +1,155 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getSideDockBuiltInItems } from '@/features/side-dock/configs/side-dock-built-in-items.config';
|
|
2
2
|
import { useSideDockStore } from '@/features/side-dock/stores/side-dock.store';
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
SideDockIconName,
|
|
5
|
+
SideDockItem,
|
|
6
|
+
SideDockPinnedItem,
|
|
7
|
+
SideDockResourceDockState,
|
|
8
|
+
} from '@/features/side-dock/types/side-dock.types';
|
|
4
9
|
import {
|
|
5
10
|
createPinnedSideDockItem,
|
|
6
11
|
mergeSideDockItems,
|
|
7
12
|
} from '@/features/side-dock/utils/side-dock-item.utils';
|
|
8
13
|
import type { DocBrowserManager } from '@/shared/components/doc-browser/managers/doc-browser.manager';
|
|
14
|
+
import type {
|
|
15
|
+
DocBrowserDockIcon,
|
|
16
|
+
DocBrowserTab,
|
|
17
|
+
} from '@/shared/components/doc-browser/types/doc-browser.types';
|
|
18
|
+
|
|
19
|
+
const SIDE_DOCK_PINNED_ITEM_ID_PREFIX = 'pinned';
|
|
20
|
+
const SIDE_DOCK_ICON_NAMES = new Set<SideDockIconName>([
|
|
21
|
+
'apps',
|
|
22
|
+
'docs',
|
|
23
|
+
'new-tab',
|
|
24
|
+
'panel-app',
|
|
25
|
+
'service-apps',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
function normalizeSideDockResourceUri(uri: string): string {
|
|
29
|
+
return uri.trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createPinnedItemId(uri: string): string {
|
|
33
|
+
let hash = 0;
|
|
34
|
+
Array.from(normalizeSideDockResourceUri(uri)).forEach((char) => {
|
|
35
|
+
hash = ((hash * 31) + char.charCodeAt(0)) >>> 0;
|
|
36
|
+
});
|
|
37
|
+
const slug = normalizeSideDockResourceUri(uri)
|
|
38
|
+
.replace(/[^a-zA-Z0-9]+/g, '-')
|
|
39
|
+
.replace(/^-+|-+$/g, '')
|
|
40
|
+
.toLowerCase();
|
|
41
|
+
return `${SIDE_DOCK_PINNED_ITEM_ID_PREFIX}-${slug || 'resource'}-${hash.toString(36)}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolveSideDockIcon(icon?: DocBrowserDockIcon): SideDockItem['icon'] | null {
|
|
45
|
+
if (icon?.type === 'builtin' && SIDE_DOCK_ICON_NAMES.has(icon.name as SideDockIconName)) {
|
|
46
|
+
return { type: 'builtin', name: icon.name as SideDockIconName };
|
|
47
|
+
}
|
|
48
|
+
if (icon?.type === 'url' && icon.url.trim().length > 0) {
|
|
49
|
+
return { type: 'url', url: icon.url.trim() };
|
|
50
|
+
}
|
|
51
|
+
if (icon?.type === 'text' && icon.value.trim().length > 0) {
|
|
52
|
+
return { type: 'text', value: icon.value.trim() };
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getFallbackIconNameForTab(tab: DocBrowserTab): SideDockIconName {
|
|
58
|
+
if (tab.kind === 'docs') {
|
|
59
|
+
return 'docs';
|
|
60
|
+
}
|
|
61
|
+
if (tab.kind === 'home') {
|
|
62
|
+
return 'new-tab';
|
|
63
|
+
}
|
|
64
|
+
if (tab.kind === 'apps') {
|
|
65
|
+
return tab.resourceUri?.includes('service-apps') ? 'service-apps' : 'apps';
|
|
66
|
+
}
|
|
67
|
+
if (tab.kind === 'panel-app') {
|
|
68
|
+
return 'panel-app';
|
|
69
|
+
}
|
|
70
|
+
return 'apps';
|
|
71
|
+
}
|
|
9
72
|
|
|
10
73
|
export class SideDockManager {
|
|
11
74
|
constructor(private readonly docBrowserManager: DocBrowserManager) {}
|
|
12
75
|
|
|
13
|
-
|
|
14
|
-
|
|
76
|
+
getItems = (): SideDockItem[] => mergeSideDockItems(
|
|
77
|
+
getSideDockBuiltInItems(),
|
|
15
78
|
useSideDockStore.getState().pinnedItems,
|
|
16
79
|
);
|
|
17
80
|
|
|
18
|
-
|
|
81
|
+
openItem = (item: Pick<SideDockItem, 'icon' | 'label' | 'target'>): void => {
|
|
19
82
|
if (item.target.type === 'right-panel-resource') {
|
|
20
|
-
this.docBrowserManager.open(item.target.uri
|
|
83
|
+
this.docBrowserManager.open(item.target.uri, {
|
|
84
|
+
dockIcon: item.icon,
|
|
85
|
+
title: item.label,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
getDockState = (tab?: DocBrowserTab): SideDockResourceDockState => {
|
|
91
|
+
const uri = this.getTabResourceUri(tab);
|
|
92
|
+
if (!uri) {
|
|
93
|
+
return {
|
|
94
|
+
canDock: false,
|
|
95
|
+
isDocked: false,
|
|
96
|
+
removable: false,
|
|
97
|
+
};
|
|
21
98
|
}
|
|
99
|
+
|
|
100
|
+
const item = this.findItemByUri(uri);
|
|
101
|
+
return {
|
|
102
|
+
canDock: true,
|
|
103
|
+
isDocked: !!item,
|
|
104
|
+
removable: item?.removable === true,
|
|
105
|
+
};
|
|
22
106
|
};
|
|
23
107
|
|
|
24
|
-
|
|
108
|
+
pinTab = (tab?: DocBrowserTab, createdAt: string = new Date().toISOString()): void => {
|
|
109
|
+
const uri = this.getTabResourceUri(tab);
|
|
110
|
+
if (!tab || !uri || this.findItemByUri(uri)) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
this.pinItem({
|
|
114
|
+
builtIn: false,
|
|
115
|
+
icon: resolveSideDockIcon(tab.dockIcon) ?? { type: 'builtin', name: getFallbackIconNameForTab(tab) },
|
|
116
|
+
id: createPinnedItemId(uri),
|
|
117
|
+
label: tab.title.trim() || uri,
|
|
118
|
+
removable: true,
|
|
119
|
+
target: { type: 'right-panel-resource', uri },
|
|
120
|
+
}, createdAt);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
unpinTab = (tab?: DocBrowserTab): void => {
|
|
124
|
+
const uri = this.getTabResourceUri(tab);
|
|
125
|
+
const item = uri ? this.findItemByUri(uri) : undefined;
|
|
126
|
+
if (!item?.removable) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
this.unpinItem(item.id);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
pinItem = (item: SideDockItem, createdAt: string = new Date().toISOString()): void => {
|
|
25
133
|
if (!item.removable) {
|
|
26
134
|
return;
|
|
27
135
|
}
|
|
136
|
+
const uri = normalizeSideDockResourceUri(item.target.uri);
|
|
28
137
|
this.setPinnedItems((items) => {
|
|
29
|
-
if (items.some((pinnedItem) => pinnedItem.id === item.id || pinnedItem.target.uri ===
|
|
138
|
+
if (items.some((pinnedItem) => pinnedItem.id === item.id || normalizeSideDockResourceUri(pinnedItem.target.uri) === uri)) {
|
|
30
139
|
return items;
|
|
31
140
|
}
|
|
32
|
-
return [...items, createPinnedSideDockItem(
|
|
141
|
+
return [...items, createPinnedSideDockItem({
|
|
142
|
+
...item,
|
|
143
|
+
target: { ...item.target, uri },
|
|
144
|
+
}, createdAt)];
|
|
33
145
|
});
|
|
34
146
|
};
|
|
35
147
|
|
|
36
|
-
|
|
148
|
+
unpinItem = (itemId: string): void => {
|
|
37
149
|
this.setPinnedItems((items) => items.filter((item) => item.id !== itemId));
|
|
38
150
|
};
|
|
39
151
|
|
|
40
|
-
|
|
152
|
+
reorderPinnedItems = (itemIds: string[]): void => {
|
|
41
153
|
this.setPinnedItems((items) => {
|
|
42
154
|
const itemsById = new Map(items.map((item) => [item.id, item]));
|
|
43
155
|
const orderedItems = itemIds
|
|
@@ -49,10 +161,21 @@ export class SideDockManager {
|
|
|
49
161
|
});
|
|
50
162
|
};
|
|
51
163
|
|
|
52
|
-
private
|
|
164
|
+
private setPinnedItems = (
|
|
53
165
|
updater: (items: SideDockPinnedItem[]) => SideDockPinnedItem[],
|
|
54
166
|
): void => {
|
|
55
167
|
const { pinnedItems, setPinnedItems } = useSideDockStore.getState();
|
|
56
168
|
setPinnedItems(updater(pinnedItems));
|
|
57
169
|
};
|
|
170
|
+
|
|
171
|
+
private getTabResourceUri = (tab?: DocBrowserTab): string | null => {
|
|
172
|
+
const uri = tab?.resourceUri ?? tab?.currentUrl;
|
|
173
|
+
const normalized = uri ? normalizeSideDockResourceUri(uri) : '';
|
|
174
|
+
return normalized.length > 0 ? normalized : null;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
private findItemByUri = (uri: string): SideDockItem | undefined => {
|
|
178
|
+
const normalized = normalizeSideDockResourceUri(uri);
|
|
179
|
+
return this.getItems().find((item) => normalizeSideDockResourceUri(item.target.uri) === normalized);
|
|
180
|
+
};
|
|
58
181
|
}
|
|
@@ -5,17 +5,18 @@ export type SideDockItemTarget = {
|
|
|
5
5
|
uri: string;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
-
export type SideDockIconName = 'apps' | 'docs' | 'new-tab' | 'service-apps';
|
|
8
|
+
export type SideDockIconName = 'apps' | 'docs' | 'new-tab' | 'panel-app' | 'service-apps';
|
|
9
9
|
|
|
10
10
|
export type SideDockItemIcon =
|
|
11
11
|
| { type: 'builtin'; name: SideDockIconName }
|
|
12
|
-
| { type: 'url'; url: string }
|
|
12
|
+
| { type: 'url'; url: string }
|
|
13
|
+
| { type: 'text'; value: string };
|
|
13
14
|
|
|
14
15
|
export type SideDockItem = {
|
|
15
16
|
builtIn: boolean;
|
|
16
17
|
icon: SideDockItemIcon;
|
|
17
18
|
id: SideDockItemId;
|
|
18
|
-
|
|
19
|
+
label: string;
|
|
19
20
|
removable: boolean;
|
|
20
21
|
target: SideDockItemTarget;
|
|
21
22
|
};
|
|
@@ -24,6 +25,12 @@ export type SideDockPinnedItem = {
|
|
|
24
25
|
createdAt: string;
|
|
25
26
|
icon: SideDockItemIcon;
|
|
26
27
|
id: SideDockItemId;
|
|
27
|
-
|
|
28
|
+
label: string;
|
|
28
29
|
target: SideDockItemTarget;
|
|
29
30
|
};
|
|
31
|
+
|
|
32
|
+
export type SideDockResourceDockState = {
|
|
33
|
+
canDock: boolean;
|
|
34
|
+
isDocked: boolean;
|
|
35
|
+
removable: boolean;
|
|
36
|
+
};
|
|
@@ -10,7 +10,7 @@ const builtInItem: SideDockItem = {
|
|
|
10
10
|
builtIn: true,
|
|
11
11
|
icon: { type: 'builtin', name: 'apps' },
|
|
12
12
|
id: 'apps',
|
|
13
|
-
|
|
13
|
+
label: 'Apps',
|
|
14
14
|
removable: false,
|
|
15
15
|
target: { type: 'right-panel-resource', uri: 'nextclaw://apps' },
|
|
16
16
|
};
|
|
@@ -20,16 +20,16 @@ describe('side-dock-item utils', () => {
|
|
|
20
20
|
const items = normalizeSideDockPinnedItems([
|
|
21
21
|
{
|
|
22
22
|
createdAt: '2026-06-02T00:00:00.000Z',
|
|
23
|
-
icon: { type: '
|
|
23
|
+
icon: { type: 'text', value: 'D' },
|
|
24
24
|
id: 'docs-custom',
|
|
25
|
-
|
|
25
|
+
label: 'Help Docs',
|
|
26
26
|
target: { type: 'right-panel-resource', uri: 'nextclaw://docs' },
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
createdAt: '2026-06-02T00:00:00.000Z',
|
|
30
30
|
icon: { type: 'builtin', name: 'unknown' },
|
|
31
31
|
id: 'bad-icon',
|
|
32
|
-
|
|
32
|
+
label: 'Bad',
|
|
33
33
|
target: { type: 'right-panel-resource', uri: 'nextclaw://bad' },
|
|
34
34
|
},
|
|
35
35
|
]);
|
|
@@ -37,9 +37,9 @@ describe('side-dock-item utils', () => {
|
|
|
37
37
|
expect(items).toEqual([
|
|
38
38
|
{
|
|
39
39
|
createdAt: '2026-06-02T00:00:00.000Z',
|
|
40
|
-
icon: { type: '
|
|
40
|
+
icon: { type: 'text', value: 'D' },
|
|
41
41
|
id: 'docs-custom',
|
|
42
|
-
|
|
42
|
+
label: 'Help Docs',
|
|
43
43
|
target: { type: 'right-panel-resource', uri: 'nextclaw://docs' },
|
|
44
44
|
},
|
|
45
45
|
]);
|
|
@@ -58,14 +58,14 @@ describe('side-dock-item utils', () => {
|
|
|
58
58
|
createdAt: '2026-06-02T00:00:00.000Z',
|
|
59
59
|
icon: { type: 'builtin', name: 'docs' },
|
|
60
60
|
id: 'docs-custom',
|
|
61
|
-
|
|
61
|
+
label: 'Help Docs',
|
|
62
62
|
target: { type: 'right-panel-resource', uri: 'nextclaw://docs' },
|
|
63
63
|
},
|
|
64
64
|
{
|
|
65
65
|
createdAt: '2026-06-02T00:00:00.000Z',
|
|
66
66
|
icon: { type: 'builtin', name: 'apps' },
|
|
67
67
|
id: 'apps-copy',
|
|
68
|
-
|
|
68
|
+
label: 'Apps',
|
|
69
69
|
target: { type: 'right-panel-resource', uri: 'nextclaw://apps' },
|
|
70
70
|
},
|
|
71
71
|
],
|
|
@@ -9,6 +9,7 @@ const SIDE_DOCK_BUILTIN_ICON_NAMES: SideDockIconName[] = [
|
|
|
9
9
|
'apps',
|
|
10
10
|
'docs',
|
|
11
11
|
'new-tab',
|
|
12
|
+
'panel-app',
|
|
12
13
|
'service-apps',
|
|
13
14
|
];
|
|
14
15
|
|
|
@@ -39,6 +40,13 @@ function normalizeSideDockIcon(value: unknown): SideDockItemIcon | null {
|
|
|
39
40
|
};
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
if (value.type === 'text' && typeof value.value === 'string' && value.value.trim().length > 0) {
|
|
44
|
+
return {
|
|
45
|
+
type: 'text',
|
|
46
|
+
value: value.value.trim(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
42
50
|
return null;
|
|
43
51
|
}
|
|
44
52
|
|
|
@@ -47,8 +55,8 @@ export function normalizeSideDockPinnedItem(value: unknown): SideDockPinnedItem
|
|
|
47
55
|
!isRecord(value)
|
|
48
56
|
|| typeof value.id !== 'string'
|
|
49
57
|
|| value.id.trim().length === 0
|
|
50
|
-
|| typeof value.
|
|
51
|
-
|| value.
|
|
58
|
+
|| typeof value.label !== 'string'
|
|
59
|
+
|| value.label.trim().length === 0
|
|
52
60
|
|| typeof value.createdAt !== 'string'
|
|
53
61
|
|| value.createdAt.trim().length === 0
|
|
54
62
|
|| !isRecord(value.target)
|
|
@@ -68,7 +76,7 @@ export function normalizeSideDockPinnedItem(value: unknown): SideDockPinnedItem
|
|
|
68
76
|
createdAt: value.createdAt,
|
|
69
77
|
icon,
|
|
70
78
|
id: value.id.trim(),
|
|
71
|
-
|
|
79
|
+
label: value.label.trim(),
|
|
72
80
|
target: {
|
|
73
81
|
type: 'right-panel-resource',
|
|
74
82
|
uri: value.target.uri.trim(),
|
|
@@ -96,7 +104,7 @@ export function createPinnedSideDockItem(item: SideDockItem, createdAt: string):
|
|
|
96
104
|
createdAt,
|
|
97
105
|
icon: item.icon,
|
|
98
106
|
id: item.id,
|
|
99
|
-
|
|
107
|
+
label: item.label,
|
|
100
108
|
target: item.target,
|
|
101
109
|
};
|
|
102
110
|
}
|
|
@@ -106,7 +114,7 @@ export function createSideDockItemFromPinnedItem(item: SideDockPinnedItem): Side
|
|
|
106
114
|
builtIn: false,
|
|
107
115
|
icon: item.icon,
|
|
108
116
|
id: item.id,
|
|
109
|
-
|
|
117
|
+
label: item.label,
|
|
110
118
|
removable: true,
|
|
111
119
|
target: item.target,
|
|
112
120
|
};
|
|
@@ -9,6 +9,7 @@ import { DesktopWindowChrome } from "@/platforms/desktop/components/desktop-wind
|
|
|
9
9
|
import { isWindowsDesktopHost } from "@/platforms/desktop/utils/desktop-host.utils";
|
|
10
10
|
import { MobileBottomNav } from "@/platforms/mobile";
|
|
11
11
|
import type { DocBrowserCustomTabRenderers } from "@/shared/components/doc-browser/doc-browser-renderer.types";
|
|
12
|
+
import type { DocBrowserDockControls } from "@/shared/components/doc-browser/doc-browser-context";
|
|
12
13
|
import { cn } from "@/shared/lib/utils";
|
|
13
14
|
|
|
14
15
|
const DocBrowser = lazy(async () => ({
|
|
@@ -20,6 +21,7 @@ type DesktopAppShellProps = {
|
|
|
20
21
|
isMobileLayout?: boolean;
|
|
21
22
|
isDocBrowserOpen: boolean;
|
|
22
23
|
docBrowserMode: "floating" | "docked";
|
|
24
|
+
docBrowserDockControls?: DocBrowserDockControls;
|
|
23
25
|
docBrowserRenderers?: DocBrowserCustomTabRenderers;
|
|
24
26
|
sideDock?: React.ReactNode;
|
|
25
27
|
children: React.ReactNode;
|
|
@@ -30,6 +32,7 @@ export function DesktopAppShell({
|
|
|
30
32
|
isMobileLayout = false,
|
|
31
33
|
isDocBrowserOpen,
|
|
32
34
|
docBrowserMode,
|
|
35
|
+
docBrowserDockControls,
|
|
33
36
|
docBrowserRenderers = {},
|
|
34
37
|
sideDock,
|
|
35
38
|
children,
|
|
@@ -67,7 +70,7 @@ export function DesktopAppShell({
|
|
|
67
70
|
</div>
|
|
68
71
|
{isDocBrowserOpen && docBrowserMode === "docked" ? (
|
|
69
72
|
<Suspense fallback={null}>
|
|
70
|
-
<DocBrowser customTabRenderers={docBrowserRenderers} />
|
|
73
|
+
<DocBrowser customTabRenderers={docBrowserRenderers} dockControls={docBrowserDockControls} />
|
|
71
74
|
</Suspense>
|
|
72
75
|
) : null}
|
|
73
76
|
{sideDock}
|
|
@@ -76,7 +79,7 @@ export function DesktopAppShell({
|
|
|
76
79
|
{showMobileBottomNav ? <MobileBottomNav /> : null}
|
|
77
80
|
{isDocBrowserOpen && docBrowserMode === "floating" ? (
|
|
78
81
|
<Suspense fallback={null}>
|
|
79
|
-
<DocBrowser customTabRenderers={docBrowserRenderers} />
|
|
82
|
+
<DocBrowser customTabRenderers={docBrowserRenderers} dockControls={docBrowserDockControls} />
|
|
80
83
|
</Suspense>
|
|
81
84
|
) : null}
|
|
82
85
|
</div>
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
import { MobileBottomNav } from "@/platforms/mobile/components/mobile-bottom-nav";
|
|
7
7
|
import { MobileTopbar } from "@/platforms/mobile/components/mobile-topbar";
|
|
8
8
|
import type { DocBrowserCustomTabRenderers } from "@/shared/components/doc-browser/doc-browser-renderer.types";
|
|
9
|
+
import type { DocBrowserDockControls } from "@/shared/components/doc-browser/doc-browser-context";
|
|
9
10
|
|
|
10
11
|
const DocBrowser = lazy(async () => ({
|
|
11
12
|
default: (await import("@/shared/components/doc-browser/doc-browser")).DocBrowser,
|
|
@@ -14,6 +15,7 @@ const DocBrowser = lazy(async () => ({
|
|
|
14
15
|
type MobileAppShellProps = {
|
|
15
16
|
pathname: string;
|
|
16
17
|
isDocBrowserOpen: boolean;
|
|
18
|
+
docBrowserDockControls?: DocBrowserDockControls;
|
|
17
19
|
docBrowserRenderers?: DocBrowserCustomTabRenderers;
|
|
18
20
|
topbarLeadingInset?: string;
|
|
19
21
|
children: React.ReactNode;
|
|
@@ -22,6 +24,7 @@ type MobileAppShellProps = {
|
|
|
22
24
|
export function MobileAppShell({
|
|
23
25
|
pathname,
|
|
24
26
|
isDocBrowserOpen,
|
|
27
|
+
docBrowserDockControls,
|
|
25
28
|
docBrowserRenderers = {},
|
|
26
29
|
topbarLeadingInset,
|
|
27
30
|
children,
|
|
@@ -45,7 +48,7 @@ export function MobileAppShell({
|
|
|
45
48
|
{showBottomNav ? <MobileBottomNav /> : null}
|
|
46
49
|
{isDocBrowserOpen ? (
|
|
47
50
|
<Suspense fallback={null}>
|
|
48
|
-
<DocBrowser customTabRenderers={docBrowserRenderers} displayMode="fullscreen" />
|
|
51
|
+
<DocBrowser customTabRenderers={docBrowserRenderers} displayMode="fullscreen" dockControls={docBrowserDockControls} />
|
|
49
52
|
</Suspense>
|
|
50
53
|
) : null}
|
|
51
54
|
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { resolveEditableModels, serializeModelsForSave } from './provider-form-support';
|
|
3
|
+
|
|
4
|
+
describe('provider form model defaults', () => {
|
|
5
|
+
it('keeps persisted empty models empty', () => {
|
|
6
|
+
expect(resolveEditableModels(['gpt-5.5', 'gpt-5.4'], [])).toEqual([]);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('keeps a non-empty saved override without resurrecting removed defaults', () => {
|
|
10
|
+
expect(resolveEditableModels(['deepseek-chat', 'deepseek-reasoner'], ['deepseek-chat'])).toEqual([
|
|
11
|
+
'deepseek-chat'
|
|
12
|
+
]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('serializes local model ids with the providerId prefix', () => {
|
|
16
|
+
expect(serializeModelsForSave(['gpt-5.5', 'openai-work/gpt-5.4'], 'openai-work')).toEqual([
|
|
17
|
+
'openai-work/gpt-5.5',
|
|
18
|
+
'openai-work/gpt-5.4'
|
|
19
|
+
]);
|
|
20
|
+
});
|
|
21
|
+
});
|