Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ eggs/
lib/
!packages/launcher/src/renderer/lib/
!packages/launcher/src/renderer/lib/**
!workspace/frontend/lib/
!workspace/frontend/lib/**
lib64/
parts/
sdist/
Expand Down
1 change: 1 addition & 0 deletions workspace/backend/app/routers/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ async def send_event(
"type": result.type,
"source": result.source,
"target": result.target,
"payload": result.payload,
"timestamp": result.timestamp,
"metadata": result.metadata,
})
Expand Down
30 changes: 23 additions & 7 deletions workspace/frontend/app/[workspaceId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,22 @@

import { use, Suspense } from 'react';
import { useSearchParams } from 'next/navigation';
import { WorkspaceProvider } from '@/lib/workspace-context';
import { WorkspaceProvider, useWorkspace } from '@/lib/workspace-context';
import { LayoutProvider } from '@/components/layout/layout-context';
import { Wrapper } from '@/components/layout/wrapper';
import { useOpenAgentsAuth } from '@/lib/openagents-auth-context';
import { IdentityDialog } from '@/components/identity/identity-dialog';

function IdentityGate({ children }: { children: React.ReactNode }) {
const { currentUser, setUserName } = useWorkspace();

return (
<>
<IdentityDialog open={!currentUser.name.trim()} onSubmit={setUserName} />
{children}
</>
);
}

function WorkspaceContent({ workspaceId }: { workspaceId: string }) {
const searchParams = useSearchParams();
Expand All @@ -16,9 +28,11 @@ function WorkspaceContent({ workspaceId }: { workspaceId: string }) {
if (token) {
return (
<WorkspaceProvider workspaceId={workspaceId} token={token} bearerToken={idToken || undefined}>
<LayoutProvider>
<Wrapper />
</LayoutProvider>
<IdentityGate>
<LayoutProvider>
<Wrapper />
</LayoutProvider>
</IdentityGate>
</WorkspaceProvider>
);
}
Expand All @@ -37,9 +51,11 @@ function WorkspaceContent({ workspaceId }: { workspaceId: string }) {
// User is logged in — try to access workspace via bearer token
return (
<WorkspaceProvider workspaceId={workspaceId} token="" bearerToken={idToken}>
<LayoutProvider>
<Wrapper />
</LayoutProvider>
<IdentityGate>
<LayoutProvider>
<Wrapper />
</LayoutProvider>
</IdentityGate>
</WorkspaceProvider>
);
}
Expand Down
26 changes: 22 additions & 4 deletions workspace/frontend/components/chat/chat-message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ interface Attachment {
url: string;
}

function humanColor(seed: string): string {
let hash = 0;
for (let i = 0; i < seed.length; i++) {
hash = (hash * 31 + seed.charCodeAt(i)) >>> 0;
}
return `hsl(${hash % 360} 55% 82%)`;
}

function isPreviewable(contentType: string, filename: string): boolean {
if (contentType?.startsWith('image/')) return true;
if (contentType === 'text/html' || /\.html?$/i.test(filename)) return true;
Expand Down Expand Up @@ -108,7 +116,8 @@ interface ChatMessageProps {
}

export const ChatMessage = memo(function ChatMessage({ message, agents = [] }: ChatMessageProps) {
const isHuman = message.senderType === 'human';
const { currentUser } = useWorkspace();
const isHuman = message.senderType === 'human' || message.senderType === 'user';
const isSystem = message.messageType === 'status';
const [copied, setCopied] = useState(false);

Expand Down Expand Up @@ -149,15 +158,24 @@ export const ChatMessage = memo(function ChatMessage({ message, agents = [] }: C

// ── Human message — Slack style ──
if (isHuman) {
const isCurrentUser = !!message.senderId && message.senderId === currentUser.id;
const displayName = isCurrentUser
? 'You'
: (message.senderName && message.senderName !== 'user' ? message.senderName : 'User');
const seed = message.senderId || message.senderName || 'human';

return (
<div className="py-1.5">
<div className="flex items-start gap-2">
<div className="size-9 rounded-lg shrink-0 flex items-center justify-center bg-zinc-200 dark:bg-zinc-700 mt-0.5">
<User className="size-4 text-zinc-500 dark:text-zinc-400" />
<div
className="size-9 rounded-lg shrink-0 flex items-center justify-center mt-0.5"
style={{ backgroundColor: humanColor(seed) }}
>
<User className="size-4 text-zinc-700" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-baseline gap-2">
<span className="text-[15px] font-bold text-foreground">You</span>
<span className="text-[15px] font-bold text-foreground">{displayName}</span>
{timestamp && (
<span className="text-xs text-muted-foreground">{timestamp}</span>
)}
Expand Down
14 changes: 9 additions & 5 deletions workspace/frontend/components/chat/chat-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ async function refreshCachedSession(sessionId: string): Promise<void> {
}

export function ChatView() {
const { agents, currentSessionId, sessions, updateLastMessage, setSessionActive, agentModes, updateAgentMode, toggleAgentMode, stopAllAgents, activeSessionIds, stoppingSessionIds, renameSession, addParticipant, removeParticipant, consumeSkipFocus, createRoutine } = useWorkspace();
const { agents, currentUser, currentSessionId, sessions, updateLastMessage, setSessionActive, agentModes, updateAgentMode, toggleAgentMode, stopAllAgents, activeSessionIds, stoppingSessionIds, renameSession, addParticipant, removeParticipant, consumeSkipFocus, createRoutine } = useWorkspace();
const [showCreateRoutine, setShowCreateRoutine] = useState(false);
const {
isMobile,
Expand Down Expand Up @@ -317,15 +317,17 @@ export function ChatView() {
const handleSend = useCallback(
async (content: string, mentions: string[] = [], files: PendingFile[] = []) => {
if (!currentSessionId) return;
if (!currentUser.id || !currentUser.name.trim()) return;

// Create optimistic messages for instant feedback
const timestamp = Date.now();
const userContent = content || (files.length > 0 ? files.map((f) => f.file.name).join(', ') : '');
const userOptimisticMsg: WorkspaceMessage = {
messageId: `optimistic-user-${timestamp}`,
sessionId: currentSessionId,
senderName: 'You',
senderType: 'user',
senderId: currentUser.id,
senderName: currentUser.name,
senderType: 'human',
content: userContent,
messageType: 'chat',
mentions: [],
Expand Down Expand Up @@ -368,9 +370,10 @@ export function ChatView() {
await workspaceApi.sendMessage(
currentSessionId,
content || (attachments ? attachments.map((a) => a.filename).join(', ') : ''),
'user',
currentUser.name,
mentions.length > 0 ? mentions : undefined,
attachments,
currentUser.id,
);
forceRefresh();
} catch {
Expand All @@ -379,7 +382,7 @@ export function ChatView() {
setOptimisticMessages([]);
}
},
[currentSessionId, forceRefresh, agents]
[currentSessionId, currentUser.id, currentUser.name, forceRefresh, agents]
);

const hasStatusMessages = displayMessages.some((m) => m.messageType === 'status' || m.messageType === 'thinking');
Expand Down Expand Up @@ -686,6 +689,7 @@ export function ChatView() {
onFocusChange={(focused) => focused ? notifyFocus() : notifyBlur()}
focusKey={focusKey}
onCreateRoutine={() => setShowCreateRoutine(true)}
disabled={!currentUser.name.trim()}
/>
</div>
</div>
Expand Down
70 changes: 70 additions & 0 deletions workspace/frontend/components/identity/identity-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
'use client';

import { useState } from 'react';
import { User } from 'lucide-react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';

interface IdentityDialogProps {
open: boolean;
onSubmit: (name: string) => void;
}

export function IdentityDialog({ open, onSubmit }: IdentityDialogProps) {
const [name, setName] = useState('');

const handleSubmit = () => {
const trimmed = name.trim();
if (!trimmed) return;
onSubmit(trimmed);
};

return (
<Dialog open={open}>
<DialogContent
showCloseButton={false}
onPointerDownOutside={(e) => e.preventDefault()}
onEscapeKeyDown={(e) => e.preventDefault()}
onInteractOutside={(e) => e.preventDefault()}
className="sm:max-w-sm"
>
<DialogHeader>
<div className="flex items-center gap-2">
<div className="size-8 rounded-lg bg-primary/10 flex items-center justify-center">
<User className="size-4 text-primary" />
</div>
<DialogTitle>Join Workspace</DialogTitle>
</div>
<DialogDescription>
Enter your name so others can see who you are in the chat.
</DialogDescription>
</DialogHeader>
<form
onSubmit={(e) => {
e.preventDefault();
handleSubmit();
}}
className="space-y-4"
>
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Your name"
autoFocus
maxLength={50}
/>
<Button type="submit" className="w-full" disabled={!name.trim()}>
Continue
</Button>
</form>
</DialogContent>
</Dialog>
);
}
28 changes: 25 additions & 3 deletions workspace/frontend/components/layout/sidebar-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useState, useEffect, useMemo } from 'react';
import {
Plus, MessageSquare, FileText, Globe, PlusSquare,
Settings, Copy, Check, ListTodo, CalendarClock,
LogIn, LogOut, Shield, Moon, Sun, KeyRound, Share2, X, Crown,
LogIn, LogOut, Shield, Moon, Sun, KeyRound, Share2, X, Crown, Users,
} from 'lucide-react';
import { useTheme } from 'next-themes';
import { ScrollArea } from '@/components/ui/scroll-area';
Expand Down Expand Up @@ -71,7 +71,7 @@ function NavButton({

export function SidebarContent() {
const { isSidebarOpen, sidebarToggle, viewMode, setViewMode, setSelectedAgentName } = useLayout();
const { agents, sessions, files, browserTabs, createSession, workspace, token, refreshWorkspace, todos, routines } = useWorkspace();
const { agents, sessions, files, browserTabs, createSession, workspace, token, refreshWorkspace, todos, routines, currentUser, onlineUsers } = useWorkspace();
const { user, isOpenAgentsDomain, signIn, signOut } = useOpenAgentsAuth();
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
Expand Down Expand Up @@ -241,6 +241,29 @@ export function SidebarContent() {
))}
</div>

{/* Online Users */}
{onlineUsers.length > 0 && (
<>
<p className="text-xs font-normal text-muted-foreground px-2 py-1.5 mb-0.5 mt-6">
<Users className="size-3 inline-block mr-1 -mt-0.5" />
Online ({onlineUsers.length})
</p>
<div className="space-y-0.5">
{onlineUsers.map((u) => (
<div
key={u.id}
className="flex items-center gap-2 px-2 h-8 rounded-lg text-[13px]"
>
<div className="size-2 rounded-full bg-emerald-500 shrink-0" />
<span className="truncate text-foreground">
{u.id === currentUser.id ? `${u.name} (you)` : u.name}
</span>
</div>
))}
</div>
</>
)}

