Skip to content

Commit 48ad84e

Browse files
author
Tajudeen
committed
Update todo list widget, styling, and related components
1 parent 305ecbd commit 48ad84e

File tree

6 files changed

+732
-333
lines changed

6 files changed

+732
-333
lines changed

src/vs/workbench/contrib/chat/browser/chatContentParts/chatTodoListWidget.ts

Lines changed: 147 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,37 +36,52 @@ interface ITodoListTemplate {
3636
readonly todoElement: HTMLElement;
3737
readonly statusIcon: HTMLElement;
3838
readonly iconLabel: IconLabel;
39+
readonly deleteButton?: HTMLElement;
3940
}
4041

4142
class 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

Comments
 (0)