Skip to content

Commit 1d32675

Browse files
authored
Split tasks webview init into separate queries and simplify the panel (#804)
- Replace InitResponse with separate getTasks and getTemplates queries with independent poll intervals (10s tasks, 5min templates) - Remove extension-side template cache in favor of React Query staleTime - Add resume button to TaskMessageInput for paused tasks - Reject message sends to paused tasks instead of auto-starting workspace - Extract TasksPanel component and usePersistedState hook from App - Centralize query keys in config.ts - Rename TasksPanel to TasksPanelProvider on the extension side - Add refresh bar indicator for initial load and user-triggered refresh
1 parent f06d76d commit 1d32675

File tree

21 files changed

+615
-455
lines changed

21 files changed

+615
-455
lines changed

package.json

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@
219219
"id": "coder.tasksPanel",
220220
"name": "Coder Tasks",
221221
"icon": "media/tasks-logo.svg",
222-
"when": "coder.tasksEnabled"
222+
"when": "coder.authenticated && coder.tasksEnabled"
223223
}
224224
]
225225
},
@@ -228,11 +228,6 @@
228228
"view": "myWorkspaces",
229229
"contents": "Coder is a platform that provisions remote development environments. \n[Login](command:coder.login)",
230230
"when": "!coder.authenticated && coder.loaded"
231-
},
232-
{
233-
"view": "coder.tasksPanel",
234-
"contents": "[Login](command:coder.login) to view tasks.",
235-
"when": "!coder.authenticated && coder.loaded && coder.tasksEnabled"
236231
}
237232
],
238233
"commands": [

packages/shared/src/tasks/api.ts

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,14 @@ import {
1717

1818
import type { Task, TaskDetails, TaskTemplate } from "./types";
1919

20-
export interface InitResponse {
21-
tasks: readonly Task[];
22-
templates: readonly TaskTemplate[];
23-
baseUrl: string;
24-
tasksSupported: boolean;
25-
}
26-
2720
export interface TaskIdParams {
2821
taskId: string;
2922
}
3023

31-
const init = defineRequest<void, InitResponse>("init");
32-
const getTasks = defineRequest<void, Task[]>("getTasks");
33-
const getTemplates = defineRequest<void, TaskTemplate[]>("getTemplates");
24+
const getTasks = defineRequest<void, readonly Task[] | null>("getTasks");
25+
const getTemplates = defineRequest<void, readonly TaskTemplate[] | null>(
26+
"getTemplates",
27+
);
3428
const getTask = defineRequest<TaskIdParams, Task>("getTask");
3529
const getTaskDetails = defineRequest<TaskIdParams, TaskDetails>(
3630
"getTaskDetails",
@@ -56,7 +50,9 @@ const sendTaskMessage = defineRequest<TaskIdParams & { message: string }, void>(
5650

5751
const viewInCoder = defineCommand<TaskIdParams>("viewInCoder");
5852
const viewLogs = defineCommand<TaskIdParams>("viewLogs");
59-
const closeWorkspaceLogs = defineCommand<void>("closeWorkspaceLogs");
53+
const stopStreamingWorkspaceLogs = defineCommand<void>(
54+
"stopStreamingWorkspaceLogs",
55+
);
6056

6157
const taskUpdated = defineNotification<Task>("taskUpdated");
6258
const tasksUpdated = defineNotification<Task[]>("tasksUpdated");
@@ -66,7 +62,6 @@ const showCreateForm = defineNotification<void>("showCreateForm");
6662

6763
export const TasksApi = {
6864
// Requests
69-
init,
7065
getTasks,
7166
getTemplates,
7267
getTask,
@@ -80,7 +75,7 @@ export const TasksApi = {
8075
// Commands
8176
viewInCoder,
8277
viewLogs,
83-
closeWorkspaceLogs,
78+
stopStreamingWorkspaceLogs,
8479
// Notifications
8580
taskUpdated,
8681
tasksUpdated,

packages/shared/src/tasks/utils.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ export function getTaskPermissions(task: Task): TaskPermissions {
2727
const hasWorkspace = task.workspace_id !== null;
2828
const status = task.status;
2929
const canSendMessage =
30-
task.status === "paused" ||
31-
(task.status === "active" && task.current_state?.state !== "working");
30+
task.status === "active" && task.current_state?.state !== "working";
3231
return {
3332
canPause: hasWorkspace && PAUSABLE_STATUSES.includes(status),
3433
pauseDisabled: PAUSE_DISABLED_STATUSES.includes(status),

packages/tasks/src/App.tsx

Lines changed: 14 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,19 @@
1-
import { type InitResponse } from "@repo/shared";
2-
import { getState, setState } from "@repo/webview-shared";
3-
import {
4-
VscodeCollapsible,
5-
VscodeProgressRing,
6-
VscodeScrollable,
7-
} from "@vscode-elements/react-elements";
8-
import { useEffect, useRef, useState } from "react";
9-
10-
import { CreateTaskSection } from "./components/CreateTaskSection";
111
import { ErrorState } from "./components/ErrorState";
12-
import { NoTemplateState } from "./components/NoTemplateState";
132
import { NotSupportedState } from "./components/NotSupportedState";
14-
import { TaskDetailView } from "./components/TaskDetailView";
15-
import { TaskList } from "./components/TaskList";
16-
import { useCollapsibleToggle } from "./hooks/useCollapsibleToggle";
17-
import { useScrollableHeight } from "./hooks/useScrollableHeight";
18-
import { useSelectedTask } from "./hooks/useSelectedTask";
19-
import { useTasksApi } from "./hooks/useTasksApi";
3+
import { TasksPanel } from "./components/TasksPanel";
4+
import { usePersistedState } from "./hooks/usePersistedState";
205
import { useTasksQuery } from "./hooks/useTasksQuery";
216

22-
interface PersistedState extends InitResponse {
23-
createExpanded: boolean;
24-
historyExpanded: boolean;
25-
}
26-
27-
type CollapsibleElement = React.ComponentRef<typeof VscodeCollapsible>;
28-
type ScrollableElement = React.ComponentRef<typeof VscodeScrollable>;
29-
307
export default function App() {
31-
const [restored] = useState(() => getState<PersistedState>());
32-
const { tasks, templates, tasksSupported, data, isLoading, error, refetch } =
33-
useTasksQuery(restored);
34-
35-
const { selectedTask, isLoadingDetails, selectTask, deselectTask } =
36-
useSelectedTask(tasks);
37-
38-
const [createRef, createOpen, setCreateOpen] =
39-
useCollapsibleToggle<CollapsibleElement>(restored?.createExpanded ?? true);
40-
const [historyRef, historyOpen] = useCollapsibleToggle<CollapsibleElement>(
41-
restored?.historyExpanded ?? true,
42-
);
43-
44-
const createScrollRef = useRef<ScrollableElement>(null);
45-
const historyScrollRef = useRef<HTMLDivElement>(null);
46-
useScrollableHeight(createRef, createScrollRef);
47-
useScrollableHeight(historyRef, historyScrollRef);
48-
49-
const { onShowCreateForm } = useTasksApi();
50-
useEffect(() => {
51-
return onShowCreateForm(() => setCreateOpen(true));
52-
}, [onShowCreateForm, setCreateOpen]);
53-
54-
useEffect(() => {
55-
if (data) {
56-
setState<PersistedState>({
57-
...data,
58-
createExpanded: createOpen,
59-
historyExpanded: historyOpen,
60-
});
61-
}
62-
}, [data, createOpen, historyOpen]);
63-
64-
function renderHistory() {
65-
if (selectedTask) {
66-
return <TaskDetailView details={selectedTask} onBack={deselectTask} />;
67-
}
68-
if (isLoadingDetails) {
69-
return (
70-
<div className="loading-container">
71-
<VscodeProgressRing />
72-
</div>
73-
);
74-
}
75-
return <TaskList tasks={tasks} onSelectTask={selectTask} />;
76-
}
8+
const persisted = usePersistedState();
9+
const { tasksSupported, tasks, templates, refreshing, error, refetch } =
10+
useTasksQuery({
11+
initialTasks: persisted.initialTasks,
12+
initialTemplates: persisted.initialTemplates,
13+
});
7714

78-
if (isLoading) {
79-
return (
80-
<div className="loading-container">
81-
<VscodeProgressRing />
82-
</div>
83-
);
15+
if (!tasksSupported) {
16+
return <NotSupportedState />;
8417
}
8518

8619
if (error && tasks.length === 0) {
@@ -89,35 +22,10 @@ export default function App() {
8922
);
9023
}
9124

92-
if (!tasksSupported) {
93-
return <NotSupportedState />;
94-
}
95-
96-
if (templates.length === 0) {
97-
return <NoTemplateState />;
98-
}
99-
10025
return (
101-
<div className="tasks-panel">
102-
<VscodeCollapsible
103-
ref={createRef}
104-
heading="Create new task"
105-
open={createOpen}
106-
>
107-
<VscodeScrollable ref={createScrollRef}>
108-
<CreateTaskSection templates={templates} />
109-
</VscodeScrollable>
110-
</VscodeCollapsible>
111-
112-
<VscodeCollapsible
113-
ref={historyRef}
114-
heading="Task History"
115-
open={historyOpen}
116-
>
117-
<div ref={historyScrollRef} className="collapsible-content">
118-
{renderHistory()}
119-
</div>
120-
</VscodeCollapsible>
121-
</div>
26+
<>
27+
{refreshing && <div className="refresh-bar" />}
28+
<TasksPanel tasks={tasks} templates={templates} persisted={persisted} />
29+
</>
12230
);
12331
}

packages/tasks/src/components/PromptInput.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ import {
55

66
import { isSubmit } from "../utils/keys";
77

8-
interface PromptInputProps {
8+
export interface PromptInputProps {
99
value: string;
1010
onChange: (value: string) => void;
1111
onSubmit: () => void;
1212
disabled?: boolean;
1313
loading?: boolean;
1414
placeholder?: string;
15-
actionIcon: "send" | "debug-pause";
15+
actionIcon: "send" | "debug-pause" | "debug-start";
1616
actionLabel: string;
1717
actionEnabled: boolean;
1818
}

packages/tasks/src/components/TaskMessageInput.tsx

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,31 @@
11
import {
22
getTaskLabel,
3+
getTaskPermissions,
34
isTaskWorking,
45
type Task,
5-
getTaskPermissions,
66
} from "@repo/shared";
77
import { logger } from "@repo/webview-shared/logger";
88
import { useMutation } from "@tanstack/react-query";
99
import { useState } from "react";
1010

1111
import { useTasksApi } from "../hooks/useTasksApi";
1212

13-
import { PromptInput } from "./PromptInput";
13+
import { PromptInput, type PromptInputProps } from "./PromptInput";
14+
15+
type ActionProps = Pick<
16+
PromptInputProps,
17+
| "onSubmit"
18+
| "disabled"
19+
| "loading"
20+
| "actionIcon"
21+
| "actionLabel"
22+
| "actionEnabled"
23+
>;
1424

1525
function getPlaceholder(task: Task): string {
1626
switch (task.status) {
1727
case "paused":
18-
return "Send a message to resume the task...";
28+
return "Resume the task to send messages";
1929
case "initializing":
2030
case "pending":
2131
return "Waiting for the agent to start...";
@@ -46,34 +56,62 @@ export function TaskMessageInput({ task }: TaskMessageInputProps) {
4656
const api = useTasksApi();
4757
const [message, setMessage] = useState("");
4858

49-
const { canPause, canSendMessage } = getTaskPermissions(task);
50-
const placeholder = getPlaceholder(task);
51-
const showPauseButton = isTaskWorking(task) && canPause;
52-
const canSubmitMessage = canSendMessage && message.trim().length > 0;
53-
5459
const { mutate: pauseTask, isPending: isPausing } = useMutation({
5560
mutationFn: () =>
5661
api.pauseTask({ taskId: task.id, taskName: getTaskLabel(task) }),
5762
onError: (err) => logger.error("Failed to pause task", err),
5863
});
5964

65+
const { mutate: resumeTask, isPending: isResuming } = useMutation({
66+
mutationFn: () =>
67+
api.resumeTask({ taskId: task.id, taskName: getTaskLabel(task) }),
68+
onError: (err) => logger.error("Failed to resume task", err),
69+
});
70+
6071
const { mutate: sendMessage, isPending: isSending } = useMutation({
6172
mutationFn: (msg: string) => api.sendTaskMessage(task.id, msg),
6273
onSuccess: () => setMessage(""),
6374
onError: (err) => logger.error("Failed to send message", err),
6475
});
6576

77+
const { canPause, canResume, canSendMessage } = getTaskPermissions(task);
78+
79+
let actionProps: ActionProps;
80+
if (isTaskWorking(task) && canPause) {
81+
actionProps = {
82+
onSubmit: pauseTask,
83+
loading: isPausing,
84+
actionIcon: "debug-pause",
85+
actionLabel: "Pause task",
86+
disabled: false,
87+
actionEnabled: true,
88+
};
89+
} else if (canResume) {
90+
actionProps = {
91+
onSubmit: resumeTask,
92+
loading: isResuming,
93+
actionIcon: "debug-start",
94+
actionLabel: "Resume task",
95+
disabled: true,
96+
actionEnabled: true,
97+
};
98+
} else {
99+
actionProps = {
100+
onSubmit: () => sendMessage(message),
101+
loading: isSending,
102+
actionIcon: "send",
103+
actionLabel: "Send message",
104+
disabled: !canSendMessage,
105+
actionEnabled: canSendMessage && message.trim().length > 0,
106+
};
107+
}
108+
66109
return (
67110
<PromptInput
68-
placeholder={placeholder}
111+
placeholder={getPlaceholder(task)}
69112
value={message}
70113
onChange={setMessage}
71-
onSubmit={showPauseButton ? pauseTask : () => sendMessage(message)}
72-
disabled={!canSendMessage && !showPauseButton}
73-
loading={showPauseButton ? isPausing : isSending}
74-
actionIcon={showPauseButton ? "debug-pause" : "send"}
75-
actionLabel={showPauseButton ? "Pause task" : "Send message"}
76-
actionEnabled={showPauseButton ? true : canSubmitMessage}
114+
{...actionProps}
77115
/>
78116
);
79117
}

0 commit comments

Comments
 (0)