99/// transcript; tapping it opens [detail] full-screen.
1010library ;
1111
12+ import 'dart:convert' ;
13+
1214import 'package:flutter/material.dart' ;
1315
1416import '../../store/models.dart' ;
@@ -124,7 +126,7 @@ class _DefaultDetail extends StatelessWidget {
124126 ),
125127 const SizedBox (height: 4 ),
126128 SelectableText (
127- item.args. toString ( ),
129+ const JsonEncoder . withIndent ( ' ' ). convert (item.args ),
128130 style: const TextStyle (fontFamily: 'monospace' , fontSize: 12 ),
129131 ),
130132 const SizedBox (height: 16 ),
@@ -158,6 +160,95 @@ class _DefaultDetail extends StatelessWidget {
158160// Built-in renderers
159161// ---------------------------------------------------------------------------
160162
163+ // ---------------------------------------------------------------------------
164+ // Shared detail-view helpers
165+ // ---------------------------------------------------------------------------
166+
167+ /// Titled section used in tool detail pages.
168+ class _ToolSection extends StatelessWidget {
169+ const _ToolSection ({required this .title, required this .child});
170+ final String title;
171+ final Widget child;
172+
173+ @override
174+ Widget build (BuildContext context) {
175+ return Padding (
176+ padding: const EdgeInsets .only (bottom: 16 ),
177+ child: Column (
178+ crossAxisAlignment: CrossAxisAlignment .start,
179+ children: [
180+ Text (title, style: Theme .of (context).textTheme.titleSmall),
181+ const SizedBox (height: 6 ),
182+ Container (
183+ width: double .infinity,
184+ padding: const EdgeInsets .all (12 ),
185+ decoration: BoxDecoration (
186+ color: Theme .of (context).colorScheme.surfaceContainer,
187+ borderRadius: BorderRadius .circular (10 ),
188+ ),
189+ child: child,
190+ ),
191+ ],
192+ ),
193+ );
194+ }
195+ }
196+
197+ /// Selectable monospace text — used for file content, command output, etc.
198+ class _MonoText extends StatelessWidget {
199+ const _MonoText (this .text, {this .error = false });
200+ final String text;
201+ final bool error;
202+
203+ @override
204+ Widget build (BuildContext context) => SelectableText (
205+ text,
206+ style: TextStyle (
207+ fontFamily: 'monospace' ,
208+ fontSize: 12.5 ,
209+ color: error ? Theme .of (context).colorScheme.error : null ,
210+ ),
211+ );
212+ }
213+
214+ /// Small label + value row, used to summarise tool parameters.
215+ class _ParamRow extends StatelessWidget {
216+ const _ParamRow (this .label, this .value);
217+ final String label;
218+ final String value;
219+
220+ @override
221+ Widget build (BuildContext context) {
222+ return Padding (
223+ padding: const EdgeInsets .only (bottom: 4 ),
224+ child: Row (
225+ crossAxisAlignment: CrossAxisAlignment .start,
226+ children: [
227+ SizedBox (
228+ width: 72 ,
229+ child: Text (
230+ label,
231+ style: Theme .of (context).textTheme.bodySmall? .copyWith (
232+ color: Theme .of (context).colorScheme.outline,
233+ ),
234+ ),
235+ ),
236+ Expanded (
237+ child: SelectableText (
238+ value,
239+ style: const TextStyle (fontFamily: 'monospace' , fontSize: 12.5 ),
240+ ),
241+ ),
242+ ],
243+ ),
244+ );
245+ }
246+ }
247+
248+ // ---------------------------------------------------------------------------
249+ // Built-in renderers
250+ // ---------------------------------------------------------------------------
251+
161252class _ReadRenderer extends ToolRenderer {
162253 const _ReadRenderer ();
163254 @override
@@ -166,6 +257,36 @@ class _ReadRenderer extends ToolRenderer {
166257 IconData get icon => Icons .menu_book_outlined;
167258 @override
168259 String ? subtitle (ToolCallItem item) => item.args['path' ]? .toString ();
260+
261+ @override
262+ Widget detail (BuildContext context, ToolCallItem item) {
263+ final path = item.args['path' ]? .toString () ?? '(no path)' ;
264+ final content = item.output ?? item.deltas.join ();
265+ final offset = item.args['offset' ];
266+ final limit = item.args['limit' ];
267+ return Scaffold (
268+ appBar: AppBar (title: Text (path, overflow: TextOverflow .ellipsis)),
269+ body: ListView (
270+ padding: const EdgeInsets .all (12 ),
271+ children: [
272+ if (offset != null || limit != null )
273+ _ToolSection (
274+ title: 'Range' ,
275+ child: Row (
276+ children: [
277+ if (offset != null ) _ParamRow ('offset' , '$offset ' ),
278+ if (limit != null ) _ParamRow ('limit' , '$limit ' ),
279+ ],
280+ ),
281+ ),
282+ _ToolSection (
283+ title: 'Content' ,
284+ child: _MonoText (content.isEmpty ? '(empty)' : content),
285+ ),
286+ ],
287+ ),
288+ );
289+ }
169290}
170291
171292class _WriteRenderer extends ToolRenderer {
@@ -176,6 +297,28 @@ class _WriteRenderer extends ToolRenderer {
176297 IconData get icon => Icons .edit_note_outlined;
177298 @override
178299 String ? subtitle (ToolCallItem item) => item.args['path' ]? .toString ();
300+
301+ @override
302+ Widget detail (BuildContext context, ToolCallItem item) {
303+ final path = item.args['path' ]? .toString () ?? '(no path)' ;
304+ final content =
305+ item.args['content' ]? .toString () ?? item.args['text' ]? .toString () ?? '' ;
306+ final result = item.output ?? item.summary ?? '' ;
307+ return Scaffold (
308+ appBar: AppBar (title: Text (path, overflow: TextOverflow .ellipsis)),
309+ body: ListView (
310+ padding: const EdgeInsets .all (12 ),
311+ children: [
312+ _ToolSection (
313+ title: 'Content written' ,
314+ child: _MonoText (content.isEmpty ? '(empty)' : content),
315+ ),
316+ if (result.isNotEmpty)
317+ _ToolSection (title: 'Result' , child: Text (result)),
318+ ],
319+ ),
320+ );
321+ }
179322}
180323
181324class _EditRenderer extends ToolRenderer {
@@ -205,6 +348,35 @@ class _BashRenderer extends ToolRenderer {
205348 if (cmd == null ) return null ;
206349 return cmd.length > 80 ? '${cmd .substring (0 , 80 )}…' : cmd;
207350 }
351+
352+ @override
353+ Widget detail (BuildContext context, ToolCallItem item) {
354+ final command = item.args['command' ]? .toString () ?? '' ;
355+ final output = item.deltas.isNotEmpty
356+ ? item.deltas.join ()
357+ : (item.output ?? '' );
358+ final failed = item.ended && (item.exitCode ?? 0 ) != 0 ;
359+ return Scaffold (
360+ appBar: AppBar (title: const Text ('bash' )),
361+ body: ListView (
362+ padding: const EdgeInsets .all (12 ),
363+ children: [
364+ if (command.isNotEmpty)
365+ _ToolSection (title: 'Command' , child: _MonoText (command)),
366+ if (output.isNotEmpty)
367+ _ToolSection (
368+ title: 'Output' ,
369+ child: _MonoText (output, error: failed),
370+ )
371+ else if (item.ended)
372+ _ToolSection (
373+ title: 'Result' ,
374+ child: Text (item.summary ?? 'exit ${item .exitCode ?? 0 }' ),
375+ ),
376+ ],
377+ ),
378+ );
379+ }
208380}
209381
210382class _GrepRenderer extends ToolRenderer {
@@ -219,6 +391,45 @@ class _GrepRenderer extends ToolRenderer {
219391 final g = item.args['glob' ]? .toString ();
220392 return [p, if (g != null ) 'glob:$g ' ].whereType <String >().join (' · ' );
221393 }
394+
395+ @override
396+ Widget detail (BuildContext context, ToolCallItem item) {
397+ final pattern = item.args['pattern' ]? .toString () ?? '' ;
398+ final glob = item.args['glob' ]? .toString ();
399+ final path = item.args['path' ]? .toString ();
400+ final output = item.output ?? item.deltas.join ();
401+ return Scaffold (
402+ appBar: AppBar (
403+ title: Text (
404+ pattern.isEmpty ? 'grep' : 'grep: $pattern ' ,
405+ overflow: TextOverflow .ellipsis,
406+ ),
407+ ),
408+ body: ListView (
409+ padding: const EdgeInsets .all (12 ),
410+ children: [
411+ _ToolSection (
412+ title: 'Search' ,
413+ child: Column (
414+ crossAxisAlignment: CrossAxisAlignment .start,
415+ children: [
416+ if (pattern.isNotEmpty) _ParamRow ('pattern' , pattern),
417+ if (glob != null ) _ParamRow ('glob' , glob),
418+ if (path != null ) _ParamRow ('path' , path),
419+ ],
420+ ),
421+ ),
422+ if (output.isNotEmpty)
423+ _ToolSection (title: 'Results' , child: _MonoText (output))
424+ else if (item.ended)
425+ _ToolSection (
426+ title: 'Results' ,
427+ child: Text (item.summary ?? 'No matches found' ),
428+ ),
429+ ],
430+ ),
431+ );
432+ }
222433}
223434
224435/// Registry. Order does not matter — first matching name wins.
0 commit comments