Skip to content

Commit 226f43e

Browse files
authored
Add task list support (microsoft#257214)
1 parent c9e81fb commit 226f43e

File tree

11 files changed

+584
-8
lines changed

11 files changed

+584
-8
lines changed

src/vs/workbench/contrib/chat/browser/chat.contribution.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ import { ChatDynamicVariableModel } from './contrib/chatDynamicVariables.js';
114114
import { ChatAttachmentResolveService, IChatAttachmentResolveService } from './chatAttachmentResolveService.js';
115115
import { registerLanguageModelActions } from './actions/chatLanguageModelActions.js';
116116
import { PromptUrlHandler } from './promptSyntax/promptUrlHandler.js';
117+
import { ChatTaskServiceImpl, IChatTasksService } from '../common/chatTasksService.js';
117118

118119
// Register configuration
119120
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
@@ -512,6 +513,13 @@ configurationRegistry.registerConfiguration({
512513
experiment: {
513514
mode: 'startup'
514515
}
516+
},
517+
'chat.manageTasksTool.enabled': {
518+
type: 'boolean',
519+
default: false,
520+
description: nls.localize('chat.manageTasksTool.enabled', "Enables manageTasksTool in chat. This tool allows you to use task lists in chat."),
521+
tags: ['experimental'],
522+
included: false,
515523
}
516524
}
517525
});
@@ -794,6 +802,7 @@ registerSingleton(IPromptsService, PromptsService, InstantiationType.Delayed);
794802
registerSingleton(IChatContextPickService, ChatContextPickService, InstantiationType.Delayed);
795803
registerSingleton(IChatModeService, ChatModeService, InstantiationType.Delayed);
796804
registerSingleton(IChatAttachmentResolveService, ChatAttachmentResolveService, InstantiationType.Delayed);
805+
registerSingleton(IChatTasksService, ChatTaskServiceImpl, InstantiationType.Delayed);
797806

