@@ -36,37 +36,52 @@ interface ITodoListTemplate {
3636 readonly todoElement : HTMLElement ;
3737 readonly statusIcon : HTMLElement ;
3838 readonly iconLabel : IconLabel ;
39+ readonly deleteButton ?: HTMLElement ;
3940}
4041
4142class TodoListRenderer implements IListRenderer < IChatTodo , ITodoListTemplate > {
4243 static TEMPLATE_ID = 'todoListRenderer' ;
4344 readonly templateId : string = TodoListRenderer . TEMPLATE_ID ;
4445
4546 constructor (
46- private readonly configurationService : IConfigurationService
47+ private readonly configurationService : IConfigurationService ,
48+ private readonly onTodoClick : ( todo : IChatTodo ) => void ,
49+ private readonly onTodoDelete : ( todo : IChatTodo ) => void
4750 ) { }
4851
4952 renderTemplate ( container : HTMLElement ) : ITodoListTemplate {
5053 const templateDisposables = new DisposableStore ( ) ;
5154 const todoElement = dom . append ( container , dom . $ ( 'li.todo-item' ) ) ;
5255 todoElement . setAttribute ( 'role' , 'listitem' ) ;
56+ todoElement . setAttribute ( 'tabindex' , '0' ) ;
5357
5458 const statusIcon = dom . append ( todoElement , dom . $ ( '.todo-status-icon.codicon' ) ) ;
5559 statusIcon . setAttribute ( 'aria-hidden' , 'true' ) ;
5660
5761 const todoContent = dom . append ( todoElement , dom . $ ( '.todo-content' ) ) ;
5862 const iconLabel = templateDisposables . add ( new IconLabel ( todoContent , { supportIcons : false } ) ) ;
5963
60- return { templateDisposables, todoElement, statusIcon, iconLabel } ;
64+ // Add delete button
65+ const deleteButton = dom . append ( todoElement , dom . $ ( '.todo-delete-button.codicon' ) ) ;
66+ deleteButton . classList . add ( 'codicon-close' ) ;
67+ deleteButton . setAttribute ( 'aria-label' , localize ( 'chat.todoList.deleteTodo' , 'Delete todo' ) ) ;
68+ deleteButton . setAttribute ( 'aria-hidden' , 'true' ) ;
69+ deleteButton . style . display = 'none' ; // Show on hover
70+
71+ return { templateDisposables, todoElement, statusIcon, iconLabel, deleteButton } ;
6172 }
6273
6374 renderElement ( todo : IChatTodo , index : number , templateData : ITodoListTemplate ) : void {
64- const { todoElement, statusIcon, iconLabel } = templateData ;
75+ const { todoElement, statusIcon, iconLabel, deleteButton } = templateData ;
6576
6677 // Update status icon
6778 statusIcon . className = `todo-status-icon codicon ${ this . getStatusIconClass ( todo . status ) } ` ;
6879 statusIcon . style . color = this . getStatusIconColor ( todo . status ) ;
6980
81+ // Set class for status-based styling
82+ todoElement . classList . remove ( 'todo-not-started' , 'todo-in-progress' , 'todo-completed' ) ;
83+ todoElement . classList . add ( `todo-${ todo . status === 'not-started' ? 'not-started' : todo . status === 'in-progress' ? 'in-progress' : 'completed' } ` ) ;
84+
7085 // Update title with tooltip if description exists and description field is enabled
7186 const includeDescription = this . configurationService . getValue < boolean > ( TodoListToolDescriptionFieldSettingId ) !== false ;
7287 const title = includeDescription && todo . description && todo . description . trim ( ) ? todo . description : undefined ;
@@ -78,6 +93,34 @@ class TodoListRenderer implements IListRenderer<IChatTodo, ITodoListTemplate> {
7893 ? localize ( 'chat.todoList.itemWithDescription' , '{0}, {1}, {2}' , todo . title , statusText , todo . description )
7994 : localize ( 'chat.todoList.item' , '{0}, {1}' , todo . title , statusText ) ;
8095 todoElement . setAttribute ( 'aria-label' , ariaLabel ) ;
96+
97+ // Set up click handlers
98+ todoElement . onclick = ( e ) => {
99+ if ( deleteButton && ( deleteButton . contains ( e . target as Node ) || deleteButton === e . target ) ) {
100+ e . stopPropagation ( ) ;
101+ e . preventDefault ( ) ;
102+ this . onTodoDelete ( todo ) ;
103+ return ;
104+ }
105+ this . onTodoClick ( todo ) ;
106+ } ;
107+
108+ // Set up delete button click handler separately
109+ if ( deleteButton ) {
110+ deleteButton . onclick = ( e ) => {
111+ e . stopPropagation ( ) ;
112+ e . preventDefault ( ) ;
113+ this . onTodoDelete ( todo ) ;
114+ } ;
115+
116+ // Show delete button on hover
117+ todoElement . onmouseenter = ( ) => {
118+ deleteButton . style . display = 'block' ;
119+ } ;
120+ todoElement . onmouseleave = ( ) => {
121+ deleteButton . style . display = 'none' ;
122+ } ;
123+ }
81124 }
82125
83126 disposeTemplate ( templateData : ITodoListTemplate ) : void {
@@ -111,12 +154,12 @@ class TodoListRenderer implements IListRenderer<IChatTodo, ITodoListTemplate> {
111154 private getStatusIconColor ( status : string ) : string {
112155 switch ( status ) {
113156 case 'completed' :
114- return 'var(--vscode-charts-green)' ;
157+ return 'var(--vscode-charts-green, #38b284 )' ;
115158 case 'in-progress' :
116- return 'var(--vscode-charts-blue)' ;
159+ return 'var(--vscode-charts-blue, #7b5cff )' ;
117160 case 'not-started' :
118161 default :
119- return 'var(--vscode-foreground)' ;
162+ return 'var(--vscode-foreground, rgba(255, 255, 255, 0.6) )' ;
120163 }
121164 }
122165}
@@ -132,6 +175,7 @@ export class ChatTodoListWidget extends Disposable {
132175 private expandoButton ! : Button ;
133176 private expandIcon ! : HTMLElement ;
134177 private titleElement ! : HTMLElement ;
178+ private progressBarContainer ! : HTMLElement ;
135179 private todoListContainer ! : HTMLElement ;
136180 private clearButtonContainer ! : HTMLElement ;
137181 private clearButton ! : Button ;
@@ -187,6 +231,11 @@ export class ChatTodoListWidget extends Disposable {
187231 this . titleElement . id = 'todo-list-title' ;
188232 this . titleElement . textContent = localize ( 'chat.todoList.title' , 'Todos' ) ;
189233
234+ // Add progress bar container
235+ this . progressBarContainer = dom . $ ( '.todo-progress-bar-container' ) ;
236+ this . progressBarContainer . style . display = 'none' ;
237+ titleSection . appendChild ( this . progressBarContainer ) ;
238+
190239 // Add clear button container to the expand element
191240 this . clearButtonContainer = dom . $ ( '.todo-clear-button-container' ) ;
192241 this . createClearButton ( ) ;
@@ -264,6 +313,9 @@ export class ChatTodoListWidget extends Disposable {
264313
265314 if ( ! shouldShow ) {
266315 this . domNode . classList . remove ( 'has-todos' ) ;
316+ if ( todoList . length === 0 ) {
317+ this . hideWidget ( ) ;
318+ }
267319 return ;
268320 }
269321
@@ -288,7 +340,11 @@ export class ChatTodoListWidget extends Disposable {
288340 'ChatTodoListRenderer' ,
289341 this . todoListContainer ,
290342 new TodoListDelegate ( ) ,
291- [ new TodoListRenderer ( this . configurationService ) ] ,
343+ [ new TodoListRenderer (
344+ this . configurationService ,
345+ ( todo ) => this . handleTodoClick ( todo ) ,
346+ ( todo ) => this . handleTodoDelete ( todo )
347+ ) ] ,
292348 {
293349 alwaysConsumeMouseWheel : false ,
294350 accessibilityProvider : {
@@ -337,10 +393,26 @@ export class ChatTodoListWidget extends Disposable {
337393 this . _isExpanded = ! this . _isExpanded ;
338394 this . _userManuallyExpanded = true ;
339395
396+ // Smooth transition for expand icon
397+ this . expandIcon . style . transition = 'transform 0.2s cubic-bezier(0.4, 0, 0.2, 1)' ;
340398 this . expandIcon . classList . toggle ( 'codicon-chevron-down' , this . _isExpanded ) ;
341399 this . expandIcon . classList . toggle ( 'codicon-chevron-right' , ! this . _isExpanded ) ;
342400
343- this . todoListContainer . style . display = this . _isExpanded ? 'block' : 'none' ;
401+ // Smooth fade-in for container
402+ if ( this . _isExpanded ) {
403+ this . todoListContainer . style . display = 'block' ;
404+ this . todoListContainer . style . opacity = '0' ;
405+ setTimeout ( ( ) => {
406+ this . todoListContainer . style . transition = 'opacity 0.2s ease' ;
407+ this . todoListContainer . style . opacity = '1' ;
408+ } , 10 ) ;
409+ } else {
410+ this . todoListContainer . style . transition = 'opacity 0.15s ease' ;
411+ this . todoListContainer . style . opacity = '0' ;
412+ setTimeout ( ( ) => {
413+ this . todoListContainer . style . display = 'none' ;
414+ } , 150 ) ;
415+ }
344416
345417 if ( this . _currentSessionResource ) {
346418 const todoList = this . chatTodoListService . getTodos ( this . _currentSessionResource ) ;
@@ -389,6 +461,16 @@ export class ChatTodoListWidget extends Disposable {
389461 const notStartedTodos = todoList . filter ( todo => todo . status === 'not-started' ) ;
390462 const firstNotStartedTodo = notStartedTodos . length > 0 ? notStartedTodos [ 0 ] : undefined ;
391463 const currentTaskNumber = inProgressTodos . length > 0 ? completedCount + 1 : Math . max ( 1 , completedCount ) ;
464+ const progressPercentage = totalCount > 0 ? ( completedCount / totalCount ) * 100 : 0 ;
465+
466+ // Update progress bar
467+ if ( this . progressBarContainer && totalCount > 0 ) {
468+ this . progressBarContainer . innerHTML = '' ;
469+ const progressBar = dom . $ ( '.todo-progress-bar' ) ;
470+ progressBar . style . width = `${ progressPercentage } %` ;
471+ this . progressBarContainer . appendChild ( progressBar ) ;
472+ this . progressBarContainer . style . display = this . _isExpanded ? 'block' : 'none' ;
473+ }
392474
393475 const expandButtonLabel = this . _isExpanded
394476 ? localize ( 'chat.todoList.collapseButton' , 'Collapse Todos' )
@@ -402,7 +484,13 @@ export class ChatTodoListWidget extends Disposable {
402484 localize ( 'chat.todoList.titleWithCount' , 'Todos ({0}/{1})' , currentTaskNumber , totalCount ) :
403485 localize ( 'chat.todoList.title' , 'Todos' ) ;
404486 titleElement . appendChild ( titleText ) ;
487+ if ( this . progressBarContainer ) {
488+ this . progressBarContainer . style . display = 'block' ;
489+ }
405490 } else {
491+ if ( this . progressBarContainer ) {
492+ this . progressBarContainer . style . display = 'none' ;
493+ }
406494 // Show first in-progress todo, or if none, the first not-started todo
407495 const todoToShow = firstInProgressTodo || firstNotStartedTodo ;
408496 if ( todoToShow ) {
@@ -448,4 +536,55 @@ export class ChatTodoListWidget extends Disposable {
448536 return localize ( 'chat.todoList.status.notStarted' , 'not started' ) ;
449537 }
450538 }
539+
540+ private handleTodoClick ( todo : IChatTodo ) : void {
541+ if ( ! this . _currentSessionResource ) {
542+ return ;
543+ }
544+
545+ const currentTodos = this . chatTodoListService . getTodos ( this . _currentSessionResource ) ;
546+ const updatedTodos = currentTodos . map ( t => {
547+ if ( t . id === todo . id ) {
548+ // Cycle through statuses: not-started -> in-progress -> completed -> not-started
549+ let newStatus : 'not-started' | 'in-progress' | 'completed' ;
550+ if ( t . status === 'not-started' ) {
551+ newStatus = 'in-progress' ;
552+ // If setting to in-progress, ensure no other todo is in-progress
553+ currentTodos . forEach ( other => {
554+ if ( other . id !== t . id && other . status === 'in-progress' ) {
555+ const otherIndex = currentTodos . indexOf ( other ) ;
556+ currentTodos [ otherIndex ] = { ...other , status : 'not-started' } ;
557+ }
558+ } ) ;
559+ } else if ( t . status === 'in-progress' ) {
560+ newStatus = 'completed' ;
561+ } else {
562+ newStatus = 'not-started' ;
563+ }
564+ return { ...t , status : newStatus } ;
565+ }
566+ return t ;
567+ } ) ;
568+
569+ // Add visual feedback animation
570+ const todoElement = this . _todoList ?. getHTMLElement ( ) . querySelector ( `.todo-item[aria-label*="${ todo . title } "]` ) as HTMLElement ;
571+ if ( todoElement ) {
572+ todoElement . style . animation = 'none' ;
573+ setTimeout ( ( ) => {
574+ todoElement . style . animation = 'todo-status-change 0.3s ease' ;
575+ } , 10 ) ;
576+ }
577+
578+ this . chatTodoListService . setTodos ( this . _currentSessionResource , updatedTodos ) ;
579+ }
580+
581+ private handleTodoDelete ( todo : IChatTodo ) : void {
582+ if ( ! this . _currentSessionResource ) {
583+ return ;
584+ }
585+
586+ const currentTodos = this . chatTodoListService . getTodos ( this . _currentSessionResource ) ;
587+ const updatedTodos = currentTodos . filter ( t => t . id !== todo . id ) ;
588+ this . chatTodoListService . setTodos ( this . _currentSessionResource , updatedTodos ) ;
589+ }
451590}
0 commit comments