@@ -17,6 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
1717 type Props = {
1818 messages: ChatMessage [];
1919 activities: HistoryActivity [];
20+ activeTurnId? : string | null ;
2021 interactions? : ProjectedInteraction [];
2122 answers? : Record <string , string >;
2223 onanswer? (requestId : string , id : string , value : string ): void ;
@@ -28,6 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
2829 let {
2930 messages,
3031 activities,
32+ activeTurnId = null ,
3133 interactions = [],
3234 answers = {},
3335 onanswer = () => {},
@@ -38,10 +40,18 @@ SPDX-License-Identifier: AGPL-3.0-or-later
3840 }: Props = $props ();
3941 let groups = $derived (groupMessages (messages ));
4042 let expandedCommentary = $state <Record <string , boolean >>({});
41- let latestAnswerId = $derived (
42- groups .findLast ((group ) => group .kind === ' assistant' && group .answer )?.id ,
43- );
4443 let promptGroups = $derived (groups .filter ((group ) => group .kind === ' user' ));
44+ let assistantGroups = $derived (groups .filter ((group ) => group .kind === ' assistant' ));
45+ let latestAssistantId = $derived (assistantGroups .at (- 1 )?.id );
46+ let hasActiveAssistant = $derived (
47+ Boolean (
48+ activeTurnId &&
49+ assistantGroups .some (
50+ (group ) =>
51+ group .turnId === activeTurnId || (! group .turnId && group .id === latestAssistantId ),
52+ ),
53+ ),
54+ );
4555 let unassignedInteractions = $derived (
4656 interactions .filter (
4757 (interaction ) =>
@@ -59,6 +69,36 @@ SPDX-License-Identifier: AGPL-3.0-or-later
5969 if (group .kind !== ' user' ) return [];
6070 return interactions .filter ((interaction ) => ownerGroupId (interaction ) === group .id );
6171 }
72+ function turnActivities(group : (typeof groups )[number ]): HistoryActivity [] {
73+ if (group .kind !== ' assistant' ) return [];
74+ return activities .filter ((activity ) =>
75+ activity .turnId ? activity .turnId === group .turnId : group .id === latestAssistantId ,
76+ );
77+ }
78+ function currentActivities(): HistoryActivity [] {
79+ return activities .filter ((activity ) =>
80+ activity .turnId ? activity .turnId === activeTurnId : true ,
81+ );
82+ }
83+ function regularActivities(items : HistoryActivity []): HistoryActivity [] {
84+ return items .filter (
85+ (activity ) => ! activity .label .toLowerCase ().replaceAll (' ' , ' ' ).startsWith (' filechange' ),
86+ );
87+ }
88+ function fileChanges(items : HistoryActivity []): Array <{ id: string ; paths: string [] }> {
89+ return items .flatMap ((activity ) =>
90+ activity .label .toLowerCase ().replaceAll (' ' , ' ' ).startsWith (' filechange' )
91+ ? [{ id: activity .id , paths: activity .detail .split (' \n ' ).filter (Boolean ) }]
92+ : [],
93+ );
94+ }
95+ function isLive(group : (typeof groups )[number ]): boolean {
96+ return Boolean (
97+ group .kind === ' assistant' &&
98+ activeTurnId &&
99+ (group .turnId === activeTurnId || (! group .turnId && group .id === latestAssistantId )),
100+ );
101+ }
62102 </script >
63103
64104{#snippet inline (parts : CommentaryPart [])}
@@ -73,6 +113,22 @@ SPDX-License-Identifier: AGPL-3.0-or-later
73113 {/each }
74114{/ snippet }
75115
116+ {#snippet changedFiles (items : HistoryActivity [])}
117+ {@const changes = fileChanges (items )}
118+ {#if changes .length }
119+ <section class =" file-changes" aria-label =" Files changed" >
120+ <strong >files changed</strong >
121+ <ul >
122+ {#each changes as change (change .id )}
123+ {#each change .paths as path (` ${change .id }:${path } ` )}
124+ <li ><code >{path }</code ></li >
125+ {/each }
126+ {/each }
127+ </ul >
128+ </section >
129+ {/if }
130+ {/ snippet }
131+
76132{#snippet content (text : string )}
77133 {#each renderCommentary (text ) as block , blockIndex (blockIndex )}
78134 {#if block .kind === ' code' }
@@ -129,24 +185,11 @@ SPDX-License-Identifier: AGPL-3.0-or-later
129185 {ondecision }
130186 {onretry }
131187 />
132- {:else if group .answer }
188+ {:else if group .answer !== null }
189+ {@const ownedActivities = turnActivities (group )}
133190 <section class =" answer-turn" >
134191 <div class =" entry-heading" >
135- <strong >answer</strong >
136- {#if group .commentary }
137- <button
138- class =" commentary-toggle"
139- type =" button"
140- aria-expanded ={Boolean (expandedCommentary [group .id ])}
141- aria-controls ={` commentary-${group .id } ` }
142- onclick ={() => (expandedCommentary [group .id ] = ! expandedCommentary [group .id ])}
143- >
144- <span aria-hidden ="true" >{expandedCommentary [group .id ] ? ' ⌄' : ' >' }</span >commentary
145- </button >
146- {/if }
147- {#if group .id === latestAnswerId }
148- <ActivityList {activities } />
149- {/if }
192+ <strong >{isLive (group ) ? ' working' : ' answer' }</strong >
150193 {#if group .occurredAt }
151194 <time datetime ={new Date (group .occurredAt ).toISOString ()}>
152195 {formatMessageTime (group .occurredAt )}
@@ -156,22 +199,57 @@ SPDX-License-Identifier: AGPL-3.0-or-later
156199 </time >
157200 {/if }
158201 </div >
159- {#if group .commentary && expandedCommentary [group .id ]}
202+ {#if isLive (group )}
203+ {#if group .commentary }
204+ <div class =" commentary-content live-commentary" >
205+ {@render content (group .commentary )}
206+ </div >
207+ {/if }
208+ <ActivityList activities ={regularActivities (ownedActivities )} variant =" live" />
209+ {:else if group .commentary || regularActivities (ownedActivities ).length }
210+ <div class =" answer-history" >
211+ {#if group .commentary }
212+ <button
213+ class =" commentary-toggle"
214+ type =" button"
215+ aria-expanded ={Boolean (expandedCommentary [group .id ])}
216+ aria-controls ={` commentary-${group .id } ` }
217+ onclick ={() => (expandedCommentary [group .id ] = ! expandedCommentary [group .id ])}
218+ >
219+ <span aria-hidden ="true" >{expandedCommentary [group .id ] ? ' ⌄' : ' ›' }</span >
220+ commentary
221+ </button >
222+ {/if }
223+ <ActivityList activities ={regularActivities (ownedActivities )} />
224+ </div >
225+ {/if }
226+ {#if ! isLive (group ) && group .commentary && expandedCommentary [group .id ]}
160227 <div class ="commentary-content" id ={` commentary-${group .id } ` }>
161228 {@render content (group .commentary )}
162229 </div >
163230 {/if }
231+ {@render changedFiles (ownedActivities )}
164232 <div class ="entry-content" >{@render content (group .answer )}</div >
165233 </section >
166- {:else if group .commentary }
167- <section class =" commentary-turn" >
234+ {:else if group .commentary !== null }
235+ {@const ownedActivities = turnActivities (group )}
236+ <section class ={isLive (group ) ? ' progress-turn' : ' commentary-turn' }>
168237 <div class =" entry-heading" >
238+ <strong >{isLive (group ) ? ' working' : ' commentary' }</strong >
239+ </div >
240+ {#if isLive (group )}
241+ {#if group .commentary }
242+ <div class ="entry-content" >{@render content (group .commentary )}</div >
243+ {/if }
244+ <ActivityList activities ={regularActivities (ownedActivities )} variant =" live" />
245+ {:else }
169246 <details >
170247 <summary >commentary</summary >
171248 {@render content (group .commentary )}
172249 </details >
173- <ActivityList {activities } />
174- </div >
250+ <ActivityList activities ={regularActivities (ownedActivities )} />
251+ {/if }
252+ {@render changedFiles (ownedActivities )}
175253 </section >
176254 {/if }
177255 </li >
@@ -189,6 +267,15 @@ SPDX-License-Identifier: AGPL-3.0-or-later
189267 />
190268 </li >
191269 {/if }
270+ {#if activeTurnId && ! hasActiveAssistant && currentActivities ().length }
271+ <li class =" progress-item" >
272+ <section class =" progress-turn" >
273+ <div class =" entry-heading" ><strong >working</strong ></div >
274+ <ActivityList activities ={regularActivities (currentActivities ())} variant =" live" />
275+ {@render changedFiles (currentActivities ())}
276+ </section >
277+ </li >
278+ {/if }
192279</ol >
193280
194281<style >
@@ -224,13 +311,22 @@ SPDX-License-Identifier: AGPL-3.0-or-later
224311 }
225312
226313 .commentary-turn ,
314+ .progress-turn ,
227315 .commentary-content {
228316 padding : 0.5rem 0.625rem ;
229317 background : var (--theme-surface-subtle );
230318 border-inline-start : 0.25rem solid var (--theme-info );
231319 border-radius : 0.375rem ;
232320 }
233321
322+ .live-commentary {
323+ background : transparent ;
324+ }
325+
326+ .progress-turn {
327+ background : transparent ;
328+ }
329+
234330 .entry-heading {
235331 display : flex ;
236332 align-items : baseline ;
@@ -253,6 +349,33 @@ SPDX-License-Identifier: AGPL-3.0-or-later
253349 font-size : 0.875em ;
254350 }
255351
352+ .answer-history {
353+ display : flex ;
354+ flex-wrap : wrap ;
355+ align-items : baseline ;
356+ gap : 0.25rem 0.75rem ;
357+ margin-block : 0.2rem 0.35rem ;
358+ }
359+
360+ .file-changes {
361+ margin-block : 0.5rem ;
362+ padding : 0.5rem 0.625rem ;
363+ background : var (--theme-surface-subtle );
364+ border-inline-start : 0.25rem solid var (--theme-success );
365+ border-radius : 0.375rem ;
366+ }
367+
368+ .file-changes > strong {
369+ font-size : 0.875em ;
370+ }
371+
372+ .file-changes ul {
373+ display : grid ;
374+ gap : 0.15rem ;
375+ margin : 0.25rem 0 0 ;
376+ padding-inline-start : 1.25rem ;
377+ }
378+
256379 .commentary-content {
257380 margin-block : 0.25rem 0.5rem ;
258381 }
0 commit comments