@nextclaw/ui 0.12.35-beta.5 → 0.12.35-beta.7

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 (84) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/dist/assets/api-DruAcori.js +15 -0
  3. package/dist/assets/{book-open-qc0UVn6m.js → book-open-cPX0Za7Z.js} +1 -1
  4. package/dist/assets/channels-list-page-D-tv11dc.js +8 -0
  5. package/dist/assets/chat-page-MyXEZI8R.js +105 -0
  6. package/dist/assets/{config-split-page-DW8tP9Bc.js → config-split-page-BFh_Qixx.js} +1 -1
  7. package/dist/assets/confirm-dialog-B0TP-vPd.js +5 -0
  8. package/dist/assets/{createLucideIcon-CA0sgoMJ.js → createLucideIcon-Br1EFMEr.js} +1 -1
  9. package/dist/assets/{desktop-update-config-DARSwQG8.js → desktop-update-config-oZpIKKtS.js} +1 -1
  10. package/dist/assets/{dist-D5zsy7pX.js → dist-CpwDZDtv.js} +1 -1
  11. package/dist/assets/{dist-DNxIa3pe.js → dist-rzakR_bu.js} +1 -1
  12. package/dist/assets/doc-browser-B2OFjScU.js +3 -0
  13. package/dist/assets/doc-browser-BrUQEZ1j.js +1 -0
  14. package/dist/assets/doc-browser-context-DB6TTHeE.js +1 -0
  15. package/dist/assets/doc-browser-route-registry.utils-x5ijvk0u.js +1 -0
  16. package/dist/assets/{external-link-C9fbyi6k.js → external-link-C3usm3se.js} +1 -1
  17. package/dist/assets/index-D2H2CKYR.js +101 -0
  18. package/dist/assets/index-v0fAGXQW.css +1 -0
  19. package/dist/assets/{mcp-marketplace-page-BiExW5wq.js → mcp-marketplace-page-CegP14Hl.js} +2 -2
  20. package/dist/assets/mcp-marketplace-page-MjpFEEkY.js +1 -0
  21. package/dist/assets/model-config-fIE_Asps.js +1 -0
  22. package/dist/assets/play-CqNi0u02.js +1 -0
  23. package/dist/assets/plus-BcIC8dG1.js +1 -0
  24. package/dist/assets/{provider-scoped-model-input-Bk8jlpRm.js → provider-scoped-model-input-D4zRsGZ6.js} +1 -1
  25. package/dist/assets/providers-list-CqsOk2hr.js +1 -0
  26. package/dist/assets/react-B2X_ph0a.js +1 -0
  27. package/dist/assets/remote-BMWQUPDW.js +1 -0
  28. package/dist/assets/runtime-config-page-BJ86cC8a.js +1 -0
  29. package/dist/assets/{save-CG4Hb1u8.js → save-D10M5vQg.js} +1 -1
  30. package/dist/assets/{search-lWpOIHEn.js → search-CdhgFNwf.js} +1 -1
  31. package/dist/assets/search-config-TbCPlRE7.js +1 -0
  32. package/dist/assets/{secrets-config-ZbLzjIRx.js → secrets-config-DBIEKP_V.js} +1 -1
  33. package/dist/assets/{setting-row-CNfTKDjc.js → setting-row-8VkukJoH.js} +1 -1
  34. package/dist/assets/{tabs-custom-BbZoysAl.js → tabs-custom-BBXu1OGs.js} +1 -1
  35. package/dist/assets/{tag-chip-Dl8JbhC7.js → tag-chip-ix15P-r4.js} +1 -1
  36. package/dist/assets/wrench-qVIwShTF.js +1 -0
  37. package/dist/assets/x-Chb07O-Y.js +1 -0
  38. package/dist/index.html +17 -15
  39. package/package.json +8 -8
  40. package/src/features/panel-apps/components/panel-app-list-item.test.tsx +60 -0
  41. package/src/features/panel-apps/components/panel-app-list-item.tsx +120 -30
  42. package/src/features/panel-apps/components/panel-apps-list.tsx +8 -1
  43. package/src/features/panel-apps/hooks/use-panel-apps.ts +10 -0
  44. package/src/features/panel-apps/managers/panel-app-bridge.manager.test.ts +85 -1
  45. package/src/features/panel-apps/managers/panel-app-bridge.manager.ts +123 -6
  46. package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +7 -3
  47. package/src/features/panel-apps/utils/panel-app-view.utils.test.ts +66 -0
  48. package/src/features/panel-apps/utils/panel-app-view.utils.ts +13 -2
  49. package/src/index.css +23 -0
  50. package/src/shared/components/doc-browser/doc-browser-context.test.tsx +170 -0
  51. package/src/shared/components/doc-browser/doc-browser-context.tsx +3 -0
  52. package/src/shared/components/doc-browser/doc-browser-home-page.tsx +95 -0
  53. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +0 -90
  54. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +117 -44
  55. package/src/shared/components/doc-browser/doc-browser.test.tsx +159 -1
  56. package/src/shared/components/doc-browser/doc-browser.tsx +42 -23
  57. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +183 -71
  58. package/src/shared/components/doc-browser/stores/doc-browser.store.ts +52 -1
  59. package/src/shared/components/doc-browser/types/doc-browser.types.ts +9 -0
  60. package/src/shared/components/doc-browser/utils/doc-browser-route-registry.utils.ts +232 -0
  61. package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +11 -0
  62. package/src/shared/components/doc-browser/utils/doc-browser-url.utils.ts +2 -0
  63. package/src/shared/lib/api/types.ts +1 -0
  64. package/src/shared/lib/i18n/runtime/doc-browser-labels.utils.ts +10 -0
  65. package/dist/assets/api-BTZui7nj.js +0 -15
  66. package/dist/assets/channels-list-page-DBtU_M1Y.js +0 -8
  67. package/dist/assets/chat-page-BcoJYo91.js +0 -105
  68. package/dist/assets/doc-browser-B9skxzVo.js +0 -1
  69. package/dist/assets/doc-browser-CwyITRXx.js +0 -1
  70. package/dist/assets/doc-browser-context-Do2qSrB8.js +0 -1
  71. package/dist/assets/index-BYRP_BMj.css +0 -1
  72. package/dist/assets/index-DckPGTdM.js +0 -103
  73. package/dist/assets/mcp-marketplace-page-Cj6MDSkE.js +0 -1
  74. package/dist/assets/model-config-DbdY7fF1.js +0 -1
  75. package/dist/assets/notice-card-CyiVcu_Z.js +0 -5
  76. package/dist/assets/play-D-s0C4MN.js +0 -1
  77. package/dist/assets/plus-DOwjM_kF.js +0 -1
  78. package/dist/assets/providers-list-CJjNStDV.js +0 -1
  79. package/dist/assets/react-BaRotFd6.js +0 -1
  80. package/dist/assets/remote-gl8QckNc.js +0 -1
  81. package/dist/assets/runtime-config-page-BMFc1yzf.js +0 -1
  82. package/dist/assets/search-config-DUOR26if.js +0 -1
  83. package/dist/assets/x-D6GwT_8k.js +0 -1
  84. /package/dist/assets/{config-hints-CTqBnCDp.js → config-hints-CY1lg9IE.js} +0 -0
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ DocBrowserActiveHistoryEntry,
2
3
  DocBrowserOpenOptions,
