@nextclaw/ui 0.12.35-beta.3 → 0.12.35-beta.5
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 +126 -0
- package/dist/assets/api-BTZui7nj.js +15 -0
- package/dist/assets/{book-open--T9dJHnp.js → book-open-qc0UVn6m.js} +1 -1
- package/dist/assets/channels-list-page-DBtU_M1Y.js +8 -0
- package/dist/assets/{chat-page-DOKLoiqI.js → chat-page-BcoJYo91.js} +2 -2
- package/dist/assets/config-split-page-DW8tP9Bc.js +1 -0
- package/dist/assets/createLucideIcon-CA0sgoMJ.js +1 -0
- package/dist/assets/desktop-update-config-DARSwQG8.js +1 -0
- package/dist/assets/dist-D5zsy7pX.js +1 -0
- package/dist/assets/dist-DNxIa3pe.js +41 -0
- package/dist/assets/doc-browser-B9skxzVo.js +1 -0
- package/dist/assets/doc-browser-CwyITRXx.js +1 -0
- package/dist/assets/doc-browser-context-Do2qSrB8.js +1 -0
- package/dist/assets/{external-link-CZ9bQY0L.js → external-link-C9fbyi6k.js} +1 -1
- package/dist/assets/index-BYRP_BMj.css +1 -0
- package/dist/assets/index-DckPGTdM.js +103 -0
- package/dist/assets/mcp-marketplace-page-BiExW5wq.js +40 -0
- package/dist/assets/mcp-marketplace-page-Cj6MDSkE.js +1 -0
- package/dist/assets/model-config-DbdY7fF1.js +1 -0
- package/dist/assets/notice-card-CyiVcu_Z.js +5 -0
- package/dist/assets/play-D-s0C4MN.js +1 -0
- package/dist/assets/plus-DOwjM_kF.js +1 -0
- package/dist/assets/provider-scoped-model-input-Bk8jlpRm.js +1 -0
- package/dist/assets/providers-list-CJjNStDV.js +1 -0
- package/dist/assets/react-BaRotFd6.js +1 -0
- package/dist/assets/remote-gl8QckNc.js +1 -0
- package/dist/assets/runtime-config-page-BMFc1yzf.js +1 -0
- package/dist/assets/{save-B6wbBLkU.js → save-CG4Hb1u8.js} +1 -1
- package/dist/assets/{search-config-BFmG4_F7.js → search-config-DUOR26if.js} +1 -1
- package/dist/assets/{search-Czito3Dk.js → search-lWpOIHEn.js} +1 -1
- package/dist/assets/secrets-config-ZbLzjIRx.js +3 -0
- package/dist/assets/setting-row-CNfTKDjc.js +1 -0
- package/dist/assets/tabs-custom-BbZoysAl.js +1 -0
- package/dist/assets/tag-chip-Dl8JbhC7.js +1 -0
- package/dist/assets/x-D6GwT_8k.js +1 -0
- package/dist/index.html +15 -17
- package/package.json +8 -9
- package/src/app/components/app-presenter-provider.tsx +21 -0
- package/src/app/components/layout/sidebar.layout.test.tsx +2 -2
- package/src/app/components/layout/sidebar.tsx +3 -3
- package/src/app/index.tsx +5 -3
- package/src/app/presenters/app.presenter.ts +24 -0
- package/src/features/account/components/account-panel.tsx +11 -11
- package/src/features/account/index.ts +1 -4
- package/src/features/account/managers/account.manager.ts +52 -27
- package/src/features/account/stores/account.store.ts +0 -15
- package/src/features/apps/components/apps-panel.tsx +43 -0
- package/src/features/apps/index.ts +1 -0
- package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +8 -8
- package/src/features/chat/components/layout/chat-sidebar.tsx +3 -2
- package/src/features/chat/managers/chat-session-list.manager.test.ts +58 -0
- package/src/features/chat/stores/chat-session-list.store.ts +58 -7
- package/src/features/panel-apps/components/panel-app-toolbar.tsx +4 -4
- package/src/features/panel-apps/components/panel-apps-list.tsx +6 -2
- package/src/features/panel-apps/index.ts +3 -1
- package/src/features/panel-apps/managers/panel-app-bridge.manager.test.ts +111 -0
- package/src/features/panel-apps/managers/panel-app-bridge.manager.ts +247 -0
- package/src/features/panel-apps/utils/panel-app-doc-browser.utils.test.ts +23 -0
- package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +42 -16
- package/src/features/remote/components/remote-access-page.test.tsx +2 -2
- package/src/features/remote/components/remote-access-page.tsx +9 -9
- package/src/features/remote/managers/remote-access.manager.ts +24 -41
- package/src/features/service-apps/components/service-action-authorization-dialog.tsx +78 -0
- package/src/features/service-apps/components/service-apps-panel.tsx +290 -0
- package/src/features/service-apps/hooks/use-service-apps.ts +66 -0
- package/src/features/service-apps/index.ts +3 -0
- package/src/features/service-apps/managers/service-action-authorization.manager.ts +10 -0
- package/src/features/service-apps/stores/service-action-authorization.store.ts +53 -0
- package/src/shared/components/doc-browser/doc-browser-context.test.tsx +150 -1
- package/src/shared/components/doc-browser/doc-browser-context.tsx +34 -443
- package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +7 -0
- package/src/shared/components/doc-browser/doc-browser.tsx +16 -1
- package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +272 -0
- package/src/shared/components/doc-browser/stores/doc-browser.store.ts +140 -0
- package/src/shared/components/doc-browser/types/doc-browser.types.ts +47 -0
- package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +47 -0
- package/src/shared/components/doc-browser/utils/doc-browser-url.utils.ts +95 -0
- package/src/shared/lib/api/managers/client.manager.ts +2 -1
- package/src/shared/lib/i18n/runtime/doc-browser-labels.utils.ts +31 -0
- package/src/shared/lib/transport/local-transport.service.ts +18 -2
- package/src/shared/lib/transport/remote-transport.service.ts +19 -3
- package/src/shared/lib/transport/request-raw-api-response.utils.ts +1 -1
- package/dist/assets/api-D3uoaOR8.js +0 -15
- package/dist/assets/channels-list-page-DHmz9iHg.js +0 -8
- package/dist/assets/config-split-page-Dutq3R5o.js +0 -1
- package/dist/assets/createLucideIcon-Cwr2o6Zq.js +0 -1
- package/dist/assets/desktop-update-config-ClbDsIOs.js +0 -1
- package/dist/assets/dist-e5WATjIW.js +0 -1
- package/dist/assets/doc-browser-ChY6hMn8.js +0 -1
- package/dist/assets/doc-browser-cR1vWRs1.js +0 -1
- package/dist/assets/doc-browser-context-CLczyX2Z.js +0 -1
- package/dist/assets/es2015-BJw-hrex.js +0 -41
- package/dist/assets/i18n-tWKGEGxB.js +0 -1
- package/dist/assets/index-DPz9DRZN.js +0 -103
- package/dist/assets/index-HvJDzfSU.css +0 -1
- package/dist/assets/key-round-B8tTb3ZJ.js +0 -1
- package/dist/assets/mcp-marketplace-page-BYPxLdUR.js +0 -40
- package/dist/assets/mcp-marketplace-page-DXCvk7wJ.js +0 -1
- package/dist/assets/model-config-DcnC_fSr.js +0 -1
- package/dist/assets/notice-card-DUDbOvqx.js +0 -5
- package/dist/assets/play-CV_kwi9X.js +0 -1
- package/dist/assets/plus-BqfNq7Tu.js +0 -1
- package/dist/assets/provider-scoped-model-input-eO4XY0iY.js +0 -1
- package/dist/assets/providers-list-BgvjRVmp.js +0 -1
- package/dist/assets/remote-BCuZ1JJe.js +0 -1
- package/dist/assets/rotate-cw-BKEhWuWp.js +0 -1
- package/dist/assets/runtime-config-page-B2XPXHrk.js +0 -1
- package/dist/assets/secrets-config-4uY5sk-V.js +0 -3
- package/dist/assets/select-CmmDgG8o.js +0 -1
- package/dist/assets/setting-row-BUO6FeN1.js +0 -1
- package/dist/assets/tabs-custom-D3IZL9WX.js +0 -1
- package/dist/assets/tag-chip-Dm_pOtE3.js +0 -1
- package/dist/assets/x-DBP3WiC9.js +0 -1
- package/src/app/components/app-manager-provider.tsx +0 -20
- package/src/app/managers/app.manager.ts +0 -12
- /package/dist/assets/{config-hints-DGfwbumO.js → config-hints-CTqBnCDp.js} +0 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DocBrowserOpenOptions,
|
|
3
|
+
DocBrowserState,
|
|
4
|
+
DocBrowserStateUpdate,
|
|
5
|
+
DocBrowserTab,
|
|
6
|
+
DocBrowserTabKind,
|
|
7
|
+
} from '@/shared/components/doc-browser/types/doc-browser.types';
|
|
8
|
+
import {
|
|
9
|
+
createDefaultDocBrowserState,
|
|
10
|
+
createDocBrowserTab,
|
|
11
|
+
} from '@/shared/components/doc-browser/utils/doc-browser-state.utils';
|
|
12
|
+
import {
|
|
13
|
+
getDefaultDocsUrl,
|
|
14
|
+
inferTabKind,
|
|
15
|
+
normalizeDocUrl,
|
|
16
|
+
normalizeUrlByKind,
|
|
17
|
+
} from '@/shared/components/doc-browser/utils/doc-browser-url.utils';
|
|
18
|
+
import { useDocBrowserStore } from '@/shared/components/doc-browser/stores/doc-browser.store';
|
|
19
|
+
|
|
20
|
+
function updateTab(
|
|
21
|
+
state: DocBrowserState,
|
|
22
|
+
tabId: string,
|
|
23
|
+
updater: (tab: DocBrowserTab) => DocBrowserTab,
|
|
24
|
+
): DocBrowserState {
|
|
25
|
+
return {
|
|
26
|
+
...state,
|
|
27
|
+
tabs: state.tabs.map((tab) => (tab.id === tabId ? updater(tab) : tab)),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function updateActiveTab(state: DocBrowserState, updater: (tab: DocBrowserTab) => DocBrowserTab): DocBrowserState {
|
|
32
|
+
return updateTab(state, state.activeTabId, updater);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function areOpenUrlsEquivalent(
|
|
36
|
+
currentUrl: string,
|
|
37
|
+
nextUrl: string,
|
|
38
|
+
currentKind: DocBrowserTabKind,
|
|
39
|
+
nextKind: DocBrowserTabKind,
|
|
40
|
+
): boolean {
|
|
41
|
+
if (currentKind === 'docs' && nextKind === 'docs') {
|
|
42
|
+
return normalizeDocUrl(currentUrl) === normalizeDocUrl(nextUrl);
|
|
43
|
+
}
|
|
44
|
+
return currentUrl === nextUrl;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function updateTabForOpen(
|
|
48
|
+
tab: DocBrowserTab,
|
|
49
|
+
url: string,
|
|
50
|
+
kind: DocBrowserTabKind,
|
|
51
|
+
options?: DocBrowserOpenOptions,
|
|
52
|
+
dedupeKey?: string,
|
|
53
|
+
): DocBrowserTab {
|
|
54
|
+
const baseTab = {
|
|
55
|
+
...tab,
|
|
56
|
+
title: options?.title || tab.title,
|
|
57
|
+
kind,
|
|
58
|
+
dedupeKey,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (areOpenUrlsEquivalent(tab.currentUrl, url, tab.kind, kind)) {
|
|
62
|
+
return baseTab;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
...baseTab,
|
|
67
|
+
currentUrl: url,
|
|
68
|
+
history: [...tab.history.slice(0, tab.historyIndex + 1), url],
|
|
69
|
+
historyIndex: tab.historyIndex + 1,
|
|
70
|
+
navVersion: tab.navVersion + 1,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
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
|
+
function openDocBrowserState(
|
|
91
|
+
prev: DocBrowserState,
|
|
92
|
+
url?: string,
|
|
93
|
+
options?: DocBrowserOpenOptions,
|
|
94
|
+
): DocBrowserState {
|
|
95
|
+
const {
|
|
96
|
+
activate,
|
|
97
|
+
dedupeKey: rawDedupeKey,
|
|
98
|
+
kind,
|
|
99
|
+
newTab: shouldForceNewTab,
|
|
100
|
+
title,
|
|
101
|
+
} = options ?? {};
|
|
102
|
+
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;
|
|
106
|
+
const matchedTab = dedupeKey
|
|
107
|
+
? prev.tabs.find((tab) => tab.dedupeKey === dedupeKey)
|
|
108
|
+
: undefined;
|
|
109
|
+
|
|
110
|
+
if (matchedTab) {
|
|
111
|
+
const next = updateTab(
|
|
112
|
+
prev,
|
|
113
|
+
matchedTab.id,
|
|
114
|
+
(tab) => updateTabForOpen(tab, targetUrl, targetKind, options, dedupeKey),
|
|
115
|
+
);
|
|
116
|
+
return {
|
|
117
|
+
...next,
|
|
118
|
+
activeTabId: activate === false ? prev.activeTabId : matchedTab.id,
|
|
119
|
+
isOpen: true,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (shouldForceNewTab || dedupeKey || !activeTab || activeTab.kind !== targetKind) {
|
|
124
|
+
const newTab = createDocBrowserTab(targetUrl, targetKind, title, dedupeKey);
|
|
125
|
+
return {
|
|
126
|
+
...prev,
|
|
127
|
+
isOpen: true,
|
|
128
|
+
tabs: [...prev.tabs, newTab],
|
|
129
|
+
activeTabId: newTab.id,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const next = updateActiveTab(
|
|
134
|
+
prev,
|
|
135
|
+
(tab) => updateTabForOpen(tab, targetUrl, targetKind, options, dedupeKey),
|
|
136
|
+
);
|
|
137
|
+
return { ...next, isOpen: true };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export class DocBrowserManager {
|
|
141
|
+
private readonly setSnapshot = (next: DocBrowserStateUpdate): void => {
|
|
142
|
+
useDocBrowserStore.getState().setSnapshot(next);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
readonly open = (url?: string, options?: DocBrowserOpenOptions): void => {
|
|
146
|
+
this.setSnapshot((prev) => openDocBrowserState(prev, url, options));
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
readonly close = (): void => {
|
|
150
|
+
this.setSnapshot((prev) => ({ ...prev, isOpen: false }));
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
readonly toggleMode = (): void => {
|
|
154
|
+
this.setSnapshot((prev) => ({ ...prev, mode: prev.mode === 'floating' ? 'docked' : 'floating' }));
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
readonly navigate = (url: string): void => {
|
|
158
|
+
this.setSnapshot((prev) => {
|
|
159
|
+
if (!prev.tabs.length) {
|
|
160
|
+
const fallbackTab = createDocBrowserTab(getDefaultDocsUrl(), 'docs', 'Docs');
|
|
161
|
+
return {
|
|
162
|
+
...prev,
|
|
163
|
+
tabs: [fallbackTab],
|
|
164
|
+
activeTabId: fallbackTab.id,
|
|
165
|
+
isOpen: true,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return updateActiveTab(prev, (tab) => {
|
|
170
|
+
if (tab.kind !== 'docs') {
|
|
171
|
+
return tab;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const targetUrl = normalizeUrlByKind(url, 'docs');
|
|
175
|
+
if (normalizeDocUrl(targetUrl) === normalizeDocUrl(tab.currentUrl)) {
|
|
176
|
+
return tab;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
...tab,
|
|
181
|
+
currentUrl: targetUrl,
|
|
182
|
+
history: [...tab.history.slice(0, tab.historyIndex + 1), targetUrl],
|
|
183
|
+
historyIndex: tab.historyIndex + 1,
|
|
184
|
+
navVersion: tab.navVersion + 1,
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
readonly syncUrl = (url: string): void => {
|
|
191
|
+
this.setSnapshot((prev) => {
|
|
192
|
+
if (!prev.tabs.length) {
|
|
193
|
+
const fallbackTab = createDocBrowserTab(getDefaultDocsUrl(), 'docs', 'Docs');
|
|
194
|
+
return {
|
|
195
|
+
...prev,
|
|
196
|
+
tabs: [fallbackTab],
|
|
197
|
+
activeTabId: fallbackTab.id,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return updateActiveTab(prev, (tab) => {
|
|
202
|
+
if (tab.kind !== 'docs') {
|
|
203
|
+
return tab;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (normalizeDocUrl(url) === normalizeDocUrl(tab.currentUrl)) {
|
|
207
|
+
return tab;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
...tab,
|
|
212
|
+
currentUrl: url,
|
|
213
|
+
history: [...tab.history.slice(0, tab.historyIndex + 1), url],
|
|
214
|
+
historyIndex: tab.historyIndex + 1,
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
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
|
+
}));
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
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
|
+
}));
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
readonly closeTab = (tabId: string): void => {
|
|
237
|
+
this.setSnapshot((prev) => {
|
|
238
|
+
if (prev.tabs.length <= 1) {
|
|
239
|
+
const fallbackState = createDefaultDocBrowserState();
|
|
240
|
+
return {
|
|
241
|
+
...fallbackState,
|
|
242
|
+
isOpen: false,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const index = prev.tabs.findIndex((tab) => tab.id === tabId);
|
|
247
|
+
if (index < 0) {
|
|
248
|
+
return prev;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const nextTabs = prev.tabs.filter((tab) => tab.id !== tabId);
|
|
252
|
+
const nextActiveId = prev.activeTabId === tabId
|
|
253
|
+
? nextTabs[Math.max(0, index - 1)]?.id ?? nextTabs[0].id
|
|
254
|
+
: prev.activeTabId;
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
...prev,
|
|
258
|
+
tabs: nextTabs,
|
|
259
|
+
activeTabId: nextActiveId,
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
readonly setActiveTab = (tabId: string): void => {
|
|
265
|
+
this.setSnapshot((prev) => {
|
|
266
|
+
if (!prev.tabs.some((tab) => tab.id === tabId)) {
|
|
267
|
+
return prev;
|
|
268
|
+
}
|
|
269
|
+
return { ...prev, activeTabId: tabId, isOpen: true };
|
|
270
|
+
});
|
|
271
|
+
};
|
|
272
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { createJSONStorage, persist } from 'zustand/middleware';
|
|
3
|
+
import type {
|
|
4
|
+
DocBrowserMode,
|
|
5
|
+
DocBrowserState,
|
|
6
|
+
DocBrowserStateUpdate,
|
|
7
|
+
DocBrowserTab,
|
|
8
|
+
} from '@/shared/components/doc-browser/types/doc-browser.types';
|
|
9
|
+
import { createDefaultDocBrowserState } from '@/shared/components/doc-browser/utils/doc-browser-state.utils';
|
|
10
|
+
|
|
11
|
+
const DOC_BROWSER_STORAGE_KEY = 'nextclaw.doc-browser.state';
|
|
12
|
+
const DOC_BROWSER_STORAGE_VERSION = 1;
|
|
13
|
+
const DOC_BROWSER_MAX_PERSISTED_TABS = 8;
|
|
14
|
+
|
|
15
|
+
type PersistedDocBrowserState = Partial<DocBrowserState>;
|
|
16
|
+
|
|
17
|
+
type DocBrowserStore = {
|
|
18
|
+
snapshot: DocBrowserState;
|
|
19
|
+
setSnapshot: (next: DocBrowserStateUpdate) => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
23
|
+
return !!value && typeof value === 'object';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isDocBrowserMode(value: unknown): value is DocBrowserMode {
|
|
27
|
+
return value === 'floating' || value === 'docked';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizePersistedStringList(value: unknown, fallback: string): string[] {
|
|
31
|
+
if (!Array.isArray(value)) {
|
|
32
|
+
return [fallback];
|
|
33
|
+
}
|
|
34
|
+
const values = value.filter((item): item is string => typeof item === 'string' && item.trim().length > 0);
|
|
35
|
+
return values.length > 0 ? values : [fallback];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizePersistedNumber(value: unknown, min: number, max: number, fallback: number): number {
|
|
39
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
40
|
+
? Math.min(max, Math.max(min, value))
|
|
41
|
+
: fallback;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizePersistedDocBrowserTab(value: unknown): DocBrowserTab | null {
|
|
45
|
+
if (
|
|
46
|
+
!isRecord(value)
|
|
47
|
+
|| typeof value.id !== 'string'
|
|
48
|
+
|| typeof value.kind !== 'string'
|
|
49
|
+
|| typeof value.title !== 'string'
|
|
50
|
+
|| typeof value.currentUrl !== 'string'
|
|
51
|
+
|| value.currentUrl.trim().length === 0
|
|
52
|
+
) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const history = normalizePersistedStringList(value.history, value.currentUrl);
|
|
56
|
+
const historyIndex = normalizePersistedNumber(value.historyIndex, 0, history.length - 1, history.length - 1);
|
|
57
|
+
const navVersion = normalizePersistedNumber(value.navVersion, 0, Number.MAX_SAFE_INTEGER, 0);
|
|
58
|
+
const dedupeKey = typeof value.dedupeKey === 'string' && value.dedupeKey.trim().length > 0
|
|
59
|
+
? value.dedupeKey
|
|
60
|
+
: undefined;
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
id: value.id,
|
|
64
|
+
kind: value.kind,
|
|
65
|
+
title: value.title,
|
|
66
|
+
currentUrl: history[historyIndex] ?? value.currentUrl,
|
|
67
|
+
dedupeKey,
|
|
68
|
+
history,
|
|
69
|
+
historyIndex,
|
|
70
|
+
navVersion,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizePersistedDocBrowserState(value: unknown): DocBrowserState | null {
|
|
75
|
+
if (!isRecord(value)) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const tabs = Array.isArray(value.tabs)
|
|
79
|
+
? value.tabs
|
|
80
|
+
.map(normalizePersistedDocBrowserTab)
|
|
81
|
+
.filter((tab): tab is DocBrowserTab => tab !== null)
|
|
82
|
+
.slice(-DOC_BROWSER_MAX_PERSISTED_TABS)
|
|
83
|
+
: [];
|
|
84
|
+
if (tabs.length === 0) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const persistedActiveTabId = typeof value.activeTabId === 'string' ? value.activeTabId : null;
|
|
88
|
+
const activeTabId = tabs.some((tab) => tab.id === persistedActiveTabId)
|
|
89
|
+
? persistedActiveTabId ?? tabs[0].id
|
|
90
|
+
: tabs[0].id;
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
isOpen: value.isOpen === true,
|
|
94
|
+
mode: isDocBrowserMode(value.mode) ? value.mode : 'docked',
|
|
95
|
+
tabs,
|
|
96
|
+
activeTabId,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resolveDocBrowserStateUpdate(prev: DocBrowserState, next: DocBrowserStateUpdate): DocBrowserState {
|
|
101
|
+
if (typeof next === 'function') {
|
|
102
|
+
return next(prev);
|
|
103
|
+
}
|
|
104
|
+
return next;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const useDocBrowserStore = create<DocBrowserStore>()(
|
|
108
|
+
persist(
|
|
109
|
+
(set) => ({
|
|
110
|
+
snapshot: createDefaultDocBrowserState(),
|
|
111
|
+
setSnapshot: (next) => set((prev) => ({
|
|
112
|
+
snapshot: resolveDocBrowserStateUpdate(prev.snapshot, next),
|
|
113
|
+
})),
|
|
114
|
+
}),
|
|
115
|
+
{
|
|
116
|
+
name: DOC_BROWSER_STORAGE_KEY,
|
|
117
|
+
version: DOC_BROWSER_STORAGE_VERSION,
|
|
118
|
+
storage: createJSONStorage(() => window.localStorage),
|
|
119
|
+
partialize: (state): { snapshot: PersistedDocBrowserState } => ({
|
|
120
|
+
snapshot: {
|
|
121
|
+
isOpen: state.snapshot.isOpen,
|
|
122
|
+
mode: state.snapshot.mode,
|
|
123
|
+
tabs: state.snapshot.tabs.slice(-DOC_BROWSER_MAX_PERSISTED_TABS),
|
|
124
|
+
activeTabId: state.snapshot.activeTabId,
|
|
125
|
+
},
|
|
126
|
+
}),
|
|
127
|
+
merge: (persistedState, currentState) => {
|
|
128
|
+
const persistedSnapshot = isRecord(persistedState) ? persistedState.snapshot : null;
|
|
129
|
+
const normalizedSnapshot = normalizePersistedDocBrowserState(persistedSnapshot);
|
|
130
|
+
if (!normalizedSnapshot) {
|
|
131
|
+
return currentState;
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
...currentState,
|
|
135
|
+
snapshot: normalizedSnapshot,
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
),
|
|
140
|
+
);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type DocBrowserMode = 'floating' | 'docked';
|
|
2
|
+
export type DocBrowserTabKind = 'docs' | 'content' | (string & {});
|
|
3
|
+
|
|
4
|
+
export type DocBrowserTab = {
|
|
5
|
+
id: string;
|
|
6
|
+
kind: DocBrowserTabKind;
|
|
7
|
+
title: string;
|
|
8
|
+
currentUrl: string;
|
|
9
|
+
dedupeKey?: string;
|
|
10
|
+
history: string[];
|
|
11
|
+
historyIndex: number;
|
|
12
|
+
/** Increments on parent-initiated navigation to trigger iframe remount */
|
|
13
|
+
navVersion: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type DocBrowserOpenOptions = {
|
|
17
|
+
activate?: boolean;
|
|
18
|
+
dedupeKey?: string;
|
|
19
|
+
newTab?: boolean;
|
|
20
|
+
title?: string;
|
|
21
|
+
kind?: DocBrowserTabKind;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type DocBrowserState = {
|
|
25
|
+
isOpen: boolean;
|
|
26
|
+
mode: DocBrowserMode;
|
|
27
|
+
tabs: DocBrowserTab[];
|
|
28
|
+
activeTabId: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type DocBrowserStateUpdate = DocBrowserState | ((prev: DocBrowserState) => DocBrowserState);
|
|
32
|
+
|
|
33
|
+
export type DocBrowserActions = {
|
|
34
|
+
open: (url?: string, options?: DocBrowserOpenOptions) => void;
|
|
35
|
+
close: () => void;
|
|
36
|
+
toggleMode: () => void;
|
|
37
|
+
navigate: (url: string) => void;
|
|
38
|
+
syncUrl: (url: string) => void;
|
|
39
|
+
goBack: () => void;
|
|
40
|
+
goForward: () => void;
|
|
41
|
+
closeTab: (tabId: string) => void;
|
|
42
|
+
setActiveTab: (tabId: string) => void;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type DocBrowserContextValue = DocBrowserState & DocBrowserActions & {
|
|
46
|
+
currentTab?: DocBrowserTab;
|
|
47
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DocBrowserState,
|
|
3
|
+
DocBrowserTab,
|
|
4
|
+
DocBrowserTabKind,
|
|
5
|
+
} from '@/shared/components/doc-browser/types/doc-browser.types';
|
|
6
|
+
import {
|
|
7
|
+
getDefaultDocsUrl,
|
|
8
|
+
inferTabTitle,
|
|
9
|
+
} from './doc-browser-url.utils';
|
|
10
|
+
|
|
11
|
+
let tabCounter = 0;
|
|
12
|
+
|
|
13
|
+
function nextTabId(): string {
|
|
14
|
+
tabCounter += 1;
|
|
15
|
+
return `doc-tab-${Date.now()}-${tabCounter}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createDocBrowserTab(
|
|
19
|
+
url: string,
|
|
20
|
+
kind: DocBrowserTabKind,
|
|
21
|
+
title?: string,
|
|
22
|
+
dedupeKey?: string,
|
|
23
|
+
): DocBrowserTab {
|
|
24
|
+
const tabTitle = title?.trim() || inferTabTitle(url, kind, kind === 'docs' ? 'Docs' : 'Detail');
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
id: nextTabId(),
|
|
28
|
+
kind,
|
|
29
|
+
title: tabTitle,
|
|
30
|
+
currentUrl: url,
|
|
31
|
+
dedupeKey,
|
|
32
|
+
history: [url],
|
|
33
|
+
historyIndex: 0,
|
|
34
|
+
navVersion: 0,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createDefaultDocBrowserState(): DocBrowserState {
|
|
39
|
+
const initialUrl = getDefaultDocsUrl();
|
|
40
|
+
const initialTab = createDocBrowserTab(initialUrl, 'docs', 'Docs');
|
|
41
|
+
return {
|
|
42
|
+
isOpen: false,
|
|
43
|
+
mode: 'docked',
|
|
44
|
+
tabs: [initialTab],
|
|
45
|
+
activeTabId: initialTab.id,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { getLanguage, type I18nLanguage } from '@/shared/lib/i18n';
|
|
2
|
+
import type { DocBrowserTabKind } from '@/shared/components/doc-browser/types/doc-browser.types';
|
|
3
|
+
|
|
4
|
+
const DOCS_PRIMARY_DOMAIN = 'docs.nextclaw.io';
|
|
5
|
+
const DOCS_PAGES_DEV = 'nextclaw-docs.pages.dev';
|
|
6
|
+
const DOCS_HOSTS = new Set([
|
|
7
|
+
DOCS_PRIMARY_DOMAIN,
|
|
8
|
+
`www.${DOCS_PRIMARY_DOMAIN}`,
|
|
9
|
+
DOCS_PAGES_DEV,
|
|
10
|
+
`www.${DOCS_PAGES_DEV}`,
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
export const DOCS_DEFAULT_BASE_URL = `https://${DOCS_PRIMARY_DOMAIN}`;
|
|
14
|
+
const DOCS_DEFAULT_GUIDE_PATH = '/guide/getting-started';
|
|
15
|
+
|
|
16
|
+
function toDocsLocale(language: I18nLanguage): 'en' | 'zh' {
|
|
17
|
+
return language === 'zh' ? 'zh' : 'en';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function ensureLocalizedDocsPath(pathname: string, locale: 'en' | 'zh'): string {
|
|
21
|
+
const normalized = pathname.startsWith('/') ? pathname : `/${pathname}`;
|
|
22
|
+
|
|
23
|
+
if (normalized === '/' || normalized === '') {
|
|
24
|
+
return `/${locale}/`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (/^\/(en|zh)(\/|$)/.test(normalized)) {
|
|
28
|
+
return normalized;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return `/${locale}${normalized}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resolveLocalizedDocsUrl(url: string): string {
|
|
35
|
+
const locale = toDocsLocale(getLanguage());
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const parsed = new URL(url, DOCS_DEFAULT_BASE_URL);
|
|
39
|
+
if (!DOCS_HOSTS.has(parsed.hostname)) {
|
|
40
|
+
return parsed.toString();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
parsed.pathname = ensureLocalizedDocsPath(parsed.pathname, locale);
|
|
44
|
+
return parsed.toString();
|
|
45
|
+
} catch {
|
|
46
|
+
return new URL(`/${locale}${DOCS_DEFAULT_GUIDE_PATH}`, DOCS_DEFAULT_BASE_URL).toString();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getDefaultDocsUrl(): string {
|
|
51
|
+
return resolveLocalizedDocsUrl(DOCS_DEFAULT_GUIDE_PATH);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function normalizeDocUrl(url: string): string {
|
|
55
|
+
try {
|
|
56
|
+
return new URL(url).pathname.replace(/\.html$/, '').replace(/\/$/, '');
|
|
57
|
+
} catch {
|
|
58
|
+
return url;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function inferTabTitle(url: string, kind: DocBrowserTabKind, fallback = 'Docs'): string {
|
|
63
|
+
try {
|
|
64
|
+
const parsed = new URL(url, DOCS_DEFAULT_BASE_URL);
|
|
65
|
+
if (parsed.protocol === 'data:') {
|
|
66
|
+
return kind === 'docs' ? fallback : 'Detail';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const segments = parsed.pathname.split('/').filter(Boolean);
|
|
70
|
+
const leaf = segments[segments.length - 1] ?? fallback;
|
|
71
|
+
return decodeURIComponent(leaf).replace(/[-_]/g, ' ').slice(0, 40) || fallback;
|
|
72
|
+
} catch {
|
|
73
|
+
return fallback;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function isDocsUrl(url: string): boolean {
|
|
78
|
+
try {
|
|
79
|
+
const parsed = new URL(url, window.location.origin);
|
|
80
|
+
return DOCS_HOSTS.has(parsed.hostname);
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function inferTabKind(url: string): DocBrowserTabKind {
|
|
87
|
+
return isDocsUrl(url) ? 'docs' : 'content';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function normalizeUrlByKind(url: string, kind: DocBrowserTabKind): string {
|
|
91
|
+
if (kind === 'docs') {
|
|
92
|
+
return resolveLocalizedDocsUrl(url);
|
|
93
|
+
}
|
|
94
|
+
return url;
|
|
95
|
+
}
|
|
@@ -11,11 +11,12 @@ import { appClient } from '@/shared/lib/transport';
|
|
|
11
11
|
import type { ApiResponse } from '@/shared/lib/api/types';
|
|
12
12
|
|
|
13
13
|
const nextclawUiTransport: NextClawTransport = {
|
|
14
|
-
request: async <T>({ body, method, path, query, signal, timeoutMs }: NextClawTransportRequestInput): Promise<T> => {
|
|
14
|
+
request: async <T>({ body, headers, method, path, query, signal, timeoutMs }: NextClawTransportRequestInput): Promise<T> => {
|
|
15
15
|
return await appClient.request({
|
|
16
16
|
method,
|
|
17
17
|
path: appendQueryToPath(path, serializeQuery(query)),
|
|
18
18
|
...(body !== undefined ? { body } : {}),
|
|
19
|
+
...(headers ? { headers } : {}),
|
|
19
20
|
...(signal ? { signal } : {}),
|
|
20
21
|
...(timeoutMs !== undefined ? { timeoutMs } : {})
|
|
21
22
|
});
|
|
@@ -10,6 +10,7 @@ export const DOC_BROWSER_LABELS = {
|
|
|
10
10
|
docBrowserNewTab: { zh: '新建标签', en: 'New Tab' },
|
|
11
11
|
docBrowserCloseTab: { zh: '关闭标签', en: 'Close Tab' },
|
|
12
12
|
docBrowserTabUntitled: { zh: '未命名', en: 'Untitled' },
|
|
13
|
+
appsTitle: { zh: '应用', en: 'Apps' },
|
|
13
14
|
panelAppsTitle: { zh: '面板应用', en: 'Panel Apps' },
|
|
14
15
|
panelAppsLoading: { zh: '加载面板应用中...', en: 'Loading panel apps...' },
|
|
15
16
|
panelAppsLoadFailed: { zh: '面板应用加载失败', en: 'Failed to load panel apps' },
|
|
@@ -26,4 +27,34 @@ export const DOC_BROWSER_LABELS = {
|
|
|
26
27
|
panelAppsUnfavorite: { zh: '取消收藏面板应用', en: 'Unfavorite panel app' },
|
|
27
28
|
panelAppsLastOpened: { zh: '上次打开', en: 'Last opened' },
|
|
28
29
|
panelAppsUpdated: { zh: '更新于', en: 'Updated' },
|
|
30
|
+
serviceAppsTitle: { zh: '服务应用', en: 'Service Apps' },
|
|
31
|
+
serviceAppsLoading: { zh: '加载服务应用中...', en: 'Loading service apps...' },
|
|
32
|
+
serviceAppsLoadFailed: { zh: '服务应用加载失败', en: 'Failed to load service apps' },
|
|
33
|
+
serviceAppsEmpty: { zh: '暂无服务应用', en: 'No service apps yet' },
|
|
34
|
+
serviceAppsRefresh: { zh: '刷新服务应用', en: 'Refresh service apps' },
|
|
35
|
+
serviceAppsDiscoverActions: { zh: '发现运行时动作', en: 'Discover runtime actions' },
|
|
36
|
+
serviceAppsRestart: { zh: '重启服务应用', en: 'Restart service app' },
|
|
37
|
+
serviceAppsGrantedTo: { zh: '已授权给', en: 'Granted to' },
|
|
38
|
+
serviceAppsRevokeGrant: { zh: '撤销授权', en: 'Revoke grant' },
|
|
39
|
+
serviceAppsRuntimeState_matched: { zh: '已匹配', en: 'Matched' },
|
|
40
|
+
serviceAppsRuntimeState_missing: { zh: '运行时缺失', en: 'Missing' },
|
|
41
|
+
serviceAppsRuntimeState_undeclared: { zh: '未声明', en: 'Undeclared' },
|
|
42
|
+
serviceAppsCommand: { zh: '命令', en: 'Command' },
|
|
43
|
+
serviceAppsCwd: { zh: '工作目录', en: 'Working directory' },
|
|
44
|
+
serviceAppsManifest: { zh: 'Manifest', en: 'Manifest' },
|
|
45
|
+
serviceAppsLastStarted: { zh: '上次启动', en: 'Last started' },
|
|
46
|
+
serviceAppsLastReady: { zh: '上次就绪', en: 'Last ready' },
|
|
47
|
+
serviceAppsLastFailed: { zh: '上次失败', en: 'Last failed' },
|
|
48
|
+
serviceActionAuthorizationTitle: { zh: '授权服务动作', en: 'Authorize service action' },
|
|
49
|
+
serviceActionAuthorizationDescription: {
|
|
50
|
+
zh: '此面板应用正在请求调用本地服务应用动作。允许后,同一动作会为该面板应用记住授权。',
|
|
51
|
+
en: 'This panel app is requesting a local service action. If allowed, this action will stay granted for this panel app.',
|
|
52
|
+
},
|
|
53
|
+
serviceActionAuthorizationSource: { zh: '来源', en: 'Source' },
|
|
54
|
+
serviceActionAuthorizationAction: { zh: '动作', en: 'Action' },
|
|
55
|
+
serviceActionAuthorizationPurpose: { zh: '用途', en: 'Purpose' },
|
|
56
|
+
serviceActionAuthorizationRisk: { zh: '风险', en: 'Risk' },
|
|
57
|
+
serviceActionAuthorizationInput: { zh: '输入', en: 'Input' },
|
|
58
|
+
serviceActionAuthorizationReject: { zh: '拒绝', en: 'Reject' },
|
|
59
|
+
serviceActionAuthorizationAllow: { zh: '允许', en: 'Allow' },
|
|
29
60
|
} as const;
|
|
@@ -6,9 +6,25 @@ import { resolveTransportWebSocketUrl } from './transport-websocket-url.utils';
|
|
|
6
6
|
|
|
7
7
|
type EventHandler = (event: AppEvent) => void;
|
|
8
8
|
|
|
9
|
-
function createTransportError(
|
|
9
|
+
function createTransportError(
|
|
10
|
+
response: {
|
|
11
|
+
ok: boolean;
|
|
12
|
+
error?: {
|
|
13
|
+
code?: string;
|
|
14
|
+
message?: string;
|
|
15
|
+
details?: Record<string, unknown>;
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
fallback: string
|
|
19
|
+
): Error {
|
|
10
20
|
if (!response.ok) {
|
|
11
|
-
|
|
21
|
+
const error = new Error(response.error?.message || fallback) as Error & {
|
|
22
|
+
code?: string;
|
|
23
|
+
details?: Record<string, unknown>;
|
|
24
|
+
};
|
|
25
|
+
error.code = response.error?.code;
|
|
26
|
+
error.details = response.error?.details;
|
|
27
|
+
return error;
|
|
12
28
|
}
|
|
13
29
|
return new Error(fallback);
|
|
14
30
|
}
|