798807
registerWorkbenchContribution2(ChatEditingNotebookFileSystemProviderContrib.ID, ChatEditingNotebookFileSystemProviderContrib, WorkbenchPhase.BlockStartup);
799808

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as dom from '../../../../../../base/browser/dom.js';
7+
import { localize } from '../../../../../../nls.js';
8+
import { IChatTasksContent, IChatToolInvocation, IChatToolInvocationSerialized } from '../../../common/chatService.js';
9+
import { IChatCodeBlockInfo } from '../../chat.js';
10+
import { BaseChatToolInvocationSubPart } from './chatToolInvocationSubPart.js';
11+
12+
export class ChatTaskListSubPart extends BaseChatToolInvocationSubPart {
13+
public readonly domNode: HTMLElement;
14+
public override readonly codeblocks: IChatCodeBlockInfo[] = [];
15+
16+
private _isExpanded: boolean = true;
17+
private expandoElement!: HTMLElement;
18+
private taskListContainer!: HTMLElement;
19+
20+
constructor(
21+
toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized,
22+
private readonly taskData: IChatTasksContent,
23+
) {
24+
super(toolInvocation);
25+
26+
this.domNode = this.createTaskListPart();
27+
}
28+
29+
private createTaskListPart(): HTMLElement {
30+
const container = dom.$('.chat-task-list-part');
31+
32+
this.expandoElement = dom.$('.task-expando');
33+
this.expandoElement.setAttribute('role', 'button');
34+
this.expandoElement.setAttribute('aria-expanded', 'true');
35+
this.expandoElement.setAttribute('tabindex', '0');
36+
37+
const expandIcon = dom.$('.expand-icon.codicon');
38+
expandIcon.classList.add(this._isExpanded ? 'codicon-chevron-down' : 'codicon-chevron-right');
39+
40+
const titleElement = dom.$('.task-title');
41+
titleElement.textContent = localize('chat.task.title', 'Tasks');
42+
43+
this.expandoElement.appendChild(expandIcon);
44+
this.expandoElement.appendChild(titleElement);
45+
46+
this.taskListContainer = dom.$('.task-list-container');
47+
this.taskListContainer.style.display = this._isExpanded ? 'block' : 'none';
48+
49+
this.renderTaskList();
50+
51+
container.appendChild(this.expandoElement);
52+
container.appendChild(this.taskListContainer);
53+
54+
this._register(dom.addDisposableListener(this.expandoElement, 'click', () => {
55+
this.toggleExpanded();
56+
}));
57+
58+
this._register(dom.addDisposableListener(this.expandoElement, 'keydown', (e) => {
59+
if (e.key === 'Enter' || e.key === ' ') {
60+
e.preventDefault();
61+
this.toggleExpanded();
62+
}
63+
}));
64+
65+
return container;
66+
}
67+
68+
private renderTaskList(): void {
69+
dom.clearNode(this.taskListContainer);
70+
71+
const listElement = dom.$('.task-list');
72+
73+
this.taskData.tasks.forEach((task, index) => {
74+
const taskItem = dom.$('.task-item');
75+
76+
const statusIcon = dom.$('.task-status-icon.codicon');
77+
const iconClass = this.getStatusIconClass(task.status);
78+
const iconColor = this.getStatusIconColor(task.status);
79+
statusIcon.classList.add(iconClass);
80+
statusIcon.style.color = iconColor;
81+
82+
// Create content
83+
const contentElement = dom.$('.task-content');
84+
contentElement.textContent = task.title;
85+
86+
taskItem.appendChild(statusIcon);
87+
taskItem.appendChild(contentElement);
88+
89+
listElement.appendChild(taskItem);
90+
});
91+
92+
this.taskListContainer.appendChild(listElement);
93+
}
94+
95+
private toggleExpanded(): void {
96+
this._isExpanded = !this._isExpanded;
97+
98+
const expandIcon = this.expandoElement.querySelector('.expand-icon') as HTMLElement;
99+
if (expandIcon) {
100+
expandIcon.classList.toggle('codicon-chevron-down', this._isExpanded);
101+
expandIcon.classList.toggle('codicon-chevron-right', !this._isExpanded);
102+
}
103+
104+
this.expandoElement.setAttribute('aria-expanded', this._isExpanded.toString());
105+
this.taskListContainer.style.display = this._isExpanded ? 'block' : 'none';
106+
107+
this._onDidChangeHeight.fire();
108+
}
109+
110+
private getStatusIconClass(status: string): string {
111+
switch (status) {
112+
case 'completed':
113+
return 'codicon-check';
114+
case 'in-progress':
115+
return 'codicon-record';
116+
case 'not-started':
117+
default:
118+
return 'codicon-circle-large-outline';
119+
}
120+
}
121+
122+
private getStatusIconColor(status: string): string {
123+
switch (status) {
124+
case 'completed':
125+
return 'var(--vscode-charts-green)';
126+
case 'in-progress':
127+
return 'var(--vscode-charts-blue)';
128+
case 'not-started':
129+
default:
130+
return 'var(--vscode-foreground)';
131+
}
132+
}
133+
}

src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { TerminalConfirmationWidgetSubPart } from './chatTerminalToolSubPart.js'
2424
import { ToolConfirmationSubPart } from './chatToolConfirmationSubPart.js';
2525
import { BaseChatToolInvocationSubPart } from './chatToolInvocationSubPart.js';
2626
import { ChatToolProgressSubPart } from './chatToolProgressPart.js';
27+
import { ChatTaskListSubPart } from './chatTaskListSubPart.js';
2728

