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 packages/server/src/hooks/pending.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface PendingApprovalDTO {
eventName: string;
toolName: string;
toolInput: Record<string, unknown>;
sessionId: string;
timestamp: number;
analysis?: AnalysisResult;
riskLevel?: 'low' | 'medium' | 'high';
Expand Down Expand Up @@ -104,6 +105,7 @@ export class PendingApprovalManager extends EventEmitter {
eventName: a.eventName,
toolName: a.toolName,
toolInput: a.toolInput,
sessionId: a.sessionId,
timestamp: a.timestamp,
analysis: a.analysis,
isDriftBlock: a.isDriftBlock,
Expand Down
2 changes: 2 additions & 0 deletions packages/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export function createServer(config: LaymanConfig): LaymanServer {
eventName: approval.eventName,
toolName: approval.toolName,
toolInput: approval.toolInput,
sessionId: approval.sessionId,
timestamp: approval.timestamp,
analysis: approval.analysis,
},
Expand All @@ -142,6 +143,7 @@ export function createServer(config: LaymanConfig): LaymanServer {
eventName: approval.eventName,
toolName: approval.toolName,
toolInput: approval.toolInput,
sessionId: approval.sessionId,
timestamp: approval.timestamp,
analysis: approval.analysis,
},
Expand Down
2 changes: 1 addition & 1 deletion packages/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export function App() {
<SetupBanner onInstall={handleSetupInstall} />
<div className="flex-1 overflow-hidden">
<Suspense fallback={<div className="flex items-center justify-center h-full text-[#484f58] text-xs">Loading dashboard...</div>}>
<DashboardView />
<DashboardView onSend={send} />
</Suspense>
</div>
<StatusBar />
Expand Down
82 changes: 63 additions & 19 deletions packages/web/src/components/dashboard/DashboardView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ import React, { useMemo, useState, useCallback, useEffect } from 'react';
import { useSessionStore } from '../../stores/sessionStore.js';
import { SessionCard } from './SessionCard.js';
import { SidePanel } from './SidePanel.js';
import type { ClientMessage } from '../../lib/ws-protocol.js';
import './dashboard.css';

export function DashboardView() {
interface DashboardViewProps {
onSend: (msg: ClientMessage) => void;
}

export function DashboardView({ onSend }: DashboardViewProps) {
const {
sessions,
events: allEvents,
Expand Down Expand Up @@ -179,30 +184,66 @@ export function DashboardView() {

{/* Cards grid */}
{(() => {
const isFew = orderedSessions.length > 0 && orderedSessions.length <= 2;
const count = orderedSessions.length;
const isFew = count > 0 && count <= 2;
const isThree = count === 3;

// For 3 sessions: reorder so the featured (focused or first) is at index 0
const featuredId = isThree
? (dashboardFocusedSession && orderedSessions.some(s => s.sessionId === dashboardFocusedSession)
? dashboardFocusedSession
: orderedSessions[0]?.sessionId)
: null;
const displaySessions = isThree && featuredId
? [
orderedSessions.find(s => s.sessionId === featuredId)!,
...orderedSessions.filter(s => s.sessionId !== featuredId),
]
: orderedSessions;

let gridTemplateColumns: string;
let gridTemplateRows: string;
let overflow: string;

if (count === 1) {
gridTemplateColumns = '1fr';
gridTemplateRows = '1fr';
overflow = 'hidden';
} else if (count === 2) {
gridTemplateColumns = 'repeat(2, 1fr)';
gridTemplateRows = '1fr';
overflow = 'hidden';
} else if (isThree) {
gridTemplateColumns = 'repeat(2, 1fr)';
gridTemplateRows = 'repeat(2, 1fr)';
overflow = 'hidden';
} else {
gridTemplateColumns = 'repeat(auto-fill, minmax(380px, 1fr))';
gridTemplateRows = 'auto';
overflow = 'auto';
}

return (
<div
className="flex-1 p-4"
style={{ overflow: isFew ? 'hidden' : 'auto' }}
className="flex-1 min-h-0 p-4 flex flex-col"
style={{ overflow }}
>
{orderedSessions.length === 0 ? (
{count === 0 ? (
<EmptyState />
) : (
<div
className="grid gap-4"
className="grid gap-4 flex-1 min-h-0"
style={{
gridTemplateColumns: orderedSessions.length === 1
? '1fr'
: orderedSessions.length === 2
? 'repeat(2, 1fr)'
: 'repeat(auto-fill, minmax(380px, 1fr))',
height: isFew ? '100%' : 'auto',
gridTemplateRows: isFew ? '1fr' : 'auto',
gridTemplateColumns,
gridTemplateRows,
height: isFew || isThree ? '100%' : 'auto',
maxWidth: '100%',
}}
onClick={() => setDashboardFocusedSession(null)}
>
{orderedSessions.map((session, index) => (
{displaySessions.map((session, displayIndex) => {
const orderIndex = orderedSessions.indexOf(session);
return (
<SessionCard
key={session.sessionId}
session={session}
Expand All @@ -212,15 +253,18 @@ export function DashboardView() {
onDismiss={handleDismiss}
onDrilldown={handleDrilldown}
onDrilldownToLogs={handleDrilldownToLogs}
index={index}
onSend={onSend}
index={orderIndex}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
isDragging={dragIndex === index}
isDragOver={dragOverIndex === index}
totalCards={orderedSessions.length}
isDragging={dragIndex === orderIndex}
isDragOver={dragOverIndex === orderIndex}
totalCards={count}
isSpanning={isThree && displayIndex === 0}
/>
))}
);
})}
</div>
)}
</div>
Expand Down
62 changes: 43 additions & 19 deletions packages/web/src/components/dashboard/SessionCard.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, { useMemo, useCallback, useRef, useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { useSessionStore } from '../../stores/sessionStore.js';
import { usePendingApprovals } from '../../hooks/usePendingApprovals.js';
import { ApprovalBar } from '../controls/ApprovalBar.js';
import { EVENT_ICONS, BORDER_COLORS, NODE_BORDER_COLORS, AGENT_BADGES } from '../../lib/event-styles.js';
import { RiskBadge } from '../shared/RiskBadge.js';
import type { TimelineEvent } from '../../lib/types.js';
import type { SessionInfo } from '../../lib/ws-protocol.js';
import type { SessionInfo, ClientMessage } from '../../lib/ws-protocol.js';

interface SessionCardProps {
session: SessionInfo;
Expand All @@ -14,6 +16,7 @@ interface SessionCardProps {
onDismiss: (sessionId: string) => void;
onDrilldown: (sessionId: string, eventId: string) => void;
onDrilldownToLogs: (sessionId: string, eventId: string) => void;
onSend: (msg: ClientMessage) => void;
index: number;
onDragStart: (index: number) => void;
onDragOver: (index: number) => void;
Expand All @@ -22,6 +25,8 @@ interface SessionCardProps {
isDragOver: boolean;
/** How many total session cards are displayed */
totalCards: number;
/** Whether this card should span 2 rows in the grid (3-session layout) */
isSpanning?: boolean;
}

function getSessionDisplayName(session: SessionInfo): string {
Expand Down Expand Up @@ -471,19 +476,20 @@ function DashboardEventFeed({
events,
sessionId,
onDrilldownToLogs,
maxItems,
onSend,
}: {
events: TimelineEvent[];
sessionId: string;
onDrilldownToLogs: (sessionId: string, eventId: string) => void;
maxItems: number;
onSend: (msg: ClientMessage) => void;
}) {
const [tooltip, setTooltip] = useState<{ content: string; x: number; y: number } | null>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const { approvals } = usePendingApprovals();

const filteredEvents = useMemo(
() => events.filter(e => e.type !== 'session_metrics').slice(-maxItems),
[events, maxItems]
() => events.filter(e => e.type !== 'session_metrics'),
[events]
);

// Keep newest entry visible
Expand All @@ -508,16 +514,34 @@ function DashboardEventFeed({

return (
<div ref={scrollRef} className="flex-1 min-h-0 overflow-y-auto py-1">
{filteredEvents.map((event, i) => (
<DashboardEventRow
key={event.id}
event={event}
globalIndex={i}
onClick={() => onDrilldownToLogs(sessionId, event.id)}
onMouseEnter={(e) => handleMouseEnter(event, e)}
onMouseLeave={handleMouseLeave}
/>
))}
{filteredEvents.map((event, i) => {
const isPendingEvent = event.type === 'tool_call_pending';
const pendingApproval = isPendingEvent
? approvals.find(
(a) => a.sessionId === sessionId && a.toolName === event.data.toolName && Math.abs(a.timestamp - event.timestamp) < 5000
)
: undefined;
return (
<React.Fragment key={event.id}>
<DashboardEventRow
event={event}
globalIndex={i}
onClick={() => onDrilldownToLogs(sessionId, event.id)}
onMouseEnter={(e) => handleMouseEnter(event, e)}
onMouseLeave={handleMouseLeave}
/>
{pendingApproval && (
<div className="mx-2 mb-1 px-2 py-1.5 rounded bg-[#1c1a0f] border border-[#d29922]/20">
<ApprovalBar
approvalId={pendingApproval.id}
toolName={pendingApproval.toolName}
onSend={onSend}
/>
</div>
)}
</React.Fragment>
);
})}
{tooltip && <EventTooltip content={tooltip.content} x={tooltip.x} y={tooltip.y} />}
</div>
);
Expand Down Expand Up @@ -564,8 +588,8 @@ function RateLimitMini({ label, pct, resetsAt }: { label: string; pct: number; r
}

export function SessionCard({
session, events, isFocused, onFocus, onDismiss, onDrilldown, onDrilldownToLogs, index,
onDragStart, onDragOver, onDragEnd, isDragging, isDragOver, totalCards,
session, events, isFocused, onFocus, onDismiss, onDrilldown, onDrilldownToLogs, onSend, index,
onDragStart, onDragOver, onDragEnd, isDragging, isDragOver, totalCards, isSpanning,
}: SessionCardProps) {
const sessionMetrics = useSessionStore(s => s.sessionMetrics);
const metrics = sessionMetrics.get(session.sessionId);
Expand Down Expand Up @@ -608,7 +632,7 @@ export function SessionCard({
<div
ref={dragRef}
className={`dash-card dash-card-enter flex flex-col ${isFocused ? 'dash-card--focused' : ''} ${isDragging ? 'dash-card--dragging' : ''} ${isDragOver ? 'dash-card--drag-over' : ''}`}
style={{ animationDelay: `${index * 80}ms` }}
style={{ animationDelay: `${index * 80}ms`, gridRow: isSpanning ? 'span 2' : undefined }}
onClick={handleClick}
draggable
onDragStart={(e) => {
Expand Down Expand Up @@ -753,7 +777,7 @@ export function SessionCard({
events={events}
sessionId={session.sessionId}
onDrilldownToLogs={onDrilldownToLogs}
maxItems={chainCapacity}
onSend={onSend}
/>
</div>

Expand Down
1 change: 1 addition & 0 deletions packages/web/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ export interface PendingApprovalDTO {
eventName: string;
toolName: string;
toolInput: Record<string, unknown>;
sessionId: string;
timestamp: number;
analysis?: AnalysisResult;
riskLevel?: 'low' | 'medium' | 'high';
Expand Down