@vertesia/ui 1.5.0-dev.20260901.045442Z → 1.5.0-dev.20260905.011013Z
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/lib/features/agent/chat/AgentRightPanel.d.ts +2 -3
- package/lib/features/agent/chat/AgentRightPanel.d.ts.map +1 -1
- package/lib/features/agent/chat/AgentRightPanel.js +3 -3
- package/lib/features/agent/chat/AgentRightPanel.js.map +1 -1
- package/lib/features/agent/chat/DocumentPanel.d.ts +7 -7
- package/lib/features/agent/chat/DocumentPanel.d.ts.map +1 -1
- package/lib/features/agent/chat/DocumentPanel.js +6 -12
- package/lib/features/agent/chat/DocumentPanel.js.map +1 -1
- package/lib/features/agent/chat/DocumentsTab.d.ts +15 -0
- package/lib/features/agent/chat/DocumentsTab.d.ts.map +1 -0
- package/lib/features/agent/chat/DocumentsTab.js +33 -0
- package/lib/features/agent/chat/DocumentsTab.js.map +1 -0
- package/lib/features/agent/chat/ModernAgentConversation.js +2 -2
- package/lib/features/agent/chat/ModernAgentConversation.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/Header.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/Header.js +15 -10
- package/lib/features/agent/chat/ModernAgentOutput/Header.js.map +1 -1
- package/lib/features/agent/chat/hooks/useDocumentPanel.d.ts +2 -2
- package/lib/features/agent/chat/hooks/useDocumentPanel.d.ts.map +1 -1
- package/lib/features/agent/chat/hooks/useDocumentPanel.js +0 -18
- package/lib/features/agent/chat/hooks/useDocumentPanel.js.map +1 -1
- package/lib/features/oauth/McpConnectionsButton.d.ts.map +1 -1
- package/lib/features/oauth/McpConnectionsButton.js +4 -1
- package/lib/features/oauth/McpConnectionsButton.js.map +1 -1
- package/lib/features/oauth/McpConnectionsDialog.d.ts.map +1 -1
- package/lib/features/oauth/McpConnectionsDialog.js +3 -2
- package/lib/features/oauth/McpConnectionsDialog.js.map +1 -1
- package/lib/features/oauth/useMcpConnections.d.ts +6 -7
- package/lib/features/oauth/useMcpConnections.d.ts.map +1 -1
- package/lib/features/oauth/useMcpConnections.js +27 -20
- package/lib/features/oauth/useMcpConnections.js.map +1 -1
- package/lib/features/store/types/TypeRegistry.d.ts +10 -5
- package/lib/features/store/types/TypeRegistry.d.ts.map +1 -1
- package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts +10 -5
- package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts.map +1 -1
- package/lib/i18n/locales/ar.json +10 -0
- package/lib/i18n/locales/de.json +6 -0
- package/lib/i18n/locales/en.json +6 -0
- package/lib/i18n/locales/es.json +6 -0
- package/lib/i18n/locales/fr.json +6 -0
- package/lib/i18n/locales/it.json +6 -0
- package/lib/i18n/locales/ja.json +6 -0
- package/lib/i18n/locales/ko.json +6 -0
- package/lib/i18n/locales/pt.json +6 -0
- package/lib/i18n/locales/ru.json +8 -0
- package/lib/i18n/locales/tr.json +6 -0
- package/lib/i18n/locales/zh-TW.json +6 -0
- package/lib/i18n/locales/zh.json +6 -0
- package/lib/router/NestedNavigationContext.d.ts +3 -1
- package/lib/router/NestedNavigationContext.d.ts.map +1 -1
- package/lib/router/NestedNavigationContext.js +2 -2
- package/lib/router/NestedNavigationContext.js.map +1 -1
- package/lib/vertesia-ui-features.js +14 -14
- package/lib/vertesia-ui-features.js.map +1 -1
- package/lib/vertesia-ui-i18n.3yHWompP.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.3yHWompP.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.4XKCcnL0.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.4XKCcnL0.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.78KG4J25.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.78KG4J25.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.BN06mpp1.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.BN06mpp1.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.CSzKLlHJ.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.CSzKLlHJ.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.Ccus7aRh.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.Ccus7aRh.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.D9j3c6gI.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.D9j3c6gI.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.DAUg4rrI.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.DAUg4rrI.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.DSxVZKXy.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.DSxVZKXy.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.aahVhHvR.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.aahVhHvR.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.js +2 -2
- package/lib/vertesia-ui-i18n.nZ3xJ26v.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.nZ3xJ26v.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.xPYbuFzT.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.xPYbuFzT.chunk.js.map +1 -0
- package/lib/vertesia-ui-router.js +1 -1
- package/lib/vertesia-ui-router.js.map +1 -1
- package/package.json +7 -7
- package/src/features/agent/chat/AgentRightPanel.tsx +4 -8
- package/src/features/agent/chat/DocumentPanel.tsx +39 -48
- package/src/features/agent/chat/DocumentsTab.test.tsx +87 -0
- package/src/features/agent/chat/DocumentsTab.tsx +124 -0
- package/src/features/agent/chat/ModernAgentConversation.test.tsx +0 -1
- package/src/features/agent/chat/ModernAgentConversation.tsx +0 -2
- package/src/features/agent/chat/ModernAgentOutput/Header.tsx +27 -14
- package/src/features/agent/chat/hooks/useDocumentPanel.test.tsx +33 -0
- package/src/features/agent/chat/hooks/useDocumentPanel.ts +3 -20
- package/src/features/oauth/McpConnectionsButton.test.tsx +79 -8
- package/src/features/oauth/McpConnectionsButton.tsx +6 -3
- package/src/features/oauth/McpConnectionsDialog.tsx +9 -3
- package/src/features/oauth/useMcpConnections.ts +33 -27
- package/src/i18n/locales/ar.json +10 -0
- package/src/i18n/locales/de.json +6 -0
- package/src/i18n/locales/en.json +6 -0
- package/src/i18n/locales/es.json +6 -0
- package/src/i18n/locales/fr.json +6 -0
- package/src/i18n/locales/it.json +6 -0
- package/src/i18n/locales/ja.json +6 -0
- package/src/i18n/locales/ko.json +6 -0
- package/src/i18n/locales/pt.json +6 -0
- package/src/i18n/locales/ru.json +8 -0
- package/src/i18n/locales/tr.json +6 -0
- package/src/i18n/locales/zh-TW.json +6 -0
- package/src/i18n/locales/zh.json +6 -0
- package/src/router/NestedNavigationContext.test.tsx +54 -0
- package/src/router/NestedNavigationContext.tsx +9 -2
- package/lib/features/agent/chat/DocumentTabBar.d.ts +0 -9
- package/lib/features/agent/chat/DocumentTabBar.d.ts.map +0 -1
- package/lib/features/agent/chat/DocumentTabBar.js +0 -9
- package/lib/features/agent/chat/DocumentTabBar.js.map +0 -1
- package/lib/vertesia-ui-i18n.BJBGsq-8.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BJBGsq-8.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.BY5YG5cX.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BY5YG5cX.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.Bd5_1BXO.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.Bd5_1BXO.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.Bvwej2HH.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.Bvwej2HH.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.CMT3f4gf.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.CMT3f4gf.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.D56DNPJW.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.D56DNPJW.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DDzKLNx6.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DDzKLNx6.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.D_Am1LpF.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.D_Am1LpF.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DrZizgGV.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DrZizgGV.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DwHsq86z.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DwHsq86z.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.PLpo0Bbz.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.PLpo0Bbz.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.xVbXALqz.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.xVbXALqz.chunk.js.map +0 -1
- package/src/features/agent/chat/DocumentTabBar.tsx +0 -41
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Button, Input, VTooltip } from '@vertesia/ui/core';
|
|
2
|
+
import { useUITranslation } from '@vertesia/ui/i18n';
|
|
3
|
+
import { NavLink } from '@vertesia/ui/router';
|
|
4
|
+
import { ExternalLinkIcon, FileTextIcon } from 'lucide-react';
|
|
5
|
+
import React, { useCallback, useId, useMemo, useState } from 'react';
|
|
6
|
+
import { DocumentPanel } from './DocumentPanel.js';
|
|
7
|
+
import type { OpenDocument } from './types/document.js';
|
|
8
|
+
|
|
9
|
+
/** Below this many documents the list is short enough that a filter box is just noise. */
|
|
10
|
+
const FILTER_VISIBILITY_THRESHOLD = 5;
|
|
11
|
+
|
|
12
|
+
interface DocumentsTabProps {
|
|
13
|
+
documents: OpenDocument[];
|
|
14
|
+
/** Document shown in the detail view; `null` shows the list. */
|
|
15
|
+
activeDocumentId: string | null;
|
|
16
|
+
onSelectDocument: (id: string | null) => void;
|
|
17
|
+
onUpdateDocumentTitle?: (id: string, title: string) => void;
|
|
18
|
+
refreshKey: number;
|
|
19
|
+
runId?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function DocumentsTabComponent({
|
|
23
|
+
documents,
|
|
24
|
+
activeDocumentId,
|
|
25
|
+
onSelectDocument,
|
|
26
|
+
onUpdateDocumentTitle,
|
|
27
|
+
refreshKey,
|
|
28
|
+
runId,
|
|
29
|
+
}: DocumentsTabProps) {
|
|
30
|
+
const { t } = useUITranslation();
|
|
31
|
+
const [filterValue, setFilterValue] = useState('');
|
|
32
|
+
const filterInputId = useId();
|
|
33
|
+
|
|
34
|
+
const normalizedFilterValue = filterValue.trim().toLocaleLowerCase();
|
|
35
|
+
const filteredDocuments = useMemo(() => {
|
|
36
|
+
if (!normalizedFilterValue) return documents;
|
|
37
|
+
return documents.filter((doc) => doc.title.toLocaleLowerCase().includes(normalizedFilterValue));
|
|
38
|
+
}, [documents, normalizedFilterValue]);
|
|
39
|
+
|
|
40
|
+
const handleBack = useCallback(() => onSelectDocument(null), [onSelectDocument]);
|
|
41
|
+
|
|
42
|
+
const activeDocument = activeDocumentId ? documents.find((doc) => doc.id === activeDocumentId) : undefined;
|
|
43
|
+
|
|
44
|
+
if (activeDocumentId) {
|
|
45
|
+
return (
|
|
46
|
+
<DocumentPanel
|
|
47
|
+
documentId={activeDocumentId}
|
|
48
|
+
title={activeDocument?.title}
|
|
49
|
+
onBack={handleBack}
|
|
50
|
+
onUpdateDocumentTitle={onUpdateDocumentTitle}
|
|
51
|
+
refreshKey={refreshKey}
|
|
52
|
+
runId={runId}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="flex flex-col h-full">
|
|
59
|
+
{/* Top bar */}
|
|
60
|
+
<div className="flex shrink-0 flex-col gap-2 border-b px-3 py-2">
|
|
61
|
+
<span className="text-xs text-muted">
|
|
62
|
+
{normalizedFilterValue
|
|
63
|
+
? t('agent.documentsFiltered', {
|
|
64
|
+
visible: filteredDocuments.length,
|
|
65
|
+
total: documents.length,
|
|
66
|
+
})
|
|
67
|
+
: t('agent.documentCount', { count: documents.length })}
|
|
68
|
+
</span>
|
|
69
|
+
{documents.length > FILTER_VISIBILITY_THRESHOLD && (
|
|
70
|
+
<>
|
|
71
|
+
<label htmlFor={filterInputId} className="sr-only">
|
|
72
|
+
{t('form.filter')}
|
|
73
|
+
</label>
|
|
74
|
+
<Input
|
|
75
|
+
id={filterInputId}
|
|
76
|
+
type="text"
|
|
77
|
+
role="searchbox"
|
|
78
|
+
autoComplete="off"
|
|
79
|
+
placeholder={t('store.searchPlaceholder')}
|
|
80
|
+
value={filterValue}
|
|
81
|
+
onChange={setFilterValue}
|
|
82
|
+
clearable={true}
|
|
83
|
+
/>
|
|
84
|
+
</>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* List */}
|
|
89
|
+
<div className="flex-1 overflow-y-auto px-3 py-2">
|
|
90
|
+
{filteredDocuments.length > 0 ? (
|
|
91
|
+
<div className="min-w-0">
|
|
92
|
+
{filteredDocuments.map((doc) => (
|
|
93
|
+
<div key={doc.id} className="flex min-w-0 items-center gap-1">
|
|
94
|
+
<Button
|
|
95
|
+
variant="unstyled"
|
|
96
|
+
className="flex min-w-0 flex-1 items-center justify-start gap-1.5 rounded px-1 py-1 text-start text-sm hover:bg-muted/30"
|
|
97
|
+
onClick={() => onSelectDocument(doc.id)}
|
|
98
|
+
title={doc.title}
|
|
99
|
+
>
|
|
100
|
+
<FileTextIcon className="size-4 shrink-0 text-muted" />
|
|
101
|
+
<span className="min-w-0 truncate">{doc.title}</span>
|
|
102
|
+
</Button>
|
|
103
|
+
<NavLink
|
|
104
|
+
href={`/store/objects/${doc.id}#overview`}
|
|
105
|
+
topLevelNav
|
|
106
|
+
className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-muted hover:bg-muted/20 hover:text-foreground"
|
|
107
|
+
>
|
|
108
|
+
<VTooltip description={t('agent.openDocument')} placement="top" size="xs" asChild>
|
|
109
|
+
<ExternalLinkIcon className="size-3.5" aria-hidden="true" />
|
|
110
|
+
</VTooltip>
|
|
111
|
+
<span className="sr-only">{t('agent.openDocument')}</span>
|
|
112
|
+
</NavLink>
|
|
113
|
+
</div>
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
116
|
+
) : (
|
|
117
|
+
<div className="px-1 py-6 text-sm text-muted">{t('agent.noMatchingDocuments')}</div>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const DocumentsTab = React.memo(DocumentsTabComponent);
|
|
@@ -282,7 +282,6 @@ describe('ModernAgentConversation send handling', () => {
|
|
|
282
282
|
isDocPanelOpen: false,
|
|
283
283
|
docRefreshKey: 0,
|
|
284
284
|
closeDocPanel: vi.fn(),
|
|
285
|
-
closeDocument: vi.fn(),
|
|
286
285
|
selectDocument: vi.fn(),
|
|
287
286
|
openDocInPanel: vi.fn(),
|
|
288
287
|
updateDocumentTitle: vi.fn(),
|
|
@@ -1561,7 +1561,6 @@ function ModernAgentConversationInner({
|
|
|
1561
1561
|
isDocPanelOpen,
|
|
1562
1562
|
docRefreshKey,
|
|
1563
1563
|
closeDocPanel: handleCloseDocPanel,
|
|
1564
|
-
closeDocument: handleCloseDocument,
|
|
1565
1564
|
selectDocument,
|
|
1566
1565
|
openDocInPanel,
|
|
1567
1566
|
updateDocumentTitle,
|
|
@@ -3062,7 +3061,6 @@ function ModernAgentConversationInner({
|
|
|
3062
3061
|
openDocuments={openDocuments}
|
|
3063
3062
|
activeDocumentId={activeDocumentId}
|
|
3064
3063
|
onSelectDocument={selectDocument}
|
|
3065
|
-
onCloseDocument={handleCloseDocument}
|
|
3066
3064
|
onUpdateDocumentTitle={updateDocumentTitle}
|
|
3067
3065
|
docRefreshKey={docRefreshKey}
|
|
3068
3066
|
runId={agentRunId}
|
|
@@ -65,6 +65,19 @@ export interface HeaderProps {
|
|
|
65
65
|
className?: string;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Opens an internal route in a new browser tab, carrying the active tenant sticky params (a/p) so
|
|
70
|
+
* the new tab keeps the current account/project; absolute/external URLs are left untouched.
|
|
71
|
+
*/
|
|
72
|
+
function useOpenInNewTab() {
|
|
73
|
+
const { router } = useRouterContext();
|
|
74
|
+
return (url: string) => {
|
|
75
|
+
const href = url.startsWith('/') ? router.getTopRouter().navigator.addStickyParams(url) : url;
|
|
76
|
+
window.open(href, '_blank', 'noopener,noreferrer');
|
|
77
|
+
return href;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
68
81
|
export default function Header({
|
|
69
82
|
title,
|
|
70
83
|
variant = 'full',
|
|
@@ -93,6 +106,7 @@ export default function Header({
|
|
|
93
106
|
className,
|
|
94
107
|
}: HeaderProps) {
|
|
95
108
|
const { t } = useUITranslation();
|
|
109
|
+
const openInNewTab = useOpenInNewTab();
|
|
96
110
|
const isCompact = variant === 'compact';
|
|
97
111
|
const nextViewMode: AgentConversationViewMode = viewMode === 'sliding' ? 'stacked' : 'sliding';
|
|
98
112
|
const verboseLabel = t('agent.verbose');
|
|
@@ -195,6 +209,19 @@ export default function Header({
|
|
|
195
209
|
</Button>
|
|
196
210
|
)}
|
|
197
211
|
|
|
212
|
+
{agentRunId && (
|
|
213
|
+
<Button
|
|
214
|
+
type="button"
|
|
215
|
+
size={isCompact ? 'icon' : 'xs'}
|
|
216
|
+
variant="ghost"
|
|
217
|
+
onClick={() => openInNewTab(`/store/agent-runner/${agentRunId}`)}
|
|
218
|
+
title={t('agent.openInNewTab')}
|
|
219
|
+
className={cn('transition-all duration-200 rounded-md', isCompact && 'size-8 rounded-lg')}
|
|
220
|
+
>
|
|
221
|
+
<ExternalLink className="size-4" />
|
|
222
|
+
</Button>
|
|
223
|
+
)}
|
|
224
|
+
|
|
198
225
|
{resetWorkflow && (
|
|
199
226
|
<Button
|
|
200
227
|
type="button"
|
|
@@ -316,7 +343,6 @@ function MoreDropdown({
|
|
|
316
343
|
const toast = useToast();
|
|
317
344
|
const { client } = useUserSession();
|
|
318
345
|
const builder = usePayloadBuilder();
|
|
319
|
-
const { router } = useRouterContext();
|
|
320
346
|
|
|
321
347
|
const cancelWorkflow = async () => {
|
|
322
348
|
try {
|
|
@@ -360,14 +386,6 @@ function MoreDropdown({
|
|
|
360
386
|
}
|
|
361
387
|
};
|
|
362
388
|
|
|
363
|
-
const openUrl = (url: string) => {
|
|
364
|
-
// Carry the active tenant sticky params (a/p) on internal routes so the new tab keeps the
|
|
365
|
-
// current account/project; leave absolute/external URLs untouched.
|
|
366
|
-
const href = url.startsWith('/') ? router.getTopRouter().navigator.addStickyParams(url) : url;
|
|
367
|
-
window.open(href, '_blank');
|
|
368
|
-
return href;
|
|
369
|
-
};
|
|
370
|
-
|
|
371
389
|
const copyAgentRunId = () => {
|
|
372
390
|
navigator.clipboard.writeText(agentRunId);
|
|
373
391
|
toast({
|
|
@@ -401,11 +419,6 @@ function MoreDropdown({
|
|
|
401
419
|
}
|
|
402
420
|
>
|
|
403
421
|
<MenuGroup label="Actions">
|
|
404
|
-
{isModal && (
|
|
405
|
-
<MenuItem onClick={() => openUrl(`/store/agent-runner/${agentRunId}`)}>
|
|
406
|
-
<ExternalLink className="size-3.5 text-muted" /> {t('agent.openInNewTab')}
|
|
407
|
-
</MenuItem>
|
|
408
|
-
)}
|
|
409
422
|
<MenuItem onClick={copyAgentRunId}>
|
|
410
423
|
<CopyIcon className="size-3.5 text-muted" /> {t('agent.copyAgentRunId')}
|
|
411
424
|
</MenuItem>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { act, renderHook } from '@testing-library/react';
|
|
2
|
+
import { type AgentMessage, AgentMessageType } from '@vertesia/common';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { useDocumentPanel } from './useDocumentPanel';
|
|
5
|
+
|
|
6
|
+
function documentCreated(documentId: string, title: string): AgentMessage {
|
|
7
|
+
return {
|
|
8
|
+
type: AgentMessageType.UPDATE,
|
|
9
|
+
message: '',
|
|
10
|
+
timestamp: Date.now(),
|
|
11
|
+
details: { event_class: 'document_created', document_id: documentId, title },
|
|
12
|
+
} as unknown as AgentMessage;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('useDocumentPanel', () => {
|
|
16
|
+
it('opens the newest document reported by the agent', () => {
|
|
17
|
+
const messages = [documentCreated('doc-1', 'Brief'), documentCreated('doc-2', 'Summary')];
|
|
18
|
+
const { result } = renderHook(() => useDocumentPanel(messages));
|
|
19
|
+
|
|
20
|
+
expect(result.current.openDocuments.map((doc) => doc.id)).toEqual(['doc-1', 'doc-2']);
|
|
21
|
+
expect(result.current.activeDocumentId).toBe('doc-2');
|
|
22
|
+
expect(result.current.isDocPanelOpen).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('selects null to go back to the list while keeping the documents open', () => {
|
|
26
|
+
const { result } = renderHook(() => useDocumentPanel([documentCreated('doc-1', 'Brief')]));
|
|
27
|
+
|
|
28
|
+
act(() => result.current.selectDocument(null));
|
|
29
|
+
|
|
30
|
+
expect(result.current.activeDocumentId).toBeNull();
|
|
31
|
+
expect(result.current.openDocuments).toHaveLength(1);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -8,8 +8,8 @@ export interface UseDocumentPanelResult {
|
|
|
8
8
|
isDocPanelOpen: boolean;
|
|
9
9
|
docRefreshKey: number;
|
|
10
10
|
closeDocPanel: () => void;
|
|
11
|
-
|
|
12
|
-
selectDocument: (docId: string) => void;
|
|
11
|
+
/** Select a document to display, or `null` to go back to the document list. */
|
|
12
|
+
selectDocument: (docId: string | null) => void;
|
|
13
13
|
openDocInPanel: (docId: string) => void;
|
|
14
14
|
updateDocumentTitle: (docId: string, title: string) => void;
|
|
15
15
|
}
|
|
@@ -137,23 +137,7 @@ export function useDocumentPanel(messages: AgentMessage[]): UseDocumentPanelResu
|
|
|
137
137
|
setIsDocPanelOpen(false);
|
|
138
138
|
}, []);
|
|
139
139
|
|
|
140
|
-
const
|
|
141
|
-
setOpenDocuments((prev) => {
|
|
142
|
-
const next = prev.filter((d) => d.id !== docId);
|
|
143
|
-
if (next.length === 0) {
|
|
144
|
-
setIsDocPanelOpen(false);
|
|
145
|
-
setActiveDocumentId(null);
|
|
146
|
-
} else {
|
|
147
|
-
setActiveDocumentId((current) => {
|
|
148
|
-
if (current === docId) return next[0].id;
|
|
149
|
-
return current;
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
return next;
|
|
153
|
-
});
|
|
154
|
-
}, []);
|
|
155
|
-
|
|
156
|
-
const selectDocument = useCallback((docId: string) => {
|
|
140
|
+
const selectDocument = useCallback((docId: string | null) => {
|
|
157
141
|
setActiveDocumentId(docId);
|
|
158
142
|
}, []);
|
|
159
143
|
|
|
@@ -176,7 +160,6 @@ export function useDocumentPanel(messages: AgentMessage[]): UseDocumentPanelResu
|
|
|
176
160
|
isDocPanelOpen,
|
|
177
161
|
docRefreshKey,
|
|
178
162
|
closeDocPanel,
|
|
179
|
-
closeDocument,
|
|
180
163
|
selectDocument,
|
|
181
164
|
openDocInPanel,
|
|
182
165
|
updateDocumentTitle,
|
|
@@ -47,8 +47,23 @@ describe('MCP connection controls', () => {
|
|
|
47
47
|
manifest: {
|
|
48
48
|
name: 'mcp-app',
|
|
49
49
|
title: 'MCP App',
|
|
50
|
-
oauth_providers: {},
|
|
51
50
|
tool_collections: [
|
|
51
|
+
{
|
|
52
|
+
type: 'mcp',
|
|
53
|
+
id: 'service-account',
|
|
54
|
+
name: 'Service Account',
|
|
55
|
+
namespace: 'service_account',
|
|
56
|
+
url: 'https://service-account-mcp.example.com',
|
|
57
|
+
oauth_provider: 'service-account',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
type: 'mcp',
|
|
61
|
+
id: 'service-calendar',
|
|
62
|
+
name: 'Service Calendar',
|
|
63
|
+
namespace: 'service_calendar',
|
|
64
|
+
url: 'https://service-calendar-mcp.example.com',
|
|
65
|
+
oauth_provider: 'service-account',
|
|
66
|
+
},
|
|
52
67
|
{
|
|
53
68
|
type: 'mcp',
|
|
54
69
|
id: 'jira',
|
|
@@ -86,11 +101,23 @@ describe('MCP connection controls', () => {
|
|
|
86
101
|
auth: 'api_key',
|
|
87
102
|
},
|
|
88
103
|
],
|
|
104
|
+
oauth_providers: {
|
|
105
|
+
'service-account': {
|
|
106
|
+
display_name: 'Service Account OAuth',
|
|
107
|
+
grant_type: 'client_credentials',
|
|
108
|
+
},
|
|
109
|
+
},
|
|
89
110
|
},
|
|
90
|
-
oauth_collection_ids: ['jira', 'miro', 'linear', 'github'],
|
|
111
|
+
oauth_collection_ids: ['jira', 'miro', 'linear', 'github', 'service-account', 'service-calendar'],
|
|
91
112
|
},
|
|
92
113
|
]);
|
|
93
114
|
mocks.getStatus.mockResolvedValue([
|
|
115
|
+
{
|
|
116
|
+
authenticated: true,
|
|
117
|
+
collection_id: 'service-account',
|
|
118
|
+
collection_name: 'Service Account',
|
|
119
|
+
mcp_server_url: 'https://service-account-mcp.example.com',
|
|
120
|
+
},
|
|
94
121
|
{
|
|
95
122
|
authenticated: true,
|
|
96
123
|
collection_id: 'jira',
|
|
@@ -124,7 +151,9 @@ describe('MCP connection controls', () => {
|
|
|
124
151
|
]);
|
|
125
152
|
const onChange = vi.fn();
|
|
126
153
|
|
|
127
|
-
renderWithProviders(
|
|
154
|
+
renderWithProviders(
|
|
155
|
+
<McpConnectionsInlineList disabledCollections={['linear', 'service-calendar']} onChange={onChange} />,
|
|
156
|
+
);
|
|
128
157
|
|
|
129
158
|
await screen.findByText('Jira');
|
|
130
159
|
await waitFor(() => expect(mocks.getStatus).toHaveBeenCalledTimes(1));
|
|
@@ -133,16 +162,25 @@ describe('MCP connection controls', () => {
|
|
|
133
162
|
expect(screen.getByText('Linear')).not.toBeNull();
|
|
134
163
|
expect(screen.getByText('GitHub')).not.toBeNull();
|
|
135
164
|
expect(screen.getByText('API Key Tools')).not.toBeNull();
|
|
165
|
+
expect(screen.getByText('Service Account')).not.toBeNull();
|
|
166
|
+
expect(screen.getByText('Service Calendar')).not.toBeNull();
|
|
167
|
+
expect(screen.queryByText('Service Account OAuth')).toBeNull();
|
|
136
168
|
const apiKeyBadge = screen.getByText('API key');
|
|
137
169
|
expect(apiKeyBadge.className).toContain('w-32');
|
|
170
|
+
const managedByAppBadges = screen.getAllByText('Managed by app');
|
|
171
|
+
expect(managedByAppBadges).toHaveLength(2);
|
|
172
|
+
expect(managedByAppBadges[0].className).toContain('w-32');
|
|
138
173
|
expect(screen.queryByText('Connected')).toBeNull();
|
|
139
174
|
expect(screen.getAllByRole('button', { name: 'Disconnect' })).toHaveLength(3);
|
|
140
175
|
const connectButton = screen.getByRole('button', { name: 'Connect' });
|
|
141
176
|
const disconnectButton = screen.getAllByRole('button', { name: 'Disconnect' })[0];
|
|
142
177
|
expect(connectButton.className).toContain('w-32');
|
|
143
178
|
expect(disconnectButton.className).toContain('w-32');
|
|
144
|
-
expect(screen.getAllByText('Enabled')).toHaveLength(
|
|
145
|
-
expect(screen.
|
|
179
|
+
expect(screen.getAllByText('Enabled')).toHaveLength(5);
|
|
180
|
+
expect(screen.getAllByText('Disabled')).toHaveLength(2);
|
|
181
|
+
|
|
182
|
+
fireEvent.click(screen.getByRole('switch', { name: 'Activate Service Calendar for this conversation' }));
|
|
183
|
+
expect(onChange).toHaveBeenCalledWith(['linear']);
|
|
146
184
|
});
|
|
147
185
|
|
|
148
186
|
it('renders MCP rows before connection statuses finish loading', async () => {
|
|
@@ -207,8 +245,29 @@ describe('MCP connection controls', () => {
|
|
|
207
245
|
manifest: {
|
|
208
246
|
name: 'jira-app',
|
|
209
247
|
title: 'Jira',
|
|
210
|
-
oauth_providers: {
|
|
248
|
+
oauth_providers: {
|
|
249
|
+
'service-account': {
|
|
250
|
+
display_name: 'Service Account OAuth',
|
|
251
|
+
grant_type: 'client_credentials',
|
|
252
|
+
},
|
|
253
|
+
},
|
|
211
254
|
tool_collections: [
|
|
255
|
+
{
|
|
256
|
+
type: 'mcp',
|
|
257
|
+
id: 'service-account',
|
|
258
|
+
name: 'Service Account',
|
|
259
|
+
namespace: 'service_account',
|
|
260
|
+
url: 'https://service-account-mcp.example.com',
|
|
261
|
+
oauth_provider: 'service-account',
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
type: 'mcp',
|
|
265
|
+
id: 'service-calendar',
|
|
266
|
+
name: 'Service Calendar',
|
|
267
|
+
namespace: 'service_calendar',
|
|
268
|
+
url: 'https://service-calendar-mcp.example.com',
|
|
269
|
+
oauth_provider: 'service-account',
|
|
270
|
+
},
|
|
212
271
|
{
|
|
213
272
|
type: 'mcp',
|
|
214
273
|
id: 'jira',
|
|
@@ -225,10 +284,16 @@ describe('MCP connection controls', () => {
|
|
|
225
284
|
},
|
|
226
285
|
],
|
|
227
286
|
},
|
|
228
|
-
oauth_collection_ids: ['jira', 'github'],
|
|
287
|
+
oauth_collection_ids: ['jira', 'github', 'service-account', 'service-calendar'],
|
|
229
288
|
},
|
|
230
289
|
]);
|
|
231
290
|
mocks.getStatus.mockResolvedValue([
|
|
291
|
+
{
|
|
292
|
+
authenticated: true,
|
|
293
|
+
collection_id: 'service-account',
|
|
294
|
+
collection_name: 'Service Account',
|
|
295
|
+
mcp_server_url: 'https://service-account-mcp.example.com',
|
|
296
|
+
},
|
|
232
297
|
{
|
|
233
298
|
authenticated: true,
|
|
234
299
|
collection_id: 'jira',
|
|
@@ -254,12 +319,18 @@ describe('MCP connection controls', () => {
|
|
|
254
319
|
|
|
255
320
|
const menuItem = await screen.findByText('MCP');
|
|
256
321
|
expect(menuItem).not.toBeNull();
|
|
257
|
-
expect(screen.getByText('
|
|
322
|
+
expect(screen.getByText('3')).not.toBeNull();
|
|
258
323
|
|
|
259
324
|
fireEvent.click(menuItem);
|
|
260
325
|
|
|
261
326
|
await waitFor(() => {
|
|
262
327
|
expect(screen.getByText('Manage the MCP servers available to this conversation.')).not.toBeNull();
|
|
263
328
|
});
|
|
329
|
+
expect(screen.getByText('Service Account')).not.toBeNull();
|
|
330
|
+
expect(screen.getByText('Service Calendar')).not.toBeNull();
|
|
331
|
+
expect(screen.queryByText('Service Account OAuth')).toBeNull();
|
|
332
|
+
expect(screen.getAllByText('Managed by app')).toHaveLength(2);
|
|
333
|
+
expect(screen.getAllByRole('button', { name: 'Disconnect' })).toHaveLength(1);
|
|
334
|
+
expect(screen.getAllByRole('button', { name: 'Connect' })).toHaveLength(1);
|
|
264
335
|
});
|
|
265
336
|
});
|
|
@@ -128,6 +128,7 @@ export function McpConnectionsInlineList({
|
|
|
128
128
|
{groups.map((group) => {
|
|
129
129
|
const active = !isGroupDisabled(group, disabledCollections);
|
|
130
130
|
const connected = group.authStatus?.authenticated === true;
|
|
131
|
+
const interactiveOAuth = group.authType === 'oauth' && group.oauthGrantType !== 'client_credentials';
|
|
131
132
|
const StatusIcon = connected ? Link2 : Link2Off;
|
|
132
133
|
return (
|
|
133
134
|
<div key={group.key} className="flex flex-wrap items-center gap-x-3 gap-y-2 py-2 text-sm">
|
|
@@ -155,7 +156,7 @@ export function McpConnectionsInlineList({
|
|
|
155
156
|
</span>
|
|
156
157
|
</VTooltip>
|
|
157
158
|
</div>
|
|
158
|
-
{statusLoading && !group.authStatus ? (
|
|
159
|
+
{interactiveOAuth && statusLoading && !group.authStatus ? (
|
|
159
160
|
<Button
|
|
160
161
|
variant="outline"
|
|
161
162
|
size="sm"
|
|
@@ -165,7 +166,7 @@ export function McpConnectionsInlineList({
|
|
|
165
166
|
>
|
|
166
167
|
<Spinner className="size-3" />
|
|
167
168
|
</Button>
|
|
168
|
-
) :
|
|
169
|
+
) : interactiveOAuth ? (
|
|
169
170
|
<RemoteMcpConnectionButton
|
|
170
171
|
appId={group.appId}
|
|
171
172
|
collectionId={group.representativeId}
|
|
@@ -178,7 +179,9 @@ export function McpConnectionsInlineList({
|
|
|
178
179
|
/>
|
|
179
180
|
) : (
|
|
180
181
|
<Badge variant="outline" className="h-6 w-32 justify-center px-2 text-xs">
|
|
181
|
-
{
|
|
182
|
+
{group.authType === 'api_key'
|
|
183
|
+
? t('mcpConnections.apiKey')
|
|
184
|
+
: t('mcpConnections.managedByApp')}
|
|
182
185
|
</Badge>
|
|
183
186
|
)}
|
|
184
187
|
{onChange && (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useUITranslation } from '@vertesia/ui/i18n';
|
|
2
2
|
import { Link2, Link2Off } from 'lucide-react';
|
|
3
|
-
import { Button, Modal, ModalBody, ModalTitle, Spinner, Switch, VTooltip } from '../../core/index.js';
|
|
3
|
+
import { Badge, Button, Modal, ModalBody, ModalTitle, Spinner, Switch, VTooltip } from '../../core/index.js';
|
|
4
4
|
import { RemoteMcpConnectionButton } from './RemoteMcpConnectionButton.js';
|
|
5
5
|
import { isGroupDisabled, type McpConnectionGroup, toggleGroupDisabled } from './useMcpConnections.js';
|
|
6
6
|
|
|
@@ -62,6 +62,8 @@ export function McpConnectionsDialog({
|
|
|
62
62
|
{groups.map((group) => {
|
|
63
63
|
const active = !isGroupDisabled(group, disabledCollections);
|
|
64
64
|
const connected = group.authStatus?.authenticated === true;
|
|
65
|
+
const interactiveOAuth =
|
|
66
|
+
group.authType === 'oauth' && group.oauthGrantType !== 'client_credentials';
|
|
65
67
|
const StatusIcon = connected ? Link2 : Link2Off;
|
|
66
68
|
return (
|
|
67
69
|
<div key={group.key} className="flex items-center justify-between gap-3 py-3">
|
|
@@ -92,7 +94,7 @@ export function McpConnectionsDialog({
|
|
|
92
94
|
</VTooltip>
|
|
93
95
|
</div>
|
|
94
96
|
<div className="flex shrink-0 items-center gap-3">
|
|
95
|
-
{statusLoading && !group.authStatus ? (
|
|
97
|
+
{interactiveOAuth && statusLoading && !group.authStatus ? (
|
|
96
98
|
<Button
|
|
97
99
|
variant="outline"
|
|
98
100
|
size="sm"
|
|
@@ -102,7 +104,7 @@ export function McpConnectionsDialog({
|
|
|
102
104
|
>
|
|
103
105
|
<Spinner className="size-3" />
|
|
104
106
|
</Button>
|
|
105
|
-
) :
|
|
107
|
+
) : interactiveOAuth ? (
|
|
106
108
|
<RemoteMcpConnectionButton
|
|
107
109
|
appId={group.appId}
|
|
108
110
|
collectionId={group.representativeId}
|
|
@@ -113,6 +115,10 @@ export function McpConnectionsDialog({
|
|
|
113
115
|
showDisconnect
|
|
114
116
|
readOnly={readOnly}
|
|
115
117
|
/>
|
|
118
|
+
) : group.authType === 'oauth' ? (
|
|
119
|
+
<Badge variant="outline" className="h-6 w-32 justify-center px-2 text-xs">
|
|
120
|
+
{t('mcpConnections.managedByApp')}
|
|
121
|
+
</Badge>
|
|
116
122
|
) : null}
|
|
117
123
|
{onChange && (
|
|
118
124
|
<Switch
|
|
@@ -7,22 +7,21 @@ import {
|
|
|
7
7
|
import { useUserSession } from '@vertesia/ui/session';
|
|
8
8
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
9
9
|
|
|
10
|
-
/**
|
|
11
|
-
* A group of MCP tool collections that share authentication and activation state.
|
|
12
|
-
*/
|
|
10
|
+
/** A visible MCP collection row. OAuth authentication may be shared with sibling rows. */
|
|
13
11
|
export interface McpConnectionGroup {
|
|
14
12
|
key: string;
|
|
15
13
|
appId: string;
|
|
16
14
|
appName: string;
|
|
17
|
-
/** Display label
|
|
15
|
+
/** Display label for the MCP collection. */
|
|
18
16
|
label: string;
|
|
19
|
-
/**
|
|
17
|
+
/** Collection ID used for status and OAuth operations; shared when collections use the same provider. */
|
|
20
18
|
representativeId: string;
|
|
21
|
-
/**
|
|
19
|
+
/** Collection IDs controlled by this row's per-conversation activation toggle. */
|
|
22
20
|
memberIds: string[];
|
|
23
|
-
/**
|
|
21
|
+
/** Collection names represented by this row. */
|
|
24
22
|
memberNames: string[];
|
|
25
23
|
authType: 'oauth' | 'api_key';
|
|
24
|
+
oauthGrantType?: 'authorization_code' | 'client_credentials';
|
|
26
25
|
authStatus?: OAuthAuthStatus;
|
|
27
26
|
}
|
|
28
27
|
|
|
@@ -80,29 +79,36 @@ export function useMcpConnections() {
|
|
|
80
79
|
}
|
|
81
80
|
|
|
82
81
|
for (const [providerKey, cols] of providerMap) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
82
|
+
const representativeId = cols[0].id;
|
|
83
|
+
for (const col of cols) {
|
|
84
|
+
allGroups.push({
|
|
85
|
+
key: `${inst.id}:provider:${providerKey}:${col.id}`,
|
|
86
|
+
appId: inst.id,
|
|
87
|
+
appName: inst.manifest.title || inst.manifest.name,
|
|
88
|
+
label: col.name,
|
|
89
|
+
representativeId,
|
|
90
|
+
memberIds: [col.id],
|
|
91
|
+
memberNames: [col.name],
|
|
92
|
+
authType: 'oauth',
|
|
93
|
+
oauthGrantType: manifestProviders[providerKey]?.grant_type,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
93
96
|
}
|
|
94
97
|
|
|
95
98
|
for (const [oauthApp, cols] of oauthAppMap) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
99
|
+
const representativeId = cols[0].id;
|
|
100
|
+
for (const col of cols) {
|
|
101
|
+
allGroups.push({
|
|
102
|
+
key: `${inst.id}:oauthapp:${oauthApp}:${col.id}`,
|
|
103
|
+
appId: inst.id,
|
|
104
|
+
appName: inst.manifest.title || inst.manifest.name,
|
|
105
|
+
label: col.name,
|
|
106
|
+
representativeId,
|
|
107
|
+
memberIds: [col.id],
|
|
108
|
+
memberNames: [col.name],
|
|
109
|
+
authType: 'oauth',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
for (const col of individualCols) {
|