From 7dd0fb1a86e7bab0e08a72a42a85c1e4b019790c Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Tue, 24 Feb 2026 16:51:36 +0100 Subject: [PATCH 1/3] Fix SVG icons for VS Code theme compatibility and reduce tasks font size Activity bar icons use CSS mask-image, so only the alpha channel matters. Use a mask cutout on the C[] path to create a transparent card area for the bot icon, matching the original design where the bot sits on a white card overlapping the bottom-right of the square bracket. Align both activity bar icons to the same viewBox origin so C[] portions line up vertically. Simplify TreeItem icon SVGs by inlining fill colors instead of using CSS classes. Reduce the tasks webview base font size to 90% of the VS Code default for a more compact sidebar appearance. --- media/logo-black.svg | 14 +++----------- media/logo-white.svg | 14 +++----------- media/shorthand-logo.svg | 3 +++ media/tasks-logo.svg | 14 +++++++++++--- package.json | 2 +- packages/tasks/src/index.css | 4 +++- src/webviews/tasks/tasksPanelProvider.ts | 19 ++++--------------- 7 files changed, 28 insertions(+), 42 deletions(-) create mode 100644 media/shorthand-logo.svg diff --git a/media/logo-black.svg b/media/logo-black.svg index f488e635..78fb2813 100644 --- a/media/logo-black.svg +++ b/media/logo-black.svg @@ -1,17 +1,9 @@ - - - - - + + - \ No newline at end of file + diff --git a/media/logo-white.svg b/media/logo-white.svg index f60ab682..81b43fcd 100644 --- a/media/logo-white.svg +++ b/media/logo-white.svg @@ -1,19 +1,11 @@ - - - - - + + - \ No newline at end of file + diff --git a/media/shorthand-logo.svg b/media/shorthand-logo.svg new file mode 100644 index 00000000..12831eb1 --- /dev/null +++ b/media/shorthand-logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/media/tasks-logo.svg b/media/tasks-logo.svg index 987cd421..3c4aa8a7 100644 --- a/media/tasks-logo.svg +++ b/media/tasks-logo.svg @@ -1,4 +1,12 @@ - - - + + + + + + + + + + + diff --git a/package.json b/package.json index 0dfa1642..b8885154 100644 --- a/package.json +++ b/package.json @@ -188,7 +188,7 @@ { "id": "coder", "title": "Coder Remote", - "icon": "media/logo-white.svg" + "icon": "media/shorthand-logo.svg" }, { "id": "coderTasks", diff --git a/packages/tasks/src/index.css b/packages/tasks/src/index.css index b2f92251..5ef4769d 100644 --- a/packages/tasks/src/index.css +++ b/packages/tasks/src/index.css @@ -6,7 +6,7 @@ body { margin: 0; padding: 0; font-family: var(--vscode-font-family); - font-size: var(--vscode-font-size); + font-size: calc(var(--vscode-font-size) * 0.9); color: var(--vscode-foreground); background: var(--vscode-sideBar-background); overflow: hidden; @@ -251,6 +251,8 @@ vscode-collapsible::part(body) { border-radius: 50%; flex-shrink: 0; background: var(--status-color); + box-shadow: 0 0 0 0.25em + color-mix(in srgb, var(--status-color) 25%, transparent); } .status-dot.active { diff --git a/src/webviews/tasks/tasksPanelProvider.ts b/src/webviews/tasks/tasksPanelProvider.ts index 75711d0b..d726b350 100644 --- a/src/webviews/tasks/tasksPanelProvider.ts +++ b/src/webviews/tasks/tasksPanelProvider.ts @@ -107,8 +107,8 @@ export class TasksPanelProvider getTaskDetails: (p) => this.handleGetTaskDetails(p.taskId), createTask: (p) => this.handleCreateTask(p), deleteTask: (p) => this.handleDeleteTask(p.taskId, p.taskName), - pauseTask: (p) => this.handlePauseTask(p.taskId, p.taskName), - resumeTask: (p) => this.handleResumeTask(p.taskId, p.taskName), + pauseTask: (p) => this.handlePauseTask(p.taskId), + resumeTask: (p) => this.handleResumeTask(p.taskId), downloadLogs: (p) => this.handleDownloadLogs(p.taskId), sendTaskMessage: (p) => this.handleSendMessage(p.taskId, p.message), }); @@ -285,10 +285,7 @@ export class TasksPanelProvider ); } - private async handlePauseTask( - taskId: string, - taskName: string, - ): Promise { + private async handlePauseTask(taskId: string): Promise { const task = await this.client.getTask("me", taskId); if (!task.workspace_id) { throw new Error("Task has no workspace"); @@ -297,13 +294,9 @@ export class TasksPanelProvider await this.client.stopWorkspace(task.workspace_id); await this.refreshAndNotifyTask(taskId); - vscode.window.showInformationMessage(`Task "${taskName}" paused`); } - private async handleResumeTask( - taskId: string, - taskName: string, - ): Promise { + private async handleResumeTask(taskId: string): Promise { const task = await this.client.getTask("me", taskId); if (!task.workspace_id) { throw new Error("Task has no workspace"); @@ -315,7 +308,6 @@ export class TasksPanelProvider ); await this.refreshAndNotifyTask(taskId); - vscode.window.showInformationMessage(`Task "${taskName}" resumed`); } private async handleSendMessage( @@ -343,9 +335,6 @@ export class TasksPanelProvider } await this.refreshAndNotifyTask(taskId); - vscode.window.showInformationMessage( - `Message sent to "${getTaskLabel(task)}"`, - ); } private async handleViewInCoder(taskId: string): Promise { From e43d839710f3be3175bd0d1c4b802857627c8095 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Wed, 25 Feb 2026 00:19:14 +0100 Subject: [PATCH 2/3] Self-review --- media/shorthand-logo.svg | 2 +- media/tasks-logo.svg | 4 +- packages/shared/src/tasks/types.ts | 4 +- .../src/components/CreateTaskSection.tsx | 37 ++++++++++++++----- packages/tasks/src/index.css | 29 ++++++--------- src/webviews/tasks/tasksPanelProvider.ts | 10 +++-- test/mocks/tasks.ts | 5 +-- .../webviews/tasks/tasksPanelProvider.test.ts | 17 ++++++++- test/webview/tasks/CreateTaskSection.test.tsx | 19 +++++++--- 9 files changed, 80 insertions(+), 47 deletions(-) diff --git a/media/shorthand-logo.svg b/media/shorthand-logo.svg index 12831eb1..5757274a 100644 --- a/media/shorthand-logo.svg +++ b/media/shorthand-logo.svg @@ -1,3 +1,3 @@ - + diff --git a/media/tasks-logo.svg b/media/tasks-logo.svg index 3c4aa8a7..98bf0630 100644 --- a/media/tasks-logo.svg +++ b/media/tasks-logo.svg @@ -1,7 +1,7 @@ - + - + diff --git a/packages/shared/src/tasks/types.ts b/packages/shared/src/tasks/types.ts index 2d222ec9..23fc5cba 100644 --- a/packages/shared/src/tasks/types.ts +++ b/packages/shared/src/tasks/types.ts @@ -17,8 +17,7 @@ export type { Preset, Task, TaskLogEntry, TaskState, TaskStatus, Template }; export interface TaskTemplate { id: string; name: string; - displayName: string; - icon: string; + description: string; activeVersionId: string; presets: TaskPreset[]; } @@ -26,6 +25,7 @@ export interface TaskTemplate { export interface TaskPreset { id: string; name: string; + description: string; isDefault: boolean; } diff --git a/packages/tasks/src/components/CreateTaskSection.tsx b/packages/tasks/src/components/CreateTaskSection.tsx index f97ffe52..c0a302a9 100644 --- a/packages/tasks/src/components/CreateTaskSection.tsx +++ b/packages/tasks/src/components/CreateTaskSection.tsx @@ -10,7 +10,7 @@ import { useTasksApi } from "../hooks/useTasksApi"; import { PromptInput } from "./PromptInput"; -import type { CreateTaskParams, TaskTemplate } from "@repo/shared"; +import type { CreateTaskParams, TaskPreset, TaskTemplate } from "@repo/shared"; interface CreateTaskSectionProps { templates: readonly TaskTemplate[]; @@ -20,15 +20,16 @@ export function CreateTaskSection({ templates }: CreateTaskSectionProps) { const api = useTasksApi(); const [prompt, setPrompt] = useState(""); const [templateId, setTemplateId] = useState(templates[0]?.id || ""); - const [presetId, setPresetId] = useState(""); + const selectedTemplate = templates.find((t) => t.id === templateId); + const [presetId, setPresetId] = useState(() => + defaultPresetId(selectedTemplate?.presets ?? []), + ); const { mutate, isPending, error } = useMutation({ mutationFn: (params: CreateTaskParams) => api.createTask(params), onSuccess: () => setPrompt(""), onError: (err) => logger.error("Failed to create task", err), }); - - const selectedTemplate = templates.find((t) => t.id === templateId); const presets = selectedTemplate?.presets ?? []; const canSubmit = prompt.trim().length > 0 && selectedTemplate && !isPending; @@ -63,14 +64,20 @@ export function CreateTaskSection({ templates }: CreateTaskSectionProps) { className="option-select" value={templateId} onChange={(e) => { - setTemplateId((e.target as HTMLSelectElement).value); - setPresetId(""); + const newId = (e.target as HTMLSelectElement).value; + setTemplateId(newId); + const newTemplate = templates.find((t) => t.id === newId); + setPresetId(defaultPresetId(newTemplate?.presets ?? [])); }} disabled={isPending} > {templates.map((template) => ( - - {template.displayName} + + {template.name} ))} @@ -86,9 +93,12 @@ export function CreateTaskSection({ templates }: CreateTaskSectionProps) { } disabled={isPending} > - No preset {presets.map((preset) => ( - + {preset.name} {preset.isDefault ? " (Default)" : ""} @@ -100,3 +110,10 @@ export function CreateTaskSection({ templates }: CreateTaskSectionProps) { ); } + +function defaultPresetId(presets: readonly TaskPreset[]): string { + if (presets.length === 0) { + return ""; + } + return (presets.find((p) => p.isDefault) ?? presets[0]).id; +} diff --git a/packages/tasks/src/index.css b/packages/tasks/src/index.css index 5ef4769d..f6232e99 100644 --- a/packages/tasks/src/index.css +++ b/packages/tasks/src/index.css @@ -6,7 +6,7 @@ body { margin: 0; padding: 0; font-family: var(--vscode-font-family); - font-size: calc(var(--vscode-font-size) * 0.9); + font-size: var(--vscode-font-size); color: var(--vscode-foreground); background: var(--vscode-sideBar-background); overflow: hidden; @@ -205,7 +205,8 @@ vscode-collapsible::part(body) { } .task-title, -.task-subtitle { +.task-subtitle, +.task-detail-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -230,7 +231,8 @@ vscode-collapsible::part(body) { opacity: 0.7; } -.task-item-spinner { +.task-item-spinner, +.action-menu-spinner { width: 1em; height: 1em; } @@ -251,7 +253,7 @@ vscode-collapsible::part(body) { border-radius: 50%; flex-shrink: 0; background: var(--status-color); - box-shadow: 0 0 0 0.25em + box-shadow: 0 0 0 0.2em color-mix(in srgb, var(--status-color) 25%, transparent); } @@ -415,8 +417,6 @@ vscode-icon.disabled { } .action-menu-spinner { - width: 1em; - height: 1em; margin-inline-end: 4px; } @@ -440,19 +440,11 @@ vscode-icon.disabled { var(--vscode-sideBarSectionHeader-border, var(--vscode-panel-border)); } -.task-detail-header .status-dot { - width: 0.7em; - height: 0.7em; -} - .task-detail-title { flex: 1; min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; font-weight: 500; - font-size: 1.05em; + margin-inline-start: 0.25em; } .error-banner { @@ -516,14 +508,17 @@ vscode-icon.disabled { word-break: break-word; } +.log-entry-input, +.log-entry-output { + padding-inline-start: 6px; +} + .log-entry-input { border-inline-start: 2px solid var(--vscode-textLink-foreground); - padding-inline-start: 6px; } .log-entry-output { border-inline-start: 2px solid var(--vscode-descriptionForeground); - padding-inline-start: 6px; } .log-entry-role { diff --git a/src/webviews/tasks/tasksPanelProvider.ts b/src/webviews/tasks/tasksPanelProvider.ts index d726b350..42e37255 100644 --- a/src/webviews/tasks/tasksPanelProvider.ts +++ b/src/webviews/tasks/tasksPanelProvider.ts @@ -479,7 +479,9 @@ export class TasksPanelProvider } try { - const templates = await this.client.getTemplates({}); + const templates = await this.client.getTemplates({ + q: "has-ai-task:true", + }); return await Promise.all( templates.map(async (template: Template): Promise => { @@ -495,13 +497,13 @@ export class TasksPanelProvider return { id: template.id, - name: template.name, - displayName: template.display_name || template.name, - icon: template.icon, + name: template.display_name || template.name, + description: template.description, activeVersionId: template.active_version_id, presets: presets.map((p) => ({ id: p.ID, name: p.Name, + description: p.Description, isDefault: p.Default, })), }; diff --git a/test/mocks/tasks.ts b/test/mocks/tasks.ts index b03fac97..3231a48e 100644 --- a/test/mocks/tasks.ts +++ b/test/mocks/tasks.ts @@ -167,9 +167,8 @@ export function taskTemplate( ): TaskTemplate { return { id: "template-1", - name: "test-template", - displayName: "Test Template", - icon: "/icon.svg", + name: "Test Template", + description: "A test template", activeVersionId: "version-1", presets: [], ...overrides, diff --git a/test/unit/webviews/tasks/tasksPanelProvider.test.ts b/test/unit/webviews/tasks/tasksPanelProvider.test.ts index cef5c94e..3b0ece0f 100644 --- a/test/unit/webviews/tasks/tasksPanelProvider.test.ts +++ b/test/unit/webviews/tasks/tasksPanelProvider.test.ts @@ -255,9 +255,22 @@ describe("TasksPanelProvider", () => { const res = await h.request(TasksApi.getTemplates); + expect(h.client.getTemplates).toHaveBeenCalledWith({ + q: "has-ai-task:true", + }); expect(res.data?.[0].presets).toEqual([ - { id: "p1", name: "Default", isDefault: true }, - { id: "p2", name: "Custom", isDefault: false }, + { + id: "p1", + name: "Default", + description: "Test preset", + isDefault: true, + }, + { + id: "p2", + name: "Custom", + description: "Test preset", + isDefault: false, + }, ]); }); diff --git a/test/webview/tasks/CreateTaskSection.test.tsx b/test/webview/tasks/CreateTaskSection.test.tsx index 2f6373f9..561d79fc 100644 --- a/test/webview/tasks/CreateTaskSection.test.tsx +++ b/test/webview/tasks/CreateTaskSection.test.tsx @@ -40,10 +40,10 @@ describe("CreateTaskSection", () => { vi.clearAllMocks(); }); - it("renders template options by displayName", () => { + it("renders template options by name", () => { renderSection([ - taskTemplate({ id: "t1", displayName: "First" }), - taskTemplate({ id: "t2", displayName: "Second" }), + taskTemplate({ id: "t1", name: "First" }), + taskTemplate({ id: "t2", name: "Second" }), ]); expect(screen.queryByText("First")).toBeInTheDocument(); expect(screen.queryByText("Second")).toBeInTheDocument(); @@ -57,7 +57,14 @@ describe("CreateTaskSection", () => { it("renders preset dropdown when template has presets", () => { renderSection([ taskTemplate({ - presets: [{ id: "p1", name: "Fast Mode", isDefault: false }], + presets: [ + { + id: "p1", + name: "Fast Mode", + description: "A fast preset", + isDefault: false, + }, + ], }), ]); expect(screen.queryByText("Preset:")).toBeInTheDocument(); @@ -94,8 +101,8 @@ describe("CreateTaskSection", () => { }); it("syncs templateId when templates change", () => { - const templates1 = [taskTemplate({ id: "t1", displayName: "Old" })]; - const templates2 = [taskTemplate({ id: "t2", displayName: "New" })]; + const templates1 = [taskTemplate({ id: "t1", name: "Old" })]; + const templates2 = [taskTemplate({ id: "t2", name: "New" })]; const { rerender } = renderWithQuery( , From 100955c135d2423072b9d8460296ec91dc7e558e Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Wed, 25 Feb 2026 10:15:03 +0100 Subject: [PATCH 3/3] Improve polling of logs when task is complete --- packages/shared/src/tasks/utils.ts | 11 +++++++--- .../webviews/tasks/tasksPanelProvider.test.ts | 20 ++++++++++++------- test/webview/shared/tasks/utils.test.ts | 2 +- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/packages/shared/src/tasks/utils.ts b/packages/shared/src/tasks/utils.ts index ad68e376..ace8ecb0 100644 --- a/packages/shared/src/tasks/utils.ts +++ b/packages/shared/src/tasks/utils.ts @@ -1,4 +1,4 @@ -import type { Task, TaskPermissions, TaskStatus } from "./types"; +import type { Task, TaskPermissions, TaskState, TaskStatus } from "./types"; export function getTaskLabel(task: Task): string { return task.display_name || task.name || task.id; @@ -43,15 +43,20 @@ export function isTaskWorking(task: Task): boolean { /** * Task statuses where logs won't change (stable/terminal states). - * "complete" is a TaskState (sub-state of active), checked separately. */ const STABLE_STATUSES: readonly TaskStatus[] = ["error", "paused"]; +/** + * Task states where logs won't change (stable/terminal states). + */ +const STABLE_STATES: readonly TaskState[] = ["failed", "idle"]; + /** Whether a task is in a stable state where its logs won't change. */ export function isStableTask(task: Task): boolean { return ( STABLE_STATUSES.includes(task.status) || - (task.current_state !== null && task.current_state.state !== "working") + (task.current_state !== null && + STABLE_STATES.includes(task.current_state.state)) ); } diff --git a/test/unit/webviews/tasks/tasksPanelProvider.test.ts b/test/unit/webviews/tasks/tasksPanelProvider.test.ts index 3b0ece0f..3fc686b2 100644 --- a/test/unit/webviews/tasks/tasksPanelProvider.test.ts +++ b/test/unit/webviews/tasks/tasksPanelProvider.test.ts @@ -29,6 +29,7 @@ import { workspace } from "../../../mocks/workspace"; import type { ProvisionerJobLog, Task, + TaskState, WorkspaceAgentLog, } from "coder/site/src/api/typesGenerated"; @@ -307,11 +308,6 @@ describe("TasksPanelProvider", () => { }); }); - interface LogCachingTestCase { - name: string; - state: "complete" | "working"; - expectedCalls: number; - } it("returns logsStatus not_available on 409", async () => { const h = createHarness(); h.client.getTask.mockResolvedValue(task()); @@ -327,10 +323,15 @@ describe("TasksPanelProvider", () => { }); }); + interface LogCachingTestCase { + name: string; + state: TaskState; + expectedCalls: number; + } it.each([ { - name: "caches logs for completed tasks", - state: "complete", + name: "caches logs for idle tasks", + state: "idle", expectedCalls: 1, }, { @@ -338,6 +339,11 @@ describe("TasksPanelProvider", () => { state: "working", expectedCalls: 2, }, + { + name: "refetches logs for completed tasks", + state: "complete", + expectedCalls: 2, + }, ])("$name", async ({ state, expectedCalls }) => { const h = createHarness(); h.client.getTask.mockResolvedValue( diff --git a/test/webview/shared/tasks/utils.test.ts b/test/webview/shared/tasks/utils.test.ts index e3900f72..3ddd3678 100644 --- a/test/webview/shared/tasks/utils.test.ts +++ b/test/webview/shared/tasks/utils.test.ts @@ -153,7 +153,7 @@ describe("isStableTask", () => { { name: "complete state", overrides: { current_state: state("complete") }, - expected: true, + expected: false, }, { name: "failed state",