{/* Collaboration */}
<p className="text-xs font-normal text-muted-foreground px-2 py-1.5 mb-0.5 mt-6">
Collaboration
Expand Down Expand Up @@ -575,4 +598,3 @@ function SettingsDialogPortal({ open, onOpenChange, workspace, refreshWorkspace
</Dialog>
);
}

25 changes: 19 additions & 6 deletions workspace/frontend/components/monitor/monitor-overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '@/components/ui/dialog';
import { ChatMessages } from '@/components/chat/chat-messages';
import { ChatInput, type PendingFile } from '@/components/chat/chat-input';
import { CreateRoutineDialog } from '@/components/routines/create-routine-dialog';
import { useWorkspace } from '@/lib/workspace-context';
import { useMessagePolling } from '@/hooks/use-polling';
import { workspaceApi } from '@/lib/api';
Expand All @@ -24,7 +25,8 @@ interface MonitorOverlayProps {
}

export function MonitorOverlay({ sessionId, session, initialMessages, open, onOpenChange }: MonitorOverlayProps) {
const { agents, activeSessionIds, stoppingSessionIds, stopAllAgents, renameSession } = useWorkspace();
const { agents, currentUser, activeSessionIds, stoppingSessionIds, stopAllAgents, renameSession, createRoutine } = useWorkspace();
const [showCreateRoutine, setShowCreateRoutine] = useState(false);
const [editingTitle, setEditingTitle] = useState(false);
const [titleDraft, setTitleDraft] = useState('');
const titleInputRef = useRef<HTMLInputElement>(null);
Expand Down Expand Up @@ -107,14 +109,17 @@ export function MonitorOverlay({ sessionId, session, initialMessages, open, onOp

const handleSend = useCallback(
async (content: string, mentions: string[] = [], files: PendingFile[] = []) => {
if (!currentUser.id || !currentUser.name.trim()) return;

// Optimistic messages
const timestamp = Date.now();
const userContent = content || (files.length > 0 ? files.map((f) => f.file.name).join(', ') : '');
const userOptimisticMsg: WorkspaceMessage = {
messageId: `optimistic-user-${timestamp}`,
sessionId,
senderName: 'You',
senderType: 'user',
senderId: currentUser.id,
senderName: currentUser.name,
senderType: 'human',
content: userContent,
messageType: 'chat',
mentions: [],
Expand Down Expand Up @@ -156,16 +161,17 @@ export function MonitorOverlay({ sessionId, session, initialMessages, open, onOp
await workspaceApi.sendMessage(
sessionId,
content || (attachments ? attachments.map((a) => a.filename).join(', ') : ''),
'user',
currentUser.name,
mentions.length > 0 ? mentions : undefined,
attachments,
currentUser.id,
);
forceRefresh();
} catch {
setOptimisticMessages([]);
}
},
[sessionId, forceRefresh, agents]
[sessionId, currentUser.id, currentUser.name, forceRefresh, agents]
);

return (
Expand Down Expand Up @@ -237,11 +243,18 @@ export function MonitorOverlay({ sessionId, session, initialMessages, open, onOp
{/* Input */}
<div className="px-4 py-3 border-t">
<div className="max-w-3xl mx-auto w-full">
<ChatInput onSend={handleSend} agents={agents} focusKey={focusKey} />
<ChatInput onSend={handleSend} agents={agents} disabled={!currentUser.name.trim()} focusKey={focusKey} onCreateRoutine={() => setShowCreateRoutine(true)} />
</div>
</div>
</div>
</DialogContent>

<CreateRoutineDialog
open={showCreateRoutine}
onOpenChange={setShowCreateRoutine}
agents={agents}
onCreateRoutine={createRoutine}
/>
</Dialog>
);
}
Loading
Loading