Skip to content

Commit ea84a40

Browse files
authored
Polish tasks panel UI and filter task templates (#807)
- Fix SVG activity bar icons for VS Code theme compatibility using mask-based cutouts and inline fills - Filter templates to only show task-capable ones (`has-ai-task:true`) - Auto-select the default preset (or first) instead of showing a "No preset" option - Show template and preset descriptions in dropdowns - Simplify `TaskTemplate` type: rename `displayName` to `name`, remove unused `icon` field, add `description` - Consolidate duplicate CSS rules and remove redundant status dot/spinner overrides - Remove noisy toast notifications for pause/resume/send actions - Fix log polling to refetch for `complete` tasks (only cache for `idle`/`failed`) Closes #806
1 parent 13744b2 commit ea84a40

File tree

14 files changed

+124
-94
lines changed

14 files changed

+124
-94
lines changed

media/logo-black.svg

Lines changed: 3 additions & 11 deletions
Loading

media/logo-white.svg

Lines changed: 3 additions & 11 deletions
Loading

media/shorthand-logo.svg

Lines changed: 3 additions & 0 deletions
Loading

media/tasks-logo.svg

Lines changed: 11 additions & 3 deletions
Loading

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@
188188
{
189189
"id": "coder",
190190
"title": "Coder Remote",
191-
"icon": "media/logo-white.svg"
191+
"icon": "media/shorthand-logo.svg"
192192
},
193193
{
194194
"id": "coderTasks",

packages/shared/src/tasks/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@ export type { Preset, Task, TaskLogEntry, TaskState, TaskStatus, Template };
1717
export interface TaskTemplate {
1818
id: string;
1919
name: string;
20-
displayName: string;
21-
icon: string;
20+
description: string;
2221
activeVersionId: string;
2322
presets: TaskPreset[];
2423
}
2524

2625
export interface TaskPreset {
2726
id: string;
2827
name: string;
28+
description: string;
2929
isDefault: boolean;
3030
}
3131

packages/shared/src/tasks/utils.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Task, TaskPermissions, TaskStatus } from "./types";
1+
import type { Task, TaskPermissions, TaskState, TaskStatus } from "./types";
22

33
export function getTaskLabel(task: Task): string {
44
return task.display_name || task.name || task.id;
@@ -43,15 +43,20 @@ export function isTaskWorking(task: Task): boolean {
4343

4444
/**
4545
* Task statuses where logs won't change (stable/terminal states).
46-
* "complete" is a TaskState (sub-state of active), checked separately.
4746
*/
4847
const STABLE_STATUSES: readonly TaskStatus[] = ["error", "paused"];
4948

49+
/**
50+
* Task states where logs won't change (stable/terminal states).
51+
*/
52+
const STABLE_STATES: readonly TaskState[] = ["failed", "idle"];
53+
5054
/** Whether a task is in a stable state where its logs won't change. */
5155
export function isStableTask(task: Task): boolean {
5256
return (
5357
STABLE_STATUSES.includes(task.status) ||
54-
(task.current_state !== null && task.current_state.state !== "working")
58+
(task.current_state !== null &&
59+
STABLE_STATES.includes(task.current_state.state))
5560
);
5661
}
5762

packages/tasks/src/components/CreateTaskSection.tsx

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { useTasksApi } from "../hooks/useTasksApi";
1010

1111
import { PromptInput } from "./PromptInput";
1212

13-
import type { CreateTaskParams, TaskTemplate } from "@repo/shared";
13+
import type { CreateTaskParams, TaskPreset, TaskTemplate } from "@repo/shared";
1414

1515
interface CreateTaskSectionProps {
1616
templates: readonly TaskTemplate[];
@@ -20,15 +20,16 @@ export function CreateTaskSection({ templates }: CreateTaskSectionProps) {
2020
const api = useTasksApi();
2121
const [prompt, setPrompt] = useState("");
2222
const [templateId, setTemplateId] = useState(templates[0]?.id || "");
23-
const [presetId, setPresetId] = useState("");
23+
const selectedTemplate = templates.find((t) => t.id === templateId);
24+
const [presetId, setPresetId] = useState(() =>
25+
defaultPresetId(selectedTemplate?.presets ?? []),
26+
);
2427

2528
const { mutate, isPending, error } = useMutation({
2629
mutationFn: (params: CreateTaskParams) => api.createTask(params),
2730
onSuccess: () => setPrompt(""),
2831
onError: (err) => logger.error("Failed to create task", err),
2932
});
30-
31-
const selectedTemplate = templates.find((t) => t.id === templateId);
3233
const presets = selectedTemplate?.presets ?? [];
3334
const canSubmit = prompt.trim().length > 0 && selectedTemplate && !isPending;
3435

@@ -63,14 +64,20 @@ export function CreateTaskSection({ templates }: CreateTaskSectionProps) {
6364
className="option-select"
6465
value={templateId}
6566
onChange={(e) => {
66-
setTemplateId((e.target as HTMLSelectElement).value);
67-
setPresetId("");
67+
const newId = (e.target as HTMLSelectElement).value;
68+
setTemplateId(newId);
69+
const newTemplate = templates.find((t) => t.id === newId);
70+
setPresetId(defaultPresetId(newTemplate?.presets ?? []));
6871
}}
6972
disabled={isPending}
7073
>
7174
{templates.map((template) => (
72-
<VscodeOption key={template.id} value={template.id}>
73-
{template.displayName}
75+
<VscodeOption
76+
key={template.id}
77+
value={template.id}
78+
description={template.description}
79+
>
80+
{template.name}
7481
</VscodeOption>
7582
))}
7683
</VscodeSingleSelect>
@@ -86,9 +93,12 @@ export function CreateTaskSection({ templates }: CreateTaskSectionProps) {
8693
}
8794
disabled={isPending}
8895
>
89-
<VscodeOption value="">No preset</VscodeOption>
9096
{presets.map((preset) => (
91-
<VscodeOption key={preset.id} value={preset.id}>
97+
<VscodeOption
98+
key={preset.id}
99+
value={preset.id}
100+
description={preset.description}
101+
>
92102
{preset.name}
93103
{preset.isDefault ? " (Default)" : ""}
94104
</VscodeOption>
@@ -100,3 +110,10 @@ export function CreateTaskSection({ templates }: CreateTaskSectionProps) {
100110
</div>
101111
);
102112
}
113+
114+
function defaultPresetId(presets: readonly TaskPreset[]): string {
115+
if (presets.length === 0) {
116+
return "";
117+
}
118+
return (presets.find((p) => p.isDefault) ?? presets[0]).id;
119+
}

packages/tasks/src/index.css

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,8 @@ vscode-collapsible::part(body) {
205205
}
206206

207207
.task-title,
208-
.task-subtitle {
208+
.task-subtitle,
209+
.task-detail-title {
209210
overflow: hidden;
210211
text-overflow: ellipsis;
211212
white-space: nowrap;
@@ -230,7 +231,8 @@ vscode-collapsible::part(body) {
230231
opacity: 0.7;
231232
}
232233

233-
.task-item-spinner {
234+
.task-item-spinner,
235+
.action-menu-spinner {
234236
width: 1em;
235237
height: 1em;
236238
}
@@ -251,6 +253,8 @@ vscode-collapsible::part(body) {
251253
border-radius: 50%;
252254
flex-shrink: 0;
253255
background: var(--status-color);
256+
box-shadow: 0 0 0 0.2em
257+
color-mix(in srgb, var(--status-color) 25%, transparent);
254258
}
255259

256260
.status-dot.active {
@@ -413,8 +417,6 @@ vscode-icon.disabled {
413417
}
414418

415419
.action-menu-spinner {
416-
width: 1em;
417-
height: 1em;
418420
margin-inline-end: 4px;
419421
}
420422

@@ -438,19 +440,11 @@ vscode-icon.disabled {
438440
var(--vscode-sideBarSectionHeader-border, var(--vscode-panel-border));
439441
}
440442

441-
.task-detail-header .status-dot {
442-
width: 0.7em;
443-
height: 0.7em;
444-
}
445-
446443
.task-detail-title {
447444
flex: 1;
448445
min-width: 0;
449-
overflow: hidden;
450-
text-overflow: ellipsis;
451-
white-space: nowrap;
452446
font-weight: 500;
453-
font-size: 1.05em;
447+
margin-inline-start: 0.25em;
454448
}
455449

456450
.error-banner {
@@ -514,14 +508,17 @@ vscode-icon.disabled {
514508
word-break: break-word;
515509
}
516510

511+
.log-entry-input,
512+
.log-entry-output {
513+
padding-inline-start: 6px;
514+
}
515+
517516
.log-entry-input {
518517
border-inline-start: 2px solid var(--vscode-textLink-foreground);
519-
padding-inline-start: 6px;
520518
}
521519

522520
.log-entry-output {
523521
border-inline-start: 2px solid var(--vscode-descriptionForeground);
524-
padding-inline-start: 6px;
525522
}
526523

527524
.log-entry-role {

src/webviews/tasks/tasksPanelProvider.ts

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,8 @@ export class TasksPanelProvider
107107
getTaskDetails: (p) => this.handleGetTaskDetails(p.taskId),
108108
createTask: (p) => this.handleCreateTask(p),
109109
deleteTask: (p) => this.handleDeleteTask(p.taskId, p.taskName),
110-
pauseTask: (p) => this.handlePauseTask(p.taskId, p.taskName),
111-
resumeTask: (p) => this.handleResumeTask(p.taskId, p.taskName),
110+
pauseTask: (p) => this.handlePauseTask(p.taskId),
111+
resumeTask: (p) => this.handleResumeTask(p.taskId),
112112
downloadLogs: (p) => this.handleDownloadLogs(p.taskId),
113113
sendTaskMessage: (p) => this.handleSendMessage(p.taskId, p.message),
114114
});
@@ -285,10 +285,7 @@ export class TasksPanelProvider
285285
);
286286
}
287287

288-
private async handlePauseTask(
289-
taskId: string,
290-
taskName: string,
291-
): Promise<void> {
288+
private async handlePauseTask(taskId: string): Promise<void> {
292289
const task = await this.client.getTask("me", taskId);
293290
if (!task.workspace_id) {
294291
throw new Error("Task has no workspace");
@@ -297,13 +294,9 @@ export class TasksPanelProvider
297294
await this.client.stopWorkspace(task.workspace_id);
298295

299296
await this.refreshAndNotifyTask(taskId);
300-
vscode.window.showInformationMessage(`Task "${taskName}" paused`);
301297
}
302298

303-
private async handleResumeTask(
304-
taskId: string,
305-
taskName: string,
306-
): Promise<void> {
299+
private async handleResumeTask(taskId: string): Promise<void> {
307300
const task = await this.client.getTask("me", taskId);
308301
if (!task.workspace_id) {
309302
throw new Error("Task has no workspace");
@@ -315,7 +308,6 @@ export class TasksPanelProvider
315308
);
316309

317310
await this.refreshAndNotifyTask(taskId);
318-
vscode.window.showInformationMessage(`Task "${taskName}" resumed`);
319311
}
320312

321313
private async handleSendMessage(
@@ -343,9 +335,6 @@ export class TasksPanelProvider
343335
}
344336

345337
await this.refreshAndNotifyTask(taskId);
346-
vscode.window.showInformationMessage(
347-
`Message sent to "${getTaskLabel(task)}"`,
348-
);
349338
}
350339

351340
private async handleViewInCoder(taskId: string): Promise<void> {
@@ -490,7 +479,9 @@ export class TasksPanelProvider
490479
}
491480

492481
try {
493-
const templates = await this.client.getTemplates({});
482+
const templates = await this.client.getTemplates({
483+
q: "has-ai-task:true",
484+
});
494485

495486
return await Promise.all(
496487
templates.map(async (template: Template): Promise<TaskTemplate> => {
@@ -506,13 +497,13 @@ export class TasksPanelProvider
506497

507498
return {
508499
id: template.id,
509-
name: template.name,
510-
displayName: template.display_name || template.name,
511-
icon: template.icon,
500+
name: template.display_name || template.name,
501+
description: template.description,
512502
activeVersionId: template.active_version_id,
513503
presets: presets.map((p) => ({
514504
id: p.ID,
515505
name: p.Name,
506+
description: p.Description,
516507
isDefault: p.Default,
517508
})),
518509
};

0 commit comments

Comments
 (0)