@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.
Files changed (142) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/dist/assets/api-BCDb13Uv.js +8 -0
  3. package/dist/assets/app-presenter-provider-C-QMjK7m.js +3 -0
  4. package/dist/assets/arrow-left-rIXFA2Zy.js +1 -0
  5. package/dist/assets/channels-list-page-DnqwsKGe.js +8 -0
  6. package/dist/assets/chat-page-CsQx20xL.js +105 -0
  7. package/dist/assets/config-split-page-BwhGqmUS.js +1 -0
  8. package/dist/assets/confirm-dialog-DBkKcQ1q.js +5 -0
  9. package/dist/assets/desktop-update-config-BE9KjiXi.js +1 -0
  10. package/dist/assets/{dist-CpwDZDtv.js → dist-BI0bX8jv.js} +1 -1
  11. package/dist/assets/dist-C8JRt-tf.js +1 -0
  12. package/dist/assets/doc-browser-context-VHUOIUXL.js +1 -0
  13. package/dist/assets/doc-browser-eqdNEerh.js +1 -0
  14. package/dist/assets/doc-browser-lpF_pyx4.js +1 -0
  15. package/dist/assets/ellipsis-C9VB2CxT.js +1 -0
  16. package/dist/assets/es2015-BCw3Q7nY.js +41 -0
  17. package/dist/assets/external-link-Bp7_4s9t.js +1 -0
  18. package/dist/assets/index-BCq7kbC8.css +1 -0
  19. package/dist/assets/index-DTdPDqto.js +101 -0
  20. package/dist/assets/key-round-Ccy6oND3.js +1 -0
  21. package/dist/assets/loader-circle-OpH4EErP.js +1 -0
  22. package/dist/assets/{mcp-marketplace-page-Dvi8mCBd.js → mcp-marketplace-page-B-GxKOqS.js} +2 -2
  23. package/dist/assets/mcp-marketplace-page-Dm-0-w51.js +1 -0
  24. package/dist/assets/model-config-BXPnV_7n.js +1 -0
  25. package/dist/assets/notice-card-Dsbkl7u7.js +1 -0
  26. package/dist/assets/play-CvgYhNbo.js +1 -0
  27. package/dist/assets/plus-DHcP-K1U.js +1 -0
  28. package/dist/assets/popover-BTLZE5Iz.js +1 -0
  29. package/dist/assets/provider-models-C_yOh6DE.js +1 -0
  30. package/dist/assets/provider-scoped-model-input-DKI9KN7N.js +1 -0
  31. package/dist/assets/providers-list-Bko6B7dE.js +1 -0
  32. package/dist/assets/react-BqS8oMyM.js +8 -0
  33. package/dist/assets/refresh-cw-BIaj7txb.js +1 -0
  34. package/dist/assets/remote-DHn_b8QU.js +1 -0
  35. package/dist/assets/rotate-cw-DMqSkRJQ.js +1 -0
  36. package/dist/assets/runtime-config-page-DX_wSK1J.js +1 -0
  37. package/dist/assets/save-BEoORten.js +1 -0
  38. package/dist/assets/search-COFh_SFI.js +1 -0
  39. package/dist/assets/{search-config-G2O1fcRb.js → search-config-DA5By-tE.js} +1 -1
  40. package/dist/assets/{secrets-config-TenzxYbE.js → secrets-config-eDxG7iN4.js} +2 -2
  41. package/dist/assets/{status-dot-BSMG7ZaC.js → status-dot-oWBjTovo.js} +1 -1
  42. package/dist/assets/{tabs-D1CjpWNX.js → tabs-CGaY0BVO.js} +1 -1
  43. package/dist/assets/{tabs-custom-CRSN1ekg.js → tabs-custom-D2fPYb4G.js} +1 -1
  44. package/dist/assets/{tag-chip-DEashiw6.js → tag-chip-TVkgofS1.js} +1 -1
  45. package/dist/assets/tooltip-BwHPKBv9.js +1 -0
  46. package/dist/assets/trash-2-D3psAGrz.js +1 -0
  47. package/dist/assets/x-CHAxzyp2.js +1 -0
  48. package/dist/index.html +25 -23
  49. package/dist/logos/mimo.svg +9 -0
  50. package/package.json +8 -8
  51. package/public/logos/mimo.svg +9 -0
  52. package/src/app/components/layout/app-layout.tsx +10 -2
  53. package/src/features/agents/components/agents-page.tsx +6 -4
  54. package/src/features/chat/components/chat-welcome.test.tsx +15 -0
  55. package/src/features/chat/components/chat-welcome.tsx +4 -7
  56. package/src/features/chat/hooks/use-ncp-chat-page-data.ts +17 -10
  57. package/src/features/marketplace/components/marketplace-list-card.tsx +22 -1
  58. package/src/features/marketplace/components/marketplace-page.tsx +3 -1
  59. package/src/features/marketplace/hooks/use-marketplace.ts +3 -1
  60. package/src/features/marketplace/utils/marketplace-installed-cache.utils.ts +8 -0
  61. package/src/features/right-panel-resources/configs/right-panel-resource-routes.config.ts +70 -6
  62. package/src/features/right-panel-resources/utils/right-panel-resource-route-resolver.utils.test.ts +25 -1
  63. package/src/features/right-panel-resources/utils/right-panel-resource-route-resolver.utils.ts +1 -1
  64. package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.test.ts +42 -0
  65. package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.ts +22 -0
  66. package/src/features/service-apps/components/service-apps-panel.test.tsx +88 -0
  67. package/src/features/service-apps/components/service-apps-panel.tsx +246 -56
  68. package/src/features/service-apps/hooks/use-service-apps.ts +14 -0
  69. package/src/features/side-dock/components/side-dock.test.tsx +58 -3
  70. package/src/features/side-dock/components/side-dock.tsx +77 -24
  71. package/src/features/side-dock/configs/side-dock-built-in-items.config.ts +37 -34
  72. package/src/features/side-dock/index.ts +2 -0
  73. package/src/features/side-dock/managers/side-dock.manager.test.ts +84 -3
  74. package/src/features/side-dock/managers/side-dock.manager.ts +135 -12
  75. package/src/features/side-dock/types/side-dock.types.ts +11 -4
  76. package/src/features/side-dock/utils/side-dock-item.utils.test.ts +8 -8
  77. package/src/features/side-dock/utils/side-dock-item.utils.ts +13 -5
  78. package/src/platforms/desktop/components/desktop-app-shell.tsx +5 -2
  79. package/src/platforms/mobile/components/mobile-app-shell.tsx +4 -1
  80. package/src/shared/components/config/provider-form-support.test.ts +21 -0
  81. package/src/shared/components/config/provider-form-support.ts +38 -78
  82. package/src/shared/components/config/provider-form.tsx +54 -67
  83. package/src/shared/components/config/providers-list.tsx +211 -73
  84. package/src/shared/components/doc-browser/doc-browser-context.test.tsx +73 -0
  85. package/src/shared/components/doc-browser/doc-browser-context.tsx +3 -0
  86. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +184 -98
  87. package/src/shared/components/doc-browser/doc-browser.test.tsx +168 -43
  88. package/src/shared/components/doc-browser/doc-browser.tsx +38 -7
  89. package/src/shared/components/doc-browser/index.ts +1 -0
  90. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +30 -6
  91. package/src/shared/components/doc-browser/stores/doc-browser.store.ts +29 -0
  92. package/src/shared/components/doc-browser/types/doc-browser.types.ts +23 -0
  93. package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +7 -0
  94. package/src/shared/components/model-config.tsx +5 -4
  95. package/src/shared/components/provider-auth-section.tsx +2 -2
  96. package/src/shared/components/ui/switch.tsx +5 -2
  97. package/src/shared/hooks/use-config.ts +27 -6
  98. package/src/shared/lib/api/marketplace.types.ts +193 -0
  99. package/src/shared/lib/api/types.ts +23 -200
  100. package/src/shared/lib/api/utils/config.utils.ts +17 -7
  101. package/src/shared/lib/i18n/index.ts +10 -12
  102. package/src/shared/lib/i18n/{marketplace.ts → marketplace-labels.utils.ts} +3 -0
  103. package/src/shared/lib/i18n/runtime/doc-browser-labels.utils.ts +46 -2
  104. package/src/shared/lib/provider-models/index.test.ts +93 -15
  105. package/src/shared/lib/provider-models/index.ts +35 -20
  106. package/src/shared/lib/transport/request-raw-api-response.utils.ts +1 -1
  107. package/dist/assets/api-DruAcori.js +0 -15
  108. package/dist/assets/app-presenter-provider-DIcnNB9s.js +0 -3
  109. package/dist/assets/book-open-CRotBOF2.js +0 -1
  110. package/dist/assets/boxes-BvA-I63l.js +0 -1
  111. package/dist/assets/channels-list-page-wK3l7p5R.js +0 -8
  112. package/dist/assets/chat-page-DgehvdJT.js +0 -105
  113. package/dist/assets/config-split-page-BnTO1mAM.js +0 -1
  114. package/dist/assets/confirm-dialog-CGrKHbAW.js +0 -5
  115. package/dist/assets/createLucideIcon-Br1EFMEr.js +0 -1
  116. package/dist/assets/desktop-update-config-CehWxSSg.js +0 -1
  117. package/dist/assets/dist-rzakR_bu.js +0 -41
  118. package/dist/assets/doc-browser-BgxmVsTx.js +0 -1
  119. package/dist/assets/doc-browser-CZn5-Qbu.js +0 -1
  120. package/dist/assets/doc-browser-context-B8gk7HMr.js +0 -1
  121. package/dist/assets/external-link-DHjFdCpy.js +0 -1
  122. package/dist/assets/index-DORmh0Cy.css +0 -1
  123. package/dist/assets/index-DcFpvnge.js +0 -101
  124. package/dist/assets/key-round-D41Oe35x.js +0 -1
  125. package/dist/assets/mcp-marketplace-page-DBLhJWOJ.js +0 -1
  126. package/dist/assets/model-config-B-EJXGtO.js +0 -1
  127. package/dist/assets/play-C-0vo7hi.js +0 -1
  128. package/dist/assets/plus-Cr_akLp2.js +0 -1
  129. package/dist/assets/popover-YieElO8c.js +0 -1
  130. package/dist/assets/provider-scoped-model-input-BPRNGqYn.js +0 -1
  131. package/dist/assets/providers-list-CtBpL1ZY.js +0 -1
  132. package/dist/assets/react-B2X_ph0a.js +0 -1
  133. package/dist/assets/remote-BGHbHoHf.js +0 -1
  134. package/dist/assets/rotate-cw-jueVCNJQ.js +0 -1
  135. package/dist/assets/runtime-config-page-C1Hv7mad.js +0 -1
  136. package/dist/assets/save-aYdSdK97.js +0 -1
  137. package/dist/assets/search-CdhgFNwf.js +0 -1
  138. package/dist/assets/setting-row-BNu0qhOM.js +0 -1
  139. package/dist/assets/trash-2-DSWTsARW.js +0 -1
  140. package/dist/assets/x-D1v_vb54.js +0 -1
  141. package/src/shared/components/provider-enabled-field.tsx +0 -23
  142. /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 const SIDE_DOCK_BUILT_IN_ITEMS: SideDockItem[] = [
11
- {
12
- builtIn: true,
13
- icon: { type: 'builtin', name: 'apps' },
14
- id: 'apps',
15
- labelKey: 'appsTitle',
16
- removable: false,
17
- target: { type: 'right-panel-resource', uri: RIGHT_PANEL_APPS_URL },
18
- },
19
- {
20
- builtIn: true,
21
- icon: { type: 'builtin', name: 'service-apps' },
22
- id: 'service-apps',
23
- labelKey: 'serviceAppsTitle',
24
- removable: false,
25
- target: { type: 'right-panel-resource', uri: RIGHT_PANEL_SERVICE_APPS_URL },
26
- },
27
- {
28
- builtIn: true,
29
- icon: { type: 'builtin', name: 'docs' },
30
- id: 'docs',
31
- labelKey: 'docBrowserHelp',
32
- removable: false,
33
- target: { type: 'right-panel-resource', uri: SIDE_DOCK_DOCS_URL },
34
- },
35
- {
36
- builtIn: true,
37
- icon: { type: 'builtin', name: 'new-tab' },
38
- id: 'new-tab',
39
- labelKey: 'docBrowserHomeTitle',
40
- removable: false,
41
- target: { type: 'right-panel-resource', uri: RIGHT_PANEL_HOME_URL },
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
- labelKey: 'docBrowserHelp',
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
- labelKey: 'docBrowserHelp',
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 { SIDE_DOCK_BUILT_IN_ITEMS } from '@/features/side-dock/configs/side-dock-built-in-items.config';
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 { SideDockItem, SideDockPinnedItem } from '@/features/side-dock/types/side-dock.types';
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
- readonly getItems = (): SideDockItem[] => mergeSideDockItems(
14
- SIDE_DOCK_BUILT_IN_ITEMS,
76
+ getItems = (): SideDockItem[] => mergeSideDockItems(
77
+ getSideDockBuiltInItems(),
15
78
  useSideDockStore.getState().pinnedItems,
16
79
  );
17
80
 
18
- readonly openItem = (item: Pick<SideDockItem, 'target'>): void => {
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
- readonly pinItem = (item: SideDockItem, createdAt: string = new Date().toISOString()): void => {
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 === item.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(item, createdAt)];
141
+ return [...items, createPinnedSideDockItem({
142
+ ...item,
143
+ target: { ...item.target, uri },
144
+ }, createdAt)];
33
145
  });
34
146
  };
35
147
 
36
- readonly unpinItem = (itemId: string): void => {
148
+ unpinItem = (itemId: string): void => {
37
149
  this.setPinnedItems((items) => items.filter((item) => item.id !== itemId));
38
150
  };
39
151
 
40
- readonly reorderPinnedItems = (itemIds: string[]): void => {
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 readonly setPinnedItems = (
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
- labelKey: string;
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
- labelKey: string;
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
- labelKey: 'appsTitle',
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: 'builtin', name: 'docs' },
23
+ icon: { type: 'text', value: 'D' },
24
24
  id: 'docs-custom',
25
- labelKey: 'docBrowserHelp',
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
- labelKey: 'bad',
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: 'builtin', name: 'docs' },
40
+ icon: { type: 'text', value: 'D' },
41
41
  id: 'docs-custom',
42
- labelKey: 'docBrowserHelp',
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
- labelKey: 'docBrowserHelp',
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
- labelKey: 'appsTitle',
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.labelKey !== 'string'
51
- || value.labelKey.trim().length === 0
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
- labelKey: value.labelKey.trim(),
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
- labelKey: item.labelKey,
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
- labelKey: item.labelKey,
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
+ });