diff --git a/src/components/sessions/EventBlock.tsx b/src/components/sessions/EventBlock.tsx
new file mode 100644
index 0000000..0d3b467
--- /dev/null
+++ b/src/components/sessions/EventBlock.tsx
@@ -0,0 +1,87 @@
+import { useState } from 'react'
+import type { SessionEvent } from '../../../api/types/session'
+
+function truncate(s: string, n: number): string {
+ return s.length > n ? s.slice(0, n) + '…' : s
+}
+
+interface Props {
+ event: SessionEvent
+ defaultCollapsed: boolean
+}
+
+export function EventBlock({ event, defaultCollapsed }: Props) {
+ const [collapsed, setCollapsed] = useState(defaultCollapsed)
+
+ const base = 'rounded border px-3 py-2 my-1 text-sm'
+ switch (event.type) {
+ case 'user':
+ if (event.tool_result !== null && event.tool_result !== undefined) {
+ return (
+
+
+ {!collapsed && (
+
+ {typeof event.tool_result === 'string' ? event.tool_result : JSON.stringify(event.tool_result, null, 2)}
+
+ )}
+
+ )
+ }
+ return (
+
+
user
+
{event.text ?? '—'}
+
+ )
+ case 'assistant':
+ if (event.tool_name) {
+ return (
+
+
+ {!collapsed && event.tool_input && (
+
+ {JSON.stringify(event.tool_input, null, 2)}
+
+ )}
+
+ )
+ }
+ return (
+
+
assistant
+
{truncate(event.text ?? '—', collapsed ? 300 : Number.MAX_SAFE_INTEGER)}
+ {(event.text?.length ?? 0) > 300 && (
+
+ )}
+
+ )
+ case 'system':
+ return (
+
+
+ {!collapsed &&
{event.text ?? JSON.stringify(event.raw)}
}
+
+ )
+ case 'attachment':
+ return (
+
+ 📎 attachment
+
+ )
+ default:
+ return (
+
+ {event.type}
+
+ )
+ }
+}
diff --git a/src/pages/SessionDetail.tsx b/src/pages/SessionDetail.tsx
index f92514b..adfb23a 100644
--- a/src/pages/SessionDetail.tsx
+++ b/src/pages/SessionDetail.tsx
@@ -1,3 +1,83 @@
+import { useEffect, useState } from 'react'
+import { useParams, Link } from 'react-router-dom'
+import { getSession } from '../lib/api/sessions'
+import { EventBlock } from '../components/sessions/EventBlock'
+import type { SessionMeta, SessionEvent } from '../../api/types/session'
+
+type FilterMode = 'all' | 'no-system' | 'tools-only' | 'prompts-only'
+
export default function SessionDetail() {
- return Em construção — Task 7.
+ const { id } = useParams<{ id: string }>()
+ const [meta, setMeta] = useState(null)
+ const [events, setEvents] = useState([])
+ const [mode, setMode] = useState('all')
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+
+ useEffect(() => {
+ if (!id) return
+ setLoading(true)
+ getSession(id)
+ .then((r) => {
+ setMeta(r.meta)
+ setEvents(r.events)
+ })
+ .catch((e: Error) => setError(e.message))
+ .finally(() => setLoading(false))
+ }, [id])
+
+ const visible = events.filter((e) => {
+ if (mode === 'no-system') return e.type !== 'system'
+ if (mode === 'tools-only') return e.tool_name !== null || e.tool_result !== null
+ if (mode === 'prompts-only') return e.type === 'user' && e.tool_result === null
+ return true
+ })
+
+ if (loading) return A carregar…
+ if (error) return {error}
+ if (!meta) return Sessão não encontrada.
+
+ return (
+
+
+ ← Voltar à lista
+
+
+
+ {meta.first_prompt?.slice(0, 120) ?? meta.session_id}
+
+ {new Date(meta.started_at).toLocaleString('pt-PT')}
+ Projecto: {meta.project_slug}
+ Duração: {meta.duration_sec ?? 0}s
+ Eventos: {meta.event_count}
+ Tool calls: {meta.tool_calls}
+ Skills: {meta.skills_invoked.join(', ') || '—'}
+ Resultado: {meta.outcome}
+
+ JSONL: {meta.jsonl_path}
+
+
+
+ {(['all', 'no-system', 'tools-only', 'prompts-only'] as FilterMode[]).map((m) => (
+
+ ))}
+
+
+
+ A mostrar {visible.length} de {events.length} eventos.
+
+
+
+ {visible.map((e) => (
+
+ ))}
+
+
+ )
}