agent-relay 2.0.6 → 2.0.7
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/deploy/workspace/entrypoint.sh +80 -22
- package/deploy/workspace/gh-credential-relay +90 -0
- package/dist/dashboard/out/404.html +1 -1
- package/{packages/dashboard/ui-dist/_next/static/chunks/677-7323947c23b35979.js → dist/dashboard/out/_next/static/chunks/320-22ebe7be58cf982a.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/page-9914652442f7e4fb.js +1 -0
- package/{packages/dashboard/ui-dist/_next/static/chunks/app/app/page-2e525b1dcc790967.js → dist/dashboard/out/_next/static/chunks/app/app/page-9d6bc8729b429956.js} +1 -1
- package/{packages/dashboard/ui-dist/_next/static/chunks/app/cloud/link/page-5011ae044b90449d.js → dist/dashboard/out/_next/static/chunks/app/cloud/link/page-fa1d5842aa90e8a6.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-113060009ef35bc2.js +1 -0
- package/{packages/dashboard/ui-dist/_next/static/chunks/app/history/page-b2ce7c96ed0931da.js → dist/dashboard/out/_next/static/chunks/app/history/page-9965d2483011b846.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/app/layout-6b91e33784c20610.js +1 -0
- package/{packages/dashboard/ui-dist/_next/static/chunks/app/login/page-6ec54eee75877971.js → dist/dashboard/out/_next/static/chunks/app/login/page-a0ca6f7ca6a100b8.js} +1 -1
- package/{packages/dashboard/ui-dist/_next/static/chunks/app/metrics/page-bf2cb1e5915bc92d.js → dist/dashboard/out/_next/static/chunks/app/metrics/page-1e37ef8e73940b40.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/app/{page-4e64923d73c35bc9.js → page-487fa38f041815c1.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/app/pricing/page-9db3ebdfa567a7c9.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/providers/page-bcf46064ac4474ce.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/providers/setup/[provider]/{page-84161c802b020a1f.js → page-4dbe33f0f7691b7c.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/app/signup/{page-18a4665665f6be11.js → page-1ede2205b58649ca.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/main-app-fdbeb09028f57c9f.js +1 -0
- package/dist/dashboard/out/app/onboarding.html +1 -1
- package/dist/dashboard/out/app/onboarding.txt +2 -2
- package/dist/dashboard/out/app.html +1 -1
- package/dist/dashboard/out/app.txt +2 -2
- package/dist/dashboard/out/cloud/link.html +1 -1
- package/dist/dashboard/out/cloud/link.txt +2 -2
- package/dist/dashboard/out/connect-repos.html +1 -1
- package/dist/dashboard/out/connect-repos.txt +2 -2
- package/dist/dashboard/out/history.html +1 -1
- package/dist/dashboard/out/history.txt +2 -2
- package/dist/dashboard/out/index.html +1 -1
- package/dist/dashboard/out/index.txt +2 -2
- package/dist/dashboard/out/login.html +2 -2
- package/dist/dashboard/out/login.txt +2 -2
- package/dist/dashboard/out/metrics.html +1 -1
- package/dist/dashboard/out/metrics.txt +2 -2
- package/dist/dashboard/out/pricing.html +2 -2
- package/dist/dashboard/out/pricing.txt +2 -2
- package/dist/dashboard/out/providers/setup/claude.html +1 -1
- package/dist/dashboard/out/providers/setup/claude.txt +2 -2
- package/dist/dashboard/out/providers/setup/codex.html +1 -1
- package/dist/dashboard/out/providers/setup/codex.txt +2 -2
- package/dist/dashboard/out/providers/setup/cursor.html +1 -1
- package/dist/dashboard/out/providers/setup/cursor.txt +2 -2
- package/dist/dashboard/out/providers.html +1 -1
- package/dist/dashboard/out/providers.txt +2 -2
- package/dist/dashboard/out/signup.html +2 -2
- package/dist/dashboard/out/signup.txt +2 -2
- package/package.json +14 -14
- package/packages/api-types/package.json +1 -1
- package/packages/bridge/dist/spawner.js +9 -0
- package/packages/bridge/package.json +7 -7
- package/packages/cloud/dist/server.js +3 -3
- package/packages/cloud/package.json +6 -6
- package/packages/config/package.json +2 -2
- package/packages/continuity/package.json +1 -1
- package/packages/daemon/package.json +12 -12
- package/packages/dashboard/dist/server.js +9 -9
- package/packages/dashboard/package.json +12 -12
- package/packages/dashboard/ui/react-components/App.tsx +21 -435
- package/packages/dashboard/ui/react-components/ChannelChat.tsx +29 -75
- package/packages/dashboard/ui/react-components/MessageComposer.tsx +457 -0
- package/packages/dashboard/ui-dist/404.html +1 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/320-22ebe7be58cf982a.js +1 -0
- package/{dist/dashboard/out/_next/static/chunks/app/app/page-2e525b1dcc790967.js → packages/dashboard/ui-dist/_next/static/chunks/app/app/page-9d6bc8729b429956.js} +1 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/{page-4e64923d73c35bc9.js → page-487fa38f041815c1.js} +1 -1
- package/packages/dashboard/ui-dist/app/onboarding.html +1 -1
- package/packages/dashboard/ui-dist/app/onboarding.txt +2 -2
- package/packages/dashboard/ui-dist/app.html +1 -1
- package/packages/dashboard/ui-dist/app.txt +2 -2
- package/packages/dashboard/ui-dist/cloud/link.html +1 -1
- package/packages/dashboard/ui-dist/cloud/link.txt +2 -2
- package/packages/dashboard/ui-dist/connect-repos.html +1 -1
- package/packages/dashboard/ui-dist/connect-repos.txt +2 -2
- package/packages/dashboard/ui-dist/history.html +1 -1
- package/packages/dashboard/ui-dist/history.txt +2 -2
- package/packages/dashboard/ui-dist/index.html +1 -1
- package/packages/dashboard/ui-dist/index.txt +2 -2
- package/packages/dashboard/ui-dist/login.html +2 -2
- package/packages/dashboard/ui-dist/login.txt +2 -2
- package/packages/dashboard/ui-dist/metrics.html +1 -1
- package/packages/dashboard/ui-dist/metrics.txt +2 -2
- package/packages/dashboard/ui-dist/pricing.html +2 -2
- package/packages/dashboard/ui-dist/pricing.txt +2 -2
- package/packages/dashboard/ui-dist/providers/setup/claude.html +1 -1
- package/packages/dashboard/ui-dist/providers/setup/claude.txt +2 -2
- package/packages/dashboard/ui-dist/providers/setup/codex.html +1 -1
- package/packages/dashboard/ui-dist/providers/setup/codex.txt +2 -2
- package/packages/dashboard/ui-dist/providers/setup/cursor.html +1 -1
- package/packages/dashboard/ui-dist/providers/setup/cursor.txt +2 -2
- package/packages/dashboard/ui-dist/providers.html +1 -1
- package/packages/dashboard/ui-dist/providers.txt +2 -2
- package/packages/dashboard/ui-dist/signup.html +2 -2
- package/packages/dashboard/ui-dist/signup.txt +2 -2
- package/packages/dashboard-server/dist/server.js +5 -5
- package/packages/dashboard-server/package.json +12 -12
- package/packages/hooks/package.json +4 -4
- package/packages/mcp/package.json +2 -2
- package/packages/memory/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/protocol/package.json +1 -1
- package/packages/resiliency/dist/cgroup-manager.d.ts +152 -0
- package/packages/resiliency/dist/cgroup-manager.js +394 -0
- package/packages/resiliency/dist/index.d.ts +1 -0
- package/packages/resiliency/dist/index.js +1 -0
- package/packages/resiliency/package.json +1 -1
- package/packages/sdk/package.json +2 -2
- package/packages/spawner/package.json +1 -1
- package/packages/state/package.json +1 -1
- package/packages/storage/package.json +2 -2
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/package.json +1 -1
- package/packages/wrapper/dist/relay-pty-orchestrator.d.ts +8 -4
- package/packages/wrapper/dist/relay-pty-orchestrator.js +47 -25
- package/packages/wrapper/package.json +6 -6
- package/dist/dashboard/out/_next/static/chunks/320-900169c942e31422.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/page-f746f29e01fffc43.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/cloud/link/page-5011ae044b90449d.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-03ac6f35a6654ea6.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/history/page-b2ce7c96ed0931da.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/layout-c0d118c0f92d969c.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/login/page-6ec54eee75877971.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/metrics/page-bf2cb1e5915bc92d.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/pricing/page-0efa024c28ba4597.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/providers/page-e65a0010da6ea5be.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/main-app-6e8e8d3ef4e0192a.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/320-900169c942e31422.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/app/onboarding/page-f746f29e01fffc43.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/app/page-44813aa26ad19681.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/connect-repos/page-03ac6f35a6654ea6.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/layout-c0d118c0f92d969c.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/page-7993778218818ace.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/pricing/page-0efa024c28ba4597.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/providers/page-e65a0010da6ea5be.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/providers/setup/[provider]/page-84161c802b020a1f.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/signup/page-18a4665665f6be11.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/main-app-6e8e8d3ef4e0192a.js +0 -1
- /package/dist/dashboard/out/_next/static/{lIJs7zSKBaI58kpqegulQ → loxKCRf0rbwVD8vl_Gw60}/_buildManifest.js +0 -0
- /package/dist/dashboard/out/_next/static/{lIJs7zSKBaI58kpqegulQ → loxKCRf0rbwVD8vl_Gw60}/_ssgManifest.js +0 -0
- /package/packages/dashboard/ui-dist/_next/static/{KIxE0Ds_zdGuDJDQu7_sb → 1yt8VDAusp2yTBf4JFA7F}/_buildManifest.js +0 -0
- /package/packages/dashboard/ui-dist/_next/static/{KIxE0Ds_zdGuDJDQu7_sb → 1yt8VDAusp2yTBf4JFA7F}/_ssgManifest.js +0 -0
- /package/packages/dashboard/ui-dist/_next/static/{SoK46dEi3IsNBVWXD9x0L → chdCrViSD2yReZElqRfk0}/_buildManifest.js +0 -0
- /package/packages/dashboard/ui-dist/_next/static/{SoK46dEi3IsNBVWXD9x0L → chdCrViSD2yReZElqRfk0}/_ssgManifest.js +0 -0
- /package/packages/dashboard/ui-dist/_next/static/{lIJs7zSKBaI58kpqegulQ → loxKCRf0rbwVD8vl_Gw60}/_buildManifest.js +0 -0
- /package/packages/dashboard/ui-dist/_next/static/{lIJs7zSKBaI58kpqegulQ → loxKCRf0rbwVD8vl_Gw60}/_ssgManifest.js +0 -0
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Chat view for a channel or DM conversation.
|
|
5
5
|
* Displays messages and provides input for sending new messages.
|
|
6
|
+
* Uses shared MessageComposer for consistent attachment/paste support.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
|
-
import React, {
|
|
9
|
+
import React, { useCallback, useRef, useEffect } from 'react';
|
|
9
10
|
import type { ChannelMessage } from './hooks/useChannels';
|
|
10
11
|
import type { Agent } from '../types';
|
|
11
12
|
import type { UserPresence } from './hooks/usePresence';
|
|
12
13
|
import { MessageSenderName } from './MessageSenderName';
|
|
14
|
+
import { MessageComposer } from './MessageComposer';
|
|
15
|
+
import type { HumanUser } from './MentionAutocomplete';
|
|
13
16
|
|
|
14
17
|
export interface ChannelChatProps {
|
|
15
18
|
/** Current channel name */
|
|
@@ -18,8 +21,8 @@ export interface ChannelChatProps {
|
|
|
18
21
|
messages: ChannelMessage[];
|
|
19
22
|
/** Current user's username */
|
|
20
23
|
currentUser: string;
|
|
21
|
-
/** Send a message */
|
|
22
|
-
onSendMessage: (body: string, thread?: string) => void;
|
|
24
|
+
/** Send a message (now supports attachments) */
|
|
25
|
+
onSendMessage: (body: string, thread?: string, attachmentIds?: string[]) => void;
|
|
23
26
|
/** Online users for mentions */
|
|
24
27
|
onlineUsers?: string[];
|
|
25
28
|
/** Agents list for profile lookup */
|
|
@@ -30,6 +33,8 @@ export interface ChannelChatProps {
|
|
|
30
33
|
onAgentClick?: (agent: Agent) => void;
|
|
31
34
|
/** Callback when user name is clicked */
|
|
32
35
|
onUserClick?: (user: UserPresence) => void;
|
|
36
|
+
/** Whether message sending is in progress */
|
|
37
|
+
isSending?: boolean;
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
export function ChannelChat({
|
|
@@ -42,10 +47,9 @@ export function ChannelChat({
|
|
|
42
47
|
onlineUserPresence = [],
|
|
43
48
|
onAgentClick,
|
|
44
49
|
onUserClick,
|
|
50
|
+
isSending = false,
|
|
45
51
|
}: ChannelChatProps) {
|
|
46
|
-
const [inputValue, setInputValue] = useState('');
|
|
47
52
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
48
|
-
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
49
53
|
|
|
50
54
|
// Filter messages for this channel
|
|
51
55
|
const channelMessages = messages.filter(m => {
|
|
@@ -65,27 +69,24 @@ export function ChannelChat({
|
|
|
65
69
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
66
70
|
}, [channelMessages.length]);
|
|
67
71
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (!
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
inputRef.current?.focus();
|
|
75
|
-
}, [inputValue, onSendMessage]);
|
|
76
|
-
|
|
77
|
-
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
78
|
-
if (e.key === 'Enter' && !e.shiftKey) {
|
|
79
|
-
e.preventDefault();
|
|
80
|
-
handleSend();
|
|
81
|
-
}
|
|
82
|
-
}, [handleSend]);
|
|
72
|
+
// Handle message send with attachments
|
|
73
|
+
const handleSend = useCallback(async (content: string, attachmentIds?: string[]): Promise<boolean> => {
|
|
74
|
+
if (!content.trim() && (!attachmentIds || attachmentIds.length === 0)) return false;
|
|
75
|
+
onSendMessage(content, undefined, attachmentIds);
|
|
76
|
+
return true;
|
|
77
|
+
}, [onSendMessage]);
|
|
83
78
|
|
|
84
79
|
const isDm = channel.startsWith('dm:');
|
|
85
80
|
const channelDisplay = isDm
|
|
86
81
|
? channel.split(':').slice(1).filter(u => u !== currentUser).join(', ')
|
|
87
82
|
: channel;
|
|
88
83
|
|
|
84
|
+
// Convert online user presence to HumanUser format for mentions
|
|
85
|
+
const humanUsers: HumanUser[] = onlineUserPresence.map(u => ({
|
|
86
|
+
username: u.username,
|
|
87
|
+
avatarUrl: u.avatarUrl,
|
|
88
|
+
}));
|
|
89
|
+
|
|
89
90
|
return (
|
|
90
91
|
<div style={{
|
|
91
92
|
display: 'flex',
|
|
@@ -163,65 +164,18 @@ export function ChannelChat({
|
|
|
163
164
|
<div ref={messagesEndRef} />
|
|
164
165
|
</div>
|
|
165
166
|
|
|
166
|
-
{/* Input */}
|
|
167
|
+
{/* Input - Using shared MessageComposer for attachment support */}
|
|
167
168
|
<div style={{
|
|
168
169
|
padding: '16px 20px',
|
|
169
170
|
borderTop: '1px solid var(--border-color, #313244)',
|
|
170
171
|
}}>
|
|
171
|
-
<
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
value={inputValue}
|
|
179
|
-
onChange={(e) => setInputValue(e.target.value)}
|
|
180
|
-
onKeyDown={handleKeyDown}
|
|
181
|
-
placeholder={`Message ${channelDisplay}...`}
|
|
182
|
-
style={{
|
|
183
|
-
flex: 1,
|
|
184
|
-
padding: '12px 16px',
|
|
185
|
-
backgroundColor: 'var(--bg-secondary, #1e1e2e)',
|
|
186
|
-
border: '1px solid var(--border-color, #313244)',
|
|
187
|
-
borderRadius: '8px',
|
|
188
|
-
color: 'var(--text-primary, #cdd6f4)',
|
|
189
|
-
fontSize: '14px',
|
|
190
|
-
resize: 'none',
|
|
191
|
-
minHeight: '44px',
|
|
192
|
-
maxHeight: '120px',
|
|
193
|
-
outline: 'none',
|
|
194
|
-
fontFamily: 'inherit',
|
|
195
|
-
}}
|
|
196
|
-
rows={1}
|
|
197
|
-
/>
|
|
198
|
-
<button
|
|
199
|
-
onClick={handleSend}
|
|
200
|
-
disabled={!inputValue.trim()}
|
|
201
|
-
style={{
|
|
202
|
-
padding: '12px 20px',
|
|
203
|
-
backgroundColor: inputValue.trim()
|
|
204
|
-
? 'var(--accent-color, #89b4fa)'
|
|
205
|
-
: 'var(--bg-tertiary, #313244)',
|
|
206
|
-
border: 'none',
|
|
207
|
-
borderRadius: '8px',
|
|
208
|
-
color: inputValue.trim() ? '#11111b' : 'var(--text-muted, #6c7086)',
|
|
209
|
-
fontSize: '14px',
|
|
210
|
-
fontWeight: 500,
|
|
211
|
-
cursor: inputValue.trim() ? 'pointer' : 'default',
|
|
212
|
-
transition: 'all 0.15s ease',
|
|
213
|
-
}}
|
|
214
|
-
>
|
|
215
|
-
Send
|
|
216
|
-
</button>
|
|
217
|
-
</div>
|
|
218
|
-
<div style={{
|
|
219
|
-
fontSize: '12px',
|
|
220
|
-
color: 'var(--text-muted, #6c7086)',
|
|
221
|
-
marginTop: '8px',
|
|
222
|
-
}}>
|
|
223
|
-
Press Enter to send, Shift+Enter for new line
|
|
224
|
-
</div>
|
|
172
|
+
<MessageComposer
|
|
173
|
+
onSend={handleSend}
|
|
174
|
+
isSending={isSending}
|
|
175
|
+
placeholder={`Message ${channelDisplay}...`}
|
|
176
|
+
agents={agents}
|
|
177
|
+
humanUsers={humanUsers}
|
|
178
|
+
/>
|
|
225
179
|
</div>
|
|
226
180
|
</div>
|
|
227
181
|
);
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MessageComposer - Shared message input component with attachment support
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Image paste from clipboard
|
|
6
|
+
* - File upload via button
|
|
7
|
+
* - @-mention autocomplete (optional)
|
|
8
|
+
* - File path autocomplete (optional)
|
|
9
|
+
* - Typing indicator support
|
|
10
|
+
* - Multi-line support (Shift+Enter)
|
|
11
|
+
*
|
|
12
|
+
* Used by both DMs and Channels for consistent messaging experience.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
|
16
|
+
import { api } from '../lib/api';
|
|
17
|
+
import { MentionAutocomplete, getMentionQuery, type HumanUser } from './MentionAutocomplete';
|
|
18
|
+
import { FileAutocomplete, getFileQuery } from './FileAutocomplete';
|
|
19
|
+
import type { Agent } from '../types';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Pending attachment state during upload
|
|
23
|
+
*/
|
|
24
|
+
export interface PendingAttachment {
|
|
25
|
+
id: string;
|
|
26
|
+
file: File;
|
|
27
|
+
preview: string;
|
|
28
|
+
isUploading: boolean;
|
|
29
|
+
uploadedId?: string;
|
|
30
|
+
error?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Props for the MessageComposer component
|
|
35
|
+
*/
|
|
36
|
+
export interface MessageComposerProps {
|
|
37
|
+
/** Called when user sends a message */
|
|
38
|
+
onSend: (content: string, attachmentIds?: string[]) => Promise<boolean>;
|
|
39
|
+
/** Called when typing state changes */
|
|
40
|
+
onTyping?: (isTyping: boolean) => void;
|
|
41
|
+
/** Whether a send is in progress */
|
|
42
|
+
isSending?: boolean;
|
|
43
|
+
/** Whether input is disabled */
|
|
44
|
+
disabled?: boolean;
|
|
45
|
+
/** Placeholder text */
|
|
46
|
+
placeholder?: string;
|
|
47
|
+
/** Error message to display */
|
|
48
|
+
error?: string | null;
|
|
49
|
+
/** Agent list for @-mention autocomplete */
|
|
50
|
+
agents?: Agent[];
|
|
51
|
+
/** Human user list for @-mention autocomplete */
|
|
52
|
+
humanUsers?: HumanUser[];
|
|
53
|
+
/** Enable file path autocomplete */
|
|
54
|
+
enableFileAutocomplete?: boolean;
|
|
55
|
+
/** Mention to insert (triggered externally) */
|
|
56
|
+
insertMention?: string;
|
|
57
|
+
/** Called after mention is inserted */
|
|
58
|
+
onMentionInserted?: () => void;
|
|
59
|
+
/** Custom class for the form container */
|
|
60
|
+
className?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function MessageComposer({
|
|
64
|
+
onSend,
|
|
65
|
+
onTyping,
|
|
66
|
+
isSending = false,
|
|
67
|
+
disabled = false,
|
|
68
|
+
placeholder = 'Type a message...',
|
|
69
|
+
error,
|
|
70
|
+
agents = [],
|
|
71
|
+
humanUsers = [],
|
|
72
|
+
enableFileAutocomplete = false,
|
|
73
|
+
insertMention,
|
|
74
|
+
onMentionInserted,
|
|
75
|
+
className = '',
|
|
76
|
+
}: MessageComposerProps) {
|
|
77
|
+
const [message, setMessage] = useState('');
|
|
78
|
+
const [cursorPosition, setCursorPosition] = useState(0);
|
|
79
|
+
const [showMentions, setShowMentions] = useState(false);
|
|
80
|
+
const [showFiles, setShowFiles] = useState(false);
|
|
81
|
+
const [attachments, setAttachments] = useState<PendingAttachment[]>([]);
|
|
82
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
83
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
84
|
+
|
|
85
|
+
// Handle insertMention prop - insert @username when triggered from outside
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (insertMention && onMentionInserted) {
|
|
88
|
+
const mentionText = `@${insertMention} `;
|
|
89
|
+
const textarea = textareaRef.current;
|
|
90
|
+
if (textarea) {
|
|
91
|
+
const start = textarea.selectionStart || message.length;
|
|
92
|
+
const newMessage = message.slice(0, start) + mentionText + message.slice(start);
|
|
93
|
+
setMessage(newMessage);
|
|
94
|
+
setTimeout(() => {
|
|
95
|
+
textarea.focus();
|
|
96
|
+
const newPos = start + mentionText.length;
|
|
97
|
+
textarea.setSelectionRange(newPos, newPos);
|
|
98
|
+
}, 0);
|
|
99
|
+
} else {
|
|
100
|
+
setMessage(prev => prev + mentionText);
|
|
101
|
+
}
|
|
102
|
+
onMentionInserted();
|
|
103
|
+
}
|
|
104
|
+
}, [insertMention, onMentionInserted, message]);
|
|
105
|
+
|
|
106
|
+
// Process image files (used by both paste and file input)
|
|
107
|
+
const processImageFiles = useCallback(async (imageFiles: File[]) => {
|
|
108
|
+
for (const file of imageFiles) {
|
|
109
|
+
const id = crypto.randomUUID();
|
|
110
|
+
const preview = URL.createObjectURL(file);
|
|
111
|
+
|
|
112
|
+
// Add to pending attachments
|
|
113
|
+
setAttachments(prev => [...prev, {
|
|
114
|
+
id,
|
|
115
|
+
file,
|
|
116
|
+
preview,
|
|
117
|
+
isUploading: true,
|
|
118
|
+
}]);
|
|
119
|
+
|
|
120
|
+
// Upload the file
|
|
121
|
+
try {
|
|
122
|
+
const result = await api.uploadAttachment(file);
|
|
123
|
+
if (result.success && result.data) {
|
|
124
|
+
setAttachments(prev => prev.map(a =>
|
|
125
|
+
a.id === id
|
|
126
|
+
? { ...a, isUploading: false, uploadedId: result.data!.attachment.id }
|
|
127
|
+
: a
|
|
128
|
+
));
|
|
129
|
+
} else {
|
|
130
|
+
setAttachments(prev => prev.map(a =>
|
|
131
|
+
a.id === id
|
|
132
|
+
? { ...a, isUploading: false, error: result.error || 'Upload failed' }
|
|
133
|
+
: a
|
|
134
|
+
));
|
|
135
|
+
}
|
|
136
|
+
} catch (err) {
|
|
137
|
+
setAttachments(prev => prev.map(a =>
|
|
138
|
+
a.id === id
|
|
139
|
+
? { ...a, isUploading: false, error: 'Upload failed' }
|
|
140
|
+
: a
|
|
141
|
+
));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}, []);
|
|
145
|
+
|
|
146
|
+
// Handle file selection from file input
|
|
147
|
+
const handleFileSelect = useCallback((files: FileList | null) => {
|
|
148
|
+
if (!files || files.length === 0) return;
|
|
149
|
+
|
|
150
|
+
const imageFiles = Array.from(files).filter(file =>
|
|
151
|
+
file.type.startsWith('image/')
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
if (imageFiles.length > 0) {
|
|
155
|
+
processImageFiles(imageFiles);
|
|
156
|
+
}
|
|
157
|
+
}, [processImageFiles]);
|
|
158
|
+
|
|
159
|
+
// Handle paste for clipboard images
|
|
160
|
+
const handlePaste = useCallback((e: React.ClipboardEvent) => {
|
|
161
|
+
const clipboardData = e.clipboardData;
|
|
162
|
+
if (!clipboardData) return;
|
|
163
|
+
|
|
164
|
+
let imageFiles: File[] = [];
|
|
165
|
+
|
|
166
|
+
// Method 1: Check clipboardData.files (works for file pastes)
|
|
167
|
+
if (clipboardData.files && clipboardData.files.length > 0) {
|
|
168
|
+
imageFiles = Array.from(clipboardData.files).filter(file =>
|
|
169
|
+
file.type.startsWith('image/')
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Method 2: Check clipboardData.items (works for screenshots/copied images)
|
|
174
|
+
if (imageFiles.length === 0 && clipboardData.items) {
|
|
175
|
+
const items = Array.from(clipboardData.items);
|
|
176
|
+
for (const item of items) {
|
|
177
|
+
if (item.kind === 'file' && item.type.startsWith('image/')) {
|
|
178
|
+
const file = item.getAsFile();
|
|
179
|
+
if (file) {
|
|
180
|
+
imageFiles.push(file);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Process any found images
|
|
187
|
+
if (imageFiles.length > 0) {
|
|
188
|
+
e.preventDefault();
|
|
189
|
+
processImageFiles(imageFiles);
|
|
190
|
+
}
|
|
191
|
+
}, [processImageFiles]);
|
|
192
|
+
|
|
193
|
+
// Remove an attachment
|
|
194
|
+
const removeAttachment = useCallback((id: string) => {
|
|
195
|
+
setAttachments(prev => {
|
|
196
|
+
const attachment = prev.find(a => a.id === id);
|
|
197
|
+
if (attachment) {
|
|
198
|
+
URL.revokeObjectURL(attachment.preview);
|
|
199
|
+
}
|
|
200
|
+
return prev.filter(a => a.id !== id);
|
|
201
|
+
});
|
|
202
|
+
}, []);
|
|
203
|
+
|
|
204
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
205
|
+
const value = e.target.value;
|
|
206
|
+
const cursorPos = e.target.selectionStart || 0;
|
|
207
|
+
setMessage(value);
|
|
208
|
+
setCursorPosition(cursorPos);
|
|
209
|
+
|
|
210
|
+
// Send typing indicator when user has content
|
|
211
|
+
onTyping?.(value.trim().length > 0);
|
|
212
|
+
|
|
213
|
+
// Check for file autocomplete first (@ followed by path-like pattern)
|
|
214
|
+
if (enableFileAutocomplete) {
|
|
215
|
+
const fileQuery = getFileQuery(value, cursorPos);
|
|
216
|
+
if (fileQuery !== null) {
|
|
217
|
+
setShowFiles(true);
|
|
218
|
+
setShowMentions(false);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Check for mention autocomplete (@ at start without path patterns)
|
|
224
|
+
if (agents.length > 0 || humanUsers.length > 0) {
|
|
225
|
+
const mentionQuery = getMentionQuery(value, cursorPos);
|
|
226
|
+
if (mentionQuery !== null) {
|
|
227
|
+
setShowMentions(true);
|
|
228
|
+
setShowFiles(false);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Neither - hide both
|
|
234
|
+
setShowMentions(false);
|
|
235
|
+
setShowFiles(false);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
239
|
+
// Don't handle Enter/Tab when autocomplete is visible
|
|
240
|
+
if ((showMentions || showFiles) && (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Tab')) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (e.key === 'Enter' && !e.shiftKey && !showMentions && !showFiles) {
|
|
245
|
+
e.preventDefault();
|
|
246
|
+
if ((message.trim() || attachments.length > 0) && !isSending && !disabled) {
|
|
247
|
+
handleSubmit(e as unknown as React.FormEvent);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const handleMentionSelect = (mention: string, newValue: string) => {
|
|
253
|
+
setMessage(newValue);
|
|
254
|
+
setShowMentions(false);
|
|
255
|
+
setShowFiles(false);
|
|
256
|
+
setTimeout(() => {
|
|
257
|
+
if (textareaRef.current) {
|
|
258
|
+
textareaRef.current.focus();
|
|
259
|
+
const pos = newValue.indexOf(' ') + 1;
|
|
260
|
+
textareaRef.current.setSelectionRange(pos, pos);
|
|
261
|
+
}
|
|
262
|
+
}, 0);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const handleFilePathSelect = (filePath: string, newValue: string) => {
|
|
266
|
+
setMessage(newValue);
|
|
267
|
+
setShowFiles(false);
|
|
268
|
+
setShowMentions(false);
|
|
269
|
+
setTimeout(() => {
|
|
270
|
+
if (textareaRef.current) {
|
|
271
|
+
textareaRef.current.focus();
|
|
272
|
+
const pos = newValue.indexOf(' ', 1) + 1;
|
|
273
|
+
textareaRef.current.setSelectionRange(pos, pos);
|
|
274
|
+
}
|
|
275
|
+
}, 0);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
279
|
+
e.preventDefault();
|
|
280
|
+
|
|
281
|
+
const hasMessage = message.trim().length > 0;
|
|
282
|
+
const hasAttachments = attachments.length > 0;
|
|
283
|
+
if ((!hasMessage && !hasAttachments) || isSending || disabled) return;
|
|
284
|
+
|
|
285
|
+
// Check if any attachments are still uploading
|
|
286
|
+
const stillUploading = attachments.some(a => a.isUploading);
|
|
287
|
+
if (stillUploading) return;
|
|
288
|
+
|
|
289
|
+
// Get uploaded attachment IDs
|
|
290
|
+
const attachmentIds = attachments
|
|
291
|
+
.filter(a => a.uploadedId)
|
|
292
|
+
.map(a => a.uploadedId!);
|
|
293
|
+
|
|
294
|
+
// If no message but has attachments, send with default text
|
|
295
|
+
let content = message.trim();
|
|
296
|
+
if (!content && attachmentIds.length > 0) {
|
|
297
|
+
content = '[Screenshot attached]';
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const success = await onSend(
|
|
301
|
+
content,
|
|
302
|
+
attachmentIds.length > 0 ? attachmentIds : undefined
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
if (success) {
|
|
306
|
+
// Clean up previews
|
|
307
|
+
attachments.forEach(a => URL.revokeObjectURL(a.preview));
|
|
308
|
+
setMessage('');
|
|
309
|
+
setAttachments([]);
|
|
310
|
+
setShowMentions(false);
|
|
311
|
+
setShowFiles(false);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// Check if we can send
|
|
316
|
+
const canSend = (message.trim() || attachments.length > 0) &&
|
|
317
|
+
!isSending &&
|
|
318
|
+
!disabled &&
|
|
319
|
+
!attachments.some(a => a.isUploading);
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
<form className={`flex flex-col gap-1.5 sm:gap-2 ${className}`} onSubmit={handleSubmit}>
|
|
323
|
+
{/* Attachment previews */}
|
|
324
|
+
{attachments.length > 0 && (
|
|
325
|
+
<div className="flex flex-wrap gap-1.5 sm:gap-2 p-1.5 sm:p-2 bg-bg-card rounded-lg border border-border-subtle">
|
|
326
|
+
{attachments.map(attachment => (
|
|
327
|
+
<div key={attachment.id} className="relative group">
|
|
328
|
+
<img
|
|
329
|
+
src={attachment.preview}
|
|
330
|
+
alt={attachment.file.name}
|
|
331
|
+
className={`h-16 w-auto rounded-lg object-cover ${attachment.isUploading ? 'opacity-50' : ''} ${attachment.error ? 'border-2 border-error' : ''}`}
|
|
332
|
+
/>
|
|
333
|
+
{attachment.isUploading && (
|
|
334
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
335
|
+
<svg className="animate-spin h-5 w-5 text-accent-cyan" viewBox="0 0 24 24">
|
|
336
|
+
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" fill="none" strokeDasharray="32" strokeLinecap="round" />
|
|
337
|
+
</svg>
|
|
338
|
+
</div>
|
|
339
|
+
)}
|
|
340
|
+
{attachment.error && (
|
|
341
|
+
<div className="absolute bottom-0 left-0 right-0 bg-error/90 text-white text-[10px] px-1 py-0.5 truncate">
|
|
342
|
+
{attachment.error}
|
|
343
|
+
</div>
|
|
344
|
+
)}
|
|
345
|
+
<button
|
|
346
|
+
type="button"
|
|
347
|
+
onClick={() => removeAttachment(attachment.id)}
|
|
348
|
+
className="absolute -top-1.5 -right-1.5 w-5 h-5 bg-bg-tertiary border border-border-subtle rounded-full flex items-center justify-center text-text-muted hover:text-error hover:border-error transition-colors opacity-0 group-hover:opacity-100"
|
|
349
|
+
title="Remove"
|
|
350
|
+
>
|
|
351
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
|
|
352
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
353
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
354
|
+
</svg>
|
|
355
|
+
</button>
|
|
356
|
+
</div>
|
|
357
|
+
))}
|
|
358
|
+
</div>
|
|
359
|
+
)}
|
|
360
|
+
|
|
361
|
+
{/* Input row */}
|
|
362
|
+
<div className="flex items-center gap-1.5 sm:gap-3">
|
|
363
|
+
{/* Image upload button */}
|
|
364
|
+
<input
|
|
365
|
+
ref={fileInputRef}
|
|
366
|
+
type="file"
|
|
367
|
+
accept="image/*"
|
|
368
|
+
multiple
|
|
369
|
+
className="hidden"
|
|
370
|
+
onChange={(e) => handleFileSelect(e.target.files)}
|
|
371
|
+
/>
|
|
372
|
+
<button
|
|
373
|
+
type="button"
|
|
374
|
+
onClick={() => fileInputRef.current?.click()}
|
|
375
|
+
disabled={disabled}
|
|
376
|
+
className="p-2 sm:p-2.5 bg-bg-card border border-border-subtle rounded-lg sm:rounded-xl text-text-muted hover:text-accent-cyan hover:border-accent-cyan/50 transition-colors flex-shrink-0 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
377
|
+
title="Attach screenshot (or paste from clipboard)"
|
|
378
|
+
>
|
|
379
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="sm:w-[18px] sm:h-[18px]">
|
|
380
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
|
381
|
+
<circle cx="8.5" cy="8.5" r="1.5" />
|
|
382
|
+
<polyline points="21 15 16 10 5 21" />
|
|
383
|
+
</svg>
|
|
384
|
+
</button>
|
|
385
|
+
|
|
386
|
+
<div className="flex-1 relative min-w-0">
|
|
387
|
+
{/* Agent mention autocomplete */}
|
|
388
|
+
{(agents.length > 0 || humanUsers.length > 0) && (
|
|
389
|
+
<MentionAutocomplete
|
|
390
|
+
agents={agents}
|
|
391
|
+
humanUsers={humanUsers}
|
|
392
|
+
inputValue={message}
|
|
393
|
+
cursorPosition={cursorPosition}
|
|
394
|
+
onSelect={handleMentionSelect}
|
|
395
|
+
onClose={() => setShowMentions(false)}
|
|
396
|
+
isVisible={showMentions}
|
|
397
|
+
/>
|
|
398
|
+
)}
|
|
399
|
+
{/* File path autocomplete */}
|
|
400
|
+
{enableFileAutocomplete && (
|
|
401
|
+
<FileAutocomplete
|
|
402
|
+
inputValue={message}
|
|
403
|
+
cursorPosition={cursorPosition}
|
|
404
|
+
onSelect={handleFilePathSelect}
|
|
405
|
+
onClose={() => setShowFiles(false)}
|
|
406
|
+
isVisible={showFiles}
|
|
407
|
+
/>
|
|
408
|
+
)}
|
|
409
|
+
<textarea
|
|
410
|
+
ref={textareaRef}
|
|
411
|
+
className="w-full py-2 sm:py-3 px-3 sm:px-4 bg-bg-card border border-border-subtle rounded-lg sm:rounded-xl text-sm font-sans text-text-primary outline-none transition-all duration-200 resize-none min-h-[40px] sm:min-h-[44px] max-h-[100px] sm:max-h-[120px] overflow-y-auto focus:border-accent-cyan/50 focus:shadow-[0_0_0_3px_rgba(0,217,255,0.1)] placeholder:text-text-muted disabled:opacity-50 disabled:cursor-not-allowed"
|
|
412
|
+
placeholder={placeholder}
|
|
413
|
+
value={message}
|
|
414
|
+
onChange={handleInputChange}
|
|
415
|
+
onKeyDown={handleKeyDown}
|
|
416
|
+
onPaste={handlePaste}
|
|
417
|
+
onSelect={(e) => setCursorPosition((e.target as HTMLTextAreaElement).selectionStart || 0)}
|
|
418
|
+
disabled={disabled || isSending}
|
|
419
|
+
rows={1}
|
|
420
|
+
/>
|
|
421
|
+
</div>
|
|
422
|
+
<button
|
|
423
|
+
type="submit"
|
|
424
|
+
className="py-2 sm:py-3 px-3 sm:px-5 bg-gradient-to-r from-accent-cyan to-[#00b8d9] text-bg-deep font-semibold border-none rounded-lg sm:rounded-xl text-xs sm:text-sm cursor-pointer transition-all duration-150 hover:shadow-glow-cyan hover:-translate-y-0.5 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:translate-y-0 disabled:hover:shadow-none flex-shrink-0"
|
|
425
|
+
disabled={!canSend}
|
|
426
|
+
title={isSending ? 'Sending...' : attachments.some(a => a.isUploading) ? 'Uploading...' : 'Send message'}
|
|
427
|
+
>
|
|
428
|
+
{isSending ? (
|
|
429
|
+
<span className="hidden sm:inline">Sending...</span>
|
|
430
|
+
) : attachments.some(a => a.isUploading) ? (
|
|
431
|
+
<span className="hidden sm:inline">Uploading...</span>
|
|
432
|
+
) : (
|
|
433
|
+
<span className="flex items-center gap-1 sm:gap-2">
|
|
434
|
+
<span className="hidden sm:inline">Send</span>
|
|
435
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
436
|
+
<line x1="22" y1="2" x2="11" y2="13"></line>
|
|
437
|
+
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
|
438
|
+
</svg>
|
|
439
|
+
</span>
|
|
440
|
+
)}
|
|
441
|
+
</button>
|
|
442
|
+
{error && <span className="text-error text-xs ml-2">{error}</span>}
|
|
443
|
+
</div>
|
|
444
|
+
|
|
445
|
+
{/* Helper text */}
|
|
446
|
+
<p className="text-xs text-text-muted px-1">
|
|
447
|
+
<kbd className="px-1 py-0.5 bg-bg-tertiary rounded text-[10px]">Enter</kbd> to send,{' '}
|
|
448
|
+
<kbd className="px-1 py-0.5 bg-bg-tertiary rounded text-[10px]">Shift+Enter</kbd> for new line
|
|
449
|
+
{(agents.length > 0 || humanUsers.length > 0) && (
|
|
450
|
+
<>, <kbd className="px-1 py-0.5 bg-bg-tertiary rounded text-[10px]">@</kbd> to mention</>
|
|
451
|
+
)}
|
|
452
|
+
</p>
|
|
453
|
+
</form>
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export default MessageComposer;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/_next/static/css/99c2552394077586.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-1cdd8ed57114d5e1.js"/><script src="/_next/static/chunks/fd9d1056-609918ca7b6280bb.js" async=""></script><script src="/_next/static/chunks/117-c8afed19e821a35d.js" async=""></script><script src="/_next/static/chunks/main-app-
|
|
1
|
+
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/_next/static/css/99c2552394077586.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-1cdd8ed57114d5e1.js"/><script src="/_next/static/chunks/fd9d1056-609918ca7b6280bb.js" async=""></script><script src="/_next/static/chunks/117-c8afed19e821a35d.js" async=""></script><script src="/_next/static/chunks/main-app-fdbeb09028f57c9f.js" async=""></script><meta name="robots" content="noindex"/><title>404: This page could not be found.</title><title>Agent Relay Dashboard</title><meta name="description" content="Fleet control dashboard for Agent Relay"/><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body><div style="font-family:system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><script src="/_next/static/chunks/webpack-1cdd8ed57114d5e1.js" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0]);self.__next_f.push([2,null])</script><script>self.__next_f.push([1,"1:HL[\"/_next/static/css/99c2552394077586.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"2:I[2846,[],\"\"]\n4:I[4707,[],\"\"]\n5:I[6423,[],\"\"]\nb:I[1060,[],\"\"]\n6:{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"}\n7:{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"}\n8:{\"display\":\"inline-block\"}\n9:{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0}\nc:[]\n"])</script><script>self.__next_f.push([1,"0:[\"$\",\"$L2\",null,{\"buildId\":\"loxKCRf0rbwVD8vl_Gw60\",\"assetPrefix\":\"\",\"urlParts\":[\"\",\"_not-found\"],\"initialTree\":[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],\"initialSeedData\":[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{},[[\"$L3\",[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":\"404\"}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],null],null],null]},[null,[\"$\",\"$L4\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\",\"/_not-found\",\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L5\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"notFoundStyles\":\"$undefined\"}]],null]},[[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/99c2552394077586.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"children\":[\"$\",\"$L4\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L5\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":\"$6\",\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":\"$7\",\"children\":\"404\"}],[\"$\",\"div\",null,{\"style\":\"$8\",\"children\":[\"$\",\"h2\",null,{\"style\":\"$9\",\"children\":\"This page could not be found.\"}]}]]}]}]],\"notFoundStyles\":[]}]}]}]],null],null],\"couldBeIntercepted\":false,\"initialHead\":[[\"$\",\"meta\",null,{\"name\":\"robots\",\"content\":\"noindex\"}],\"$La\"],\"globalErrorComponent\":\"$b\",\"missingSlots\":\"$Wc\"}]\n"])</script><script>self.__next_f.push([1,"a:[[\"$\",\"meta\",\"0\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}],[\"$\",\"meta\",\"1\",{\"charSet\":\"utf-8\"}],[\"$\",\"title\",\"2\",{\"children\":\"Agent Relay Dashboard\"}],[\"$\",\"meta\",\"3\",{\"name\":\"description\",\"content\":\"Fleet control dashboard for Agent Relay\"}]]\n3:null\n"])</script></body></html>
|