3
4
  DocBrowserState,
4
5
  DocBrowserStateUpdate,
@@ -6,15 +7,17 @@ import type {
6
7
  DocBrowserTabKind,
7
8
  } from '@/shared/components/doc-browser/types/doc-browser.types';
8
9
  import {
10
+ createDocBrowserActiveHistoryEntry,
9
11
  createDefaultDocBrowserState,
10
12
  createDocBrowserTab,
11
13
  } from '@/shared/components/doc-browser/utils/doc-browser-state.utils';
12
14
  import {
15
+ DOC_BROWSER_HOME_TAB_KIND,
13
16
  getDefaultDocsUrl,
14
- inferTabKind,
15
- normalizeDocUrl,
16
- normalizeUrlByKind,
17
17
  } from '@/shared/components/doc-browser/utils/doc-browser-url.utils';
18
+ import {
19
+ docBrowserRouteRegistry,
20
+ } from '@/shared/components/doc-browser/utils/doc-browser-route-registry.utils';
18
21
  import { useDocBrowserStore } from '@/shared/components/doc-browser/stores/doc-browser.store';
19
22
 
20
23
  function updateTab(
@@ -32,61 +35,103 @@ function updateActiveTab(state: DocBrowserState, updater: (tab: DocBrowserTab) =
32
35
  return updateTab(state, state.activeTabId, updater);
33
36
  }
34
37
 
35
- function areOpenUrlsEquivalent(
36
- currentUrl: string,
37
- nextUrl: string,
38
- currentKind: DocBrowserTabKind,
39
- nextKind: DocBrowserTabKind,
38
+ function appendManualNavigation(tab: DocBrowserTab, url: string): Pick<DocBrowserTab, 'history' | 'historyIndex'> {
39
+ const history = [...tab.history.slice(0, tab.historyIndex + 1), url];
40
+ return {
41
+ history,
42
+ historyIndex: history.length - 1,
43
+ };
44
+ }
45
+
46
+ function areActiveHistoryEntriesEquivalent(
47
+ current: DocBrowserActiveHistoryEntry,
48
+ next: DocBrowserActiveHistoryEntry,
40
49
  ): boolean {
41
- if (currentKind === 'docs' && nextKind === 'docs') {
42
- return normalizeDocUrl(currentUrl) === normalizeDocUrl(nextUrl);
50
+ return current.tabId === next.tabId
51
+ && current.kind === next.kind
52
+ && docBrowserRouteRegistry.areUrlsEquivalent(current.url, next.url, current.kind, next.kind);
53
+ }
54
+
55
+ function pushActiveHistory(state: DocBrowserState, entry: DocBrowserActiveHistoryEntry): DocBrowserState {
56
+ const current = state.activeHistory[state.activeHistoryIndex];
57
+ if (current && areActiveHistoryEntriesEquivalent(current, entry)) {
58
+ return state;
59
+ }
60
+ const activeHistory = [...state.activeHistory.slice(0, state.activeHistoryIndex + 1), entry];
61
+ return {
62
+ ...state,
63
+ activeHistory,
64
+ activeHistoryIndex: activeHistory.length - 1,
65
+ };
66
+ }
67
+
68
+ function findTabHistoryIndex(tab: DocBrowserTab, url: string): number {
69
+ for (let index = tab.history.length - 1; index >= 0; index -= 1) {
70
+ if (docBrowserRouteRegistry.areUrlsEquivalent(tab.history[index], url, tab.kind, tab.kind)) {
71
+ return index;
72
+ }
43
73
  }
44
- return currentUrl === nextUrl;
74
+ return -1;
75
+ }
76
+
77
+ function restoreActiveHistoryEntry(state: DocBrowserState, entry: DocBrowserActiveHistoryEntry): DocBrowserState {
78
+ const tab = state.tabs.find((item) => item.id === entry.tabId);
79
+ if (!tab) {
80
+ return state;
81
+ }
82
+ const target = docBrowserRouteRegistry.resolveOpenTarget({
83
+ activeTab: tab,
84
+ kind: entry.kind,
85
+ url: entry.url,
86
+ });
87
+ const historyIndex = findTabHistoryIndex(tab, target.url);
88
+ return updateTab(
89
+ {
90
+ ...state,
91
+ activeTabId: tab.id,
92
+ isOpen: true,
93
+ },
94
+ tab.id,
95
+ (currentTab) => ({
96
+ ...currentTab,
97
+ currentUrl: target.url,
98
+ dedupeKey: target.dedupeKey,
99
+ historyIndex: historyIndex >= 0 ? historyIndex : currentTab.historyIndex,
100
+ kind: target.kind,
101
+ title: target.title,
102
+ }),
103
+ );
45
104
  }
46
105
 
47
106
  function updateTabForOpen(
48
107
  tab: DocBrowserTab,
49
- url: string,
50
- kind: DocBrowserTabKind,
108
+ target: {
109
+ kind: DocBrowserTabKind;
110
+ title: string;
111
+ url: string;
112
+ },
51
113
  options?: DocBrowserOpenOptions,
52
114
  dedupeKey?: string,
53
115
  ): DocBrowserTab {
54
116
  const baseTab = {
55
117
  ...tab,
56
- title: options?.title || tab.title,
57
- kind,
118
+ title: options?.title || target.title || tab.title,
119
+ kind: target.kind,
58
120
  dedupeKey,
59
121
  };
60
122
 
61
- if (areOpenUrlsEquivalent(tab.currentUrl, url, tab.kind, kind)) {
123
+ if (docBrowserRouteRegistry.areUrlsEquivalent(tab.currentUrl, target.url, tab.kind, target.kind)) {
62
124
  return baseTab;
63
125
  }
64
126
 
65
127
  return {
66
128
  ...baseTab,
67
- currentUrl: url,
68
- history: [...tab.history.slice(0, tab.historyIndex + 1), url],
69
- historyIndex: tab.historyIndex + 1,
129
+ currentUrl: target.url,
130
+ ...appendManualNavigation(tab, target.url),
70
131
  navVersion: tab.navVersion + 1,
71
132
  };
72
133
  }
73
134
 
74
- function resolveOpenTargetUrl(params: {
75
- url?: string;
76
- kind: DocBrowserTabKind;
77
- activeTab?: DocBrowserTab;
78
- }): string {
79
- const { activeTab, kind, url } = params;
80
- if (url && url.trim().length > 0) {
81
- return normalizeUrlByKind(url, kind);
82
- }
83
-
84
- if (kind === 'docs') {
85
- return getDefaultDocsUrl();
86
- }
87
- return activeTab?.currentUrl ?? getDefaultDocsUrl();
88
- }
89
-
90
135
  function openDocBrowserState(
91
136
  prev: DocBrowserState,
92
137
  url?: string,
@@ -100,9 +145,12 @@ function openDocBrowserState(
100
145
  title,
101
146
  } = options ?? {};
102
147
  const activeTab = prev.tabs.find((tab) => tab.id === prev.activeTabId) ?? prev.tabs[0];
103
- const targetKind = kind ?? (url ? inferTabKind(url) : activeTab?.kind ?? 'docs');
104
- const targetUrl = resolveOpenTargetUrl({ url, kind: targetKind, activeTab });
105
- const dedupeKey = rawDedupeKey?.trim() || undefined;
148
+ const target = docBrowserRouteRegistry.resolveOpenTarget({
149
+ activeTab,
150
+ kind,
151
+ url,
152
+ });
153
+ const dedupeKey = rawDedupeKey?.trim() || target.dedupeKey;
106
154
  const matchedTab = dedupeKey
107
155
  ? prev.tabs.find((tab) => tab.dedupeKey === dedupeKey)
108
156
  : undefined;
@@ -111,30 +159,40 @@ function openDocBrowserState(
111
159
  const next = updateTab(
112
160
  prev,
113
161
  matchedTab.id,
114
- (tab) => updateTabForOpen(tab, targetUrl, targetKind, options, dedupeKey),
162
+ (tab) => updateTabForOpen(tab, target, options, dedupeKey),
115
163
  );
116
- return {
164
+ const shouldActivate = activate !== false;
165
+ const activeTabId = shouldActivate ? matchedTab.id : prev.activeTabId;
166
+ const nextState = {
117
167
  ...next,
118
- activeTabId: activate === false ? prev.activeTabId : matchedTab.id,
168
+ activeTabId,
119
169
  isOpen: true,
120
170
  };
171
+ const shouldPushActiveHistory = shouldActivate || matchedTab.id === prev.activeTabId;
172
+ return shouldPushActiveHistory
173
+ ? pushActiveHistory(nextState, { kind: target.kind, tabId: matchedTab.id, url: target.url })
174
+ : nextState;
121
175
  }
122
176
 
123
- if (shouldForceNewTab || dedupeKey || !activeTab || activeTab.kind !== targetKind) {
124
- const newTab = createDocBrowserTab(targetUrl, targetKind, title, dedupeKey);
125
- return {
177
+ if (shouldForceNewTab || dedupeKey || !activeTab || activeTab.kind !== target.kind) {
178
+ const newTab = createDocBrowserTab(target.url, target.kind, title ?? target.title, dedupeKey);
179
+ const nextState = {
126
180
  ...prev,
127
181
  isOpen: true,
128
182
  tabs: [...prev.tabs, newTab],
129
183
  activeTabId: newTab.id,
130
184
  };
185
+ return pushActiveHistory(nextState, createDocBrowserActiveHistoryEntry(newTab));
131
186
  }
132
187
 
133
188
  const next = updateActiveTab(
134
189
  prev,
135
- (tab) => updateTabForOpen(tab, targetUrl, targetKind, options, dedupeKey),
190
+ (tab) => updateTabForOpen(tab, target, options, dedupeKey),
191
+ );
192
+ return pushActiveHistory(
193
+ { ...next, isOpen: true },
194
+ { kind: target.kind, tabId: prev.activeTabId, url: target.url },
136
195
  );
137
- return { ...next, isOpen: true };
138
196
  }
139
197
 
140
198
  export class DocBrowserManager {
@@ -146,6 +204,10 @@ export class DocBrowserManager {
146
204
  this.setSnapshot((prev) => openDocBrowserState(prev, url, options));
147
205
  };
148
206
 
207
+ readonly openNewTab = (): void => {
208
+ this.open(undefined, { kind: DOC_BROWSER_HOME_TAB_KIND, newTab: true });
209
+ };
210
+
149
211
  readonly close = (): void => {
150
212
  this.setSnapshot((prev) => ({ ...prev, isOpen: false }));
151
213
  };
@@ -166,24 +228,30 @@ export class DocBrowserManager {
166
228
  };
167
229
  }
168
230
 
169
- return updateActiveTab(prev, (tab) => {
170
- if (tab.kind !== 'docs') {
231
+ const next = updateActiveTab(prev, (tab) => {
232
+ if (!docBrowserRouteRegistry.usesManagedHistory(tab)) {
171
233
  return tab;
172
234
  }
235
+ const target = docBrowserRouteRegistry.resolveOpenTarget({ activeTab: tab, url });
173
236
 
174
- const targetUrl = normalizeUrlByKind(url, 'docs');
175
- if (normalizeDocUrl(targetUrl) === normalizeDocUrl(tab.currentUrl)) {
237
+ if (docBrowserRouteRegistry.areUrlsEquivalent(tab.currentUrl, target.url, tab.kind, target.kind)) {
176
238
  return tab;
177
239
  }
178
240
 
179
241
  return {
180
242
  ...tab,
181
- currentUrl: targetUrl,
182
- history: [...tab.history.slice(0, tab.historyIndex + 1), targetUrl],
183
- historyIndex: tab.historyIndex + 1,
243
+ currentUrl: target.url,
244
+ dedupeKey: target.dedupeKey,
245
+ ...appendManualNavigation(tab, target.url),
246
+ kind: target.kind,
184
247
  navVersion: tab.navVersion + 1,
248
+ title: target.title,
185
249
  };
186
250
  });
251
+ const currentTab = next.tabs.find((tab) => tab.id === next.activeTabId);
252
+ return currentTab
253
+ ? pushActiveHistory(next, createDocBrowserActiveHistoryEntry(currentTab))
254
+ : next;
187
255
  });
188
256
  };
189
257
 
@@ -198,39 +266,58 @@ export class DocBrowserManager {
198
266
  };
199
267
  }
200
268
 
201
- return updateActiveTab(prev, (tab) => {
202
- if (tab.kind !== 'docs') {
269
+ const next = updateActiveTab(prev, (tab) => {
270
+ if (!docBrowserRouteRegistry.usesManagedHistory(tab)) {
203
271
  return tab;
204
272
  }
273
+ const target = docBrowserRouteRegistry.resolveOpenTarget({ activeTab: tab, kind: tab.kind, url });
205
274
 
206
- if (normalizeDocUrl(url) === normalizeDocUrl(tab.currentUrl)) {
275
+ if (docBrowserRouteRegistry.areUrlsEquivalent(tab.currentUrl, target.url, tab.kind, target.kind)) {
207
276
  return tab;
208
277
  }
209
278
 
210
279
  return {
211
280
  ...tab,
212
- currentUrl: url,
213
- history: [...tab.history.slice(0, tab.historyIndex + 1), url],
214
- historyIndex: tab.historyIndex + 1,
281
+ currentUrl: target.url,
282
+ dedupeKey: target.dedupeKey,
283
+ ...appendManualNavigation(tab, target.url),
284
+ kind: target.kind,
285
+ title: target.title,
215
286
  };
216
287
  });
288
+ const currentTab = next.tabs.find((tab) => tab.id === next.activeTabId);
289
+ return currentTab
290
+ ? pushActiveHistory(next, createDocBrowserActiveHistoryEntry(currentTab))
291
+ : next;
217
292
  });
218
293
  };
219
294
 
220
295
  readonly goBack = (): void => {
221
- this.setSnapshot((prev) => updateActiveTab(prev, (tab) => {
222
- if (tab.kind !== 'docs' || tab.historyIndex <= 0) return tab;
223
- const newIndex = tab.historyIndex - 1;
224
- return { ...tab, historyIndex: newIndex, currentUrl: tab.history[newIndex] };
225
- }));
296
+ this.setSnapshot((prev) => {
297
+ if (prev.activeHistoryIndex <= 0) {
298
+ return prev;
299
+ }
300
+ const activeHistoryIndex = prev.activeHistoryIndex - 1;
301
+ const entry = prev.activeHistory[activeHistoryIndex];
302
+ if (!entry) {
303
+ return prev;
304
+ }
305
+ return restoreActiveHistoryEntry({ ...prev, activeHistoryIndex }, entry);
306
+ });
226
307
  };
227
308
 
228
309
  readonly goForward = (): void => {
229
- this.setSnapshot((prev) => updateActiveTab(prev, (tab) => {
230
- if (tab.kind !== 'docs' || tab.historyIndex >= tab.history.length - 1) return tab;
231
- const newIndex = tab.historyIndex + 1;
232
- return { ...tab, historyIndex: newIndex, currentUrl: tab.history[newIndex] };
233
- }));
310
+ this.setSnapshot((prev) => {
311
+ if (prev.activeHistoryIndex >= prev.activeHistory.length - 1) {
312
+ return prev;
313
+ }
314
+ const activeHistoryIndex = prev.activeHistoryIndex + 1;
315
+ const entry = prev.activeHistory[activeHistoryIndex];
316
+ if (!entry) {
317
+ return prev;
318
+ }
319
+ return restoreActiveHistoryEntry({ ...prev, activeHistoryIndex }, entry);
320
+ });
234
321
  };
235
322
 
236
323
  readonly closeTab = (tabId: string): void => {
@@ -253,11 +340,29 @@ export class DocBrowserManager {
253
340
  ? nextTabs[Math.max(0, index - 1)]?.id ?? nextTabs[0].id
254
341
  : prev.activeTabId;
255
342
 
256
- return {
343
+ const nextState = {
257
344
  ...prev,
258
345
  tabs: nextTabs,
259
346
  activeTabId: nextActiveId,
260
347
  };
348
+ const activeHistory = nextState.activeHistory.filter((entry) => entry.tabId !== tabId);
349
+ const activeHistoryBeforeIndex = nextState.activeHistory
350
+ .slice(0, nextState.activeHistoryIndex + 1)
351
+ .filter((entry) => entry.tabId !== tabId);
352
+ const nextActiveTab = nextTabs.find((tab) => tab.id === nextActiveId) ?? nextTabs[0];
353
+ const reconciledState = {
354
+ ...nextState,
355
+ activeHistory: activeHistory.length > 0
356
+ ? activeHistory
357
+ : [createDocBrowserActiveHistoryEntry(nextActiveTab)],
358
+ activeHistoryIndex: Math.min(
359
+ Math.max(0, activeHistoryBeforeIndex.length - 1),
360
+ Math.max(0, activeHistory.length - 1),
361
+ ),
362
+ };
363
+ return prev.activeTabId === tabId
364
+ ? pushActiveHistory(reconciledState, createDocBrowserActiveHistoryEntry(nextActiveTab))
365
+ : reconciledState;
261
366
  });
262
367
  };
263
368
 
@@ -266,7 +371,14 @@ export class DocBrowserManager {
266
371
  if (!prev.tabs.some((tab) => tab.id === tabId)) {
267
372
  return prev;
268
373
  }
269
- return { ...prev, activeTabId: tabId, isOpen: true };
374
+ const tab = prev.tabs.find((item) => item.id === tabId);
375
+ if (!tab) {
376
+ return prev;
377
+ }
378
+ return pushActiveHistory(
379
+ { ...prev, activeTabId: tabId, isOpen: true },
380
+ createDocBrowserActiveHistoryEntry(tab),
381
+ );
270
382
  });
271
383
  };
272
384
  }
@@ -1,6 +1,7 @@
1
1
  import { create } from 'zustand';
2
2
  import { createJSONStorage, persist } from 'zustand/middleware';
3
3
  import type {
4
+ DocBrowserActiveHistoryEntry,
4
5
  DocBrowserMode,
5
6
  DocBrowserState,
6
7
  DocBrowserStateUpdate,
@@ -71,6 +72,36 @@ function normalizePersistedDocBrowserTab(value: unknown): DocBrowserTab | null {
71
72
  };
72
73
  }
73
74
 
75
+ function createActiveHistoryEntryFromTab(tab: DocBrowserTab): DocBrowserActiveHistoryEntry {
76
+ return {
77
+ kind: tab.kind,
78
+ tabId: tab.id,
79
+ url: tab.currentUrl,
80
+ };
81
+ }
82
+
83
+ function normalizePersistedActiveHistoryEntry(
84
+ value: unknown,
85
+ tabsById: Map<string, DocBrowserTab>,
86
+ ): DocBrowserActiveHistoryEntry | null {
87
+ if (
88
+ !isRecord(value)
89
+ || typeof value.tabId !== 'string'
90
+ || typeof value.kind !== 'string'
91
+ || typeof value.url !== 'string'
92
+ || value.url.trim().length === 0
93
+ || !tabsById.has(value.tabId)
94
+ ) {
95
+ return null;
96
+ }
97
+
98
+ return {
99
+ kind: value.kind,
100
+ tabId: value.tabId,
101
+ url: value.url,
102
+ };
103
+ }
104
+
74
105
  function normalizePersistedDocBrowserState(value: unknown): DocBrowserState | null {
75
106
  if (!isRecord(value)) {
76
107
  return null;
@@ -88,12 +119,30 @@ function normalizePersistedDocBrowserState(value: unknown): DocBrowserState | nu
88
119
  const activeTabId = tabs.some((tab) => tab.id === persistedActiveTabId)
89
120
  ? persistedActiveTabId ?? tabs[0].id
90
121
  : tabs[0].id;
122
+ const tabsById = new Map(tabs.map((tab) => [tab.id, tab]));
123
+ const activeHistory = Array.isArray(value.activeHistory)
124
+ ? value.activeHistory
125
+ .map((entry) => normalizePersistedActiveHistoryEntry(entry, tabsById))
126
+ .filter((entry): entry is DocBrowserActiveHistoryEntry => entry !== null)
127
+ : [];
128
+ const fallbackHistory = [createActiveHistoryEntryFromTab(tabsById.get(activeTabId) ?? tabs[0])];
129
+ const resolvedActiveHistory = activeHistory.length > 0 ? activeHistory : fallbackHistory;
130
+ const activeHistoryIndex = normalizePersistedNumber(
131
+ value.activeHistoryIndex,
132
+ 0,
133
+ resolvedActiveHistory.length - 1,
134
+ resolvedActiveHistory.length - 1,
135
+ );
136
+ const activeHistoryEntry = resolvedActiveHistory[activeHistoryIndex];
137
+ const resolvedActiveTabId = tabsById.has(activeHistoryEntry.tabId) ? activeHistoryEntry.tabId : activeTabId;
91
138
 
92
139
  return {
93
140
  isOpen: value.isOpen === true,
94
141
  mode: isDocBrowserMode(value.mode) ? value.mode : 'docked',
95
142
  tabs,
96
- activeTabId,
143
+ activeTabId: resolvedActiveTabId,
144
+ activeHistory: resolvedActiveHistory,
145
+ activeHistoryIndex,
97
146
  };
98
147
  }
99
148
 
@@ -122,6 +171,8 @@ export const useDocBrowserStore = create<DocBrowserStore>()(
122
171
  mode: state.snapshot.mode,
123
172
  tabs: state.snapshot.tabs.slice(-DOC_BROWSER_MAX_PERSISTED_TABS),
124
173
  activeTabId: state.snapshot.activeTabId,
174
+ activeHistory: state.snapshot.activeHistory,
175
+ activeHistoryIndex: state.snapshot.activeHistoryIndex,
125
176
  },
126
177
  }),
127
178
  merge: (persistedState, currentState) => {
@@ -13,6 +13,12 @@ export type DocBrowserTab = {
13
13
  navVersion: number;
14
14
  };
15
15
 
16
+ export type DocBrowserActiveHistoryEntry = {
17
+ kind: DocBrowserTabKind;
18
+ tabId: string;
19
+ url: string;
20
+ };
21
+
16
22
  export type DocBrowserOpenOptions = {
17
23
  activate?: boolean;
18
24
  dedupeKey?: string;
@@ -26,12 +32,15 @@ export type DocBrowserState = {
26
32
  mode: DocBrowserMode;
27
33
  tabs: DocBrowserTab[];
28
34
  activeTabId: string;
35
+ activeHistory: DocBrowserActiveHistoryEntry[];
36
+ activeHistoryIndex: number;
29
37
  };
30
38
 
31
39
  export type DocBrowserStateUpdate = DocBrowserState | ((prev: DocBrowserState) => DocBrowserState);
32
40
 
33
41
  export type DocBrowserActions = {
34
42
  open: (url?: string, options?: DocBrowserOpenOptions) => void;
43
+ openNewTab: () => void;
35
44
  close: () => void;
36
45
  toggleMode: () => void;
37
46
  navigate: (url: string) => void;