2829
export class ChatToolInvocationPart extends Disposable implements IChatContentPart {
2930
public readonly domNode: HTMLElement;
@@ -83,6 +84,9 @@ export class ChatToolInvocationPart extends Disposable implements IChatContentPa
8384
if (this.toolInvocation.toolSpecificData?.kind === 'extensions') {
8485
return this.instantiationService.createInstance(ExtensionsInstallConfirmationWidgetSubPart, this.toolInvocation, this.context);
8586
}
87+
if (this.toolInvocation.toolSpecificData?.kind === 'tasks') {
88+
return this.instantiationService.createInstance(ChatTaskListSubPart, this.toolInvocation, this.toolInvocation.toolSpecificData);
89+
}
8690
if (this.toolInvocation.confirmationMessages) {
8791
if (this.toolInvocation.toolSpecificData?.kind === 'terminal' || this.toolInvocation.toolSpecificData?.kind === 'terminal2') {
8892
return this.instantiationService.createInstance(TerminalConfirmationWidgetSubPart, this.toolInvocation, this.toolInvocation.toolSpecificData, this.context, this.renderer, this.editorPool, this.currentWidthDelegate, this.codeBlockStartIndex);

src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ class ChatResponseAccessibleProvider extends Disposable implements IAccessibleVi
8080
? toolInvocation.toolSpecificData.commandLine.userEdited ?? toolInvocation.toolSpecificData.commandLine.toolEdited ?? toolInvocation.toolSpecificData.commandLine.original
8181
: toolInvocation.toolSpecificData?.kind === 'extensions'
8282
? JSON.stringify(toolInvocation.toolSpecificData.extensions)
83-
: JSON.stringify(toolInvocation.toolSpecificData.rawInput);
83+
: toolInvocation.toolSpecificData?.kind === 'tasks'
84+
? JSON.stringify(toolInvocation.toolSpecificData.tasks)
85+
: JSON.stringify(toolInvocation.toolSpecificData.rawInput);
8486
}
8587
responseContent += `${title}`;
8688
if (input) {

src/vs/workbench/contrib/chat/browser/media/chat.css

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,105 @@
458458
}
459459
}
460460

461+
/* Task List Styles */
462+
.chat-task-list-part {
463+
border: 1px solid var(--vscode-widget-border);
464+
border-radius: 6px;
465+
background: var(--vscode-editor-background);
466+
margin: 4px 0;
467+
overflow: hidden;
468+
}
469+
470+
.chat-task-list-part .task-expando {
471+
display: flex;
472+
align-items: center;
473+
padding: 8px 12px;
474+
background: var(--vscode-editorWidget-background);
475+
border-bottom: 1px solid var(--vscode-widget-border);
476+
cursor: pointer;
477+
user-select: none;
478+
gap: 6px;
479+
}
480+
481+
.chat-task-list-part .task-expando:hover {
482+
background: var(--vscode-list-hoverBackground);
483+
}
484+
485+
.chat-task-list-part .task-expando:focus {
486+
outline: 1px solid var(--vscode-focusBorder);
487+
outline-offset: -1px;
488+
}
489+
490+
.chat-task-list-part .expand-icon {
491+
flex-shrink: 0;
492+
width: 16px;
493+
height: 16px;
494+
display: flex;
495+
align-items: center;
496+
justify-content: center;
497+
}
498+
499+
.chat-task-list-part .task-title {
500+
font-weight: 600;
501+
font-size: 13px;
502+
color: var(--vscode-foreground);
503+
}
504+
505+
.chat-task-list-part .task-list-container {
506+
padding: 8px 0;
507+
}
508+
509+
.chat-task-list-part .task-list {
510+
list-style: none;
511+
padding: 0;
512+
margin: 0;
513+
}
514+
515+
.chat-task-list-part .task-item {
516+
display: flex;
517+
align-items: center;
518+
padding: 4px 12px;
519+
gap: 8px;
520+
min-height: 24px;
521+
font-size: 13px;
522+
}
523+
524+
.chat-task-list-part .task-item:hover {
525+
background: var(--vscode-list-hoverBackground);
526+
}
527+
528+
.chat-task-list-part .task-number {
529+
color: var(--vscode-descriptionForeground);
530+
font-weight: 500;
531+
min-width: 20px;
532+
text-align: right;
533+
}
534+
535+
.chat-task-list-part .task-status-icon {
536+
flex-shrink: 0;
537+
width: 16px;
538+
height: 16px;
539+
display: flex;
540+
align-items: center;
541+
justify-content: center;
542+
}
543+
544+
.chat-task-list-part .task-content {
545+
flex: 1;
546+
color: var(--vscode-foreground);
547+
line-height: 1.4;
548+
}
549+
550+
.chat-task-list-part .task-status-text {
551+
font-size: 11px;
552+
color: var(--vscode-descriptionForeground);
553+
background: var(--vscode-badge-background);
554+
border-radius: 10px;
555+
padding: 2px 6px;
556+
margin-left: auto;
557+
white-space: nowrap;
558+
}
559+
461560
.interactive-item-container .value > .rendered-markdown li > p {
462561
margin: 0;
463562
}

src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { DeferredPromise } from '../../../../../base/common/async.js';
77
import { IMarkdownString } from '../../../../../base/common/htmlContent.js';
88
import { observableValue } from '../../../../../base/common/observable.js';
99
import { localize } from '../../../../../nls.js';
10-
import { IChatExtensionsContent, IChatTerminalToolInvocationData, IChatToolInputInvocationData, IChatToolInvocation, IChatToolInvocationSerialized, type IChatTerminalToolInvocationData2 } from '../chatService.js';
10+
import { IChatExtensionsContent, IChatTerminalToolInvocationData, IChatToolInputInvocationData, IChatTasksContent, IChatToolInvocation, IChatToolInvocationSerialized, type IChatTerminalToolInvocationData2 } from '../chatService.js';
1111
import { IPreparedToolInvocation, IToolConfirmationMessages, IToolData, IToolProgressStep, IToolResult } from '../languageModelToolsService.js';
1212

1313
export class ChatToolInvocation implements IChatToolInvocation {
@@ -45,7 +45,7 @@ export class ChatToolInvocation implements IChatToolInvocation {
4545
public readonly presentation: IPreparedToolInvocation['presentation'];
4646
public readonly toolId: string;
4747

48-
public readonly toolSpecificData?: IChatTerminalToolInvocationData | IChatTerminalToolInvocationData2 | IChatToolInputInvocationData | IChatExtensionsContent;
48+
public readonly toolSpecificData?: IChatTerminalToolInvocationData | IChatTerminalToolInvocationData2 | IChatToolInputInvocationData | IChatExtensionsContent | IChatTasksContent;
4949

5050
public readonly progress = observableValue<{ message?: string | IMarkdownString; progress: number }>(this, { progress: 0 });
5151

src/vs/workbench/contrib/chat/common/chatService.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ export interface IChatToolInputInvocationData {
263263

264264
export interface IChatToolInvocation {
265265
presentation: IPreparedToolInvocation['presentation'];
266-
toolSpecificData?: IChatTerminalToolInvocationData | IChatTerminalToolInvocationData2 | IChatToolInputInvocationData | IChatExtensionsContent;
266+
toolSpecificData?: IChatTerminalToolInvocationData | IChatTerminalToolInvocationData2 | IChatToolInputInvocationData | IChatExtensionsContent | IChatTasksContent;
267267
/** Presence of this property says that confirmation is required */
268268
confirmationMessages?: IToolConfirmationMessages;
269269
confirmed: DeferredPromise<boolean>;
@@ -288,7 +288,7 @@ export interface IChatToolInvocation {
288288
*/
289289
export interface IChatToolInvocationSerialized {
290290
presentation: IPreparedToolInvocation['presentation'];
291-
toolSpecificData?: IChatTerminalToolInvocationData | IChatTerminalToolInvocationData2 | IChatToolInputInvocationData | IChatExtensionsContent;
291+
toolSpecificData?: IChatTerminalToolInvocationData | IChatTerminalToolInvocationData2 | IChatToolInputInvocationData | IChatExtensionsContent | IChatTasksContent;
292292
invocationMessage: string | IMarkdownString;
293293
originMessage: string | IMarkdownString | undefined;
294294
pastTenseMessage: string | IMarkdownString | undefined;
@@ -305,6 +305,17 @@ export interface IChatExtensionsContent {
305305
kind: 'extensions';
306306
}
307307

308+
export interface IChatTasksContent {
309+
kind: 'tasks';
310+
sessionId: string;
311+
tasks: Array<{
312+
id: string;
313+
title: string;
314+
description: string;
315+
status: 'not-started' | 'in-progress' | 'completed';
316+
}>;
317+
}
318+
308319
export interface IChatPrepareToolInvocationPart {
309320
readonly kind: 'prepareToolInvocation';
310321
readonly toolName: string;

0 commit comments

Comments
 (0)