Skip to content
Open
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
10 changes: 10 additions & 0 deletions packages/web/src/components/dashboard/DashboardView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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 { saveAndBookmarkSession } from '../../lib/bookmarks-api.js';
import type { ClientMessage } from '../../lib/ws-protocol.js';
import './dashboard.css';

Expand All @@ -13,6 +14,7 @@ export function DashboardView({ onSend }: DashboardViewProps) {
const {
sessions,
events: allEvents,
bookmarks,
dashboardFocusedSession,
setDashboardFocusedSession,
dashboardSessionOrder,
Expand All @@ -21,8 +23,14 @@ export function DashboardView({ onSend }: DashboardViewProps) {
dismissDashboardSession,
navigateFromDashboard,
navigateFromDashboardToLogs,
navigateToLogsForSession,
} = useSessionStore();

const bookmarkedSessionIds = useMemo(
() => new Set(bookmarks.map((b) => b.sessionId)),
[bookmarks]
);

// Drag state
const [dragIndex, setDragIndex] = useState<number | null>(null);
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
Expand Down Expand Up @@ -253,6 +261,8 @@ export function DashboardView({ onSend }: DashboardViewProps) {
onDismiss={handleDismiss}
onDrilldown={handleDrilldown}
onDrilldownToLogs={handleDrilldownToLogs}
onOpenInLogs={navigateToLogsForSession}
onBookmark={bookmarkedSessionIds.has(session.sessionId) ? undefined : saveAndBookmarkSession}
onSend={onSend}
index={orderIndex}
onDragStart={handleDragStart}
Expand Down
72 changes: 70 additions & 2 deletions packages/web/src/components/dashboard/SessionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ interface SessionCardProps {
onDismiss: (sessionId: string) => void;
onDrilldown: (sessionId: string, eventId: string) => void;
onDrilldownToLogs: (sessionId: string, eventId: string) => void;
onOpenInLogs?: (sessionId: string) => void;
onBookmark?: (sessionId: string, name: string) => void;
onSend: (msg: ClientMessage) => void;
index: number;
onDragStart: (index: number) => void;
Expand Down Expand Up @@ -596,7 +598,7 @@ function RateLimitMini({ label, pct, resetsAt }: { label: string; pct: number; r
}

export function SessionCard({
session, events, isFocused, onFocus, onDismiss, onDrilldown, onDrilldownToLogs, onSend, index,
session, events, isFocused, onFocus, onDismiss, onDrilldown, onDrilldownToLogs, onOpenInLogs, onBookmark, onSend, index,
onDragStart, onDragOver, onDragEnd, isDragging, isDragOver, totalCards, isSpanning,
}: SessionCardProps) {
const { sessionMetrics, investigatedSessions } = useSessionStore(s => ({
Expand All @@ -610,6 +612,8 @@ export function SessionCard({
const dragRef = useRef<HTMLDivElement>(null);
const chainContainerRef = useRef<HTMLDivElement>(null);
const [chainCapacity, setChainCapacity] = useState(6);
const [showBookmarkInput, setShowBookmarkInput] = useState(false);
const [bookmarkName, setBookmarkName] = useState('');

// Measure the chain container width and compute how many nodes fit.
// Each node is 28px, each connector is 12px β†’ N nodes need (N-1)*40 + 28 px.
Expand All @@ -625,6 +629,12 @@ export function SessionCard({
return () => observer.disconnect();
}, []);

const submitBookmark = useCallback(() => {
const name = bookmarkName.trim() || getSessionDisplayName(session);
onBookmark?.(session.sessionId, name);
setShowBookmarkInput(false);
}, [bookmarkName, session, onBookmark]);

const handleClick = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
onFocus(session.sessionId);
Expand Down Expand Up @@ -684,15 +694,18 @@ export function SessionCard({
)}
</div>

{/* Session name */}
{/* Session name β€” double-click opens in Logs */}
<span
style={{
fontFamily: 'var(--dash-font-display)',
fontSize: 13,
fontWeight: 600,
color: isFocused ? 'var(--dash-accent)' : 'var(--dash-text-primary)',
cursor: onOpenInLogs ? 'pointer' : undefined,
}}
className="truncate"
title={onOpenInLogs ? 'Double-click to open in Logs' : undefined}
onDoubleClick={(e) => { e.stopPropagation(); onOpenInLogs?.(session.sessionId); }}
>
{getSessionDisplayName(session)}
</span>
Expand Down Expand Up @@ -754,6 +767,24 @@ export function SessionCard({
</span>
)}

{/* Bookmark button */}
{onBookmark && !showBookmarkInput && (
<button
title="Bookmark this session"
onClick={(e) => {
e.stopPropagation();
setBookmarkName(getSessionDisplayName(session));
setShowBookmarkInput(true);
}}
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '0 2px', flexShrink: 0, color: 'var(--dash-text-muted)', opacity: 0.5, transition: 'opacity 0.15s' }}
className="hover:!opacity-100"
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
</svg>
</button>
)}

{/* Dismiss button */}
<button
title="Close session (deactivates monitoring; resume via Session History)"
Expand All @@ -778,6 +809,43 @@ export function SessionCard({
</button>
</div>

{/* Inline bookmark name input */}
{showBookmarkInput && (
<div className="px-3 py-1.5 shrink-0 flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
<input
autoFocus
type="text"
value={bookmarkName}
onChange={(e) => setBookmarkName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') { submitBookmark(); }
if (e.key === 'Escape') { setShowBookmarkInput(false); }
}}
placeholder="Bookmark name"
style={{
flex: 1,
fontSize: 11,
fontFamily: 'var(--dash-font-data)',
background: 'var(--dash-bg)',
border: '1px solid var(--dash-accent)',
borderRadius: 4,
color: 'var(--dash-text-primary)',
padding: '2px 6px',
outline: 'none',
minWidth: 0,
}}
/>
<button
onClick={submitBookmark}
style={{ fontSize: 10, color: 'var(--dash-success)', background: 'none', border: 'none', cursor: 'pointer', padding: '0 2px' }}
>βœ“</button>
<button
onClick={() => setShowBookmarkInput(false)}
style={{ fontSize: 10, color: 'var(--dash-text-muted)', background: 'none', border: 'none', cursor: 'pointer', padding: '0 2px' }}
>βœ•</button>
</div>
)}

{/* Activity chain */}
<div ref={chainContainerRef} className="px-3 shrink-0" style={{ maxWidth: totalCards === 1 ? '50%' : '100%' }}>
<MiniActivityChain events={events} onDrilldown={onDrilldown} sessionId={session.sessionId} maxItems={chainCapacity} />
Expand Down
68 changes: 67 additions & 1 deletion packages/web/src/components/layout/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import React, { useMemo, useState, useCallback, useEffect } from 'react';
import { useSessionStore } from '../../stores/sessionStore.js';
import { SessionLaymansTerms } from '../shared/SessionLaymansTerms.js';
import { saveAndBookmarkSession } from '../../lib/bookmarks-api.js';

function getSessionName(cwd: string, sessionId: string, agentType?: string, showAgentPrefix?: boolean, sessionName?: string): string {
const name = sessionName || (cwd ? (cwd.split('/').filter(Boolean).pop() ?? cwd) : sessionId.slice(0, 8));
Expand Down Expand Up @@ -77,8 +78,29 @@ export function Header() {
dashboardFocusedSession, setDashboardFocusedSession,
sessionMetrics,
investigatedSessions,
bookmarks,
} = useSessionStore();

const bookmarkedSessionIds = useMemo(
() => new Set(bookmarks.map((b) => b.sessionId)),
[bookmarks]
);

const [showBookmarkInput, setShowBookmarkInput] = useState(false);
const [bookmarkName, setBookmarkName] = useState('');

// Reset bookmark UI when the active session changes
useEffect(() => {
setShowBookmarkInput(false);
setBookmarkName('');
}, [activeSessionId]);

const handleBookmarkSession = useCallback(async (name: string) => {
if (!activeSessionId) return;
setShowBookmarkInput(false);
void saveAndBookmarkSession(activeSessionId, name.trim() || activeSessionId.slice(0, 8));
}, [activeSessionId]);

const statusConfig = {
connecting: { dot: 'bg-[#d29922]', text: 'Connecting...', textColor: 'text-[#d29922]' },
connected: { dot: 'bg-[#3fb950]', text: 'Connected', textColor: 'text-[#3fb950]' },
Expand Down Expand Up @@ -229,6 +251,50 @@ export function Header() {
})}
</div>

{/* Bookmark button β€” shown in Logs view when a session is active and not yet bookmarked */}
{currentView === 'stream' && activeSessionId && !bookmarkedSessionIds.has(activeSessionId) && (
<>
{showBookmarkInput ? (
<div className="flex items-center gap-1">
<input
autoFocus
type="text"
value={bookmarkName}
onChange={(e) => setBookmarkName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') { void handleBookmarkSession(bookmarkName); }
if (e.key === 'Escape') { setShowBookmarkInput(false); }
}}
placeholder="Bookmark name..."
className="text-xs bg-[#0d1117] border border-[#58a6ff] rounded px-2 py-0.5 text-[#e6edf3] placeholder-[#484f58] focus:outline-none w-40"
/>
<button
onClick={() => { void handleBookmarkSession(bookmarkName); }}
className="text-xs text-[#3fb950] hover:text-[#56d364] transition-colors"
>βœ“</button>
<button
onClick={() => setShowBookmarkInput(false)}
className="text-xs text-[#484f58] hover:text-[#8b949e] transition-colors"
>βœ•</button>
</div>
) : (
<button
onClick={() => {
const s = sessions.find(x => x.sessionId === activeSessionId);
setBookmarkName(s ? getSessionName(s.cwd, s.sessionId, s.agentType, false, s.sessionName) : activeSessionId.slice(0, 8));
setShowBookmarkInput(true);
}}
className="p-1.5 rounded-md text-[#8b949e] hover:text-[#d29922] hover:bg-[#30363d] transition-colors"
title="Bookmark current session"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
</svg>
</button>
)}
</>
)}

{/* Divider */}
<div className="h-5 w-px bg-[#30363d]" />

Expand Down
Loading