@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,719 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XTermLogViewer Component
|
|
3
|
+
*
|
|
4
|
+
* Terminal-based log viewer using xterm.js for proper ANSI sequence handling.
|
|
5
|
+
* Used in panel mode for full terminal emulation with colors and formatting.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useRef, useEffect, useCallback, useState } from 'react';
|
|
9
|
+
import { Terminal } from '@xterm/xterm';
|
|
10
|
+
import { FitAddon } from '@xterm/addon-fit';
|
|
11
|
+
import { SearchAddon } from '@xterm/addon-search';
|
|
12
|
+
import { getAgentColor } from '../lib/colors';
|
|
13
|
+
import { useWorkspaceWsUrl } from './WorkspaceContext';
|
|
14
|
+
|
|
15
|
+
export interface XTermLogViewerProps {
|
|
16
|
+
/** Agent name to stream logs from */
|
|
17
|
+
agentName: string;
|
|
18
|
+
/** Maximum height of the terminal */
|
|
19
|
+
maxHeight?: string;
|
|
20
|
+
/** Whether to show the header bar */
|
|
21
|
+
showHeader?: boolean;
|
|
22
|
+
/** Callback when close button is clicked */
|
|
23
|
+
onClose?: () => void;
|
|
24
|
+
/** Custom class name */
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Theme matching the dashboard dark theme
|
|
29
|
+
const TERMINAL_THEME = {
|
|
30
|
+
background: '#0d0f14',
|
|
31
|
+
foreground: '#c9d1d9',
|
|
32
|
+
cursor: '#58a6ff',
|
|
33
|
+
cursorAccent: '#0d0f14',
|
|
34
|
+
selectionBackground: '#264f78',
|
|
35
|
+
selectionForeground: '#ffffff',
|
|
36
|
+
black: '#484f58',
|
|
37
|
+
red: '#f85149',
|
|
38
|
+
green: '#3fb950',
|
|
39
|
+
yellow: '#d29922',
|
|
40
|
+
blue: '#58a6ff',
|
|
41
|
+
magenta: '#bc8cff',
|
|
42
|
+
cyan: '#39c5cf',
|
|
43
|
+
white: '#b1bac4',
|
|
44
|
+
brightBlack: '#6e7681',
|
|
45
|
+
brightRed: '#ff7b72',
|
|
46
|
+
brightGreen: '#56d364',
|
|
47
|
+
brightYellow: '#e3b341',
|
|
48
|
+
brightBlue: '#79c0ff',
|
|
49
|
+
brightMagenta: '#d2a8ff',
|
|
50
|
+
brightCyan: '#56d4dd',
|
|
51
|
+
brightWhite: '#ffffff',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// getLogStreamUrl removed - now using useWorkspaceWsUrl hook
|
|
55
|
+
|
|
56
|
+
export function XTermLogViewer({
|
|
57
|
+
agentName,
|
|
58
|
+
maxHeight = '500px',
|
|
59
|
+
showHeader = true,
|
|
60
|
+
onClose,
|
|
61
|
+
className = '',
|
|
62
|
+
}: XTermLogViewerProps) {
|
|
63
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
64
|
+
const terminalRef = useRef<Terminal | null>(null);
|
|
65
|
+
const fitAddonRef = useRef<FitAddon | null>(null);
|
|
66
|
+
const searchAddonRef = useRef<SearchAddon | null>(null);
|
|
67
|
+
const wsRef = useRef<WebSocket | null>(null);
|
|
68
|
+
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
69
|
+
const reconnectAttemptsRef = useRef(0);
|
|
70
|
+
|
|
71
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
72
|
+
const [isConnecting, setIsConnecting] = useState(false);
|
|
73
|
+
const [error, setError] = useState<Error | null>(null);
|
|
74
|
+
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
|
75
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
76
|
+
const [lineCount, setLineCount] = useState(0);
|
|
77
|
+
const [isTerminalReady, setIsTerminalReady] = useState(false);
|
|
78
|
+
|
|
79
|
+
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
80
|
+
const colors = getAgentColor(agentName);
|
|
81
|
+
|
|
82
|
+
// Get WebSocket URL from workspace context (handles cloud vs local mode)
|
|
83
|
+
const logStreamUrl = useWorkspaceWsUrl(`/ws/logs/${encodeURIComponent(agentName)}`);
|
|
84
|
+
|
|
85
|
+
// Initialize terminal
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (!containerRef.current) return;
|
|
88
|
+
|
|
89
|
+
const terminal = new Terminal({
|
|
90
|
+
theme: TERMINAL_THEME,
|
|
91
|
+
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
|
|
92
|
+
fontSize: 12,
|
|
93
|
+
lineHeight: 1.4,
|
|
94
|
+
convertEol: true,
|
|
95
|
+
scrollback: 10000,
|
|
96
|
+
cursorBlink: false,
|
|
97
|
+
cursorStyle: 'bar',
|
|
98
|
+
disableStdin: true, // Read-only log viewer
|
|
99
|
+
allowProposedApi: true,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const fitAddon = new FitAddon();
|
|
103
|
+
const searchAddon = new SearchAddon();
|
|
104
|
+
|
|
105
|
+
terminal.loadAddon(fitAddon);
|
|
106
|
+
terminal.loadAddon(searchAddon);
|
|
107
|
+
|
|
108
|
+
terminal.open(containerRef.current);
|
|
109
|
+
fitAddon.fit();
|
|
110
|
+
|
|
111
|
+
terminalRef.current = terminal;
|
|
112
|
+
fitAddonRef.current = fitAddon;
|
|
113
|
+
searchAddonRef.current = searchAddon;
|
|
114
|
+
setIsTerminalReady(true);
|
|
115
|
+
|
|
116
|
+
// Handle resize
|
|
117
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
118
|
+
fitAddon.fit();
|
|
119
|
+
});
|
|
120
|
+
resizeObserver.observe(containerRef.current);
|
|
121
|
+
|
|
122
|
+
return () => {
|
|
123
|
+
resizeObserver.disconnect();
|
|
124
|
+
terminal.dispose();
|
|
125
|
+
terminalRef.current = null;
|
|
126
|
+
fitAddonRef.current = null;
|
|
127
|
+
searchAddonRef.current = null;
|
|
128
|
+
setIsTerminalReady(false);
|
|
129
|
+
};
|
|
130
|
+
}, []);
|
|
131
|
+
|
|
132
|
+
// Mobile touch scrolling - attach handlers to container, not viewport
|
|
133
|
+
// xterm.js renders to a canvas which intercepts events; we need to handle
|
|
134
|
+
// touch scrolling at the container level and use xterm's scroll API directly
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (!containerRef.current || !isTerminalReady || !terminalRef.current) return;
|
|
137
|
+
|
|
138
|
+
// Only enable on touch devices
|
|
139
|
+
if (typeof window !== 'undefined' &&
|
|
140
|
+
!window.matchMedia('(pointer: coarse)').matches) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const container = containerRef.current;
|
|
145
|
+
const terminal = terminalRef.current;
|
|
146
|
+
|
|
147
|
+
// Calculate line height from terminal settings
|
|
148
|
+
const fontSize = 12; // matches Terminal config
|
|
149
|
+
const lineHeight = 1.4; // matches Terminal config
|
|
150
|
+
const lineHeightPx = fontSize * lineHeight;
|
|
151
|
+
|
|
152
|
+
let startY = 0;
|
|
153
|
+
let lastY = 0;
|
|
154
|
+
let scrollAccumulator = 0; // accumulate sub-line scroll deltas
|
|
155
|
+
let isTouchScrolling = false;
|
|
156
|
+
|
|
157
|
+
const handleTouchStart = (event: TouchEvent) => {
|
|
158
|
+
if (event.touches.length !== 1) return;
|
|
159
|
+
startY = event.touches[0].clientY;
|
|
160
|
+
lastY = startY;
|
|
161
|
+
scrollAccumulator = 0;
|
|
162
|
+
isTouchScrolling = false;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const handleTouchMove = (event: TouchEvent) => {
|
|
166
|
+
if (event.touches.length !== 1) return;
|
|
167
|
+
|
|
168
|
+
const currentY = event.touches[0].clientY;
|
|
169
|
+
const delta = lastY - currentY; // positive = scroll down
|
|
170
|
+
lastY = currentY;
|
|
171
|
+
|
|
172
|
+
// Determine if this is a scroll gesture (vs a tap)
|
|
173
|
+
if (!isTouchScrolling && Math.abs(currentY - startY) > 10) {
|
|
174
|
+
isTouchScrolling = true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!isTouchScrolling) return;
|
|
178
|
+
|
|
179
|
+
// Accumulate scroll delta for smooth scrolling
|
|
180
|
+
scrollAccumulator += delta;
|
|
181
|
+
|
|
182
|
+
// Convert accumulated pixels to lines and scroll
|
|
183
|
+
const linesToScroll = Math.trunc(scrollAccumulator / lineHeightPx);
|
|
184
|
+
if (linesToScroll !== 0) {
|
|
185
|
+
terminal.scrollLines(linesToScroll);
|
|
186
|
+
scrollAccumulator -= linesToScroll * lineHeightPx;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Prevent page scroll while scrolling terminal content
|
|
190
|
+
event.preventDefault();
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const handleTouchEnd = () => {
|
|
194
|
+
isTouchScrolling = false;
|
|
195
|
+
scrollAccumulator = 0;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Attach to container - this captures touches over the entire terminal area
|
|
199
|
+
container.addEventListener('touchstart', handleTouchStart, { passive: true });
|
|
200
|
+
container.addEventListener('touchmove', handleTouchMove, { passive: false });
|
|
201
|
+
container.addEventListener('touchend', handleTouchEnd, { passive: true });
|
|
202
|
+
container.addEventListener('touchcancel', handleTouchEnd, { passive: true });
|
|
203
|
+
|
|
204
|
+
return () => {
|
|
205
|
+
container.removeEventListener('touchstart', handleTouchStart);
|
|
206
|
+
container.removeEventListener('touchmove', handleTouchMove);
|
|
207
|
+
container.removeEventListener('touchend', handleTouchEnd);
|
|
208
|
+
container.removeEventListener('touchcancel', handleTouchEnd);
|
|
209
|
+
};
|
|
210
|
+
}, [isTerminalReady]);
|
|
211
|
+
|
|
212
|
+
// Connect to WebSocket
|
|
213
|
+
const connect = useCallback(() => {
|
|
214
|
+
if (wsRef.current?.readyState === WebSocket.OPEN ||
|
|
215
|
+
wsRef.current?.readyState === WebSocket.CONNECTING) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
setIsConnecting(true);
|
|
220
|
+
setError(null);
|
|
221
|
+
|
|
222
|
+
const ws = new WebSocket(logStreamUrl);
|
|
223
|
+
wsRef.current = ws;
|
|
224
|
+
|
|
225
|
+
ws.onopen = () => {
|
|
226
|
+
setIsConnected(true);
|
|
227
|
+
setIsConnecting(false);
|
|
228
|
+
setError(null);
|
|
229
|
+
reconnectAttemptsRef.current = 0;
|
|
230
|
+
|
|
231
|
+
terminalRef.current?.writeln(`\x1b[90m[Connected to ${agentName} log stream]\x1b[0m`);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
ws.onclose = (event) => {
|
|
235
|
+
setIsConnected(false);
|
|
236
|
+
setIsConnecting(false);
|
|
237
|
+
wsRef.current = null;
|
|
238
|
+
|
|
239
|
+
if (reconnectTimeoutRef.current) {
|
|
240
|
+
clearTimeout(reconnectTimeoutRef.current);
|
|
241
|
+
reconnectTimeoutRef.current = null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Don't reconnect for agent not found
|
|
245
|
+
if (event.code === 4404) {
|
|
246
|
+
terminalRef.current?.writeln(`\x1b[31m[Agent not found]\x1b[0m`);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Schedule reconnect
|
|
251
|
+
const delay = Math.min(1000 * Math.pow(2, reconnectAttemptsRef.current), 30000);
|
|
252
|
+
reconnectAttemptsRef.current++;
|
|
253
|
+
|
|
254
|
+
terminalRef.current?.writeln(`\x1b[90m[Disconnected. Reconnecting in ${delay / 1000}s...]\x1b[0m`);
|
|
255
|
+
|
|
256
|
+
reconnectTimeoutRef.current = setTimeout(() => {
|
|
257
|
+
connect();
|
|
258
|
+
}, delay);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
ws.onerror = () => {
|
|
262
|
+
setError(new Error('WebSocket connection error'));
|
|
263
|
+
setIsConnecting(false);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
ws.onmessage = (event) => {
|
|
267
|
+
try {
|
|
268
|
+
const data = JSON.parse(event.data);
|
|
269
|
+
|
|
270
|
+
// Handle different message types
|
|
271
|
+
if (data.type === 'error') {
|
|
272
|
+
terminalRef.current?.writeln(`\x1b[31mError: ${data.error}\x1b[0m`);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (data.type === 'subscribed') {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Handle history (initial log dump)
|
|
281
|
+
if (data.type === 'history' && Array.isArray(data.lines)) {
|
|
282
|
+
data.lines.forEach((line: string) => {
|
|
283
|
+
terminalRef.current?.writeln(line);
|
|
284
|
+
setLineCount((c) => c + 1);
|
|
285
|
+
});
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Handle live output
|
|
290
|
+
if (data.type === 'log' || data.type === 'output') {
|
|
291
|
+
const content = data.content || data.data || data.message || '';
|
|
292
|
+
if (content) {
|
|
293
|
+
// Write raw content - xterm.js handles ANSI codes natively
|
|
294
|
+
terminalRef.current?.write(content);
|
|
295
|
+
// Count newlines for line count
|
|
296
|
+
const newlines = (content.match(/\n/g) || []).length;
|
|
297
|
+
if (newlines > 0) {
|
|
298
|
+
setLineCount((c) => c + newlines);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Handle batch of lines
|
|
305
|
+
if (data.lines && Array.isArray(data.lines)) {
|
|
306
|
+
data.lines.forEach((line: string | { content: string }) => {
|
|
307
|
+
const content = typeof line === 'string' ? line : line.content;
|
|
308
|
+
terminalRef.current?.writeln(content);
|
|
309
|
+
setLineCount((c) => c + 1);
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
} catch {
|
|
313
|
+
// Plain text message
|
|
314
|
+
if (typeof event.data === 'string') {
|
|
315
|
+
terminalRef.current?.write(event.data);
|
|
316
|
+
const newlines = (event.data.match(/\n/g) || []).length;
|
|
317
|
+
if (newlines > 0) {
|
|
318
|
+
setLineCount((c) => c + newlines);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
}, [logStreamUrl, agentName]);
|
|
324
|
+
|
|
325
|
+
// Disconnect from WebSocket
|
|
326
|
+
const disconnect = useCallback(() => {
|
|
327
|
+
if (reconnectTimeoutRef.current) {
|
|
328
|
+
clearTimeout(reconnectTimeoutRef.current);
|
|
329
|
+
reconnectTimeoutRef.current = null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (wsRef.current) {
|
|
333
|
+
wsRef.current.close();
|
|
334
|
+
wsRef.current = null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
setIsConnected(false);
|
|
338
|
+
setIsConnecting(false);
|
|
339
|
+
}, []);
|
|
340
|
+
|
|
341
|
+
// Clear terminal
|
|
342
|
+
const clear = useCallback(() => {
|
|
343
|
+
terminalRef.current?.clear();
|
|
344
|
+
setLineCount(0);
|
|
345
|
+
}, []);
|
|
346
|
+
|
|
347
|
+
// Search functionality
|
|
348
|
+
const handleSearch = useCallback((query: string) => {
|
|
349
|
+
setSearchQuery(query);
|
|
350
|
+
if (query && searchAddonRef.current) {
|
|
351
|
+
searchAddonRef.current.findNext(query, { caseSensitive: false });
|
|
352
|
+
}
|
|
353
|
+
}, []);
|
|
354
|
+
|
|
355
|
+
const findNext = useCallback(() => {
|
|
356
|
+
if (searchQuery && searchAddonRef.current) {
|
|
357
|
+
searchAddonRef.current.findNext(searchQuery, { caseSensitive: false });
|
|
358
|
+
}
|
|
359
|
+
}, [searchQuery]);
|
|
360
|
+
|
|
361
|
+
const findPrevious = useCallback(() => {
|
|
362
|
+
if (searchQuery && searchAddonRef.current) {
|
|
363
|
+
searchAddonRef.current.findPrevious(searchQuery, { caseSensitive: false });
|
|
364
|
+
}
|
|
365
|
+
}, [searchQuery]);
|
|
366
|
+
|
|
367
|
+
// Auto-connect on mount
|
|
368
|
+
useEffect(() => {
|
|
369
|
+
connect();
|
|
370
|
+
return () => {
|
|
371
|
+
disconnect();
|
|
372
|
+
};
|
|
373
|
+
}, [connect, disconnect]);
|
|
374
|
+
|
|
375
|
+
// Keyboard shortcuts
|
|
376
|
+
useEffect(() => {
|
|
377
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
378
|
+
// Cmd/Ctrl + F to open search
|
|
379
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'f') {
|
|
380
|
+
e.preventDefault();
|
|
381
|
+
setIsSearchOpen(true);
|
|
382
|
+
setTimeout(() => searchInputRef.current?.focus(), 0);
|
|
383
|
+
}
|
|
384
|
+
// Escape to close search
|
|
385
|
+
if (e.key === 'Escape' && isSearchOpen) {
|
|
386
|
+
setIsSearchOpen(false);
|
|
387
|
+
setSearchQuery('');
|
|
388
|
+
}
|
|
389
|
+
// Enter to find next
|
|
390
|
+
if (e.key === 'Enter' && isSearchOpen) {
|
|
391
|
+
e.preventDefault();
|
|
392
|
+
if (e.shiftKey) {
|
|
393
|
+
findPrevious();
|
|
394
|
+
} else {
|
|
395
|
+
findNext();
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
401
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
402
|
+
}, [isSearchOpen, findNext, findPrevious]);
|
|
403
|
+
|
|
404
|
+
return (
|
|
405
|
+
<div
|
|
406
|
+
className={`xterm-log-viewer flex flex-col min-h-0 rounded-xl overflow-hidden border border-[#2a2d35] shadow-2xl ${className}`}
|
|
407
|
+
style={{
|
|
408
|
+
background: 'linear-gradient(180deg, #0d0f14 0%, #0a0c10 100%)',
|
|
409
|
+
boxShadow: `0 0 60px -15px ${colors.primary}25, 0 25px 50px -12px rgba(0, 0, 0, 0.8), inset 0 1px 0 rgba(255,255,255,0.02)`,
|
|
410
|
+
}}
|
|
411
|
+
>
|
|
412
|
+
{/* Mobile touch scroll styles for xterm.js
|
|
413
|
+
Touch scrolling is handled via JavaScript (touchstart/touchmove on container).
|
|
414
|
+
We set touch-action: none to prevent browser's default scroll handling
|
|
415
|
+
so our custom handler has full control. */}
|
|
416
|
+
<style>{`
|
|
417
|
+
.xterm-log-viewer .xterm {
|
|
418
|
+
height: 100%;
|
|
419
|
+
}
|
|
420
|
+
.xterm-log-viewer .xterm-viewport {
|
|
421
|
+
height: 100%;
|
|
422
|
+
max-height: 100%;
|
|
423
|
+
overscroll-behavior: contain;
|
|
424
|
+
}
|
|
425
|
+
/* On touch devices, disable browser touch handling so our JS handler works */
|
|
426
|
+
@media (pointer: coarse) {
|
|
427
|
+
.xterm-log-viewer .xterm,
|
|
428
|
+
.xterm-log-viewer .xterm-viewport,
|
|
429
|
+
.xterm-log-viewer .xterm-screen,
|
|
430
|
+
.xterm-log-viewer .xterm-screen canvas {
|
|
431
|
+
touch-action: none;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
`}</style>
|
|
435
|
+
{/* Header */}
|
|
436
|
+
{showHeader && (
|
|
437
|
+
<div
|
|
438
|
+
className="flex items-center justify-between px-4 py-3 border-b border-[#21262d]"
|
|
439
|
+
style={{
|
|
440
|
+
background: 'linear-gradient(180deg, #161b22 0%, #0d1117 100%)',
|
|
441
|
+
}}
|
|
442
|
+
>
|
|
443
|
+
<div className="flex items-center gap-3">
|
|
444
|
+
<div className="flex items-center gap-2">
|
|
445
|
+
{/* Traffic light buttons */}
|
|
446
|
+
<div className="flex gap-1.5">
|
|
447
|
+
<div className="w-3 h-3 rounded-full bg-[#ff5f56] border border-[#e0443e] transition-shadow hover:shadow-[0_0_8px_rgba(255,95,86,0.5)]" />
|
|
448
|
+
<div className="w-3 h-3 rounded-full bg-[#ffbd2e] border border-[#dea123] transition-shadow hover:shadow-[0_0_8px_rgba(255,189,46,0.5)]" />
|
|
449
|
+
<div className="w-3 h-3 rounded-full bg-[#27c93f] border border-[#1aab29] transition-shadow hover:shadow-[0_0_8px_rgba(39,201,63,0.5)]" />
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
452
|
+
<div className="w-px h-4 bg-[#30363d]" />
|
|
453
|
+
<div className="flex items-center gap-2">
|
|
454
|
+
<TerminalIcon />
|
|
455
|
+
<span className="text-sm font-semibold" style={{ color: colors.primary }}>
|
|
456
|
+
{agentName}
|
|
457
|
+
</span>
|
|
458
|
+
<ConnectionBadge isConnected={isConnected} isConnecting={isConnecting} />
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
<div className="flex items-center gap-1.5">
|
|
462
|
+
{/* Search toggle */}
|
|
463
|
+
<button
|
|
464
|
+
className={`p-1.5 rounded-lg transition-all duration-200 ${
|
|
465
|
+
isSearchOpen
|
|
466
|
+
? 'bg-accent-cyan/20 text-accent-cyan shadow-[0_0_12px_rgba(0,217,255,0.25)]'
|
|
467
|
+
: 'hover:bg-[#21262d] text-[#8b949e] hover:text-[#c9d1d9]'
|
|
468
|
+
}`}
|
|
469
|
+
onClick={() => {
|
|
470
|
+
setIsSearchOpen(!isSearchOpen);
|
|
471
|
+
if (!isSearchOpen) {
|
|
472
|
+
setTimeout(() => searchInputRef.current?.focus(), 0);
|
|
473
|
+
}
|
|
474
|
+
}}
|
|
475
|
+
title="Search (Cmd+F)"
|
|
476
|
+
>
|
|
477
|
+
<SearchIcon />
|
|
478
|
+
</button>
|
|
479
|
+
{/* Clear logs */}
|
|
480
|
+
<button
|
|
481
|
+
className="p-1.5 rounded-lg hover:bg-[#21262d] text-[#8b949e] hover:text-[#c9d1d9] transition-all duration-200"
|
|
482
|
+
onClick={clear}
|
|
483
|
+
title="Clear logs"
|
|
484
|
+
>
|
|
485
|
+
<TrashIcon />
|
|
486
|
+
</button>
|
|
487
|
+
{/* Connection toggle */}
|
|
488
|
+
<button
|
|
489
|
+
className={`p-1.5 rounded-lg transition-all duration-200 ${
|
|
490
|
+
isConnected
|
|
491
|
+
? 'hover:bg-[#f85149]/10 text-[#8b949e] hover:text-[#f85149]'
|
|
492
|
+
: 'bg-[#3fb950]/20 text-[#3fb950] shadow-[0_0_12px_rgba(63,185,80,0.25)]'
|
|
493
|
+
}`}
|
|
494
|
+
onClick={isConnected ? disconnect : connect}
|
|
495
|
+
title={isConnected ? 'Disconnect' : 'Connect'}
|
|
496
|
+
>
|
|
497
|
+
{isConnected ? <PauseIcon /> : <PlayIcon />}
|
|
498
|
+
</button>
|
|
499
|
+
{/* Close button */}
|
|
500
|
+
{onClose && (
|
|
501
|
+
<>
|
|
502
|
+
<div className="w-px h-4 bg-[#30363d] mx-1" />
|
|
503
|
+
<button
|
|
504
|
+
className="p-1.5 rounded-lg hover:bg-[#f85149]/10 text-[#8b949e] hover:text-[#f85149] transition-all duration-200"
|
|
505
|
+
onClick={onClose}
|
|
506
|
+
title="Close"
|
|
507
|
+
>
|
|
508
|
+
<CloseIcon />
|
|
509
|
+
</button>
|
|
510
|
+
</>
|
|
511
|
+
)}
|
|
512
|
+
</div>
|
|
513
|
+
</div>
|
|
514
|
+
)}
|
|
515
|
+
|
|
516
|
+
{/* Search bar */}
|
|
517
|
+
{isSearchOpen && (
|
|
518
|
+
<div className="flex items-center gap-3 px-4 py-2 border-b border-[#21262d] bg-[#161b22]">
|
|
519
|
+
<SearchIcon />
|
|
520
|
+
<input
|
|
521
|
+
ref={searchInputRef}
|
|
522
|
+
type="text"
|
|
523
|
+
className="flex-1 bg-transparent border-none text-sm text-[#c9d1d9] placeholder:text-[#484f58] outline-none font-mono"
|
|
524
|
+
placeholder="Search logs... (Enter: next, Shift+Enter: prev)"
|
|
525
|
+
value={searchQuery}
|
|
526
|
+
onChange={(e) => handleSearch(e.target.value)}
|
|
527
|
+
/>
|
|
528
|
+
<div className="flex items-center gap-1">
|
|
529
|
+
<button
|
|
530
|
+
className="px-2 py-0.5 text-xs rounded bg-[#21262d] text-[#8b949e] hover:text-[#c9d1d9] transition-colors"
|
|
531
|
+
onClick={findPrevious}
|
|
532
|
+
title="Previous (Shift+Enter)"
|
|
533
|
+
>
|
|
534
|
+
↑
|
|
535
|
+
</button>
|
|
536
|
+
<button
|
|
537
|
+
className="px-2 py-0.5 text-xs rounded bg-[#21262d] text-[#8b949e] hover:text-[#c9d1d9] transition-colors"
|
|
538
|
+
onClick={findNext}
|
|
539
|
+
title="Next (Enter)"
|
|
540
|
+
>
|
|
541
|
+
↓
|
|
542
|
+
</button>
|
|
543
|
+
</div>
|
|
544
|
+
</div>
|
|
545
|
+
)}
|
|
546
|
+
|
|
547
|
+
{/* Error message */}
|
|
548
|
+
{error && (
|
|
549
|
+
<div className="px-4 py-2 bg-[#3d1d20] border-b border-[#f85149]/30 text-sm text-[#f85149] flex items-center gap-2">
|
|
550
|
+
<ErrorIcon />
|
|
551
|
+
<span>{error.message}</span>
|
|
552
|
+
<button
|
|
553
|
+
className="ml-auto text-xs px-2 py-0.5 rounded bg-[#f85149]/20 hover:bg-[#f85149]/30 transition-colors"
|
|
554
|
+
onClick={connect}
|
|
555
|
+
>
|
|
556
|
+
Retry
|
|
557
|
+
</button>
|
|
558
|
+
</div>
|
|
559
|
+
)}
|
|
560
|
+
|
|
561
|
+
{/* Terminal container - touch handlers attached via useEffect */}
|
|
562
|
+
<div
|
|
563
|
+
className="flex-1 min-h-0 overflow-hidden"
|
|
564
|
+
style={{ height: maxHeight, maxHeight, minHeight: '200px' }}
|
|
565
|
+
>
|
|
566
|
+
<div
|
|
567
|
+
ref={containerRef}
|
|
568
|
+
className="h-full w-full"
|
|
569
|
+
/>
|
|
570
|
+
</div>
|
|
571
|
+
|
|
572
|
+
{/* Footer status bar */}
|
|
573
|
+
<div
|
|
574
|
+
className="flex items-center justify-between px-4 py-2.5 border-t border-[#21262d] text-xs"
|
|
575
|
+
style={{
|
|
576
|
+
background: 'linear-gradient(180deg, #0d1117 0%, #0a0c10 100%)',
|
|
577
|
+
}}
|
|
578
|
+
>
|
|
579
|
+
<div className="flex items-center gap-3">
|
|
580
|
+
<span className="tabular-nums font-mono text-[#6e7681]">{lineCount} lines</span>
|
|
581
|
+
</div>
|
|
582
|
+
<div className="flex items-center gap-2">
|
|
583
|
+
<span className="text-[#6e7681] font-mono uppercase tracking-wider text-[10px]">
|
|
584
|
+
PTY stream
|
|
585
|
+
</span>
|
|
586
|
+
<div
|
|
587
|
+
className={`w-2 h-2 rounded-full transition-all duration-300 ${
|
|
588
|
+
isConnected
|
|
589
|
+
? 'bg-[#3fb950]'
|
|
590
|
+
: isConnecting
|
|
591
|
+
? 'bg-[#d29922] animate-pulse'
|
|
592
|
+
: 'bg-[#484f58]'
|
|
593
|
+
}`}
|
|
594
|
+
style={{
|
|
595
|
+
boxShadow: isConnected ? '0 0 8px rgba(63,185,80,0.6)' : 'none',
|
|
596
|
+
}}
|
|
597
|
+
/>
|
|
598
|
+
</div>
|
|
599
|
+
</div>
|
|
600
|
+
</div>
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Icon components
|
|
605
|
+
function TerminalIcon() {
|
|
606
|
+
return (
|
|
607
|
+
<svg
|
|
608
|
+
width="16"
|
|
609
|
+
height="16"
|
|
610
|
+
viewBox="0 0 24 24"
|
|
611
|
+
fill="none"
|
|
612
|
+
stroke="currentColor"
|
|
613
|
+
strokeWidth="2"
|
|
614
|
+
strokeLinecap="round"
|
|
615
|
+
strokeLinejoin="round"
|
|
616
|
+
className="text-[#8b949e]"
|
|
617
|
+
>
|
|
618
|
+
<polyline points="4 17 10 11 4 5" />
|
|
619
|
+
<line x1="12" y1="19" x2="20" y2="19" />
|
|
620
|
+
</svg>
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function SearchIcon() {
|
|
625
|
+
return (
|
|
626
|
+
<svg
|
|
627
|
+
width="14"
|
|
628
|
+
height="14"
|
|
629
|
+
viewBox="0 0 24 24"
|
|
630
|
+
fill="none"
|
|
631
|
+
stroke="currentColor"
|
|
632
|
+
strokeWidth="2"
|
|
633
|
+
className="text-[#8b949e]"
|
|
634
|
+
>
|
|
635
|
+
<circle cx="11" cy="11" r="8" />
|
|
636
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
637
|
+
</svg>
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function TrashIcon() {
|
|
642
|
+
return (
|
|
643
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
644
|
+
<polyline points="3 6 5 6 21 6" />
|
|
645
|
+
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
|
646
|
+
</svg>
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function CloseIcon() {
|
|
651
|
+
return (
|
|
652
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
653
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
654
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
655
|
+
</svg>
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function PlayIcon() {
|
|
660
|
+
return (
|
|
661
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
|
662
|
+
<polygon points="5 3 19 12 5 21 5 3" />
|
|
663
|
+
</svg>
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function PauseIcon() {
|
|
668
|
+
return (
|
|
669
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
|
670
|
+
<rect x="6" y="4" width="4" height="16" />
|
|
671
|
+
<rect x="14" y="4" width="4" height="16" />
|
|
672
|
+
</svg>
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function ErrorIcon() {
|
|
677
|
+
return (
|
|
678
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
679
|
+
<circle cx="12" cy="12" r="10" />
|
|
680
|
+
<line x1="12" y1="8" x2="12" y2="12" />
|
|
681
|
+
<line x1="12" y1="16" x2="12.01" y2="16" />
|
|
682
|
+
</svg>
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function ConnectionBadge({
|
|
687
|
+
isConnected,
|
|
688
|
+
isConnecting,
|
|
689
|
+
}: {
|
|
690
|
+
isConnected: boolean;
|
|
691
|
+
isConnecting: boolean;
|
|
692
|
+
}) {
|
|
693
|
+
if (isConnecting) {
|
|
694
|
+
return (
|
|
695
|
+
<span className="flex items-center gap-1 px-1.5 py-0.5 rounded-full bg-[#d29922]/20 text-[10px] text-[#d29922] uppercase tracking-wider">
|
|
696
|
+
<span className="w-1.5 h-1.5 rounded-full bg-[#d29922] animate-pulse" />
|
|
697
|
+
connecting
|
|
698
|
+
</span>
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (isConnected) {
|
|
703
|
+
return (
|
|
704
|
+
<span className="flex items-center gap-1 px-1.5 py-0.5 rounded-full bg-[#238636]/20 text-[10px] text-[#3fb950] uppercase tracking-wider">
|
|
705
|
+
<span className="w-1.5 h-1.5 rounded-full bg-[#3fb950] shadow-[0_0_4px_rgba(63,185,80,0.5)]" />
|
|
706
|
+
live
|
|
707
|
+
</span>
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return (
|
|
712
|
+
<span className="flex items-center gap-1 px-1.5 py-0.5 rounded-full bg-[#484f58]/20 text-[10px] text-[#484f58] uppercase tracking-wider">
|
|
713
|
+
<span className="w-1.5 h-1.5 rounded-full bg-[#484f58]" />
|
|
714
|
+
offline
|
|
715
|
+
</span>
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
export default XTermLogViewer;
|