@@ -17,6 +17,20 @@ import '../widgets/daemon_status_bar.dart';
1717import '../widgets/session_status_badge.dart' ;
1818import '../theme/cli_theme.dart' ;
1919
20+ bool _isChatInteraction (LogEntry log) {
21+ if (log.source == LogSource .raw) return false ;
22+ if (log.source == LogSource .local) return true ;
23+ final type = log.structuredType;
24+ if (type == null ) return true ;
25+ return type == 'text' || type == 'error' ;
26+ }
27+
28+ bool _isTimelineEvent (LogEntry log) {
29+ if (log.source != LogSource .structured) return false ;
30+ final type = log.structuredType;
31+ return type != null && type != 'text' ;
32+ }
33+
2034// ── Fade-slide-in wrapper ─────────────────────────────────────────────────────
2135class _FadeSlide extends StatefulWidget {
2236 final Widget child;
@@ -68,6 +82,8 @@ class DashboardScreen extends ConsumerWidget {
6882 final session =
6983 ref.watch (sessionProvider).valueOrNull ?? SessionState .empty;
7084 final actions = ref.read (daemonActionsProvider);
85+ final chatLogs = session.logs.where (_isChatInteraction).toList ();
86+ final timelineLogs = session.logs.where (_isTimelineEvent).toList ();
7187
7288 return CliTheme (
7389 level: session.dependenceLevel,
@@ -176,26 +192,27 @@ class DashboardScreen extends ConsumerWidget {
176192 body: session.lastFailed! .error,
177193 ),
178194 ),
195+
196+ if (timelineLogs.isNotEmpty &&
197+ session.preflightQueue.isEmpty &&
198+ session.decisionQueue.isEmpty)
199+ _FadeSlide (
200+ delay: const Duration (milliseconds: 90 ),
201+ child: _ProcessTimelineSection (logs: timelineLogs),
202+ ),
179203 ],
180204 ),
181205 ),
182206 ),
183207
184208 // ── Chat log fills remaining space ───────────────────
185- if (session.logs
186- .any ((l) =>
187- l.level != AgentLogLevel .error &&
188- l.source != LogSource .raw) &&
209+ if (chatLogs.isNotEmpty &&
189210 session.preflightQueue.isEmpty &&
190211 session.decisionQueue.isEmpty)
191212 SliverFillRemaining (
192213 hasScrollBody: true ,
193214 child: ChatMessageList (
194- logs: session.logs
195- .where ((l) =>
196- l.level != AgentLogLevel .error &&
197- l.source != LogSource .raw)
198- .toList (),
215+ logs: chatLogs,
199216 ),
200217 ),
201218
@@ -286,6 +303,122 @@ class _FloatingStatusBar extends ConsumerWidget {
286303 }
287304}
288305
306+ class _ProcessTimelineSection extends StatelessWidget {
307+ final List <LogEntry > logs;
308+ const _ProcessTimelineSection ({required this .logs});
309+
310+ @override
311+ Widget build (BuildContext context) {
312+ final cli = CliTheme .of (context);
313+ final recent = logs.length > 8 ? logs.sublist (logs.length - 8 ) : logs;
314+
315+ return Padding (
316+ padding: const EdgeInsets .fromLTRB (16 , 8 , 16 , 8 ),
317+ child: Container (
318+ decoration: cli.box (borderColor: cli.border),
319+ child: ExpansionTile (
320+ tilePadding: const EdgeInsets .symmetric (horizontal: 12 , vertical: 2 ),
321+ childrenPadding: const EdgeInsets .fromLTRB (12 , 0 , 12 , 12 ),
322+ collapsedIconColor: cli.textDim,
323+ iconColor: cli.accent,
324+ title: Text (
325+ 'PROCESS TIMELINE' ,
326+ style: cli.mono.copyWith (
327+ color: cli.textDim,
328+ fontSize: 10 ,
329+ letterSpacing: 1.4 ,
330+ fontWeight: FontWeight .bold,
331+ ),
332+ ),
333+ subtitle: Text (
334+ '${recent .length } recent events' ,
335+ style: cli.mono.copyWith (color: cli.textDim, fontSize: 10 ),
336+ ),
337+ children: [
338+ for (final entry in recent)
339+ Padding (
340+ padding: const EdgeInsets .only (bottom: 8 ),
341+ child: _TimelineEventRow (entry: entry),
342+ ),
343+ ],
344+ ),
345+ ),
346+ );
347+ }
348+ }
349+
350+ class _TimelineEventRow extends StatelessWidget {
351+ final LogEntry entry;
352+ const _TimelineEventRow ({required this .entry});
353+
354+ @override
355+ Widget build (BuildContext context) {
356+ final cli = CliTheme .of (context);
357+ final type = entry.structuredType ?? 'event' ;
358+
359+ final (IconData icon, String label, Color color) = switch (type) {
360+ 'reasoning' => (Icons .psychology_alt_outlined, 'Thinking' , cli.cyan),
361+ 'step_start' => (Icons .play_arrow_rounded, 'Step start' , cli.amber),
362+ 'step_finish' => (Icons .check_circle_outline, 'Step finish' , cli.accent),
363+ 'tool_use' => (Icons .build_circle_outlined, 'Tool' , cli.accent),
364+ 'awaiting_approval' => (Icons .help_outline, 'Awaiting approval' , cli.amber),
365+ 'approval_resolved' => (Icons .task_alt, 'Approval resolved' , cli.accent),
366+ _ => (Icons .circle_outlined, type, cli.textDim),
367+ };
368+
369+ final isLong = entry.message.length > 140 ;
370+
371+ if (! isLong) {
372+ return Row (
373+ crossAxisAlignment: CrossAxisAlignment .start,
374+ children: [
375+ Icon (icon, size: 14 , color: color),
376+ const SizedBox (width: 8 ),
377+ Expanded (
378+ child: Text (
379+ '$label · ${entry .message }' ,
380+ style: cli.mono.copyWith (color: cli.text, fontSize: 11 , height: 1.35 ),
381+ ),
382+ ),
383+ ],
384+ );
385+ }
386+
387+ return Theme (
388+ data: Theme .of (context).copyWith (dividerColor: Colors .transparent),
389+ child: ExpansionTile (
390+ tilePadding: EdgeInsets .zero,
391+ childrenPadding: const EdgeInsets .only (top: 4 ),
392+ collapsedIconColor: cli.textDim,
393+ iconColor: cli.accent,
394+ title: Row (
395+ children: [
396+ Icon (icon, size: 14 , color: color),
397+ const SizedBox (width: 8 ),
398+ Expanded (
399+ child: Text (
400+ '$label · ${entry .message .substring (0 , 120 )}...' ,
401+ maxLines: 1 ,
402+ overflow: TextOverflow .ellipsis,
403+ style: cli.mono.copyWith (color: cli.text, fontSize: 11 ),
404+ ),
405+ ),
406+ ],
407+ ),
408+ children: [
409+ Align (
410+ alignment: Alignment .centerLeft,
411+ child: Text (
412+ entry.message,
413+ style: cli.mono.copyWith (color: cli.textDim, fontSize: 11 , height: 1.35 ),
414+ ),
415+ ),
416+ ],
417+ ),
418+ );
419+ }
420+ }
421+
289422// ── Wraps a child in a CLI-styled bordered section ────────────────────────────
290423class _CliSection extends StatelessWidget {
291424 final String label;
0 commit comments