Skip to content

Commit 66e75d4

Browse files
dimakisclaude
andcommitted
feat(home): unified session feed with message preview
Replace separate Active Sessions + What's Next sections with a single SessionFeed component. Adds chip-based filtering (All / Needs me / In progress / Done), message preview capture from last assistant turn, and oldest-first sort within the working batch (48h window). Backend: preview column in EventStore, capture at turn-end in query-loop, in-memory cache in SessionOverviewEmitter, REST + SSE exposure. Frontend: useSessionFeed hook with working-batch logic, SessionFeed component with filter chips and 5-item scrollable viewport. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5709b06 commit 66e75d4

16 files changed

Lines changed: 566 additions & 1388 deletions

frontend/src/components/AttentionFeed.tsx

Lines changed: 0 additions & 120 deletions
This file was deleted.
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { useNavigate } from 'react-router-dom';
2+
import type { SessionActivity, SessionActivityState } from '@mitzo/protocol';
3+
import { useSessionFeed, type FeedFilter } from '../hooks/useSessionFeed';
4+
import { formatRelativeTime } from '../lib/formatTime';
5+
6+
// ─── State config ────────────────────────────────────────────────────────────
7+
8+
const STATE_CONFIG: Record<SessionActivityState, { icon: string; color: string; label: string }> = {
9+
working: { icon: '●', color: '#b48cff', label: 'working' },
10+
waiting: { icon: '⚠', color: '#ff6d6d', label: 'waiting' },
11+
done: { icon: '✓', color: '#4ade80', label: 'done' },
12+
idle: { icon: '○', color: 'var(--text-dim)', label: 'idle' },
13+
init: { icon: '○', color: 'var(--text-dim)', label: 'starting' },
14+
paused: { icon: '⏸', color: 'var(--text-dim)', label: 'paused' },
15+
};
16+
17+
// ─── Filter chips ────────────────────────────────────────────────────────────
18+
19+
const FILTERS: {
20+
key: FeedFilter;
21+
label: string;
22+
countKey: 'all' | 'needsMe' | 'inProgress' | 'done';
23+
}[] = [
24+
{ key: 'all', label: 'All', countKey: 'all' },
25+
{ key: 'needs_me', label: 'Needs me', countKey: 'needsMe' },
26+
{ key: 'in_progress', label: 'In progress', countKey: 'inProgress' },
27+
{ key: 'done', label: 'Done', countKey: 'done' },
28+
];
29+
30+
// ─── Feed item card ──────────────────────────────────────────────────────────
31+
32+
function FeedItem({
33+
activity,
34+
onTap,
35+
}: {
36+
activity: SessionActivity;
37+
onTap: (sessionId: string) => void;
38+
}) {
39+
const config = STATE_CONFIG[activity.state];
40+
const timeLabel = formatRelativeTime(activity.lastEventAt);
41+
42+
// Build meta label
43+
let metaText = config.label;
44+
if (activity.awaitingReply) metaText = 'awaiting reply';
45+
else if (activity.waitReason === 'permission') metaText = 'permission';
46+
else if (activity.waitReason === 'review') metaText = 'review needed';
47+
else if (activity.waitReason === 'blocked') metaText = 'blocked';
48+
else if (activity.uncommittedWork) metaText = 'uncommitted work';
49+
50+
if (activity.progress) {
51+
metaText += ` \u00B7 ${activity.progress.done}/${activity.progress.total}`;
52+
}
53+
metaText += ` \u00B7 ${timeLabel}`;
54+
55+
// Icon: mail for awaiting reply, otherwise state icon
56+
const icon = activity.awaitingReply ? '✉' : config.icon;
57+
const iconColor = activity.awaitingReply ? '#b48cff' : config.color;
58+
59+
return (
60+
<button
61+
className="feed-card"
62+
onClick={() => onTap(activity.sessionId)}
63+
style={{ '--card-accent': iconColor } as React.CSSProperties}
64+
>
65+
<span className="feed-card-icon" style={{ color: iconColor }}>
66+
{icon}
67+
</span>
68+
<div className="feed-card-body">
69+
<div className="feed-card-title">
70+
{activity.repo && <span className="feed-card-repo">{activity.repo}:</span>}{' '}
71+
{activity.title}
72+
</div>
73+
{activity.lastMessagePreview && (
74+
<div className="feed-card-preview">{activity.lastMessagePreview}</div>
75+
)}
76+
<div className="feed-card-meta">{metaText}</div>
77+
</div>
78+
</button>
79+
);
80+
}
81+
82+
// ─── Main component ──────────────────────────────────────────────────────────
83+
84+
export function SessionFeed() {
85+
const { items, counts, filter, setFilter } = useSessionFeed();
86+
const navigate = useNavigate();
87+
88+
const handleTap = (sessionId: string) => {
89+
navigate(`/chat/${sessionId}`);
90+
};
91+
92+
// Don't render if nothing in the working batch
93+
if (counts.all === 0) return null;
94+
95+
return (
96+
<div className="feed-section">
97+
{/* Filter chips */}
98+
<div className="feed-filters">
99+
{FILTERS.map(({ key, label, countKey }) => {
100+
const count = counts[countKey];
101+
const isActive = filter === key;
102+
return (
103+
<button
104+
key={key}
105+
className={`feed-filter-pill${isActive ? ' feed-filter-pill--active' : ''}`}
106+
onClick={() => setFilter(key)}
107+
>
108+
{label}
109+
{count > 0 && <span className="feed-filter-count">{count}</span>}
110+
</button>
111+
);
112+
})}
113+
</div>
114+
115+
{/* Feed list */}
116+
<div className="feed-list">
117+
{items.length === 0 && (
118+
<div className="feed-empty">{filter === 'needs_me' ? 'All clear' : 'No sessions'}</div>
119+
)}
120+
{items.map((activity) => (
121+
<FeedItem key={activity.sessionId} activity={activity} onTap={handleTap} />
122+
))}
123+
</div>
124+
</div>
125+
);
126+
}

frontend/src/components/SessionOverview.tsx

Lines changed: 0 additions & 123 deletions
This file was deleted.

0 commit comments

Comments
 (0)