Skip to content

Commit 2b6a923

Browse files
committed
Split init into separate queries and simplify tasks panel
- 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 e20a7bd commit 2b6a923

File tree

19 files changed

+469
-378
lines changed

19 files changed

+469
-378
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: 4 additions & 11 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",
@@ -66,7 +60,6 @@ const showCreateForm = defineNotification<void>("showCreateForm");
6660

6761
export const TasksApi = {
6862
// Requests
69-
init,
7063
getTasks,
7164
getTemplates,
7265
getTask,

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 { tasks, tasksSupported, 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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ interface PromptInputProps {
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: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
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";
@@ -15,7 +15,7 @@ import { PromptInput } from "./PromptInput";
1515
function getPlaceholder(task: Task): string {
1616
switch (task.status) {
1717
case "paused":
18-
return "Send a message to resume the task...";
18+
return "Resume the task to send messages";
1919
case "initializing":
2020
case "pending":
2121
return "Waiting for the agent to start...";
@@ -38,42 +38,83 @@ function getPlaceholder(task: Task): string {
3838
}
3939
}
4040

41+
function getAction(task: Task) {
42+
const { canPause, canResume, canSendMessage } = getTaskPermissions(task);
43+
44+
if (isTaskWorking(task) && canPause) {
45+
return {
46+
mode: "pause",
47+
icon: "debug-pause",
48+
label: "Pause task",
49+
inputDisabled: false,
50+
} as const;
51+
}
52+
53+
if (canResume) {
54+
return {
55+
mode: "resume",
56+
icon: "debug-start",
57+
label: "Resume task",
58+
inputDisabled: true,
59+
} as const;
60+
}
61+
62+
return {
63+
mode: "send",
64+
icon: "send",
65+
label: "Send message",
66+
inputDisabled: !canSendMessage,
67+
} as const;
68+
}
69+
4170
interface TaskMessageInputProps {
4271
task: Task;
4372
}
4473

4574
export function TaskMessageInput({ task }: TaskMessageInputProps) {
4675
const api = useTasksApi();
4776
const [message, setMessage] = useState("");
48-
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;
77+
const action = getAction(task);
5378

5479
const { mutate: pauseTask, isPending: isPausing } = useMutation({
5580
mutationFn: () =>
5681
api.pauseTask({ taskId: task.id, taskName: getTaskLabel(task) }),
5782
onError: (err) => logger.error("Failed to pause task", err),
5883
});
5984

85+
const { mutate: resumeTask, isPending: isResuming } = useMutation({
86+
mutationFn: () =>
87+
api.resumeTask({ taskId: task.id, taskName: getTaskLabel(task) }),
88+
onError: (err) => logger.error("Failed to resume task", err),
89+
});
90+
6091
const { mutate: sendMessage, isPending: isSending } = useMutation({
6192
mutationFn: (msg: string) => api.sendTaskMessage(task.id, msg),
6293
onSuccess: () => setMessage(""),
6394
onError: (err) => logger.error("Failed to send message", err),
6495
});
6596

97+
const submit = {
98+
pause: pauseTask,
99+
resume: resumeTask,
100+
send: () => sendMessage(message),
101+
};
102+
const loading = { pause: isPausing, resume: isResuming, send: isSending };
103+
const actionEnabled =
104+
action.mode !== "send" ||
105+
(!action.inputDisabled && message.trim().length > 0);
106+
66107
return (
67108
<PromptInput
68-
placeholder={placeholder}
109+
placeholder={getPlaceholder(task)}
69110
value={message}
70111
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}
112+
onSubmit={submit[action.mode]}
113+
disabled={action.inputDisabled}
114+
loading={loading[action.mode]}
115+
actionIcon={action.icon}
116+
actionLabel={action.label}
117+
actionEnabled={actionEnabled}
77118
/>
78119
);
79120
}

0 commit comments

Comments
 (0)