@nextclaw/ui 0.15.26 → 0.15.28
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 +23 -0
- package/dist/assets/{api-CK3Fro7z.js → api-a_GaOFc7.js} +4 -4
- package/dist/assets/{appearance-settings-page-zqwWO8Dy.js → appearance-settings-page-KEYKkxHu.js} +1 -1
- package/dist/assets/{channels-list-page-CVCZBlCL.js → channels-list-page-eXLXPgKT.js} +2 -2
- package/dist/assets/{chat-page-D4wehRTz.js → chat-page-jfImLdsq.js} +1 -1
- package/dist/assets/{config-split-page-CceNFvlY.js → config-split-page-ps1Aod3g.js} +1 -1
- package/dist/assets/{confirm-dialog-CWeNYWZY.js → confirm-dialog-DJSUNG33.js} +2 -2
- package/dist/assets/desktop-update-config-C20U71k1.js +1 -0
- package/dist/assets/{dist-D9xtGgdL.js → dist-BkTYNzm_.js} +1 -1
- package/dist/assets/{dist-AYxkFx2B.js → dist-DkR7_Dpl.js} +1 -1
- package/dist/assets/doc-browser-DahzJEWj.js +1 -0
- package/dist/assets/doc-browser-Q67TOGVS.js +1 -0
- package/dist/assets/{doc-browser-context-Cb7Z0j90.js → doc-browser-context-BQim8SqD.js} +1 -1
- package/dist/assets/form-actions-B2-bpFie.js +1 -0
- package/dist/assets/index-BP0Hd2jI.js +109 -0
- package/dist/assets/index-DSLK7zgR.css +1 -0
- package/dist/assets/loader-circle-DyXZU92l.js +1 -0
- package/dist/assets/mcp-marketplace-page-B0dBE5CV.js +1 -0
- package/dist/assets/mcp-marketplace-page-w8panjZG.js +9 -0
- package/dist/assets/{model-config-page-DfOLyQqR.js → model-config-page-CJCnaKOh.js} +1 -1
- package/dist/assets/{navigation-link-MiiyoIjH.js → navigation-link-mpHFXwRr.js} +2 -2
- package/dist/assets/plus-Dhb4KuMH.js +1 -0
- package/dist/assets/{provider-scoped-model-input-DXzjr5l0.js → provider-scoped-model-input-Dqu-ZoWG.js} +1 -1
- package/dist/assets/{providers-config-page-CaO8WfZH.js → providers-config-page-C6fRN2rJ.js} +1 -1
- package/dist/assets/react-BlXwvOLo.js +8 -0
- package/dist/assets/{refresh-cw-a2VVeeAF.js → refresh-cw-JFuyDj_P.js} +1 -1
- package/dist/assets/remote-Xyv2c_W9.js +1 -0
- package/dist/assets/rotate-cw-DjbbiIW_.js +1 -0
- package/dist/assets/{runtime-config-page-BuA4A02z.js → runtime-config-page-CtypMgLd.js} +1 -1
- package/dist/assets/{save-CGm23Yww.js → save-BL8P8CLX.js} +1 -1
- package/dist/assets/{search-CH2t--sH.js → search-bZL75Pgc.js} +1 -1
- package/dist/assets/{search-config-page-BAmYqoAz.js → search-config-page-DhSFjYj4.js} +1 -1
- package/dist/assets/{secrets-config-page-BlPOuql_.js → secrets-config-page-BLGcyb1D.js} +2 -2
- package/dist/assets/security-config-DmJw9QZA.js +1 -0
- package/dist/assets/{select-DnxxDpcV.js → select-rUZ8nIZu.js} +2 -2
- package/dist/assets/{settings-2-agnMYc8a.js → settings-2-BI4SCkxQ.js} +1 -1
- package/dist/assets/skeleton-CVmEPG43.js +1 -0
- package/dist/assets/{tag-chip-CPOc8b2N.js → tag-chip-BdI_yIOA.js} +1 -1
- package/dist/assets/x-BcEayhV6.js +1 -0
- package/dist/index.html +19 -19
- package/package.json +6 -6
- package/src/app/components/layout/app-layout.tsx +38 -2
- package/src/app/hooks/__tests__/use-app-event-consumers.test.ts +20 -0
- package/src/app/hooks/use-app-event-consumers.ts +21 -0
- package/src/app/presenters/app.presenter.ts +6 -0
- package/src/features/apps/components/__tests__/app-packages-panel.test.tsx +116 -14
- package/src/features/apps/components/__tests__/apps-panel.test.tsx +58 -0
- package/src/features/apps/components/app-artwork.tsx +49 -0
- package/src/features/apps/components/app-marketplace-catalog.tsx +332 -0
- package/src/features/apps/components/app-marketplace-cover.tsx +64 -0
- package/src/features/apps/components/app-marketplace-detail.tsx +161 -0
- package/src/features/apps/components/app-marketplace-dialog.tsx +107 -217
- package/src/features/apps/components/app-marketplace-operation.tsx +84 -0
- package/src/features/apps/components/app-package-card.tsx +201 -52
- package/src/features/apps/components/app-packages-panel.tsx +120 -42
- package/src/features/apps/components/apps-panel.tsx +28 -27
- package/src/features/apps/hooks/use-app-marketplace.test.ts +144 -0
- package/src/features/apps/hooks/use-app-marketplace.ts +55 -85
- package/src/features/apps/hooks/use-app-packages.ts +59 -4
- package/src/features/apps/index.ts +1 -0
- package/src/features/apps/managers/__tests__/app-package-operation-settlement.manager.test.ts +81 -0
- package/src/features/apps/managers/app-package-operation-settlement.manager.ts +80 -0
- package/src/features/apps/services/app-marketplace.service.ts +223 -0
- package/src/features/apps/types/app-marketplace.types.ts +75 -0
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +64 -3
- package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +24 -14
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +53 -0
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-run-queue.test.tsx +18 -0
- package/src/features/chat/features/conversation/hooks/use-chat-composer-reference-intent.ts +4 -1
- package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +126 -32
- package/src/features/chat/features/conversation/hooks/use-session-run-queue.ts +12 -2
- package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +31 -0
- package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +5 -0
- package/src/features/chat/features/input/utils/chat-inline-token-composer.utils.ts +14 -0
- package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +25 -0
- package/src/features/chat/features/message/hooks/use-chat-inline-token-actions.ts +8 -1
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +77 -20
- package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +56 -50
- package/src/features/chat/features/workspace/components/project-files/workspace-directory-tree.tsx +33 -11
- package/src/features/chat/features/workspace/components/project-files/workspace-project-tree-view.tsx +93 -0
- package/src/features/chat/features/workspace/stores/__tests__/workspace-project-tree.store.test.ts +38 -0
- package/src/features/chat/features/workspace/stores/workspace-project-tree.store.ts +160 -0
- package/src/features/chat/index.ts +1 -0
- package/src/features/chat/managers/__tests__/chat-composer-intent.manager.test.ts +29 -0
- package/src/features/chat/managers/chat-composer-intent.manager.ts +29 -2
- package/src/features/chat/presenters/chat.presenter.ts +2 -2
- package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +1 -1
- package/src/features/panel-apps/components/panel-apps-list.tsx +1 -6
- package/src/features/right-panel-resources/utils/__tests__/right-panel-resource-uri.utils.test.ts +34 -0
- package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.ts +29 -0
- package/src/features/service-apps/components/service-apps-panel.tsx +3 -17
- package/src/platforms/desktop/components/desktop-app-shell.tsx +5 -0
- package/src/platforms/mobile/components/mobile-app-shell.tsx +9 -1
- package/src/shared/components/actions/__tests__/navigation-link.test.tsx +12 -0
- package/src/shared/components/actions/navigation-link.tsx +6 -3
- package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +34 -0
- package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +5 -0
- package/src/shared/components/doc-browser/doc-browser.tsx +11 -1
- package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +13 -0
- package/src/shared/components/ui/tabs.tsx +24 -1
- package/src/shared/hooks/use-server-path-watch.ts +42 -0
- package/src/shared/lib/api/server-path/server-path.types.ts +10 -0
- package/src/shared/lib/api/types.ts +3 -0
- package/src/shared/lib/api/utils/server-path.utils.ts +10 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +1 -0
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +49 -7
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +1 -0
- package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +49 -7
- package/vite.config.ts +32 -1
- package/dist/assets/desktop-update-config-Dirc3bpt.js +0 -1
- package/dist/assets/doc-browser-4Sud6dWw.js +0 -1
- package/dist/assets/doc-browser-D4x_yH5F.js +0 -1
- package/dist/assets/form-actions-B2q4jgjE.js +0 -1
- package/dist/assets/index-CdyTsSBu.css +0 -1
- package/dist/assets/index-Das_bw2h.js +0 -107
- package/dist/assets/loader-circle-CpmBdaon.js +0 -1
- package/dist/assets/mcp-marketplace-page-CdOY3Sop.js +0 -1
- package/dist/assets/mcp-marketplace-page-DvjrfqEt.js +0 -9
- package/dist/assets/plus-C8FkjpSZ.js +0 -1
- package/dist/assets/react-C8XFFvuz.js +0 -8
- package/dist/assets/remote-BJHXa4mB.js +0 -1
- package/dist/assets/rotate-cw-DJ7UF3F2.js +0 -1
- package/dist/assets/security-config-CbDiY2xr.js +0 -1
- package/dist/assets/skeleton-XpqeWL62.js +0 -1
- package/dist/assets/x-odTHAtLZ.js +0 -1
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CHAT_CONVERSATION_EXCERPT_TOKEN_KIND,
|
|
3
|
+
CHAT_UI_RESOURCE_TOKEN_KIND,
|
|
3
4
|
CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND,
|
|
4
5
|
CHAT_WORKSPACE_EXCERPT_TOKEN_KIND,
|
|
5
6
|
CHAT_WORKSPACE_FILE_TOKEN_KIND,
|
|
7
|
+
readChatUiResourceReference,
|
|
8
|
+
type ChatUiResourceReference,
|
|
6
9
|
} from '@nextclaw/shared';
|
|
7
10
|
|
|
8
11
|
type ChatComposerReferenceIntentBase = {
|
|
@@ -35,17 +38,24 @@ export type ChatComposerConversationExcerptIntent = ChatComposerReferenceIntentB
|
|
|
35
38
|
excerpt: string;
|
|
36
39
|
};
|
|
37
40
|
|
|
41
|
+
export type ChatComposerUiResourceReferenceIntent = ChatComposerReferenceIntentBase & {
|
|
42
|
+
kind: typeof CHAT_UI_RESOURCE_TOKEN_KIND;
|
|
43
|
+
reference: ChatUiResourceReference;
|
|
44
|
+
};
|
|
45
|
+
|
|
38
46
|
export type ChatComposerReferenceIntent =
|
|
39
47
|
| ChatComposerFileReferenceIntent
|
|
40
48
|
| ChatComposerDirectoryReferenceIntent
|
|
41
49
|
| ChatComposerExcerptReferenceIntent
|
|
42
|
-
| ChatComposerConversationExcerptIntent
|
|
50
|
+
| ChatComposerConversationExcerptIntent
|
|
51
|
+
| ChatComposerUiResourceReferenceIntent;
|
|
43
52
|
|
|
44
53
|
type ChatComposerReferenceRequest =
|
|
45
54
|
| Omit<ChatComposerFileReferenceIntent, 'id'>
|
|
46
55
|
| Omit<ChatComposerDirectoryReferenceIntent, 'id'>
|
|
47
56
|
| Omit<ChatComposerExcerptReferenceIntent, 'id'>
|
|
48
|
-
| Omit<ChatComposerConversationExcerptIntent, 'id'
|
|
57
|
+
| Omit<ChatComposerConversationExcerptIntent, 'id'>
|
|
58
|
+
| Omit<ChatComposerUiResourceReferenceIntent, 'id'>;
|
|
49
59
|
|
|
50
60
|
function createExcerptTokenKey(prefix: string, identity: string): string {
|
|
51
61
|
let hash = 2166136261;
|
|
@@ -169,6 +179,23 @@ export class ChatComposerIntentManager {
|
|
|
169
179
|
});
|
|
170
180
|
};
|
|
171
181
|
|
|
182
|
+
requestUiResourceReference = (params: {
|
|
183
|
+
targetSessionKey: string | null;
|
|
184
|
+
reference: ChatUiResourceReference;
|
|
185
|
+
}) => {
|
|
186
|
+
const reference = readChatUiResourceReference(params.reference);
|
|
187
|
+
if (!reference) return;
|
|
188
|
+
const tokenKey = reference.uri.trim();
|
|
189
|
+
const label = reference.title.trim();
|
|
190
|
+
this.publish({
|
|
191
|
+
kind: CHAT_UI_RESOURCE_TOKEN_KIND,
|
|
192
|
+
targetSessionKey: params.targetSessionKey,
|
|
193
|
+
tokenKey,
|
|
194
|
+
label,
|
|
195
|
+
reference,
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
|
|
172
199
|
consumePending = (targetSessionKey: string | null): ChatComposerReferenceIntent | null => {
|
|
173
200
|
if (this.pendingIntent?.targetSessionKey !== targetSessionKey) {
|
|
174
201
|
return null;
|
|
@@ -2,7 +2,7 @@ import { ChatSessionListManager } from '@/features/chat/managers/chat-session-li
|
|
|
2
2
|
import { ChatUiManager } from '@/features/chat/managers/chat-ui.manager';
|
|
3
3
|
import { ChatQueryManager } from '@/features/chat/managers/chat-query.manager';
|
|
4
4
|
import { ChatThreadManager } from '@/features/chat/managers/chat-thread.manager';
|
|
5
|
-
import { ChatComposerIntentManager } from '@/features/chat/managers/chat-composer-intent.manager';
|
|
5
|
+
import type { ChatComposerIntentManager } from '@/features/chat/managers/chat-composer-intent.manager';
|
|
6
6
|
import type { AppPresenter } from '@/app/presenters/app.presenter';
|
|
7
7
|
|
|
8
8
|
export class ChatPresenter {
|
|
@@ -16,7 +16,7 @@ export class ChatPresenter {
|
|
|
16
16
|
this.chatUiManager = new ChatUiManager(appPresenter.docBrowserManager);
|
|
17
17
|
this.chatSessionListManager = new ChatSessionListManager(this.chatUiManager);
|
|
18
18
|
this.chatQueryManager = new ChatQueryManager();
|
|
19
|
-
this.chatComposerIntentManager =
|
|
19
|
+
this.chatComposerIntentManager = appPresenter.chatComposerIntentManager;
|
|
20
20
|
this.chatThreadManager = new ChatThreadManager(
|
|
21
21
|
this.chatUiManager,
|
|
22
22
|
this.chatSessionListManager,
|
|
@@ -134,7 +134,7 @@ describe('PanelAppsList', () => {
|
|
|
134
134
|
|
|
135
135
|
expect(screen.getByText('Demo Panel')).toBeTruthy();
|
|
136
136
|
|
|
137
|
-
await user.click(screen.getByRole('
|
|
137
|
+
await user.click(screen.getByRole('tab', { name: 'Favorites' }));
|
|
138
138
|
|
|
139
139
|
expect(screen.getByText('No panel apps in this view')).toBeTruthy();
|
|
140
140
|
expect(screen.queryByText('Create your first panel app')).toBeNull();
|
|
@@ -14,10 +14,8 @@ import { t } from '@/shared/lib/i18n';
|
|
|
14
14
|
const EMPTY_PANEL_APP_ENTRIES: PanelAppEntryView[] = [];
|
|
15
15
|
|
|
16
16
|
export function PanelAppsList({
|
|
17
|
-
headerContent,
|
|
18
17
|
onOpenPanelApp,
|
|
19
18
|
}: {
|
|
20
|
-
headerContent?: ReactNode;
|
|
21
19
|
onOpenPanelApp: (entry: PanelAppEntryView) => void;
|
|
22
20
|
}) {
|
|
23
21
|
const panelApps = usePanelApps();
|
|
@@ -95,11 +93,8 @@ export function PanelAppsList({
|
|
|
95
93
|
|
|
96
94
|
return (
|
|
97
95
|
<div className="flex h-full min-h-0 flex-col bg-card text-card-foreground">
|
|
98
|
-
<div className="flex items-center justify-
|
|
96
|
+
<div className="flex min-h-12 shrink-0 items-center justify-end gap-2 border-b border-border/70 px-4 py-2">
|
|
99
97
|
<div className="flex min-w-0 items-center gap-1.5">
|
|
100
|
-
{headerContent ?? (
|
|
101
|
-
<div className="truncate text-sm font-semibold text-foreground">{t('panelAppsTitle')}</div>
|
|
102
|
-
)}
|
|
103
98
|
{panelApps.data?.panelsPath ? (
|
|
104
99
|
<TooltipProvider delayDuration={250}>
|
|
105
100
|
<Tooltip>
|
package/src/features/right-panel-resources/utils/__tests__/right-panel-resource-uri.utils.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import type { PanelAppEntryView } from '@/shared/lib/api';
|
|
3
3
|
import {
|
|
4
|
+
createChatUiResourceReferenceFromTab,
|
|
4
5
|
createPanelAppContentPath,
|
|
5
6
|
createPanelAppResourceUri,
|
|
6
7
|
createPanelAppRightPanelResourceTarget,
|
|
@@ -26,6 +27,39 @@ function createPanelAppEntry(overrides: Partial<PanelAppEntryView> = {}): PanelA
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
describe('right-panel-resource uri utils', () => {
|
|
30
|
+
it('creates a stable chat reference for an addressable panel tab', () => {
|
|
31
|
+
expect(createChatUiResourceReferenceFromTab({
|
|
32
|
+
id: 'panel-demo',
|
|
33
|
+
kind: 'panel-app',
|
|
34
|
+
title: 'Demo App',
|
|
35
|
+
currentUrl: '/api/panel-apps/demo/content',
|
|
36
|
+
resourceUri: 'nextclaw://panel-app/demo',
|
|
37
|
+
contentParams: { itemId: 'note-1' },
|
|
38
|
+
history: ['/api/panel-apps/demo/content'],
|
|
39
|
+
historyIndex: 0,
|
|
40
|
+
navVersion: 0,
|
|
41
|
+
})).toEqual({
|
|
42
|
+
uri: 'nextclaw://panel-app/demo',
|
|
43
|
+
resourceKind: 'panel-app',
|
|
44
|
+
title: 'Demo App',
|
|
45
|
+
currentUrl: '/api/panel-apps/demo/content',
|
|
46
|
+
contentParams: { itemId: 'note-1' },
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('does not create chat references for empty browser tabs', () => {
|
|
51
|
+
expect(createChatUiResourceReferenceFromTab({
|
|
52
|
+
id: 'home',
|
|
53
|
+
kind: 'home',
|
|
54
|
+
title: 'Start Page',
|
|
55
|
+
currentUrl: 'nextclaw://new-tab',
|
|
56
|
+
resourceUri: 'nextclaw://new-tab',
|
|
57
|
+
history: ['nextclaw://new-tab'],
|
|
58
|
+
historyIndex: 0,
|
|
59
|
+
navVersion: 0,
|
|
60
|
+
})).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
|
|
29
63
|
it('builds matching external panel app resource and content URLs', () => {
|
|
30
64
|
expect(createPanelAppResourceUri('demo', '/tmp/demo.panel')).toBe(
|
|
31
65
|
'nextclaw://panel-app/demo?path=%2Ftmp%2Fdemo.panel',
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readChatUiResourceReference,
|
|
3
|
+
type ChatUiResourceReference,
|
|
4
|
+
} from '@nextclaw/shared';
|
|
1
5
|
import type { PanelAppEntryView } from '@/shared/lib/api';
|
|
2
6
|
import type { DocBrowserDockIcon } from '@/shared/components/doc-browser';
|
|
7
|
+
import type { DocBrowserTab } from '@/shared/components/doc-browser/doc-browser-context';
|
|
3
8
|
import type { RightPanelResourceTarget } from '@/features/right-panel-resources/types/right-panel-resource.types';
|
|
4
9
|
|
|
5
10
|
export const RIGHT_PANEL_HOME_TAB_KIND = 'home';
|
|
@@ -10,6 +15,30 @@ export const RIGHT_PANEL_PANEL_APPS_URL = `${RIGHT_PANEL_APPS_URL}?tab=panel-app
|
|
|
10
15
|
export const RIGHT_PANEL_SERVICE_APPS_URL = `${RIGHT_PANEL_APPS_URL}?tab=service-apps`;
|
|
11
16
|
export const RIGHT_PANEL_PANEL_APP_TAB_KIND = 'panel-app';
|
|
12
17
|
|
|
18
|
+
const NON_REFERENCEABLE_RESOURCE_URIS = new Set([
|
|
19
|
+
RIGHT_PANEL_HOME_URL,
|
|
20
|
+
'about:blank',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
export function createChatUiResourceReferenceFromTab(
|
|
24
|
+
tab: DocBrowserTab,
|
|
25
|
+
): ChatUiResourceReference | null {
|
|
26
|
+
const uri = tab.resourceUri?.trim() || tab.currentUrl.trim();
|
|
27
|
+
const currentUrl = tab.currentUrl.trim();
|
|
28
|
+
if (!uri || !currentUrl || NON_REFERENCEABLE_RESOURCE_URIS.has(uri)) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return readChatUiResourceReference({
|
|
32
|
+
uri,
|
|
33
|
+
resourceKind: tab.kind,
|
|
34
|
+
title: tab.title.trim() || uri,
|
|
35
|
+
currentUrl,
|
|
36
|
+
...(tab.contentParams
|
|
37
|
+
? { contentParams: structuredClone(tab.contentParams) }
|
|
38
|
+
: {}),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
13
42
|
export type RightPanelAppsTab = 'apps' | 'panel-apps' | 'service-apps';
|
|
14
43
|
|
|
15
44
|
const DEFAULT_RIGHT_PANEL_APPS_TAB: RightPanelAppsTab = 'apps';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState
|
|
1
|
+
import { useState } from "react";
|
|
2
2
|
import type {
|
|
3
3
|
ServiceActionGrantView,
|
|
4
4
|
ServiceActionListView,
|
|
@@ -25,11 +25,7 @@ import { t } from "@/shared/lib/i18n";
|
|
|
25
25
|
|
|
26
26
|
type ServiceActionView = ServiceActionListView["actions"][number];
|
|
27
27
|
|
|
28
|
-
export function ServiceAppsPanel({
|
|
29
|
-
headerContent,
|
|
30
|
-
}: {
|
|
31
|
-
headerContent?: ReactNode;
|
|
32
|
-
}) {
|
|
28
|
+
export function ServiceAppsPanel() {
|
|
33
29
|
const serviceApps = useServiceApps();
|
|
34
30
|
const serviceActions = useServiceActions();
|
|
35
31
|
const serviceActionGrants = useServiceActionGrants();
|
|
@@ -92,17 +88,7 @@ export function ServiceAppsPanel({
|
|
|
92
88
|
|
|
93
89
|
return (
|
|
94
90
|
<div className="flex h-full min-h-0 flex-col bg-card text-card-foreground">
|
|
95
|
-
<div className="flex items-center justify-
|
|
96
|
-
<div className="flex min-w-0 items-center gap-2">
|
|
97
|
-
{headerContent ?? (
|
|
98
|
-
<>
|
|
99
|
-
<Server className="h-4 w-4 text-primary" />
|
|
100
|
-
<div className="truncate text-sm font-semibold text-foreground">
|
|
101
|
-
{t("serviceAppsTitle")}
|
|
102
|
-
</div>
|
|
103
|
-
</>
|
|
104
|
-
)}
|
|
105
|
-
</div>
|
|
91
|
+
<div className="flex min-h-12 shrink-0 items-center justify-end border-b border-border/60 px-4 py-2">
|
|
106
92
|
<TooltipProvider delayDuration={250}>
|
|
107
93
|
<Tooltip>
|
|
108
94
|
<TooltipTrigger asChild>
|
|
@@ -10,6 +10,7 @@ import { isWindowsDesktopHost } from "@/platforms/desktop/utils/desktop-host.uti
|
|
|
10
10
|
import { MobileBottomNav } from "@/platforms/mobile";
|
|
11
11
|
import type { DocBrowserCustomTabRenderers } from "@/shared/components/doc-browser/doc-browser-renderer.types";
|
|
12
12
|
import type { DocBrowserDockControls } from "@/shared/components/doc-browser/doc-browser-context";
|
|
13
|
+
import type { DocBrowserTabMenuGroupsResolver } from "@/shared/components/doc-browser/doc-browser";
|
|
13
14
|
import { cn } from "@/shared/lib/utils";
|
|
14
15
|
import { useViewportLayoutStore } from "@/app/stores/viewport-layout.store";
|
|
15
16
|
import { SIDEBAR_RAIL_WIDTH_PX } from "@/app/components/layout/sidebar-rail.styles";
|
|
@@ -26,6 +27,7 @@ type DesktopAppShellProps = {
|
|
|
26
27
|
docBrowserMode: "floating" | "docked";
|
|
27
28
|
docBrowserDockControls?: DocBrowserDockControls;
|
|
28
29
|
docBrowserRenderers?: DocBrowserCustomTabRenderers;
|
|
30
|
+
docBrowserTabMenuGroups?: DocBrowserTabMenuGroupsResolver;
|
|
29
31
|
sideDock?: React.ReactNode;
|
|
30
32
|
children: React.ReactNode;
|
|
31
33
|
};
|
|
@@ -37,6 +39,7 @@ export function DesktopAppShell({
|
|
|
37
39
|
docBrowserMode,
|
|
38
40
|
docBrowserDockControls,
|
|
39
41
|
docBrowserRenderers = {},
|
|
42
|
+
docBrowserTabMenuGroups,
|
|
40
43
|
sideDock,
|
|
41
44
|
children,
|
|
42
45
|
}: DesktopAppShellProps) {
|
|
@@ -91,6 +94,7 @@ export function DesktopAppShell({
|
|
|
91
94
|
<DocBrowser
|
|
92
95
|
customTabRenderers={docBrowserRenderers}
|
|
93
96
|
dockControls={docBrowserDockControls}
|
|
97
|
+
getTabMenuGroups={docBrowserTabMenuGroups}
|
|
94
98
|
/>
|
|
95
99
|
</Suspense>
|
|
96
100
|
) : null}
|
|
@@ -103,6 +107,7 @@ export function DesktopAppShell({
|
|
|
103
107
|
<DocBrowser
|
|
104
108
|
customTabRenderers={docBrowserRenderers}
|
|
105
109
|
dockControls={docBrowserDockControls}
|
|
110
|
+
getTabMenuGroups={docBrowserTabMenuGroups}
|
|
106
111
|
/>
|
|
107
112
|
</Suspense>
|
|
108
113
|
) : null}
|
|
@@ -7,6 +7,7 @@ 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
9
|
import type { DocBrowserDockControls } from "@/shared/components/doc-browser/doc-browser-context";
|
|
10
|
+
import type { DocBrowserTabMenuGroupsResolver } from "@/shared/components/doc-browser/doc-browser";
|
|
10
11
|
|
|
11
12
|
const DocBrowser = lazy(async () => ({
|
|
12
13
|
default: (await import("@/shared/components/doc-browser/doc-browser")).DocBrowser,
|
|
@@ -17,6 +18,7 @@ type MobileAppShellProps = {
|
|
|
17
18
|
isDocBrowserOpen: boolean;
|
|
18
19
|
docBrowserDockControls?: DocBrowserDockControls;
|
|
19
20
|
docBrowserRenderers?: DocBrowserCustomTabRenderers;
|
|
21
|
+
docBrowserTabMenuGroups?: DocBrowserTabMenuGroupsResolver;
|
|
20
22
|
topbarLeadingInset?: string;
|
|
21
23
|
children: React.ReactNode;
|
|
22
24
|
};
|
|
@@ -26,6 +28,7 @@ export function MobileAppShell({
|
|
|
26
28
|
isDocBrowserOpen,
|
|
27
29
|
docBrowserDockControls,
|
|
28
30
|
docBrowserRenderers = {},
|
|
31
|
+
docBrowserTabMenuGroups,
|
|
29
32
|
topbarLeadingInset,
|
|
30
33
|
children,
|
|
31
34
|
}: MobileAppShellProps) {
|
|
@@ -48,7 +51,12 @@ export function MobileAppShell({
|
|
|
48
51
|
{showBottomNav ? <MobileBottomNav /> : null}
|
|
49
52
|
{isDocBrowserOpen ? (
|
|
50
53
|
<Suspense fallback={null}>
|
|
51
|
-
<DocBrowser
|
|
54
|
+
<DocBrowser
|
|
55
|
+
customTabRenderers={docBrowserRenderers}
|
|
56
|
+
displayMode="fullscreen"
|
|
57
|
+
dockControls={docBrowserDockControls}
|
|
58
|
+
getTabMenuGroups={docBrowserTabMenuGroups}
|
|
59
|
+
/>
|
|
52
60
|
</Suspense>
|
|
53
61
|
) : null}
|
|
54
62
|
</div>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createRef } from "react";
|
|
1
2
|
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
3
|
import { MemoryRouter, Route, Routes } from "react-router-dom";
|
|
3
4
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
@@ -55,4 +56,15 @@ describe("NavigationLink", () => {
|
|
|
55
56
|
expect(screen.getByText("Session route")).toBeTruthy();
|
|
56
57
|
expect(mocks.openExternalUrl).not.toHaveBeenCalled();
|
|
57
58
|
});
|
|
59
|
+
|
|
60
|
+
it("forwards refs to the rendered anchor for asChild consumers", () => {
|
|
61
|
+
const ref = createRef<HTMLAnchorElement>();
|
|
62
|
+
render(
|
|
63
|
+
<NavigationLink ref={ref} href="https://docs.nextclaw.io/" external>
|
|
64
|
+
View docs
|
|
65
|
+
</NavigationLink>,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(ref.current).toBe(screen.getByRole("link", { name: "View docs" }));
|
|
69
|
+
});
|
|
58
70
|
});
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
MouseEvent,
|
|
4
4
|
ReactNode,
|
|
5
5
|
} from "react";
|
|
6
|
+
import { forwardRef } from "react";
|
|
6
7
|
import { ExternalLink as ExternalLinkIcon, type LucideIcon } from "lucide-react";
|
|
7
8
|
import { Link } from "react-router-dom";
|
|
8
9
|
import { hostCapabilityManager } from "@/shared/lib/host-capabilities";
|
|
@@ -62,7 +63,7 @@ function resolveExternalHttpUrl(href: string): string | null {
|
|
|
62
63
|
* Internal links use client-side routing; external links delegate ordinary
|
|
63
64
|
* clicks to the desktop host when one is available.
|
|
64
65
|
*/
|
|
65
|
-
export function NavigationLink({
|
|
66
|
+
export const NavigationLink = forwardRef<HTMLAnchorElement, NavigationLinkProps>(function NavigationLink({
|
|
66
67
|
children,
|
|
67
68
|
className,
|
|
68
69
|
external = false,
|
|
@@ -74,7 +75,7 @@ export function NavigationLink({
|
|
|
74
75
|
size = "sm",
|
|
75
76
|
target,
|
|
76
77
|
...props
|
|
77
|
-
}
|
|
78
|
+
}, ref) {
|
|
78
79
|
const Icon = icon === undefined && external ? ExternalLinkIcon : icon;
|
|
79
80
|
const linkClassName = cn(
|
|
80
81
|
"inline-flex items-center rounded-sm font-medium text-primary underline-offset-4 transition-colors hover:text-primary-hover hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/25",
|
|
@@ -98,6 +99,7 @@ export function NavigationLink({
|
|
|
98
99
|
if (!external) {
|
|
99
100
|
return (
|
|
100
101
|
<Link
|
|
102
|
+
ref={ref}
|
|
101
103
|
{...props}
|
|
102
104
|
to={href}
|
|
103
105
|
target={target}
|
|
@@ -112,6 +114,7 @@ export function NavigationLink({
|
|
|
112
114
|
|
|
113
115
|
return (
|
|
114
116
|
<a
|
|
117
|
+
ref={ref}
|
|
115
118
|
{...props}
|
|
116
119
|
href={href}
|
|
117
120
|
target={target ?? "_blank"}
|
|
@@ -133,4 +136,4 @@ export function NavigationLink({
|
|
|
133
136
|
{content}
|
|
134
137
|
</a>
|
|
135
138
|
);
|
|
136
|
-
}
|
|
139
|
+
});
|
|
@@ -571,6 +571,40 @@ describe("DocBrowser floating interactions", () => {
|
|
|
571
571
|
expect(onDragStart).toHaveBeenCalledTimes(1);
|
|
572
572
|
});
|
|
573
573
|
|
|
574
|
+
it("opens the same tab action from right click and the more button", () => {
|
|
575
|
+
const onAddToChat = vi.fn();
|
|
576
|
+
|
|
577
|
+
render(
|
|
578
|
+
<DocBrowserTabStrip
|
|
579
|
+
tabs={docBrowserState.tabs}
|
|
580
|
+
activeTabId="docs"
|
|
581
|
+
canGoBack={false}
|
|
582
|
+
canGoForward={false}
|
|
583
|
+
isDocked={true}
|
|
584
|
+
isFullscreen={false}
|
|
585
|
+
onGoBack={vi.fn()}
|
|
586
|
+
onGoForward={vi.fn()}
|
|
587
|
+
onOpenNewTab={vi.fn()}
|
|
588
|
+
onSetActiveTab={vi.fn()}
|
|
589
|
+
onCloseTab={vi.fn()}
|
|
590
|
+
onClose={vi.fn()}
|
|
591
|
+
onDragStart={vi.fn()}
|
|
592
|
+
onToggleMode={vi.fn()}
|
|
593
|
+
getTabMenuGroups={() => [{
|
|
594
|
+
key: "chat",
|
|
595
|
+
items: [{ key: "add", label: "Add to Chat", onSelect: onAddToChat }],
|
|
596
|
+
}]}
|
|
597
|
+
/>,
|
|
598
|
+
);
|
|
599
|
+
|
|
600
|
+
fireEvent.contextMenu(screen.getByRole("button", { name: "Docs" }));
|
|
601
|
+
fireEvent.click(screen.getByRole("menuitem", { name: "Add to Chat" }));
|
|
602
|
+
fireEvent.click(screen.getByRole("button", { name: "More tab actions" }));
|
|
603
|
+
fireEvent.click(screen.getByRole("menuitem", { name: "Add to Chat" }));
|
|
604
|
+
|
|
605
|
+
expect(onAddToChat).toHaveBeenCalledTimes(2);
|
|
606
|
+
});
|
|
607
|
+
|
|
574
608
|
it("keeps the floating panel left edge stable when resizing from the right", () => {
|
|
575
609
|
docBrowserState.mode = "floating";
|
|
576
610
|
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
X,
|
|
11
11
|
} from "lucide-react";
|
|
12
12
|
import type { DocBrowserDockState, DocBrowserTab } from "./doc-browser-context";
|
|
13
|
+
import type { ContextMenuGroup } from "@/shared/components/ui/context-menu/context-menu";
|
|
13
14
|
import {
|
|
14
15
|
CompactTabStrip,
|
|
15
16
|
type CompactTabStripAction,
|
|
@@ -35,6 +36,7 @@ type DocBrowserTabStripProps = {
|
|
|
35
36
|
onClose: () => void;
|
|
36
37
|
onDragStart: (event: PointerEvent<HTMLElement>) => void;
|
|
37
38
|
onToggleMode: () => void;
|
|
39
|
+
getTabMenuGroups?: (tab: DocBrowserTab) => readonly ContextMenuGroup[] | undefined;
|
|
38
40
|
};
|
|
39
41
|
|
|
40
42
|
function shouldBlockHeaderDrag(target: EventTarget | null): boolean {
|
|
@@ -60,6 +62,7 @@ export function DocBrowserTabStrip({
|
|
|
60
62
|
onClose,
|
|
61
63
|
onDragStart,
|
|
62
64
|
onToggleMode,
|
|
65
|
+
getTabMenuGroups,
|
|
63
66
|
}: DocBrowserTabStripProps) {
|
|
64
67
|
const backLabel = t("docBrowserBack");
|
|
65
68
|
const closeLabel = t("docBrowserClose");
|
|
@@ -81,6 +84,8 @@ export function DocBrowserTabStrip({
|
|
|
81
84
|
closePlacement: "trailing",
|
|
82
85
|
onSelect: () => onSetActiveTab(tab.id),
|
|
83
86
|
onClose: () => onCloseTab(tab.id),
|
|
87
|
+
menuLabel: t("docBrowserTabMoreActions"),
|
|
88
|
+
menuGroups: getTabMenuGroups?.(tab),
|
|
84
89
|
}));
|
|
85
90
|
const actions: CompactTabStripAction[] = [
|
|
86
91
|
{ key: "back", disabled: !canGoBack, icon: <ArrowLeft className="h-3.5 w-3.5" />, label: backLabel, onClick: onGoBack },
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
import { ResizableRightPanel } from '@/shared/components/resizable-right-panel/resizable-right-panel';
|
|
29
29
|
import { cn } from '@/shared/lib/utils';
|
|
30
30
|
import { t } from '@/shared/lib/i18n';
|
|
31
|
+
import type { ContextMenuGroup } from '@/shared/components/ui/context-menu/context-menu';
|
|
31
32
|
import { GripVertical } from 'lucide-react';
|
|
32
33
|
import {
|
|
33
34
|
DOC_BROWSER_DOCKED_MAX_WIDTH,
|
|
@@ -38,8 +39,11 @@ type DocBrowserProps = {
|
|
|
38
39
|
customTabRenderers?: DocBrowserCustomTabRenderers;
|
|
39
40
|
displayMode?: 'desktop' | 'fullscreen';
|
|
40
41
|
dockControls?: DocBrowserDockControls;
|
|
42
|
+
getTabMenuGroups?: (tab: DocBrowserTab) => readonly ContextMenuGroup[] | undefined;
|
|
41
43
|
};
|
|
42
44
|
|
|
45
|
+
export type DocBrowserTabMenuGroupsResolver = NonNullable<DocBrowserProps['getTabMenuGroups']>;
|
|
46
|
+
|
|
43
47
|
type FloatingPanelInteraction = {
|
|
44
48
|
startX: number;
|
|
45
49
|
startY: number;
|
|
@@ -160,7 +164,12 @@ function useDocBrowserAddressBar({
|
|
|
160
164
|
};
|
|
161
165
|
}
|
|
162
166
|
|
|
163
|
-
export function DocBrowser({
|
|
167
|
+
export function DocBrowser({
|
|
168
|
+
customTabRenderers = {},
|
|
169
|
+
displayMode = 'desktop',
|
|
170
|
+
dockControls,
|
|
171
|
+
getTabMenuGroups,
|
|
172
|
+
}: DocBrowserProps) {
|
|
164
173
|
const {
|
|
165
174
|
isOpen,
|
|
166
175
|
mode,
|
|
@@ -367,6 +376,7 @@ export function DocBrowser({ customTabRenderers = {}, displayMode = 'desktop', d
|
|
|
367
376
|
onClose={close}
|
|
368
377
|
onDragStart={startFloatDrag}
|
|
369
378
|
onToggleMode={toggleMode}
|
|
379
|
+
getTabMenuGroups={getTabMenuGroups}
|
|
370
380
|
/>
|
|
371
381
|
|
|
372
382
|
<DocBrowserAddressToolbar
|
|
@@ -267,6 +267,19 @@ export class DocBrowserManager {
|
|
|
267
267
|
this.onRightPanelOpened?.();
|
|
268
268
|
};
|
|
269
269
|
|
|
270
|
+
readonly reloadByDedupeKeys = (dedupeKeys: readonly string[]): void => {
|
|
271
|
+
const targets = new Set(dedupeKeys);
|
|
272
|
+
if (targets.size === 0) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
this.setSnapshot((prev) => ({
|
|
276
|
+
...prev,
|
|
277
|
+
tabs: prev.tabs.map((tab) => targets.has(tab.dedupeKey ?? "")
|
|
278
|
+
? { ...tab, navVersion: tab.navVersion + 1 }
|
|
279
|
+
: tab),
|
|
280
|
+
}));
|
|
281
|
+
};
|
|
282
|
+
|
|
270
283
|
readonly openNewTab = (): void => {
|
|
271
284
|
this.open(undefined, { kind: DOC_BROWSER_HOME_TAB_KIND, newTab: true });
|
|
272
285
|
};
|
|
@@ -39,8 +39,29 @@ export function Tabs({ defaultValue: _defaultValue, value, onValueChange, childr
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export function TabsList({ children, className }: TabsListProps) {
|
|
42
|
+
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
43
|
+
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
|
|
44
|
+
const triggers = Array.from(
|
|
45
|
+
event.currentTarget.querySelectorAll<HTMLButtonElement>('[role="tab"]:not(:disabled)'),
|
|
46
|
+
);
|
|
47
|
+
if (triggers.length === 0) return;
|
|
48
|
+
const currentIndex = triggers.indexOf(document.activeElement as HTMLButtonElement);
|
|
49
|
+
const nextIndex = event.key === 'Home'
|
|
50
|
+
? 0
|
|
51
|
+
: event.key === 'End'
|
|
52
|
+
? triggers.length - 1
|
|
53
|
+
: event.key === 'ArrowRight'
|
|
54
|
+
? (Math.max(currentIndex, -1) + 1) % triggers.length
|
|
55
|
+
: (currentIndex <= 0 ? triggers.length : currentIndex) - 1;
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
triggers[nextIndex]?.focus();
|
|
58
|
+
triggers[nextIndex]?.click();
|
|
59
|
+
};
|
|
60
|
+
|
|
42
61
|
return (
|
|
43
62
|
<div
|
|
63
|
+
role="tablist"
|
|
64
|
+
onKeyDown={handleKeyDown}
|
|
44
65
|
className={cn(
|
|
45
66
|
'inline-flex h-9 items-center justify-center rounded-xl bg-muted p-1 text-muted-foreground',
|
|
46
67
|
className
|
|
@@ -61,7 +82,9 @@ export function TabsTrigger({ value, children, className }: TabsTriggerProps) {
|
|
|
61
82
|
<button
|
|
62
83
|
type="button"
|
|
63
84
|
onClick={() => context.onValueChange(value)}
|
|
64
|
-
|
|
85
|
+
role="tab"
|
|
86
|
+
aria-selected={isActive}
|
|
87
|
+
tabIndex={isActive ? 0 : -1}
|
|
65
88
|
data-state={isActive ? 'active' : 'inactive'}
|
|
66
89
|
className={cn(
|
|
67
90
|
'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-[13px] font-medium transition-all duration-fast focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border disabled:pointer-events-none disabled:opacity-50',
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { unwatchServerPaths, watchServerPaths } from '@/shared/lib/api';
|
|
3
|
+
|
|
4
|
+
const SERVER_PATH_WATCH_HEARTBEAT_MS = 2 * 60_000;
|
|
5
|
+
|
|
6
|
+
export function useServerPathWatch(directories: readonly string[]): void {
|
|
7
|
+
const signature = directories.join('\n');
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const watchedDirectories = signature.split('\n').filter(Boolean);
|
|
11
|
+
if (watchedDirectories.length === 0) return;
|
|
12
|
+
let disposed = false;
|
|
13
|
+
let subscriptionId: string | null = null;
|
|
14
|
+
let heartbeat: ReturnType<typeof setInterval> | null = null;
|
|
15
|
+
|
|
16
|
+
const subscribe = async () => {
|
|
17
|
+
try {
|
|
18
|
+
const result = await watchServerPaths({
|
|
19
|
+
directories: watchedDirectories,
|
|
20
|
+
subscriptionId,
|
|
21
|
+
});
|
|
22
|
+
if (disposed) {
|
|
23
|
+
await unwatchServerPaths(result.subscriptionId).catch(() => undefined);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
subscriptionId = result.subscriptionId;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.warn('Project file auto-refresh is unavailable; manual refresh remains available.', error);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
void subscribe().then(() => {
|
|
33
|
+
if (!disposed && subscriptionId) heartbeat = setInterval(() => void subscribe(), SERVER_PATH_WATCH_HEARTBEAT_MS);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return () => {
|
|
37
|
+
disposed = true;
|
|
38
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
39
|
+
if (subscriptionId) void unwatchServerPaths(subscriptionId).catch(() => undefined);
|
|
40
|
+
};
|
|
41
|
+
}, [signature]);
|
|
42
|
+
}
|
|
@@ -21,6 +21,16 @@ export type ServerPathBrowseView = {
|
|
|
21
21
|
locations: ServerPathLocationView[];
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
+
export type ServerPathWatchRequest = {
|
|
25
|
+
subscriptionId?: string | null;
|
|
26
|
+
directories: string[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type ServerPathWatchView = {
|
|
30
|
+
subscriptionId: string;
|
|
31
|
+
watchedDirectories: string[];
|
|
32
|
+
};
|
|
33
|
+
|
|
24
34
|
export type ServerPathSearchEntryView = {
|
|
25
35
|
name: string;
|
|
26
36
|
path: string;
|
|
@@ -449,6 +449,8 @@ export type {
|
|
|
449
449
|
ServerPathReadView,
|
|
450
450
|
ServerPathSearchEntryView,
|
|
451
451
|
ServerPathSearchView,
|
|
452
|
+
ServerPathWatchRequest,
|
|
453
|
+
ServerPathWatchView,
|
|
452
454
|
} from './server-path/server-path.types';
|
|
453
455
|
|
|
454
456
|
export type PanelAppEntryView = {
|
|
@@ -826,6 +828,7 @@ export type ConfigActionExecuteResult = {
|
|
|
826
828
|
// WebSocket events
|
|
827
829
|
export type WsEvent =
|
|
828
830
|
| { type: 'config.updated'; payload: { path: string } }
|
|
831
|
+
| { type: 'server-path.changed'; payload: { directoryPath: string } }
|
|
829
832
|
| {
|
|
830
833
|
type: 'channel.config.apply-status';
|
|
831
834
|
payload: {
|
|
@@ -12,6 +12,8 @@ import type {
|
|
|
12
12
|
ServerPathFilesUploadView,
|
|
13
13
|
ServerPathReadView,
|
|
14
14
|
ServerPathSearchView,
|
|
15
|
+
ServerPathWatchRequest,
|
|
16
|
+
ServerPathWatchView,
|
|
15
17
|
} from '@/shared/lib/api/types';
|
|
16
18
|
|
|
17
19
|
const SERVER_PATH_CONTENT_BASE_PATH = '/api/server-paths/content';
|
|
@@ -24,6 +26,14 @@ export async function fetchServerPathBrowse(params?: {
|
|
|
24
26
|
return await nextclawClient.serverPaths.browse(params);
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
export async function watchServerPaths(input: ServerPathWatchRequest): Promise<ServerPathWatchView> {
|
|
30
|
+
return await nextclawClient.serverPaths.watch(input);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function unwatchServerPaths(subscriptionId: string): Promise<void> {
|
|
34
|
+
await nextclawClient.serverPaths.unwatch(subscriptionId);
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
export async function createServerPathDirectory(
|
|
28
38
|
input: ServerPathDirectoryCreateRequest,
|
|
29
39
|
): Promise<ServerPathDirectoryCreateView> {
|