@agent-relay/dashboard 2.0.80 → 2.0.82
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/out/404.html +1 -1
- package/out/_next/static/chunks/{118-4c8241b0218335de.js → 118-ae2b650136a5a5fc.js} +1 -1
- package/out/_next/static/chunks/407-0c82986cf79c8ecb.js +1 -0
- package/out/_next/static/chunks/app/app/[[...slug]]/{page-1e81c047cff17212.js → page-f7eca1b66fb4249b.js} +1 -1
- package/out/_next/static/chunks/app/{page-6892fe2dd07fb48b.js → page-0ee604f7070d14c0.js} +1 -1
- package/out/_next/static/css/8968d98ed4c4d33f.css +1 -0
- package/out/about.html +2 -2
- package/out/about.txt +1 -1
- package/out/app/onboarding.html +1 -1
- package/out/app/onboarding.txt +1 -1
- package/out/app.html +1 -1
- package/out/app.txt +2 -2
- package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +2 -2
- package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
- package/out/blog/let-them-cook-multi-agent-orchestration.html +2 -2
- package/out/blog/let-them-cook-multi-agent-orchestration.txt +2 -2
- package/out/blog.html +2 -2
- package/out/blog.txt +1 -1
- package/out/careers.html +2 -2
- package/out/careers.txt +1 -1
- package/out/changelog.html +2 -2
- package/out/changelog.txt +1 -1
- package/out/cloud/link.html +1 -1
- package/out/cloud/link.txt +2 -2
- package/out/complete-profile.html +2 -2
- package/out/complete-profile.txt +1 -1
- package/out/connect-repos.html +1 -1
- package/out/connect-repos.txt +1 -1
- package/out/contact.html +2 -2
- package/out/contact.txt +1 -1
- package/out/docs.html +2 -2
- package/out/docs.txt +1 -1
- package/out/history.html +1 -1
- package/out/history.txt +2 -2
- package/out/index.html +1 -1
- package/out/index.txt +2 -2
- package/out/login.html +2 -2
- package/out/login.txt +1 -1
- package/out/metrics.html +1 -1
- package/out/metrics.txt +2 -2
- package/out/pricing.html +2 -2
- package/out/pricing.txt +1 -1
- package/out/privacy.html +2 -2
- package/out/privacy.txt +1 -1
- package/out/providers/setup/claude.html +1 -1
- package/out/providers/setup/claude.txt +1 -1
- package/out/providers/setup/codex.html +1 -1
- package/out/providers/setup/codex.txt +1 -1
- package/out/providers/setup/cursor.html +1 -1
- package/out/providers/setup/cursor.txt +1 -1
- package/out/providers.html +1 -1
- package/out/providers.txt +1 -1
- package/out/security.html +2 -2
- package/out/security.txt +1 -1
- package/out/signup.html +2 -2
- package/out/signup.txt +1 -1
- package/out/terms.html +2 -2
- package/out/terms.txt +1 -1
- package/package.json +7 -1
- package/src/app/about/page.tsx +7 -0
- package/src/app/app/[[...slug]]/DashboardPageClient.tsx +853 -0
- package/src/app/app/[[...slug]]/page.tsx +23 -0
- package/src/app/app/onboarding/page.tsx +394 -0
- package/src/app/apple-icon.png +0 -0
- package/src/app/blog/go-to-bed-wake-up-to-a-finished-product/page.tsx +88 -0
- package/src/app/blog/let-them-cook-multi-agent-orchestration/page.tsx +93 -0
- package/src/app/blog/page.tsx +15 -0
- package/src/app/careers/page.tsx +7 -0
- package/src/app/changelog/page.tsx +7 -0
- package/src/app/cloud/link/page.tsx +464 -0
- package/src/app/complete-profile/page.tsx +204 -0
- package/src/app/connect-repos/page.tsx +410 -0
- package/src/app/contact/page.tsx +7 -0
- package/src/app/docs/page.tsx +7 -0
- package/src/app/favicon.png +0 -0
- package/src/app/globals.css +200 -0
- package/src/app/history/page.tsx +658 -0
- package/src/app/layout.tsx +25 -0
- package/src/app/login/page.tsx +424 -0
- package/src/app/metrics/page.tsx +781 -0
- package/src/app/page.tsx +59 -0
- package/src/app/pricing/page.tsx +7 -0
- package/src/app/privacy/page.tsx +7 -0
- package/src/app/providers/page.tsx +193 -0
- package/src/app/providers/setup/[provider]/ProviderSetupClient.tsx +197 -0
- package/src/app/providers/setup/[provider]/constants.ts +35 -0
- package/src/app/providers/setup/[provider]/page.tsx +42 -0
- package/src/app/security/page.tsx +7 -0
- package/src/app/signup/page.tsx +533 -0
- package/src/app/terms/page.tsx +7 -0
- package/src/components/ActivityFeed.tsx +216 -0
- package/src/components/AddWorkspaceModal.tsx +170 -0
- package/src/components/AgentCard.test.tsx +134 -0
- package/src/components/AgentCard.tsx +585 -0
- package/src/components/AgentList.test.tsx +147 -0
- package/src/components/AgentList.tsx +419 -0
- package/src/components/AgentLogPreview.tsx +173 -0
- package/src/components/AgentProfilePanel.tsx +569 -0
- package/src/components/App.tsx +3424 -0
- package/src/components/BillingPanel.tsx +922 -0
- package/src/components/BillingResult.tsx +447 -0
- package/src/components/BroadcastComposer.tsx +690 -0
- package/src/components/ChannelAdminPanel.tsx +773 -0
- package/src/components/ChannelBrowser.tsx +385 -0
- package/src/components/ChannelChat.tsx +261 -0
- package/src/components/ChannelSidebar.tsx +399 -0
- package/src/components/CloudSessionProvider.tsx +130 -0
- package/src/components/CommandPalette.tsx +815 -0
- package/src/components/ConfirmationDialog.tsx +133 -0
- package/src/components/ConversationHistory.tsx +518 -0
- package/src/components/CoordinatorPanel.tsx +956 -0
- package/src/components/DecisionQueue.tsx +717 -0
- package/src/components/DirectMessageView.tsx +164 -0
- package/src/components/FileAutocomplete.tsx +368 -0
- package/src/components/FleetOverview.tsx +278 -0
- package/src/components/LogViewer.tsx +310 -0
- package/src/components/LogViewerPanel.tsx +482 -0
- package/src/components/Logo.tsx +284 -0
- package/src/components/MentionAutocomplete.tsx +384 -0
- package/src/components/MessageComposer.tsx +473 -0
- package/src/components/MessageList.tsx +725 -0
- package/src/components/MessageSenderName.tsx +91 -0
- package/src/components/MessageStatusIndicator.tsx +142 -0
- package/src/components/NewConversationModal.tsx +400 -0
- package/src/components/NotificationToast.tsx +488 -0
- package/src/components/OnlineUsersIndicator.tsx +164 -0
- package/src/components/Pagination.tsx +124 -0
- package/src/components/PricingPlans.tsx +386 -0
- package/src/components/ProjectList.tsx +711 -0
- package/src/components/ProviderAuthFlow.tsx +343 -0
- package/src/components/ProviderConnectionList.tsx +375 -0
- package/src/components/ProvisioningProgress.tsx +730 -0
- package/src/components/ReactionChips.tsx +70 -0
- package/src/components/ReactionPicker.tsx +121 -0
- package/src/components/RepoAccessPanel.tsx +787 -0
- package/src/components/RepositoriesPanel.tsx +901 -0
- package/src/components/ServerCard.tsx +202 -0
- package/src/components/SessionExpiredModal.tsx +128 -0
- package/src/components/SpawnModal.test.tsx +190 -0
- package/src/components/SpawnModal.tsx +1001 -0
- package/src/components/TaskAssignmentUI.tsx +375 -0
- package/src/components/TerminalProviderSetup.tsx +517 -0
- package/src/components/ThemeProvider.tsx +159 -0
- package/src/components/ThinkingIndicator.tsx +231 -0
- package/src/components/ThreadList.tsx +198 -0
- package/src/components/ThreadPanel.tsx +405 -0
- package/src/components/TrajectoryViewer.tsx +698 -0
- package/src/components/TypingIndicator.tsx +69 -0
- package/src/components/UsageBanner.tsx +231 -0
- package/src/components/UserProfilePanel.tsx +233 -0
- package/src/components/WorkspaceContext.tsx +95 -0
- package/src/components/WorkspaceSelector.tsx +234 -0
- package/src/components/WorkspaceStatusIndicator.tsx +396 -0
- package/src/components/XTermInteractive.tsx +516 -0
- package/src/components/XTermLogViewer.tsx +719 -0
- package/src/components/channels/ChannelDialogs.tsx +1411 -0
- package/src/components/channels/ChannelHeader.tsx +317 -0
- package/src/components/channels/ChannelMessageList.tsx +463 -0
- package/src/components/channels/ChannelViewV1.tsx +146 -0
- package/src/components/channels/MessageInput.tsx +302 -0
- package/src/components/channels/SearchInput.tsx +172 -0
- package/src/components/channels/SearchResults.tsx +336 -0
- package/src/components/channels/api.test.ts +1527 -0
- package/src/components/channels/api.ts +703 -0
- package/src/components/channels/index.ts +76 -0
- package/src/components/channels/mockApi.ts +344 -0
- package/src/components/channels/types.ts +566 -0
- package/src/components/hooks/index.ts +58 -0
- package/src/components/hooks/useAgentLogs.ts +504 -0
- package/src/components/hooks/useAgents.ts +127 -0
- package/src/components/hooks/useBroadcastDedup.test.ts +371 -0
- package/src/components/hooks/useBroadcastDedup.ts +86 -0
- package/src/components/hooks/useChannelAdmin.ts +329 -0
- package/src/components/hooks/useChannelBrowser.ts +239 -0
- package/src/components/hooks/useChannelCommands.ts +138 -0
- package/src/components/hooks/useChannels.ts +367 -0
- package/src/components/hooks/useDebounce.ts +29 -0
- package/src/components/hooks/useDirectMessage.test.ts +952 -0
- package/src/components/hooks/useDirectMessage.ts +141 -0
- package/src/components/hooks/useMessages.ts +310 -0
- package/src/components/hooks/useOrchestrator.test.ts +165 -0
- package/src/components/hooks/useOrchestrator.ts +424 -0
- package/src/components/hooks/usePinnedAgents.test.ts +356 -0
- package/src/components/hooks/usePinnedAgents.ts +140 -0
- package/src/components/hooks/usePresence.test.ts +245 -0
- package/src/components/hooks/usePresence.ts +377 -0
- package/src/components/hooks/useRecentRepos.ts +130 -0
- package/src/components/hooks/useSession.ts +209 -0
- package/src/components/hooks/useThread.ts +138 -0
- package/src/components/hooks/useTrajectory.ts +265 -0
- package/src/components/hooks/useWebSocket.ts +290 -0
- package/src/components/hooks/useWorkspaceMembers.ts +132 -0
- package/src/components/hooks/useWorkspaceRepos.ts +73 -0
- package/src/components/hooks/useWorkspaceStatus.ts +237 -0
- package/src/components/index.ts +81 -0
- package/src/components/layout/Header.tsx +311 -0
- package/src/components/layout/RepoContextHeader.tsx +361 -0
- package/src/components/layout/Sidebar.archive.test.tsx +126 -0
- package/src/components/layout/Sidebar.test.tsx +691 -0
- package/src/components/layout/Sidebar.tsx +900 -0
- package/src/components/layout/index.ts +7 -0
- package/src/components/settings/BillingSettingsPanel.tsx +564 -0
- package/src/components/settings/SettingsPage.tsx +683 -0
- package/src/components/settings/TeamSettingsPanel.tsx +560 -0
- package/src/components/settings/WorkspaceSettingsPanel.tsx +1368 -0
- package/src/components/settings/index.ts +11 -0
- package/src/components/settings/types.ts +79 -0
- package/src/components/utils/messageFormatting.test.tsx +331 -0
- package/src/components/utils/messageFormatting.tsx +597 -0
- package/src/index.ts +63 -0
- package/src/landing/AboutPage.tsx +77 -0
- package/src/landing/BlogContent.tsx +187 -0
- package/src/landing/BlogPage.tsx +47 -0
- package/src/landing/CareersPage.tsx +53 -0
- package/src/landing/ChangelogPage.tsx +33 -0
- package/src/landing/ContactPage.tsx +41 -0
- package/src/landing/DocsPage.tsx +43 -0
- package/src/landing/LandingPage.tsx +702 -0
- package/src/landing/PricingPage.tsx +549 -0
- package/src/landing/PrivacyPage.tsx +117 -0
- package/src/landing/SecurityPage.tsx +42 -0
- package/src/landing/StaticPage.tsx +165 -0
- package/src/landing/TermsPage.tsx +125 -0
- package/src/landing/blogData.ts +312 -0
- package/src/landing/index.ts +18 -0
- package/src/landing/styles.css +3673 -0
- package/src/lib/agent-merge.test.ts +43 -0
- package/src/lib/agent-merge.ts +35 -0
- package/src/lib/api.ts +1294 -0
- package/src/lib/cloudApi.ts +893 -0
- package/src/lib/colors.test.ts +175 -0
- package/src/lib/colors.ts +218 -0
- package/src/lib/config.ts +109 -0
- package/src/lib/hierarchy.ts +242 -0
- package/src/lib/stuckDetection.ts +142 -0
- package/src/lib/useUrlRouting.ts +190 -0
- package/src/types/index.ts +317 -0
- package/src/types/threading.ts +7 -0
- package/out/_next/static/chunks/285-dc644487a8d6500d.js +0 -1
- package/out/_next/static/css/4c58d9cf493aa626.css +0 -1
- /package/out/_next/static/{AqelRhy1vr2nBUcU0Iqcp → IxfA6RZu4trcsEMYlkQra}/_buildManifest.js +0 -0
- /package/out/_next/static/{AqelRhy1vr2nBUcU0Iqcp → IxfA6RZu4trcsEMYlkQra}/_ssgManifest.js +0 -0
- /package/out/_next/static/chunks/{528-d375bc8b46912d2c.js → 528-f5f676996d613c25.js} +0 -0
- /package/out/_next/static/chunks/app/blog/let-them-cook-multi-agent-orchestration/{page-a58308f43557b908.js → page-b194f207fbd91862.js} +0 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
import React, { useCallback, useState } from 'react';
|
|
2
|
+
import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
3
|
+
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
|
4
|
+
// Only import languages we actually need (saves ~300KB)
|
|
5
|
+
import javascript from 'react-syntax-highlighter/dist/esm/languages/prism/javascript';
|
|
6
|
+
import typescript from 'react-syntax-highlighter/dist/esm/languages/prism/typescript';
|
|
7
|
+
import python from 'react-syntax-highlighter/dist/esm/languages/prism/python';
|
|
8
|
+
import bash from 'react-syntax-highlighter/dist/esm/languages/prism/bash';
|
|
9
|
+
import json from 'react-syntax-highlighter/dist/esm/languages/prism/json';
|
|
10
|
+
import markdown from 'react-syntax-highlighter/dist/esm/languages/prism/markdown';
|
|
11
|
+
import yaml from 'react-syntax-highlighter/dist/esm/languages/prism/yaml';
|
|
12
|
+
import css from 'react-syntax-highlighter/dist/esm/languages/prism/css';
|
|
13
|
+
import go from 'react-syntax-highlighter/dist/esm/languages/prism/go';
|
|
14
|
+
import rust from 'react-syntax-highlighter/dist/esm/languages/prism/rust';
|
|
15
|
+
import sql from 'react-syntax-highlighter/dist/esm/languages/prism/sql';
|
|
16
|
+
import ruby from 'react-syntax-highlighter/dist/esm/languages/prism/ruby';
|
|
17
|
+
import java from 'react-syntax-highlighter/dist/esm/languages/prism/java';
|
|
18
|
+
import docker from 'react-syntax-highlighter/dist/esm/languages/prism/docker';
|
|
19
|
+
|
|
20
|
+
SyntaxHighlighter.registerLanguage('javascript', javascript);
|
|
21
|
+
SyntaxHighlighter.registerLanguage('typescript', typescript);
|
|
22
|
+
SyntaxHighlighter.registerLanguage('python', python);
|
|
23
|
+
SyntaxHighlighter.registerLanguage('bash', bash);
|
|
24
|
+
SyntaxHighlighter.registerLanguage('shell', bash);
|
|
25
|
+
SyntaxHighlighter.registerLanguage('json', json);
|
|
26
|
+
SyntaxHighlighter.registerLanguage('markdown', markdown);
|
|
27
|
+
SyntaxHighlighter.registerLanguage('yaml', yaml);
|
|
28
|
+
SyntaxHighlighter.registerLanguage('css', css);
|
|
29
|
+
SyntaxHighlighter.registerLanguage('go', go);
|
|
30
|
+
SyntaxHighlighter.registerLanguage('rust', rust);
|
|
31
|
+
SyntaxHighlighter.registerLanguage('sql', sql);
|
|
32
|
+
SyntaxHighlighter.registerLanguage('ruby', ruby);
|
|
33
|
+
SyntaxHighlighter.registerLanguage('java', java);
|
|
34
|
+
SyntaxHighlighter.registerLanguage('docker', docker);
|
|
35
|
+
SyntaxHighlighter.registerLanguage('dockerfile', docker);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Custom theme extending oneDark to match dashboard styling
|
|
39
|
+
*/
|
|
40
|
+
const customCodeTheme = {
|
|
41
|
+
...oneDark,
|
|
42
|
+
'pre[class*="language-"]': {
|
|
43
|
+
...oneDark['pre[class*="language-"]'],
|
|
44
|
+
background: 'rgba(15, 23, 42, 0.8)',
|
|
45
|
+
margin: '0.5rem 0',
|
|
46
|
+
padding: '1rem',
|
|
47
|
+
borderRadius: '0.5rem',
|
|
48
|
+
border: '1px solid rgba(148, 163, 184, 0.1)',
|
|
49
|
+
fontSize: '0.75rem',
|
|
50
|
+
lineHeight: '1.5',
|
|
51
|
+
},
|
|
52
|
+
'code[class*="language-"]': {
|
|
53
|
+
...oneDark['code[class*="language-"]'],
|
|
54
|
+
background: 'transparent',
|
|
55
|
+
fontSize: '0.75rem',
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* CodeBlock Component - Renders syntax highlighted code
|
|
61
|
+
*/
|
|
62
|
+
interface CodeBlockProps {
|
|
63
|
+
code: string;
|
|
64
|
+
language: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function CodeBlock({ code, language }: CodeBlockProps) {
|
|
68
|
+
const [copied, setCopied] = useState(false);
|
|
69
|
+
|
|
70
|
+
const handleCopy = useCallback(async () => {
|
|
71
|
+
try {
|
|
72
|
+
await navigator.clipboard.writeText(code);
|
|
73
|
+
setCopied(true);
|
|
74
|
+
setTimeout(() => setCopied(false), 2000);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error('Failed to copy:', err);
|
|
77
|
+
}
|
|
78
|
+
}, [code]);
|
|
79
|
+
|
|
80
|
+
// Normalize language names for syntax highlighter
|
|
81
|
+
const normalizedLanguage = language.toLowerCase().replace(/^(js|jsx)$/, 'javascript')
|
|
82
|
+
.replace(/^(ts|tsx)$/, 'typescript')
|
|
83
|
+
.replace(/^(py)$/, 'python')
|
|
84
|
+
.replace(/^(rb)$/, 'ruby')
|
|
85
|
+
.replace(/^(sh|shell|zsh)$/, 'bash');
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div className="relative group my-2">
|
|
89
|
+
{/* Language badge and copy button */}
|
|
90
|
+
<div className="absolute top-2 right-2 flex items-center gap-2 z-10">
|
|
91
|
+
{language && language !== 'text' && (
|
|
92
|
+
<span className="text-xs px-2 py-0.5 rounded bg-accent-cyan/20 text-accent-cyan font-mono">
|
|
93
|
+
{language}
|
|
94
|
+
</span>
|
|
95
|
+
)}
|
|
96
|
+
<button
|
|
97
|
+
onClick={handleCopy}
|
|
98
|
+
className="opacity-0 group-hover:opacity-100 transition-opacity text-xs px-2 py-1 rounded bg-bg-tertiary hover:bg-bg-card text-text-muted hover:text-text-primary border border-border-subtle"
|
|
99
|
+
title="Copy code"
|
|
100
|
+
>
|
|
101
|
+
{copied ? '✓ Copied' : 'Copy'}
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
<SyntaxHighlighter
|
|
105
|
+
language={normalizedLanguage}
|
|
106
|
+
style={customCodeTheme}
|
|
107
|
+
customStyle={{
|
|
108
|
+
margin: 0,
|
|
109
|
+
background: 'rgba(15, 23, 42, 0.8)',
|
|
110
|
+
}}
|
|
111
|
+
showLineNumbers={code.split('\n').length > 3}
|
|
112
|
+
lineNumberStyle={{
|
|
113
|
+
minWidth: '2.5em',
|
|
114
|
+
paddingRight: '1em',
|
|
115
|
+
color: 'rgba(148, 163, 184, 0.4)',
|
|
116
|
+
userSelect: 'none',
|
|
117
|
+
}}
|
|
118
|
+
>
|
|
119
|
+
{code.trim()}
|
|
120
|
+
</SyntaxHighlighter>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Check if a line looks like part of a table (has pipe characters)
|
|
127
|
+
*/
|
|
128
|
+
function isTableLine(line: string): boolean {
|
|
129
|
+
const pipeCount = (line.match(/\|/g) || []).length;
|
|
130
|
+
return pipeCount >= 2 || (line.trim().startsWith('|') && line.trim().endsWith('|'));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check if a line is a table separator (dashes and pipes)
|
|
135
|
+
*/
|
|
136
|
+
function isTableSeparator(line: string): boolean {
|
|
137
|
+
return /^[\s|:-]+$/.test(line) && line.includes('-') && line.includes('|');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Check if a line is a blockquote (starts with >)
|
|
142
|
+
*/
|
|
143
|
+
function isQuoteLine(line: string): boolean {
|
|
144
|
+
return line.trimStart().startsWith('>');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Extract the content from a quote line (remove the leading > and space)
|
|
149
|
+
*/
|
|
150
|
+
function getQuoteContent(line: string): string {
|
|
151
|
+
const trimmed = line.trimStart();
|
|
152
|
+
if (trimmed.startsWith('> ')) {
|
|
153
|
+
return trimmed.slice(2);
|
|
154
|
+
}
|
|
155
|
+
if (trimmed.startsWith('>')) {
|
|
156
|
+
return trimmed.slice(1);
|
|
157
|
+
}
|
|
158
|
+
return line;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
interface ContentSection {
|
|
162
|
+
type: 'text' | 'table' | 'code' | 'quote';
|
|
163
|
+
content: string;
|
|
164
|
+
language?: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Split content into text, table, and code sections
|
|
169
|
+
* Code blocks are detected by fenced code block syntax (```language ... ```)
|
|
170
|
+
*/
|
|
171
|
+
function splitContentSections(content: string): ContentSection[] {
|
|
172
|
+
const sections: ContentSection[] = [];
|
|
173
|
+
|
|
174
|
+
// First, extract code blocks using regex
|
|
175
|
+
// Matches ```language\ncode\n``` or ```\ncode\n```
|
|
176
|
+
const codeBlockRegex = /```(\w*)\n([\s\S]*?)```/g;
|
|
177
|
+
let lastIndex = 0;
|
|
178
|
+
let match;
|
|
179
|
+
|
|
180
|
+
while ((match = codeBlockRegex.exec(content)) !== null) {
|
|
181
|
+
// Add any content before this code block
|
|
182
|
+
if (match.index > lastIndex) {
|
|
183
|
+
const beforeContent = content.slice(lastIndex, match.index);
|
|
184
|
+
const beforeSections = splitTextAndTableSections(beforeContent);
|
|
185
|
+
sections.push(...beforeSections);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Add the code block
|
|
189
|
+
sections.push({
|
|
190
|
+
type: 'code',
|
|
191
|
+
language: match[1] || 'text',
|
|
192
|
+
content: match[2],
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
lastIndex = match.index + match[0].length;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Add any remaining content after the last code block
|
|
199
|
+
if (lastIndex < content.length) {
|
|
200
|
+
const afterContent = content.slice(lastIndex);
|
|
201
|
+
const afterSections = splitTextAndTableSections(afterContent);
|
|
202
|
+
sections.push(...afterSections);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// If no code blocks were found, just split text/tables
|
|
206
|
+
if (sections.length === 0) {
|
|
207
|
+
return splitTextAndTableSections(content);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return sections;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Split content into text, table, and quote sections (helper for non-code content)
|
|
215
|
+
*/
|
|
216
|
+
function splitTextAndTableSections(content: string): ContentSection[] {
|
|
217
|
+
const lines = content.split('\n');
|
|
218
|
+
const sections: ContentSection[] = [];
|
|
219
|
+
let currentSection: ContentSection | null = null;
|
|
220
|
+
|
|
221
|
+
for (const line of lines) {
|
|
222
|
+
let sectionType: 'text' | 'table' | 'quote';
|
|
223
|
+
let lineContent = line;
|
|
224
|
+
|
|
225
|
+
if (isQuoteLine(line)) {
|
|
226
|
+
sectionType = 'quote';
|
|
227
|
+
lineContent = getQuoteContent(line);
|
|
228
|
+
} else if (isTableLine(line) || isTableSeparator(line)) {
|
|
229
|
+
sectionType = 'table';
|
|
230
|
+
} else {
|
|
231
|
+
sectionType = 'text';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!currentSection || currentSection.type !== sectionType) {
|
|
235
|
+
if (currentSection) {
|
|
236
|
+
sections.push(currentSection);
|
|
237
|
+
}
|
|
238
|
+
currentSection = { type: sectionType, content: lineContent };
|
|
239
|
+
} else {
|
|
240
|
+
currentSection.content += '\n' + lineContent;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (currentSection) {
|
|
245
|
+
sections.push(currentSection);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return sections;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface FormatMessageOptions {
|
|
252
|
+
mentions?: string[];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Format message body with newline preservation, link detection, table, and code support
|
|
257
|
+
*/
|
|
258
|
+
export function formatMessageBody(content: string, options: FormatMessageOptions = {}): React.ReactNode {
|
|
259
|
+
const normalizedContent = content
|
|
260
|
+
.replace(/\\n/g, '\n')
|
|
261
|
+
.replace(/\r\n/g, '\n')
|
|
262
|
+
.replace(/\r/g, '\n');
|
|
263
|
+
|
|
264
|
+
const sections = splitContentSections(normalizedContent);
|
|
265
|
+
|
|
266
|
+
// If only one section and not a table, use simple rendering
|
|
267
|
+
if (sections.length === 1 && sections[0].type === 'text') {
|
|
268
|
+
const lines = normalizedContent.split('\n');
|
|
269
|
+
return lines.map((line, i) => (
|
|
270
|
+
<React.Fragment key={i}>
|
|
271
|
+
{i > 0 && <br />}
|
|
272
|
+
{formatLine(line, options.mentions)}
|
|
273
|
+
</React.Fragment>
|
|
274
|
+
));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Render mixed content with tables, code blocks, and quotes
|
|
278
|
+
return sections.map((section, sectionIndex) => {
|
|
279
|
+
if (section.type === 'code') {
|
|
280
|
+
return (
|
|
281
|
+
<CodeBlock
|
|
282
|
+
key={sectionIndex}
|
|
283
|
+
code={section.content}
|
|
284
|
+
language={section.language || 'text'}
|
|
285
|
+
/>
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (section.type === 'table') {
|
|
290
|
+
return (
|
|
291
|
+
<pre
|
|
292
|
+
key={sectionIndex}
|
|
293
|
+
className="font-mono text-xs leading-relaxed whitespace-pre overflow-x-auto my-2 p-3 bg-bg-tertiary/50 rounded-lg border border-border-subtle"
|
|
294
|
+
>
|
|
295
|
+
{section.content}
|
|
296
|
+
</pre>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (section.type === 'quote') {
|
|
301
|
+
const lines = section.content.split('\n');
|
|
302
|
+
return (
|
|
303
|
+
<blockquote
|
|
304
|
+
key={sectionIndex}
|
|
305
|
+
className="my-2 pl-3 py-1 border-l-2 border-accent-cyan/50 bg-bg-tertiary/30 rounded-r text-text-secondary italic"
|
|
306
|
+
>
|
|
307
|
+
{lines.map((line, i) => (
|
|
308
|
+
<React.Fragment key={i}>
|
|
309
|
+
{i > 0 && <br />}
|
|
310
|
+
{formatLine(line, options.mentions)}
|
|
311
|
+
</React.Fragment>
|
|
312
|
+
))}
|
|
313
|
+
</blockquote>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Regular text section
|
|
318
|
+
const lines = section.content.split('\n');
|
|
319
|
+
return (
|
|
320
|
+
<span key={sectionIndex}>
|
|
321
|
+
{lines.map((line, i) => (
|
|
322
|
+
<React.Fragment key={i}>
|
|
323
|
+
{i > 0 && <br />}
|
|
324
|
+
{formatLine(line, options.mentions)}
|
|
325
|
+
</React.Fragment>
|
|
326
|
+
))}
|
|
327
|
+
</span>
|
|
328
|
+
);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Check if a line is a heading and return the level (1-6) or 0 if not
|
|
334
|
+
*/
|
|
335
|
+
function getHeadingLevel(line: string): number {
|
|
336
|
+
const match = line.match(/^(#{1,6})\s+/);
|
|
337
|
+
return match ? match[1].length : 0;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Check if a line is a list item (bullet or numbered)
|
|
342
|
+
*/
|
|
343
|
+
function getListInfo(line: string): { type: 'bullet' | 'numbered' | null; content: string; indent: number } {
|
|
344
|
+
const bulletMatch = line.match(/^(\s*)([-*])\s+(.*)$/);
|
|
345
|
+
if (bulletMatch) {
|
|
346
|
+
return { type: 'bullet', content: bulletMatch[3], indent: bulletMatch[1].length };
|
|
347
|
+
}
|
|
348
|
+
const numberedMatch = line.match(/^(\s*)(\d+\.)\s+(.*)$/);
|
|
349
|
+
if (numberedMatch) {
|
|
350
|
+
return { type: 'numbered', content: numberedMatch[3], indent: numberedMatch[1].length };
|
|
351
|
+
}
|
|
352
|
+
return { type: null, content: line, indent: 0 };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Format inline markdown elements (bold, italic, strikethrough, links, code, URLs)
|
|
357
|
+
* Processes patterns sequentially to avoid regex complexity issues
|
|
358
|
+
*/
|
|
359
|
+
function formatInlineMarkdown(text: string, mentions?: string[], keyPrefix: string = ''): React.ReactNode {
|
|
360
|
+
const parts: React.ReactNode[] = [];
|
|
361
|
+
let remaining = text;
|
|
362
|
+
let partIndex = 0;
|
|
363
|
+
|
|
364
|
+
// Define patterns in order of precedence (most specific first)
|
|
365
|
+
const patterns: Array<{
|
|
366
|
+
regex: RegExp;
|
|
367
|
+
render: (match: RegExpMatchArray, key: string) => React.ReactNode;
|
|
368
|
+
}> = [
|
|
369
|
+
// Inline code `code` - process first to protect content
|
|
370
|
+
{
|
|
371
|
+
regex: /`([^`]+)`/,
|
|
372
|
+
render: (match, key) => (
|
|
373
|
+
<code
|
|
374
|
+
key={key}
|
|
375
|
+
className="px-1.5 py-0.5 mx-0.5 rounded bg-bg-elevated/80 text-accent-cyan font-mono text-[0.85em] border border-border-subtle/50"
|
|
376
|
+
>
|
|
377
|
+
{match[1]}
|
|
378
|
+
</code>
|
|
379
|
+
),
|
|
380
|
+
},
|
|
381
|
+
// Markdown link [text](url)
|
|
382
|
+
{
|
|
383
|
+
regex: /\[([^\]]+)\]\(([^)]+)\)/,
|
|
384
|
+
render: (match, key) => (
|
|
385
|
+
<a
|
|
386
|
+
key={key}
|
|
387
|
+
href={match[2]}
|
|
388
|
+
target="_blank"
|
|
389
|
+
rel="noopener noreferrer"
|
|
390
|
+
className="text-accent-cyan no-underline hover:underline"
|
|
391
|
+
>
|
|
392
|
+
{match[1]}
|
|
393
|
+
</a>
|
|
394
|
+
),
|
|
395
|
+
},
|
|
396
|
+
// Bold **text** (must come before italic *)
|
|
397
|
+
{
|
|
398
|
+
regex: /\*\*([^*]+)\*\*/,
|
|
399
|
+
render: (match, key) => (
|
|
400
|
+
<strong key={key} className="font-semibold text-text-primary">
|
|
401
|
+
{formatInlineMarkdown(match[1], mentions, `${key}-inner`)}
|
|
402
|
+
</strong>
|
|
403
|
+
),
|
|
404
|
+
},
|
|
405
|
+
// Bold __text__
|
|
406
|
+
{
|
|
407
|
+
regex: /__([^_]+)__/,
|
|
408
|
+
render: (match, key) => (
|
|
409
|
+
<strong key={key} className="font-semibold text-text-primary">
|
|
410
|
+
{formatInlineMarkdown(match[1], mentions, `${key}-inner`)}
|
|
411
|
+
</strong>
|
|
412
|
+
),
|
|
413
|
+
},
|
|
414
|
+
// Italic _text_ (using underscore to avoid conflict with bold **)
|
|
415
|
+
{
|
|
416
|
+
regex: /_([^_]+)_/,
|
|
417
|
+
render: (match, key) => (
|
|
418
|
+
<em key={key} className="italic">
|
|
419
|
+
{formatInlineMarkdown(match[1], mentions, `${key}-inner`)}
|
|
420
|
+
</em>
|
|
421
|
+
),
|
|
422
|
+
},
|
|
423
|
+
// Italic *text* (single asterisk - bold ** is matched first due to ordering)
|
|
424
|
+
{
|
|
425
|
+
regex: /\*([^*]+)\*/,
|
|
426
|
+
render: (match, key) => (
|
|
427
|
+
<em key={key} className="italic">
|
|
428
|
+
{formatInlineMarkdown(match[1], mentions, `${key}-inner`)}
|
|
429
|
+
</em>
|
|
430
|
+
),
|
|
431
|
+
},
|
|
432
|
+
// Strikethrough ~~text~~
|
|
433
|
+
{
|
|
434
|
+
regex: /~~([^~]+)~~/,
|
|
435
|
+
render: (match, key) => (
|
|
436
|
+
<del key={key} className="line-through text-text-muted">
|
|
437
|
+
{match[1]}
|
|
438
|
+
</del>
|
|
439
|
+
),
|
|
440
|
+
},
|
|
441
|
+
// URL
|
|
442
|
+
{
|
|
443
|
+
regex: /https?:\/\/[^\s]+/,
|
|
444
|
+
render: (match, key) => (
|
|
445
|
+
<a
|
|
446
|
+
key={key}
|
|
447
|
+
href={match[0]}
|
|
448
|
+
target="_blank"
|
|
449
|
+
rel="noopener noreferrer"
|
|
450
|
+
className="text-accent-cyan no-underline hover:underline"
|
|
451
|
+
>
|
|
452
|
+
{match[0]}
|
|
453
|
+
</a>
|
|
454
|
+
),
|
|
455
|
+
},
|
|
456
|
+
];
|
|
457
|
+
|
|
458
|
+
while (remaining.length > 0) {
|
|
459
|
+
// Find the earliest match among all patterns
|
|
460
|
+
let earliestMatch: { pattern: typeof patterns[0]; match: RegExpMatchArray; index: number } | null = null;
|
|
461
|
+
|
|
462
|
+
for (const pattern of patterns) {
|
|
463
|
+
const match = remaining.match(pattern.regex);
|
|
464
|
+
if (match && match.index !== undefined) {
|
|
465
|
+
if (!earliestMatch || match.index < earliestMatch.index) {
|
|
466
|
+
earliestMatch = { pattern, match, index: match.index };
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (!earliestMatch) {
|
|
472
|
+
// No more matches, add remaining text
|
|
473
|
+
parts.push(highlightMentions(remaining, mentions, `${keyPrefix}-text-${partIndex}`));
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Add text before the match
|
|
478
|
+
if (earliestMatch.index > 0) {
|
|
479
|
+
const textBefore = remaining.slice(0, earliestMatch.index);
|
|
480
|
+
parts.push(highlightMentions(textBefore, mentions, `${keyPrefix}-pre-${partIndex}`));
|
|
481
|
+
partIndex++;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Render the matched element
|
|
485
|
+
parts.push(earliestMatch.pattern.render(earliestMatch.match, `${keyPrefix}-el-${partIndex}`));
|
|
486
|
+
partIndex++;
|
|
487
|
+
|
|
488
|
+
// Move past the match
|
|
489
|
+
remaining = remaining.slice(earliestMatch.index + earliestMatch.match[0].length);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return parts.length === 1 ? parts[0] : parts;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Format a single line, detecting headings, lists, and inline markdown
|
|
497
|
+
*/
|
|
498
|
+
function formatLine(line: string, mentions?: string[]): React.ReactNode {
|
|
499
|
+
// Check for headings
|
|
500
|
+
const headingLevel = getHeadingLevel(line);
|
|
501
|
+
if (headingLevel > 0) {
|
|
502
|
+
const content = line.replace(/^#{1,6}\s+/, '');
|
|
503
|
+
// Use inline styles for font-size to override parent's text-sm
|
|
504
|
+
const headingStyles: Record<number, { className: string; style: React.CSSProperties }> = {
|
|
505
|
+
1: {
|
|
506
|
+
className: 'font-bold text-text-primary mt-4 mb-2 pb-1 border-b border-border-subtle',
|
|
507
|
+
style: { fontSize: '1.5rem', lineHeight: '2rem' }, // 24px
|
|
508
|
+
},
|
|
509
|
+
2: {
|
|
510
|
+
className: 'font-bold text-text-primary mt-3 mb-2',
|
|
511
|
+
style: { fontSize: '1.25rem', lineHeight: '1.75rem' }, // 20px
|
|
512
|
+
},
|
|
513
|
+
3: {
|
|
514
|
+
className: 'font-semibold text-text-primary mt-2.5 mb-1.5',
|
|
515
|
+
style: { fontSize: '1.125rem', lineHeight: '1.5rem' }, // 18px
|
|
516
|
+
},
|
|
517
|
+
4: {
|
|
518
|
+
className: 'font-semibold text-text-primary mt-2 mb-1',
|
|
519
|
+
style: { fontSize: '1rem', lineHeight: '1.5rem' }, // 16px
|
|
520
|
+
},
|
|
521
|
+
5: {
|
|
522
|
+
className: 'font-medium text-text-secondary mt-1.5 mb-1',
|
|
523
|
+
style: { fontSize: '0.875rem', lineHeight: '1.25rem' }, // 14px
|
|
524
|
+
},
|
|
525
|
+
6: {
|
|
526
|
+
className: 'font-medium text-text-muted mt-1 mb-0.5 uppercase tracking-wide',
|
|
527
|
+
style: { fontSize: '0.75rem', lineHeight: '1rem' }, // 12px
|
|
528
|
+
},
|
|
529
|
+
};
|
|
530
|
+
const { className, style } = headingStyles[headingLevel] || headingStyles[3];
|
|
531
|
+
return (
|
|
532
|
+
<div className={className} style={style}>
|
|
533
|
+
{formatInlineMarkdown(content, mentions, 'heading')}
|
|
534
|
+
</div>
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Check for list items
|
|
539
|
+
const listInfo = getListInfo(line);
|
|
540
|
+
if (listInfo.type) {
|
|
541
|
+
const indentStyle = { marginLeft: `${listInfo.indent * 0.5 + 0.5}rem` };
|
|
542
|
+
return (
|
|
543
|
+
<div className="flex items-start gap-2 my-0.5" style={indentStyle}>
|
|
544
|
+
<span className="text-accent-cyan flex-shrink-0 w-4 text-center">
|
|
545
|
+
{listInfo.type === 'bullet' ? '•' : line.match(/^\s*(\d+\.)/)?.[1]}
|
|
546
|
+
</span>
|
|
547
|
+
<span>{formatInlineMarkdown(listInfo.content, mentions, 'list')}</span>
|
|
548
|
+
</div>
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Regular line with inline formatting
|
|
553
|
+
return formatInlineMarkdown(line, mentions, 'line');
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function escapeRegExp(value: string): string {
|
|
557
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function highlightMentions(text: string, mentions: string[] | undefined, keyPrefix: string): React.ReactNode {
|
|
561
|
+
if (!mentions || mentions.length === 0) {
|
|
562
|
+
return text;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const escapedMentions = mentions.map(escapeRegExp).filter(Boolean);
|
|
566
|
+
if (escapedMentions.length === 0) {
|
|
567
|
+
return text;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const pattern = new RegExp(`@(${escapedMentions.join('|')})\\b`, 'g');
|
|
571
|
+
const nodes: React.ReactNode[] = [];
|
|
572
|
+
let lastIndex = 0;
|
|
573
|
+
let match;
|
|
574
|
+
|
|
575
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
576
|
+
if (match.index > lastIndex) {
|
|
577
|
+
nodes.push(text.slice(lastIndex, match.index));
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
nodes.push(
|
|
581
|
+
<span
|
|
582
|
+
key={`${keyPrefix}-mention-${match.index}`}
|
|
583
|
+
className="px-1 py-0.5 bg-accent-cyan/20 text-accent-cyan rounded"
|
|
584
|
+
>
|
|
585
|
+
@{match[1]}
|
|
586
|
+
</span>
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
lastIndex = match.index + match[0].length;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (lastIndex < text.length) {
|
|
593
|
+
nodes.push(text.slice(lastIndex));
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return nodes.length > 0 ? nodes : text;
|
|
597
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard V2 - Main Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Exports all utilities, components, and types for the v2 dashboard.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Types
|
|
8
|
+
export * from './types/index.js';
|
|
9
|
+
|
|
10
|
+
// Color coding utilities
|
|
11
|
+
export {
|
|
12
|
+
getAgentColor,
|
|
13
|
+
getAgentPrefix,
|
|
14
|
+
getAgentInitials,
|
|
15
|
+
parseAgentHierarchy,
|
|
16
|
+
groupAgentsByPrefix,
|
|
17
|
+
sortAgentsByHierarchy,
|
|
18
|
+
getAgentColorVars,
|
|
19
|
+
STATUS_COLORS,
|
|
20
|
+
type ColorScheme,
|
|
21
|
+
type AgentStatus,
|
|
22
|
+
} from './lib/colors.js';
|
|
23
|
+
|
|
24
|
+
// Hierarchy utilities
|
|
25
|
+
export {
|
|
26
|
+
buildAgentTree,
|
|
27
|
+
flattenTree,
|
|
28
|
+
groupAgents,
|
|
29
|
+
getAgentDisplayName,
|
|
30
|
+
getAgentBreadcrumb,
|
|
31
|
+
matchesSearch,
|
|
32
|
+
filterAgents,
|
|
33
|
+
getGroupStats,
|
|
34
|
+
type HierarchyNode,
|
|
35
|
+
type AgentGroup,
|
|
36
|
+
} from './lib/hierarchy.js';
|
|
37
|
+
|
|
38
|
+
// API utilities
|
|
39
|
+
export {
|
|
40
|
+
api,
|
|
41
|
+
DashboardWebSocket,
|
|
42
|
+
getWebSocket,
|
|
43
|
+
type DashboardData,
|
|
44
|
+
} from './lib/api.js';
|
|
45
|
+
|
|
46
|
+
// Reaction components
|
|
47
|
+
export { ReactionChips } from './components/ReactionChips.js';
|
|
48
|
+
export { ReactionPicker } from './components/ReactionPicker.js';
|
|
49
|
+
|
|
50
|
+
// Thread hook
|
|
51
|
+
export { useThread } from './components/hooks/useThread.js';
|
|
52
|
+
|
|
53
|
+
// React Components
|
|
54
|
+
// These require React to be installed. For Next.js consumers, use
|
|
55
|
+
// transpilePackages: ['@agent-relay/dashboard'] and import from
|
|
56
|
+
// '@agent-relay/dashboard/components/App' etc.
|
|
57
|
+
export { App } from './components/App.js';
|
|
58
|
+
export type { AppProps } from './components/App.js';
|
|
59
|
+
export { MessageList } from './components/MessageList.js';
|
|
60
|
+
export { ThreadPanel } from './components/ThreadPanel.js';
|
|
61
|
+
|
|
62
|
+
// Config
|
|
63
|
+
export { config, getWebSocketUrl, getApiBaseUrl } from './lib/config.js';
|