@@ -42,9 +42,8 @@ import {
4242 PanelTabAdd ,
4343 PanelTabs ,
4444 useTabs ,
45- Activity ,
46- ToolApproval ,
47- groupActivity ,
45+ MessageParts ,
46+ type ConversationPart ,
4847 type PartTool ,
4948 type ToolApprovalRecord ,
5049 Message ,
@@ -738,17 +737,22 @@ function PanelTabsScenario() {
738737 ) ;
739738}
740739
740+ /** A call's execution status once a decision is recorded on it. */
741+ function decidedStatus ( approval : ToolApprovalRecord ) : PartTool [ "status" ] {
742+ if ( approval . status === "pending" ) return "approval-requested" ;
743+ return approval . status === "approved" ? "output-available" : "output-denied" ;
744+ }
745+
746+ function pendingApproval ( id : string ) : ToolApprovalRecord {
747+ return { id, status : "pending" , scope : null , timeoutAt : null } ;
748+ }
749+
741750// A host's view of one agent turn: a finished draft, then a send waiting on a
742- // person. Deciding records the outcome on the call, which then folds into the
743- // run before it, the way a message renders `groupActivity` .
751+ // person. Deciding records the outcome on the call, which `MessageParts` then
752+ // folds into the run before it .
744753function ToolApprovalScenario ( ) {
745754 const { log, record } = useEventLog ( ) ;
746- const [ approval , setApproval ] = useState < ToolApprovalRecord > ( {
747- id : "approval-1" ,
748- status : "pending" ,
749- scope : null ,
750- timeoutAt : null ,
751- } ) ;
755+ const [ approval , setApproval ] = useState ( pendingApproval ( "approval-1" ) ) ;
752756 const parts : PartTool [ ] = [
753757 {
754758 kind : "tool-trace" ,
@@ -761,41 +765,98 @@ function ToolApprovalScenario() {
761765 kind : "tool-trace" ,
762766 toolCallId : "send" ,
763767 name : "mail__send" ,
764- status :
765- approval . status === "pending"
766- ? "approval-requested"
767- : approval . status === "approved"
768- ? "output-available"
769- : "output-denied" ,
768+ status : decidedStatus ( approval ) ,
770769 input : { to : "team" } ,
771770 approval,
772771 } ,
773772 ] ;
774773 return (
775774 < >
776- { groupActivity ( parts ) . map ( ( group ) =>
777- group . kind === "activity" ? (
778- < Activity key = { group . key } steps = { group . steps } />
779- ) : group . part . kind === "tool-trace" ? (
780- < ToolApproval
781- key = { group . key }
782- part = { group . part }
783- onDecide = { ( decision ) => {
784- record ( `${ decision . outcome } :${ decision . scope } ` ) ;
785- setApproval ( ( current ) => ( {
786- ...current ,
787- status : decision . outcome ,
788- scope : decision . scope ,
789- } ) ) ;
790- } }
791- />
792- ) : null ,
793- ) }
775+ < Message from = "agent" aria-label = "Agent" >
776+ < MessageParts
777+ parts = { parts }
778+ onDecide = { ( _ , decision ) => {
779+ record ( `${ decision . outcome } :${ decision . scope } ` ) ;
780+ setApproval ( ( current ) => ( {
781+ ...current ,
782+ status : decision . outcome ,
783+ scope : decision . scope ,
784+ } ) ) ;
785+ } }
786+ />
787+ </ Message >
794788 { log }
795789 </ >
796790 ) ;
797791}
798792
793+ const BATCH_CALLS = [ "notice" , "totals" , "calendar" ] as const ;
794+
795+ // Three calls that ran at once after a finished read, two of them waiting on
796+ // a person. Each decision is recorded on its call; the batch folds into the
797+ // run once none is left waiting.
798+ function ToolBatchApprovalScenario ( ) {
799+ const [ approvals , setApprovals ] = useState <
800+ Record < string , ToolApprovalRecord >
801+ > ( {
802+ notice : pendingApproval ( "approval-notice" ) ,
803+ calendar : pendingApproval ( "approval-calendar" ) ,
804+ } ) ;
805+ const parts : ConversationPart [ ] = [
806+ {
807+ kind : "tool-trace" ,
808+ toolCallId : "read" ,
809+ name : "files__read" ,
810+ status : "output-available" ,
811+ output : "Read 3 files" ,
812+ } ,
813+ {
814+ kind : "tool-batch" ,
815+ id : "batch" ,
816+ tools : BATCH_CALLS . map ( ( name ) : PartTool => {
817+ const approval = approvals [ name ] ;
818+ return approval === undefined
819+ ? {
820+ kind : "tool-trace" ,
821+ toolCallId : name ,
822+ name : `send__${ name } ` ,
823+ status : "output-available" ,
824+ output : "Sent" ,
825+ }
826+ : {
827+ kind : "tool-trace" ,
828+ toolCallId : name ,
829+ name : `send__${ name } ` ,
830+ status : decidedStatus ( approval ) ,
831+ input : { to : "team" } ,
832+ approval,
833+ } ;
834+ } ) ,
835+ } ,
836+ ] ;
837+ return (
838+ < Message from = "agent" aria-label = "Agent" >
839+ < MessageParts
840+ parts = { parts }
841+ onDecide = { ( tool , decision ) =>
842+ setApprovals ( ( current ) => {
843+ const approval = current [ tool . toolCallId ] ;
844+ if ( approval === undefined ) return current ;
845+ return {
846+ ...current ,
847+ [ tool . toolCallId ] : {
848+ ...approval ,
849+ status : decision . outcome ,
850+ scope : decision . scope ,
851+ } ,
852+ } ;
853+ } )
854+ }
855+ />
856+ </ Message >
857+ ) ;
858+ }
859+
799860/** A long answer: 40 paragraphs of fixed-height lines, past the fold. */
800861const FOLD_PARAGRAPHS = Array . from (
801862 { length : 40 } ,
@@ -1275,11 +1336,7 @@ function ThreadMessage({ message }: { message: ConversationMessage }) {
12751336 from = { own ? "own" : "agent" }
12761337 aria-label = { `${ message . author . name } ${ message . id } ` }
12771338 >
1278- < MessageContent
1279- text = { message . parts
1280- . map ( ( part ) => ( part . kind === "text" ? part . text : "" ) )
1281- . join ( "" ) }
1282- />
1339+ < MessageParts parts = { message . parts } />
12831340 </ Message >
12841341 ) ;
12851342}
@@ -1410,6 +1467,7 @@ export const scenarios: Record<string, () => ReactNode> = {
14101467 "prompt-input-toolbar" : ( ) => < PromptToolbarScenario /> ,
14111468 "panel-tabs" : ( ) => < PanelTabsScenario /> ,
14121469 "tool-approval" : ( ) => < ToolApprovalScenario /> ,
1470+ "tool-batch-approval" : ( ) => < ToolBatchApprovalScenario /> ,
14131471 "message-fold" : ( ) => < MessageFoldScenario /> ,
14141472 "message-fold-settle" : ( ) => (
14151473 < MessageFoldConversationScenario policy = "settle" />
0 commit comments