Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,33 @@ export class ChatTodoListWidget extends Disposable {
// Hide decorative icon from screen readers
statusIcon.setAttribute('aria-hidden', 'true');

const todoTitleContainer = dom.$('.todo-title-container');

const titleElement = dom.$('.todo-title');
titleElement.textContent = todo.title;

// Add edit button that appears on hover
const editButton = dom.$('span.todo-edit-button.codicon.codicon-pencil');
editButton.setAttribute('role', 'button');
editButton.setAttribute('tabindex', '0');
editButton.setAttribute('aria-label', localize('chat.todoList.editTitle', 'Edit title'));
editButton.title = localize('chat.todoList.editTitle', 'Edit title');

this._register(dom.addDisposableListener(editButton, 'click', (e) => {
e.stopPropagation();
this.startEditingTitle(titleElement, todo, index);
}));

this._register(dom.addDisposableListener(editButton, 'keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
e.stopPropagation();
this.startEditingTitle(titleElement, todo, index);
}
}));

todoTitleContainer.appendChild(titleElement);
todoTitleContainer.appendChild(editButton);

// Add hidden status text for screen readers
const statusText = this.getStatusText(todo.status);
Expand All @@ -220,7 +245,7 @@ export class ChatTodoListWidget extends Disposable {
statusElement.style.overflow = 'hidden';

const todoContent = dom.$('.todo-content');
todoContent.appendChild(titleElement);
todoContent.appendChild(todoTitleContainer);
todoContent.appendChild(statusElement);

const ariaLabel = includeDescription && todo.description && todo.description.trim()
Expand Down Expand Up @@ -335,6 +360,76 @@ export class ChatTodoListWidget extends Disposable {
}, 50);
}

private startEditingTitle(titleElement: HTMLElement, todo: IChatTodo, index: number): void {
if (!this._currentSessionId) {
return;
}

const originalText = titleElement.textContent || '';

// Make the title editable
titleElement.contentEditable = 'true';
titleElement.classList.add('editing');
titleElement.focus();

// Select all text
const range = document.createRange();
range.selectNodeContents(titleElement);
const selection = window.getSelection();
if (selection) {
selection.removeAllRanges();
selection.addRange(range);
}

const finishEditing = (save: boolean) => {
titleElement.contentEditable = 'false';
titleElement.classList.remove('editing');

if (save) {
const newTitle = titleElement.textContent?.trim() || originalText;
if (newTitle && newTitle !== originalText) {
// Update the todo in the service
const todoList = this.chatTodoListService.getTodos(this._currentSessionId!);
if (todoList[index]) {
todoList[index].title = newTitle;
this.chatTodoListService.setTodos(this._currentSessionId!, todoList);
}
} else {
// Restore original text if empty or unchanged
titleElement.textContent = originalText;
}
} else {
// Cancel: restore original text
titleElement.textContent = originalText;
}
};

// Handle Enter to save, Escape to cancel
const keydownHandler = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
finishEditing(true);
titleElement.removeEventListener('keydown', keydownHandler);
titleElement.removeEventListener('blur', blurHandler);
} else if (e.key === 'Escape') {
e.preventDefault();
finishEditing(false);
titleElement.removeEventListener('keydown', keydownHandler);
titleElement.removeEventListener('blur', blurHandler);
}
};

// Handle blur to save
const blurHandler = () => {
finishEditing(true);
titleElement.removeEventListener('keydown', keydownHandler);
titleElement.removeEventListener('blur', blurHandler);
};

titleElement.addEventListener('keydown', keydownHandler);
titleElement.addEventListener('blur', blurHandler);
}

private updateScrollShadow(): void {
this.domNode.classList.toggle('scrolled', this.todoListContainer.scrollTop > 0);
}
Expand Down
52 changes: 52 additions & 0 deletions src/vs/workbench/contrib/chat/browser/media/chat.css
Original file line number Diff line number Diff line change
Expand Up @@ -2718,6 +2718,58 @@ have to be updated for changes to the rules above, or to support more deeply nes
min-width: 0;
}

.chat-todo-list-widget .todo-title-container {
display: flex;
align-items: center;
gap: 4px;
flex: 1;
min-width: 0;
position: relative;
}

.chat-todo-list-widget .todo-title {
flex: 1;
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}

.chat-todo-list-widget .todo-title.editing {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: 1px;
padding: 2px 4px;
border-radius: 2px;
white-space: normal;
overflow: visible;
text-overflow: clip;
}

.chat-todo-list-widget .todo-edit-button {
opacity: 0;
transition: opacity 0.2s ease;
cursor: pointer;
padding: 2px;
border-radius: 3px;
font-size: 12px;
flex-shrink: 0;
color: var(--vscode-foreground);
}

.chat-todo-list-widget .todo-item:hover .todo-edit-button,
.chat-todo-list-widget .todo-edit-button:focus {
opacity: 1;
}

.chat-todo-list-widget .todo-edit-button:hover {
background-color: var(--vscode-toolbar-hoverBackground);
}

.chat-todo-list-widget .todo-edit-button:focus {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: 1px;
}

.interactive-session .interactive-response .chat-used-context-list.chat-thinking-items {
color: var(--vscode-descriptionForeground);
padding-top: 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,32 @@ suite('ChatTodoListWidget Accessibility', () => {
const todoListContainer = widget.domNode.querySelector('.todo-list-container');
assert.strictEqual(todoListContainer?.getAttribute('aria-labelledby'), 'todo-list-title');
});

test('edit buttons exist and are accessible', () => {
widget.render('test-session');

const editButtons = widget.domNode.querySelectorAll('.todo-edit-button');
assert.strictEqual(editButtons.length, sampleTodos.length, 'Should have edit button for each todo item');

editButtons.forEach(button => {
assert.strictEqual(button.getAttribute('role'), 'button', 'Edit button should have button role');
assert.strictEqual(button.getAttribute('tabindex'), '0', 'Edit button should be focusable');
assert.ok(button.getAttribute('aria-label')?.includes('Edit'), 'Edit button should have proper aria-label');
assert.ok(button.classList.contains('codicon-pencil'), 'Edit button should have pencil icon');
});
});

test('todo titles are wrapped in title containers', () => {
widget.render('test-session');

const titleContainers = widget.domNode.querySelectorAll('.todo-title-container');
assert.strictEqual(titleContainers.length, sampleTodos.length, 'Should have title container for each todo');

titleContainers.forEach(container => {
const title = container.querySelector('.todo-title');
const editButton = container.querySelector('.todo-edit-button');
assert.ok(title, 'Title container should have title element');
assert.ok(editButton, 'Title container should have edit button');
});
});
});