Skip to content

Commit 86aac04

Browse files
committed
Add support to sending messages for created tasks
1 parent f6a1e1d commit 86aac04

File tree

4 files changed

+227
-92
lines changed

4 files changed

+227
-92
lines changed

packages/tasks/src/components/TaskMessageInput.tsx

Lines changed: 59 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,57 @@ import { useTasksApi } from "../hooks/useTasksApi";
66

77
import { PromptInput } from "./PromptInput";
88

9-
function getPlaceholder(task: Task): string {
10-
if (task.status === "error" || task.current_state?.state === "failed") {
11-
return "Send a message to retry or give new instructions...";
12-
}
13-
if (task.status === "paused") {
14-
return "Send a message to resume the task...";
15-
}
16-
if (task.status === "pending" || task.status === "initializing") {
17-
return "Waiting for the agent to start...";
18-
}
19-
if (task.current_state?.state === "working") {
20-
return "Agent is working — you can pause or wait for it to finish...";
21-
}
22-
if (task.current_state?.state === "complete") {
23-
return "Task completed — send a follow-up message to continue...";
24-
}
25-
return "Send a message to the agent...";
9+
interface InputState {
10+
placeholder: string;
11+
canSend: boolean;
2612
}
2713

28-
function isInputEnabled(task: Task): boolean {
14+
function getInputState(task: Task): InputState {
2915
const state = task.current_state?.state;
30-
return (
31-
state === "idle" ||
32-
state === "complete" ||
33-
state === "failed" ||
34-
task.status === "paused"
35-
);
16+
17+
switch (task.status) {
18+
case "paused":
19+
return {
20+
placeholder: "Send a message to resume the task...",
21+
canSend: true,
22+
};
23+
case "initializing":
24+
case "pending":
25+
return {
26+
placeholder: "Waiting for the agent to start...",
27+
canSend: false,
28+
};
29+
case "active":
30+
switch (state) {
31+
case "working":
32+
return {
33+
placeholder:
34+
"Agent is working — you can pause or wait for it to finish...",
35+
canSend: false,
36+
};
37+
case "complete":
38+
return {
39+
placeholder: "Task completed — send a follow-up to continue...",
40+
canSend: true,
41+
};
42+
case "failed":
43+
return {
44+
placeholder: "Task failed — send a message to retry...",
45+
canSend: true,
46+
};
47+
default:
48+
return {
49+
placeholder: "Send a message to the agent...",
50+
canSend: true,
51+
};
52+
}
53+
case "error":
54+
case "unknown":
55+
return {
56+
placeholder: "Task is in an error state and cannot receive messages",
57+
canSend: false,
58+
};
59+
}
3660
}
3761

3862
interface TaskMessageInputProps {
@@ -44,35 +68,32 @@ export function TaskMessageInput({ task }: TaskMessageInputProps) {
4468
const [message, setMessage] = useState("");
4569

4670
const { canPause } = getTaskPermissions(task);
47-
const inputEnabled = isInputEnabled(task);
71+
const { placeholder, canSend } = getInputState(task);
4872
const showPauseButton = task.current_state?.state === "working" && canPause;
4973

50-
const { mutate: sendMessage, isPending: isSending } = useMutation({
51-
mutationFn: (msg: string) => api.sendTaskMessage(task.id, msg),
52-
onSuccess: () => setMessage(""),
53-
});
54-
5574
const { mutate: pauseTask, isPending: isPausing } = useMutation({
5675
mutationFn: () =>
5776
api.pauseTask({ taskId: task.id, taskName: getTaskLabel(task) }),
5877
});
5978

60-
const handleSend = () => {
61-
if (!message.trim() || !inputEnabled || isSending) return;
62-
sendMessage(message.trim());
63-
};
79+
const { mutate: sendMessage, isPending: isSending } = useMutation({
80+
mutationFn: () => api.sendTaskMessage(task.id, message),
81+
onSuccess: () => setMessage(""),
82+
});
83+
84+
const canSubmitMessage = canSend && message.trim().length > 0;
6485

6586
return (
6687
<PromptInput
67-
placeholder={getPlaceholder(task)}
88+
placeholder={placeholder}
6889
value={message}
6990
onChange={setMessage}
70-
onSubmit={showPauseButton ? pauseTask : handleSend}
71-
disabled={!inputEnabled}
91+
onSubmit={showPauseButton ? pauseTask : sendMessage}
92+
disabled={!canSend && !showPauseButton}
7293
loading={showPauseButton ? isPausing : isSending}
7394
actionIcon={showPauseButton ? "debug-pause" : "send"}
7495
actionLabel={showPauseButton ? "Pause task" : "Send message"}
75-
actionDisabled={!showPauseButton && (!inputEnabled || !message.trim())}
96+
actionDisabled={showPauseButton ? false : !canSubmitMessage}
7697
/>
7798
);
7899
}

src/api/coderApi.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,16 @@ export class CoderApi extends Api implements vscode.Disposable {
200200
return response.data.logs ?? [];
201201
};
202202

203+
sendTaskInput = async (
204+
user: string,
205+
taskId: string,
206+
input: string,
207+
): Promise<void> => {
208+
await this.getAxiosInstance().post(`/api/v2/tasks/${user}/${taskId}/send`, {
209+
input,
210+
});
211+
};
212+
203213
watchInboxNotifications = async (
204214
watchTemplates: string[],
205215
watchTargets: string[],

src/webviews/tasks/tasksPanel.ts

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export class TasksPanel
7474
TasksApi.resumeTask.method,
7575
TasksApi.deleteTask.method,
7676
TasksApi.downloadLogs.method,
77+
TasksApi.sendTaskMessage.method,
7778
]);
7879

7980
private view?: vscode.WebviewView;
@@ -337,7 +338,7 @@ export class TasksPanel
337338

338339
await this.client.stopWorkspace(task.workspace_id);
339340

340-
await this.refreshAndNotifyTasks();
341+
await this.refreshAndNotifyTask(taskId);
341342
vscode.window.showInformationMessage(`Task "${taskName}" paused`);
342343
}
343344

@@ -355,10 +356,44 @@ export class TasksPanel
355356
task.template_version_id,
356357
);
357358

