@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
@@ -2,6 +2,9 @@ import { useState, useRef, useCallback, useEffect } from 'react';
2
2
  import {
3
3
  DOCS_DEFAULT_BASE_URL,
4
4
  DOC_BROWSER_HOME_TAB_KIND,
5
+ type DocBrowserDockControls,
6
+ type DocBrowserDockState,
7
+ type DocBrowserTab,
5
8
  useDocBrowser,
6
9
  } from './doc-browser-context';
7
10
  import { normalizeDocUrl } from './utils/doc-browser-url.utils';
@@ -20,6 +23,7 @@ import { GripVertical } from 'lucide-react';
20
23
  type DocBrowserProps = {
21
24
  customTabRenderers?: DocBrowserCustomTabRenderers;
22
25
  displayMode?: 'desktop' | 'fullscreen';
26
+ dockControls?: DocBrowserDockControls;
23
27
  };
24
28
 
25
29
  type FloatingPanelRect = { x: number; y: number; w: number; h: number };
@@ -35,6 +39,33 @@ type FloatingPanelInteraction = {
35
39
  const FLOATING_PANEL_MARGIN = 40;
36
40
  const FLOATING_PANEL_MIN_WIDTH = 360;
37
41
  const FLOATING_PANEL_MIN_HEIGHT = 400;
42
+ const DEFAULT_IFRAME_SANDBOX = 'allow-same-origin allow-scripts allow-popups allow-forms';
43
+
44
+ function resolveIframeSandbox(
45
+ customRenderer: DocBrowserCustomTabRenderers[string] | undefined,
46
+ currentTab: DocBrowserTab | undefined,
47
+ ): string {
48
+ return currentTab ? customRenderer?.getIframeSandbox?.(currentTab) ?? DEFAULT_IFRAME_SANDBOX : DEFAULT_IFRAME_SANDBOX;
49
+ }
50
+
51
+ function useDocBrowserDockAction(
52
+ dockControls: DocBrowserDockControls | undefined,
53
+ currentTab: DocBrowserTab | undefined,
54
+ dockState: DocBrowserDockState | undefined,
55
+ ) {
56
+ return useCallback(() => {
57
+ if (!dockState?.canDock) {
58
+ return;
59
+ }
60
+ if (dockState.isDocked) {
61
+ if (dockState.removable) {
62
+ dockControls?.unpinTab(currentTab);
63
+ }
64
+ return;
65
+ }
66
+ dockControls?.pinTab(currentTab);
67
+ }, [currentTab, dockControls, dockState]);
68
+ }
38
69
 
39
70
  function clamp(value: number, min: number, max: number): number {
40
71
  return Math.min(max, Math.max(min, value));
@@ -49,10 +80,7 @@ function createInitialFloatingPanelRect(): FloatingPanelRect {
49
80
  };
50
81
  }
51
82
 
52
- export function DocBrowser({
53
- customTabRenderers = {},
54
- displayMode = 'desktop',
55
- }: DocBrowserProps) {
83
+ export function DocBrowser({ customTabRenderers = {}, displayMode = 'desktop', dockControls }: DocBrowserProps) {
56
84
  const {
57
85
  isOpen,
58
86
  mode,
@@ -89,6 +117,7 @@ export function DocBrowser({
89
117
  const canGoBack = activeHistoryIndex > 0;
90
118
  const canGoForward = activeHistoryIndex < activeHistory.length - 1;
91
119
  const customRenderer = currentTab ? customTabRenderers[currentTab.kind] : undefined;
120
+ const dockState = dockControls?.getDockState(currentTab);
92
121
 
93
122
  useEffect(() => {
94
123
  if (!isDocsTab) {
@@ -249,6 +278,8 @@ export function DocBrowser({
249
278
  setIframeReloadVersion((version) => version + 1);
250
279
  }, []);
251
280
 
281
+ const handleToggleDock = useDocBrowserDockAction(dockControls, currentTab, dockState);
282
+
252
283
  if (!isOpen) return null;
253
284
 
254
285
  const isDocked = mode === 'docked';
@@ -264,9 +295,7 @@ export function DocBrowser({
264
295
  const customContent = customRenderParams
265
296
  ? customRenderer?.renderContent?.(customRenderParams) ?? (isHomeTab ? <DocBrowserHomePage /> : null)
266
297
  : null;
267
- const iframeSandbox = currentTab
268
- ? customRenderer?.getIframeSandbox?.(currentTab) ?? 'allow-same-origin allow-scripts allow-popups allow-forms'
269
- : 'allow-same-origin allow-scripts allow-popups allow-forms';
298
+ const iframeSandbox = resolveIframeSandbox(customRenderer, currentTab);
270
299
 
271
300
  const panelContent = (
272
301
  <>
@@ -275,11 +304,13 @@ export function DocBrowser({
275
304
  activeTabId={activeTabId}
276
305
  canGoBack={canGoBack}
277
306
  canGoForward={canGoForward}
307
+ dockState={dockState}
278
308
  isDocked={isDocked}
279
309
  isFullscreen={isFullscreen}
280
310
  onGoBack={goBack}
281
311
  onGoForward={goForward}
282
312
  onOpenNewTab={openNewTab}
313
+ onToggleDock={handleToggleDock}
283
314
  onSetActiveTab={setActiveTab}
284
315
  onCloseTab={closeTab}
285
316
  onClose={close}
@@ -1,3 +1,4 @@
1
1
  export { DocBrowser } from './doc-browser';
2
2
  export { DocBrowserProvider, useDocBrowser, isDocsUrl } from './doc-browser-context';
3
+ export type { DocBrowserDockIcon } from './doc-browser-context';
3
4
  export { useDocLinkInterceptor } from './use-doc-link-interceptor';
@@ -50,9 +50,23 @@ function areActiveHistoryEntriesEquivalent(
50
50
  current: DocBrowserActiveHistoryEntry,
51
51
  next: DocBrowserActiveHistoryEntry,
52
52
  ): boolean {
53
+ const currentUri = current.resourceUri ?? current.url;
54
+ const nextUri = next.resourceUri ?? next.url;
53
55
  return current.tabId === next.tabId
54
56
  && current.kind === next.kind
55
- && routeResolver.areUrlsEquivalent(current.url, next.url, current.kind, next.kind);
57
+ && routeResolver.areUrlsEquivalent(currentUri, nextUri, current.kind, next.kind);
58
+ }
59
+
60
+ function createActiveHistoryEntryFromTarget(
61
+ tabId: string,
62
+ target: DocBrowserRouteTarget,
63
+ ): DocBrowserActiveHistoryEntry {
64
+ return {
65
+ kind: target.kind,
66
+ resourceUri: target.resourceUri ?? target.url,
67
+ tabId,
68
+ url: target.url,
69
+ };
56
70
  }
57
71
 
58
72
  function pushActiveHistory(
@@ -90,12 +104,14 @@ function restoreActiveHistoryEntry(
90
104
  if (!tab) {
91
105
  return state;
92
106
  }
107
+ const restoreUri = entry.resourceUri ?? entry.url;
93
108
  const target = routeResolver.resolveOpenTarget({
94
109
  activeTab: tab,
95
110
  kind: entry.kind,
96
- url: entry.url,
111
+ url: restoreUri,
97
112
  });
98
113
  const historyIndex = findTabHistoryIndex(routeResolver, tab, target.url);
114
+ const shouldPreserveCurrentTitle = entry.resourceUri !== undefined && entry.resourceUri === tab.resourceUri;
99
115
  return updateTab(
100
116
  {
101
117
  ...state,
@@ -107,9 +123,11 @@ function restoreActiveHistoryEntry(
107
123
  ...currentTab,
108
124
  currentUrl: target.url,
109
125
  dedupeKey: target.dedupeKey,
126
+ dockIcon: target.dockIcon ?? currentTab.dockIcon,
110
127
  historyIndex: historyIndex >= 0 ? historyIndex : currentTab.historyIndex,
111
128
  kind: target.kind,
112
- title: target.title,
129
+ resourceUri: target.resourceUri ?? target.url,
130
+ title: shouldPreserveCurrentTitle ? currentTab.title : target.title,
113
131
  }),
114
132
  );
115
133
  }
@@ -125,6 +143,8 @@ function updateTabForOpen(
125
143
  ...tab,
126
144
  title: options?.title || target.title || tab.title,
127
145
  kind: target.kind,
146
+ resourceUri: target.resourceUri ?? target.url,
147
+ dockIcon: options?.dockIcon ?? target.dockIcon,
128
148
  dedupeKey,
129
149
  };
130
150
 
@@ -180,12 +200,12 @@ function openResolvedDocBrowserState(
180
200
  };
181
201
  const shouldPushActiveHistory = shouldActivate || matchedTab.id === prev.activeTabId;
182
202
  return shouldPushActiveHistory
183
- ? pushActiveHistory(routeResolver, nextState, { kind: target.kind, tabId: matchedTab.id, url: target.url })
203
+ ? pushActiveHistory(routeResolver, nextState, createActiveHistoryEntryFromTarget(matchedTab.id, target))
184
204
  : nextState;
185
205
  }
186
206
 
187
207
  if (shouldForceNewTab || dedupeKey || !activeTab || activeTab.kind !== target.kind) {
188
- const newTab = createDocBrowserTab(target.url, target.kind, title ?? target.title, dedupeKey);
208
+ const newTab = createDocBrowserTab(target.url, target.kind, title ?? target.title, dedupeKey, target.resourceUri ?? target.url, options?.dockIcon ?? target.dockIcon);
189
209
  if (isClosedDefaultHomeState(prev, activeTab)) {
190
210
  const nextState = {
191
211
  ...prev,
@@ -213,7 +233,7 @@ function openResolvedDocBrowserState(
213
233
  return pushActiveHistory(
214
234
  routeResolver,
215
235
  { ...next, isOpen: true },
216
- { kind: target.kind, tabId: prev.activeTabId, url: target.url },
236
+ createActiveHistoryEntryFromTarget(prev.activeTabId, target),
217
237
  );
218
238
  }
219
239
 
@@ -290,9 +310,11 @@ export class DocBrowserManager {
290
310
  ...tab,
291
311
  currentUrl: target.url,
292
312
  dedupeKey: target.dedupeKey,
313
+ dockIcon: target.dockIcon,
293
314
  ...appendManualNavigation(tab, target.url),
294
315
  kind: target.kind,
295
316
  navVersion: tab.navVersion + 1,
317
+ resourceUri: target.resourceUri ?? target.url,
296
318
  title: target.title,
297
319
  };
298
320
  });
@@ -328,8 +350,10 @@ export class DocBrowserManager {
328
350
  ...tab,
329
351
  currentUrl: target.url,
330
352
  dedupeKey: target.dedupeKey,
353
+ dockIcon: target.dockIcon,
331
354
  ...appendManualNavigation(tab, target.url),
332
355
  kind: target.kind,
356
+ resourceUri: target.resourceUri ?? target.url,
333
357
  title: target.title,
334
358
  };
335
359
  });
@@ -2,6 +2,7 @@ import { create } from 'zustand';
2
2
  import { createJSONStorage, persist } from 'zustand/middleware';
3
3
  import type {
4
4
  DocBrowserActiveHistoryEntry,
5
+ DocBrowserDockIcon,
5
6
  DocBrowserMode,
6
7
  DocBrowserState,
7
8
  DocBrowserStateUpdate,
@@ -42,6 +43,22 @@ function normalizePersistedNumber(value: unknown, min: number, max: number, fall
42
43
  : fallback;
43
44
  }
44
45
 
46
+ function normalizePersistedDockIcon(value: unknown): DocBrowserDockIcon | undefined {
47
+ if (!isRecord(value) || typeof value.type !== 'string') {
48
+ return undefined;
49
+ }
50
+ if (value.type === 'builtin' && typeof value.name === 'string' && value.name.trim().length > 0) {
51
+ return { type: 'builtin', name: value.name.trim() };
52
+ }
53
+ if (value.type === 'url' && typeof value.url === 'string' && value.url.trim().length > 0) {
54
+ return { type: 'url', url: value.url.trim() };
55
+ }
56
+ if (value.type === 'text' && typeof value.value === 'string' && value.value.trim().length > 0) {
57
+ return { type: 'text', value: value.value.trim() };
58
+ }
59
+ return undefined;
60
+ }
61
+
45
62
  function normalizePersistedDocBrowserTab(value: unknown): DocBrowserTab | null {
46
63
  if (
47
64
  !isRecord(value)
@@ -59,12 +76,18 @@ function normalizePersistedDocBrowserTab(value: unknown): DocBrowserTab | null {
59
76
  const dedupeKey = typeof value.dedupeKey === 'string' && value.dedupeKey.trim().length > 0
60
77
  ? value.dedupeKey
61
78
  : undefined;
79
+ const resourceUri = typeof value.resourceUri === 'string' && value.resourceUri.trim().length > 0
80
+ ? value.resourceUri.trim()
81
+ : undefined;
82
+ const dockIcon = normalizePersistedDockIcon(value.dockIcon);
62
83
 
63
84
  return {
64
85
  id: value.id,
65
86
  kind: value.kind,
66
87
  title: value.title,
67
88
  currentUrl: history[historyIndex] ?? value.currentUrl,
89
+ resourceUri,
90
+ dockIcon,
68
91
  dedupeKey,
69
92
  history,
70
93
  historyIndex,
@@ -75,6 +98,7 @@ function normalizePersistedDocBrowserTab(value: unknown): DocBrowserTab | null {
75
98
  function createActiveHistoryEntryFromTab(tab: DocBrowserTab): DocBrowserActiveHistoryEntry {
76
99
  return {
77
100
  kind: tab.kind,
101
+ resourceUri: tab.resourceUri,
78
102
  tabId: tab.id,
79
103
  url: tab.currentUrl,
80
104
  };
@@ -95,8 +119,13 @@ function normalizePersistedActiveHistoryEntry(
95
119
  return null;
96
120
  }
97
121
 
122
+ const resourceUri = typeof value.resourceUri === 'string' && value.resourceUri.trim().length > 0
123
+ ? value.resourceUri.trim()
124
+ : undefined;
125
+
98
126
  return {
99
127
  kind: value.kind,
128
+ resourceUri,
100
129
  tabId: value.tabId,
101
130
  url: value.url,
102
131
  };
@@ -6,6 +6,8 @@ export type DocBrowserTab = {
6
6
  kind: DocBrowserTabKind;
7
7
  title: string;
8
8
  currentUrl: string;
9
+ resourceUri?: string;
10
+ dockIcon?: DocBrowserDockIcon;
9
11
  dedupeKey?: string;
10
12
  history: string[];
11
13
  historyIndex: number;
@@ -14,13 +16,32 @@ export type DocBrowserTab = {
14
16
  };
15
17
 
16
18
  export type DocBrowserRouteTarget = {
19
+ dockIcon?: DocBrowserDockIcon;
17
20
  dedupeKey?: string;
18
21
  historyPolicy: 'managed' | 'none';
19
22
  kind: DocBrowserTabKind;
23
+ resourceUri?: string;
20
24
  title: string;
21
25
  url: string;
22
26
  };
23
27
 
28
+ export type DocBrowserDockIcon =
29
+ | { type: 'builtin'; name: string }
30
+ | { type: 'url'; url: string }
31
+ | { type: 'text'; value: string };
32
+
33
+ export type DocBrowserDockState = {
34
+ canDock: boolean;
35
+ isDocked: boolean;
36
+ removable: boolean;
37
+ };
38
+
39
+ export type DocBrowserDockControls = {
40
+ getDockState: (tab?: DocBrowserTab) => DocBrowserDockState;
41
+ pinTab: (tab?: DocBrowserTab) => void;
42
+ unpinTab: (tab?: DocBrowserTab) => void;
43
+ };
44
+
24
45
  export type DocBrowserRouteResolver = {
25
46
  areUrlsEquivalent: (
26
47
  currentUrl: string,
@@ -38,6 +59,7 @@ export type DocBrowserRouteResolver = {
38
59
 
39
60
  export type DocBrowserActiveHistoryEntry = {
40
61
  kind: DocBrowserTabKind;
62
+ resourceUri?: string;
41
63
  tabId: string;
42
64
  url: string;
43
65
  };
@@ -45,6 +67,7 @@ export type DocBrowserActiveHistoryEntry = {
45
67
  export type DocBrowserOpenOptions = {
46
68
  activate?: boolean;
47
69
  dedupeKey?: string;
70
+ dockIcon?: DocBrowserDockIcon;
48
71
  newTab?: boolean;
49
72
  title?: string;
50
73
  kind?: DocBrowserTabKind;
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  DocBrowserActiveHistoryEntry,
3
+ DocBrowserDockIcon,
3
4
  DocBrowserState,
4
5
  DocBrowserTab,
5
6
  DocBrowserTabKind,
@@ -23,14 +24,19 @@ export function createDocBrowserTab(
23
24
  kind: DocBrowserTabKind,
24
25
  title?: string,
25
26
  dedupeKey?: string,
27
+ resourceUri?: string,
28
+ dockIcon?: DocBrowserDockIcon,
26
29
  ): DocBrowserTab {
27
30
  const tabTitle = title?.trim() || inferTabTitle(url, kind, kind === 'docs' ? t('docBrowserHelp') : 'Detail');
31
+ const normalizedResourceUri = resourceUri?.trim();
28
32
 
29
33
  return {
30
34
  id: nextTabId(),
31
35
  kind,
32
36
  title: tabTitle,
33
37
  currentUrl: url,
38
+ resourceUri: normalizedResourceUri ? normalizedResourceUri : undefined,
39
+ dockIcon,
34
40
  dedupeKey,
35
41
  history: [url],
36
42
  historyIndex: 0,
@@ -41,6 +47,7 @@ export function createDocBrowserTab(
41
47
  export function createDocBrowserActiveHistoryEntry(tab: DocBrowserTab): DocBrowserActiveHistoryEntry {
42
48
  return {
43
49
  kind: tab.kind,
50
+ resourceUri: tab.resourceUri,
44
51
  tabId: tab.id,
45
52
  url: tab.currentUrl,
46
53
  };
@@ -4,7 +4,7 @@ import { Input } from '@/shared/components/ui/input';
4
4
  import { Label } from '@/shared/components/ui/label';
5
5
  import { Skeleton } from '@/shared/components/ui/skeleton';
6
6
  import { ProviderScopedModelInput } from '@/shared/components/common/provider-scoped-model-input';
7
- import { useConfig, useConfigMeta, useConfigSchema, useUpdateModel } from '@/shared/hooks/use-config';
7
+ import { useConfig, useConfigSchema, useProviderTemplates, useProviders, useUpdateModel } from '@/shared/hooks/use-config';
8
8
  import { hintForPath } from '@/shared/lib/config-hints';
9
9
  import { t } from '@/shared/lib/i18n';
10
10
  import { buildProviderModelCatalog } from '@/shared/lib/provider-models';
@@ -85,7 +85,8 @@ function ModelConfigForm(props: {
85
85
 
86
86
  export function ModelConfig() {
87
87
  const { data: config, isLoading } = useConfig();
88
- const { data: meta } = useConfigMeta();
88
+ const { data: providersView } = useProviders();
89
+ const { data: templatesView } = useProviderTemplates();
89
90
  const { data: schema } = useConfigSchema();
90
91
  const updateModel = useUpdateModel();
91
92
  const uiHints = schema?.uiHints;
@@ -93,8 +94,8 @@ export function ModelConfig() {
93
94
  const workspaceHint = hintForPath('agents.defaults.workspace', uiHints);
94
95
 
95
96
  const providerCatalog = useMemo(
96
- () => buildProviderModelCatalog({ meta, config, onlyConfigured: true }),
97
- [config, meta]
97
+ () => buildProviderModelCatalog({ providersView, templatesView, config, onlyConfigured: true }),
98
+ [config, providersView, templatesView]
98
99
  );
99
100
  const modelHelpText = t('modelIdentifierHelp') || modelHint?.help || '';
100
101
  const initialModel = (config?.agents?.defaults?.model || '').trim();
@@ -2,10 +2,10 @@ import { Button } from '@/shared/components/ui/button';
2
2
  import { Label } from '@/shared/components/ui/label';
3
3
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
4
4
  import { t } from '@/shared/lib/i18n';
5
- import type { ProviderSpecView } from '@/shared/lib/api';
5
+ import type { ProviderTemplateView } from '@/shared/lib/api';
6
6
  import { ProviderPillSelector } from './provider-pill-selector';
7
7
 
8
- type ProviderAuthSectionProps = { providerAuth: ProviderSpecView['auth']; providerAuthNote: string; providerAuthMethodOptions: Array<{ value: string; label: string }>; providerAuthMethodsCount: number; selectedAuthMethodHint: string; shouldUseAuthMethodPills: boolean; resolvedAuthMethodId: string; onAuthMethodChange: (value: string) => void; onStartProviderAuth: () => void; onImportProviderAuthFromCli: () => void; startPending: boolean; importPending: boolean; authSessionId: string | null; authStatusMessage: string };
8
+ type ProviderAuthSectionProps = { providerAuth: ProviderTemplateView['auth']; providerAuthNote: string; providerAuthMethodOptions: Array<{ value: string; label: string }>; providerAuthMethodsCount: number; selectedAuthMethodHint: string; shouldUseAuthMethodPills: boolean; resolvedAuthMethodId: string; onAuthMethodChange: (value: string) => void; onStartProviderAuth: () => void; onImportProviderAuthFromCli: () => void; startPending: boolean; importPending: boolean; authSessionId: string | null; authStatusMessage: string };
9
9
 
10
10
  export function ProviderAuthSection(props: ProviderAuthSectionProps) {
11
11
  const {
@@ -4,10 +4,11 @@ import { cn } from '@/shared/lib/utils';
4
4
  interface SwitchProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onChange'> {
5
5
  checked?: boolean;
6
6
  onCheckedChange?: (checked: boolean) => void;
7
+ thumbClassName?: string;
7
8
  }
8
9
 
9
10
  const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
10
- ({ className, checked = false, onCheckedChange, ...props }, ref) => {
11
+ ({ className, checked = false, onCheckedChange, thumbClassName, ...props }, ref) => {
11
12
  return (
12
13
  <button
13
14
  type="button"
@@ -23,9 +24,11 @@ const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
23
24
  {...props}
24
25
  >
25
26
  <span
27
+ data-state={checked ? 'checked' : 'unchecked'}
26
28
  className={cn(
27
29
  'pointer-events-none block h-5 w-5 rounded-full bg-white shadow-md ring-0 transition-transform duration-fast',
28
- checked ? 'translate-x-5' : 'translate-x-0'
30
+ checked ? 'translate-x-5' : 'translate-x-0',
31
+ thumbClassName
29
32
  )}
30
33
  />
31
34
  </button>
@@ -8,6 +8,8 @@ import {
8
8
  fetchConfig,
9
9
  fetchConfigMeta,
10
10
  fetchConfigSchema,
11
+ fetchProviders,
12
+ fetchProviderTemplates,
11
13
  updateModel,
12
14
  updateSearch,
13
15
  createProvider,
@@ -61,6 +63,23 @@ export function useConfigMeta() {
61
63
  });
62
64
  }
63
65
 
66
+ export function useProviders() {
67
+ return useQuery({
68
+ queryKey: ['providers'],
69
+ queryFn: fetchProviders,
70
+ staleTime: 30_000,
71
+ refetchOnWindowFocus: true
72
+ });
73
+ }
74
+
75
+ export function useProviderTemplates() {
76
+ return useQuery({
77
+ queryKey: ['provider-templates'],
78
+ queryFn: fetchProviderTemplates,
79
+ staleTime: Infinity
80
+ });
81
+ }
82
+
64
83
  export function useConfigSchema() {
65
84
  return useQuery({
66
85
  queryKey: ['config-schema'],
@@ -104,12 +123,14 @@ export function useUpdateProvider() {
104
123
  const queryClient = useQueryClient();
105
124
 
106
125
  return useMutation({
107
- mutationFn: ({ provider, data }: { provider: string; data: unknown }) =>
126
+ mutationFn: ({ provider, data }: { provider: string; data: unknown; silentSuccess?: boolean }) =>
108
127
  updateProvider(provider, data as Parameters<typeof updateProvider>[1]),
109
- onSuccess: () => {
128
+ onSuccess: (_data, variables) => {
129
+ queryClient.invalidateQueries({ queryKey: ['providers'] });
110
130
  queryClient.invalidateQueries({ queryKey: ['config'] });
111
- queryClient.invalidateQueries({ queryKey: ['config-meta'] });
112
- toast.success(t('configSaved'));
131
+ if (!variables.silentSuccess) {
132
+ toast.success(t('configSaved'));
133
+ }
113
134
  },
114
135
  onError: (error: Error) => {
115
136
  toast.error(t('configSaveFailed') + ': ' + error.message);
@@ -124,8 +145,8 @@ export function useCreateProvider() {
124
145
  mutationFn: ({ data }: { data?: unknown }) =>
125
146
  createProvider((data ?? {}) as Parameters<typeof createProvider>[0]),
126
147
  onSuccess: () => {
148
+ queryClient.invalidateQueries({ queryKey: ['providers'] });
127
149
  queryClient.invalidateQueries({ queryKey: ['config'] });
128
- queryClient.invalidateQueries({ queryKey: ['config-meta'] });
129
150
  toast.success(t('configSaved'));
130
151
  },
131
152
  onError: (error: Error) => {
@@ -140,8 +161,8 @@ export function useDeleteProvider() {
140
161
  return useMutation({
141
162
  mutationFn: ({ provider }: { provider: string }) => deleteProvider(provider),
142
163
  onSuccess: () => {
164
+ queryClient.invalidateQueries({ queryKey: ['providers'] });
143
165
  queryClient.invalidateQueries({ queryKey: ['config'] });
144
- queryClient.invalidateQueries({ queryKey: ['config-meta'] });
145
166
  toast.success(t('configSaved'));
146
167
  },
147
168
  onError: (error: Error) => {