358-
await this.refreshAndNotifyTasks();
359+
await this.refreshAndNotifyTask(taskId);
359360
vscode.window.showInformationMessage(`Task "${taskName}" resumed`);
360361
}
361362

363+
private async handleSendMessage(
364+
taskId: string,
365+
message: string,
366+
): Promise<void> {
367+
const task = await this.client.getTask("me", taskId);
368+
369+
if (task.status === "paused") {
370+
if (!task.workspace_id) {
371+
throw new Error("Task has no workspace");
372+
}
373+
await this.client.startWorkspace(
374+
task.workspace_id,
375+
task.template_version_id,
376+
);
377+
}
378+
379+
try {
380+
await this.client.sendTaskInput("me", taskId, message);
381+
} catch (err) {
382+
if (
383+
isAxiosError(err) &&
384+
(err.response?.status === 409 || err.response?.status === 400)
385+
) {
386+
throw new Error("Task is not ready to receive messages");
387+
}
388+
throw err;
389+
}
390+
391+
await this.refreshAndNotifyTask(taskId);
392+
vscode.window.showInformationMessage(
393+
`Message sent to "${getTaskLabel(task)}"`,
394+
);
395+
}
396+
362397
private async handleViewInCoder(taskId: string): Promise<void> {
363398
const baseUrl = this.client.getHost();
364399
if (!baseUrl) return;
@@ -409,18 +444,6 @@ export class TasksPanel
409444
}
410445
}
411446

412-
/**
413-
* Placeholder handler for sending follow-up messages to a task.
414-
* The Coder API does not yet support this feature.
415-
*/
416-
private handleSendMessage(taskId: string, message: string): Promise<void> {
417-
this.logger.info(`Sending message to task ${taskId}: ${message}`);
418-
vscode.window.showInformationMessage(
419-
"Follow-up messages are not yet supported by the API",
420-
);
421-
return Promise.resolve();
422-
}
423-
424447
private async fetchTasksWithStatus(): Promise<{
425448
tasks: readonly Task[];
426449
supported: boolean;
@@ -452,6 +475,18 @@ export class TasksPanel
452475
}
453476
}
454477

478+
private async refreshAndNotifyTask(taskId: string): Promise<void> {
479+
try {
480+
const task = await this.client.getTask("me", taskId);
481+
this.sendNotification({
482+
type: TasksApi.taskUpdated.method,
483+
data: task,
484+
});
485+
} catch (err) {
486+
this.logger.warn("Failed to refresh task after action", err);
487+
}
488+
}
489+
455490
private async fetchTemplates(): Promise<TaskTemplate[]> {
456491
if (!this.client.getHost()) {
457492
return [];
@@ -529,10 +564,7 @@ export class TasksPanel
529564
const logs = await this.client.getTaskLogs("me", taskId);
530565
return { logs, status: "ok" };
531566
} catch (err) {
532-
if (
533-
isAxiosError(err) &&
534-
(err.response?.status === 400 || err.response?.status === 409)
535-
) {
567+
if (isAxiosError(err) && err.response?.status === 409) {
536568
return { logs: [], status: "not_available" };
537569
}
538570
this.logger.warn("Failed to fetch task logs", err);

0 commit comments

Comments
 (0)