Skip to content

Commit 84a0fa5

Browse files
committed
Self-review
1 parent 7175dcc commit 84a0fa5

21 files changed

+282
-418
lines changed

eslint.config.mjs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { createTypeScriptImportResolver } from "eslint-import-resolver-typescrip
88
import { flatConfigs as importXFlatConfigs } from "eslint-plugin-import-x";
99
import packageJson from "eslint-plugin-package-json";
1010
import reactPlugin from "eslint-plugin-react";
11-
import reactCompilerPlugin from "eslint-plugin-react-compiler";
1211
import reactHooksPlugin from "eslint-plugin-react-hooks";
1312
import globals from "globals";
1413

@@ -177,24 +176,31 @@ export default defineConfig(
177176
},
178177
},
179178

180-
// TSX files - React rules
179+
// React hooks and compiler rules (covers .ts hook files too)
180+
{
181+
files: ["packages/**/*.{ts,tsx}"],
182+
...reactHooksPlugin.configs.flat.recommended,
183+
rules: {
184+
...reactHooksPlugin.configs.flat.recommended.rules,
185+
// React Compiler auto-memoizes; exhaustive-deps false-positives on useCallback
186+
"react-hooks/exhaustive-deps": "off",
187+
},
188+
},
189+
190+
// TSX files - React JSX rules
181191
{
182192
files: ["**/*.tsx"],
183193
plugins: {
184194
react: reactPlugin,
185-
"react-compiler": reactCompilerPlugin,
186-
"react-hooks": reactHooksPlugin,
187195
},
188196
settings: {
189197
react: {
190198
version: "detect",
191199
},
192200
},
193201
rules: {
194-
...reactCompilerPlugin.configs.recommended.rules,
195202
...reactPlugin.configs.recommended.rules,
196203
...reactPlugin.configs["jsx-runtime"].rules, // React 17+ JSX transform
197-
...reactHooksPlugin.configs.recommended.rules,
198204
"react/prop-types": "off", // Using TypeScript
199205
},
200206
},

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,7 @@
489489
"devDependencies": {
490490
"@eslint/js": "^9.39.2",
491491
"@eslint/markdown": "^7.5.1",
492+
"@tanstack/react-query": "catalog:",
492493
"@testing-library/react": "^16.3.2",
493494
"@tsconfig/node20": "^20.1.8",
494495
"@types/mocha": "^10.0.10",
@@ -520,8 +521,7 @@
520521
"eslint-plugin-import-x": "^4.16.1",
521522
"eslint-plugin-package-json": "^0.88.2",
522523
"eslint-plugin-react": "^7.37.0",
523-
"eslint-plugin-react-compiler": "catalog:",
524-
"eslint-plugin-react-hooks": "^5.0.0",
524+
"eslint-plugin-react-hooks": "catalog:",
525525
"globals": "^17.0.0",
526526
"jsdom": "^27.4.0",
527527
"jsonc-eslint-parser": "^2.4.2",

packages/tasks/src/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,14 @@ export default function App() {
4747
const { onNotification } = useIpc();
4848
useEffect(() => {
4949
return onNotification(TasksApi.showCreateForm, () => setCreateOpen(true));
50-
});
50+
}, [onNotification, setCreateOpen]);
5151

5252
useEffect(() => {
5353
persistUiState({
5454
createExpanded: createOpen,
5555
historyExpanded: historyOpen,
5656
});
57-
});
57+
}, [createOpen, historyOpen, persistUiState]);
5858

5959
if (isLoading) {
6060
return (

packages/tasks/src/components/ActionMenu.tsx

Lines changed: 36 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// VscodeContextMenu is data-driven with { label, value, separator }[] and lacks
2-
// support for icons, per-item danger styling, loading spinners, and disabled
3-
// states. We keep a custom implementation.
2+
// support for icons, per-item danger styling, loading spinners, and disabled states.
43
import {
54
VscodeIcon,
65
VscodeProgressRing,
@@ -10,7 +9,7 @@ import { useState, useRef, useEffect } from "react";
109
interface ActionMenuAction {
1110
separator?: false;
1211
label: string;
13-
icon?: string;
12+
icon: string;
1413
onClick: () => void;
1514
disabled?: boolean;
1615
danger?: boolean;
@@ -35,41 +34,38 @@ export function ActionMenu({ items }: ActionMenuProps) {
3534
const menuRef = useRef<HTMLDivElement>(null);
3635
const buttonRef = useRef<HTMLDivElement>(null);
3736

38-
const isOpen = position !== null;
39-
40-
function open() {
41-
if (buttonRef.current) {
42-
const rect = buttonRef.current.getBoundingClientRect();
43-
setPosition({
44-
top: rect.bottom + 4,
45-
right: window.innerWidth - rect.right,
46-
});
47-
}
37+
function toggle() {
38+
setPosition((prev) => {
39+
if (prev) {
40+
return null;
41+
}
42+
const rect = buttonRef.current?.getBoundingClientRect();
43+
if (!rect) {
44+
return null;
45+
}
46+
return { top: rect.bottom, right: window.innerWidth - rect.right };
47+
});
4848
}
4949

50-
function close() {
51-
setPosition(null);
52-
}
50+
const isOpen = position !== null;
5351

5452
useEffect(() => {
55-
if (!isOpen) return undefined;
53+
if (!isOpen) return;
54+
55+
const close = () => setPosition(null);
5656

57-
function handleClickOutside(event: MouseEvent) {
57+
function onMouseDown(event: MouseEvent) {
5858
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
5959
close();
6060
}
6161
}
6262

63-
function handleScroll() {
64-
close();
65-
}
66-
67-
document.addEventListener("mousedown", handleClickOutside);
68-
window.addEventListener("scroll", handleScroll, true);
63+
document.addEventListener("mousedown", onMouseDown);
64+
window.addEventListener("scroll", close, true);
6965

7066
return () => {
71-
document.removeEventListener("mousedown", handleClickOutside);
72-
window.removeEventListener("scroll", handleScroll, true);
67+
document.removeEventListener("mousedown", onMouseDown);
68+
window.removeEventListener("scroll", close, true);
7369
};
7470
}, [isOpen]);
7571

@@ -80,14 +76,11 @@ export function ActionMenu({ items }: ActionMenuProps) {
8076
actionIcon
8177
name="ellipsis"
8278
label="More actions"
83-
onClick={() => (isOpen ? close() : open())}
79+
onClick={toggle}
8480
/>
8581
</div>
8682
{position && (
87-
<div
88-
className="action-menu-dropdown"
89-
style={{ top: position.top, right: position.right }}
90-
>
83+
<div className="action-menu-dropdown" style={position}>
9184
{items.map((item, index) =>
9285
item.separator ? (
9386
<div
@@ -99,20 +92,24 @@ export function ActionMenu({ items }: ActionMenuProps) {
9992
<button
10093
key={`${item.label}-${index}`}
10194
type="button"
102-
className={`action-menu-item ${item.danger ? "danger" : ""} ${item.loading ? "loading" : ""}`}
95+
className={[
96+
"action-menu-item",
97+
item.danger && "danger",
98+
item.loading && "loading",
99+
]
100+
.filter(Boolean)
101+
.join(" ")}
103102
onClick={() => {
104-
if (!item.loading) {
105-
item.onClick();
106-
close();
107-
}
103+
item.onClick();
104+
setPosition(null);
108105
}}
109-
disabled={item.disabled ?? item.loading}
106+
disabled={item.disabled === true || item.loading === true}
110107
>
111108
{item.loading ? (
112109
<VscodeProgressRing className="action-menu-spinner" />
113-
) : item.icon ? (
110+
) : (
114111
<VscodeIcon name={item.icon} className="action-menu-icon" />
115-
) : null}
112+
)}
116113
<span>{item.label}</span>
117114
</button>
118115
),

packages/tasks/src/components/CreateTaskSection.tsx

Lines changed: 28 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1+
import { useMutation } from "@tanstack/react-query";
12
import {
23
VscodeIcon,
34
VscodeOption,
45
VscodeProgressRing,
56
VscodeSingleSelect,
67
} from "@vscode-elements/react-elements";
7-
import { useEffect, useState } from "react";
8+
import { useState } from "react";
89

910
import { useTasksApi } from "../hooks/useTasksApi";
1011

11-
import type { TaskTemplate } from "@repo/shared";
12+
import type { CreateTaskParams, TaskTemplate } from "@repo/shared";
1213

1314
interface CreateTaskSectionProps {
1415
templates: readonly TaskTemplate[];
@@ -19,61 +20,33 @@ export function CreateTaskSection({ templates }: CreateTaskSectionProps) {
1920
const [prompt, setPrompt] = useState("");
2021
const [templateId, setTemplateId] = useState(templates[0]?.id || "");
2122
const [presetId, setPresetId] = useState("");
22-
const [isSubmitting, setIsSubmitting] = useState(false);
23-
const [error, setError] = useState<string | null>(null);
23+
24+
const { mutate, isPending, error } = useMutation({
25+
mutationFn: (vars: CreateTaskParams) => api.createTask(vars),
26+
onSuccess: () => setPrompt(""),
27+
});
2428

2529
const selectedTemplate = templates.find((t) => t.id === templateId);
2630
const presets = selectedTemplate?.presets ?? [];
31+
const canSubmit = prompt.trim().length > 0 && selectedTemplate && !isPending;
2732

28-
// Sync templateId when templates prop changes
29-
useEffect(() => {
30-
if (templates.length > 0 && !templates.find((t) => t.id === templateId)) {
31-
setTemplateId(templates[0].id);
32-
setPresetId("");
33-
}
34-
}, [templates, templateId]);
35-
36-
const handleTemplateChange = (e: Event) => {
37-
const target = e.target as HTMLSelectElement;
38-
const newTemplateId = target.value;
39-
setTemplateId(newTemplateId);
40-
setPresetId("");
41-
};
42-
43-
const handlePresetChange = (e: Event) => {
44-
const target = e.target as HTMLSelectElement;
45-
setPresetId(target.value);
46-
};
47-
48-
const handleSubmit = async () => {
49-
if (!prompt.trim() || !selectedTemplate || isSubmitting) return;
50-
51-
setIsSubmitting(true);
52-
setError(null);
53-
try {
54-
await api.createTask({
33+
const handleSubmit = () => {
34+
if (canSubmit) {
35+
mutate({
5536
templateVersionId: selectedTemplate.activeVersionId,
5637
prompt: prompt.trim(),
5738
presetId: presetId || undefined,
5839
});
59-
setPrompt("");
60-
} catch (err) {
61-
setError(err instanceof Error ? err.message : "Failed to create task");
62-
} finally {
63-
setIsSubmitting(false);
6440
}
6541
};
6642

6743
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
68-
if (e.key === "Enter" && (e.metaKey || e.ctrlKey) && !isSubmitting) {
44+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
6945
e.preventDefault();
70-
void handleSubmit();
46+
handleSubmit();
7147
}
7248
};
7349

74-
const canSubmit =
75-
prompt.trim().length > 0 && selectedTemplate && !isSubmitting;
76-
7750
return (
7851
<div className="create-task-section">
7952
<div className="prompt-input-container">
@@ -83,31 +56,34 @@ export function CreateTaskSection({ templates }: CreateTaskSectionProps) {
8356
value={prompt}
8457
onChange={(e) => setPrompt(e.target.value)}
8558
onKeyDown={handleKeyDown}
86-
disabled={isSubmitting}
59+
disabled={isPending}
8760
/>
8861
<div className="prompt-send-button">
89-
{isSubmitting ? (
62+
{isPending ? (
9063
<VscodeProgressRing />
9164
) : (
9265
<VscodeIcon
9366
actionIcon
9467
name="send"
9568
label="Send"
96-
onClick={canSubmit ? () => void handleSubmit() : undefined}
97-
className={!canSubmit ? "disabled" : ""}
69+
onClick={() => void handleSubmit()}
70+
className={canSubmit ? "" : "disabled"}
9871
/>
9972
)}
10073
</div>
10174
</div>
102-
{error && <div className="create-task-error">{error}</div>}
75+
{error && <div className="create-task-error">{error.message}</div>}
10376
<div className="create-task-options">
10477
<div className="option-row">
10578
<span className="option-label">Template:</span>
10679
<VscodeSingleSelect
10780
className="option-select"
10881
value={templateId}
109-
onChange={handleTemplateChange}
110-
disabled={isSubmitting}
82+
onChange={(e) => {
83+
setTemplateId((e.target as HTMLSelectElement).value);
84+
setPresetId("");
85+
}}
86+
disabled={isPending}
11187
>
11288
{templates.map((template) => (
11389
<VscodeOption key={template.id} value={template.id}>
@@ -122,8 +98,10 @@ export function CreateTaskSection({ templates }: CreateTaskSectionProps) {
12298
<VscodeSingleSelect
12399
className="option-select"
124100
value={presetId}
125-
onChange={handlePresetChange}
126-
disabled={isSubmitting}
101+
onChange={(e) =>
102+
setPresetId((e.target as HTMLSelectElement).value)
103+
}
104+
disabled={isPending}
127105
>
128106
<VscodeOption value="">No preset</VscodeOption>
129107
{presets.map((preset) => (

packages/tasks/src/components/ErrorState.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { VscodeButton } from "@vscode-elements/react-elements";
1+
import { VscodeButton, VscodeIcon } from "@vscode-elements/react-elements";
22

33
import { StatePanel } from "./StatePanel";
44

@@ -11,7 +11,7 @@ export function ErrorState({ message, onRetry }: ErrorStateProps) {
1111
return (
1212
<StatePanel
1313
className="error-state"
14-
icon={<span className="codicon codicon-error error-icon" />}
14+
icon={<VscodeIcon name="error" className="error-icon" />}
1515
description={message}
1616
action={<VscodeButton onClick={onRetry}>Retry</VscodeButton>}
1717
/>

packages/tasks/src/components/NoTemplateState.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,7 @@ export function NoTemplateState() {
99
<StatePanel
1010
title="No Task template found"
1111
action={
12-
<a
13-
href={DOCS_URL}
14-
target="_blank"
15-
rel="noopener noreferrer"
16-
className="text-link"
17-
>
12+
<a href={DOCS_URL} className="text-link">
1813
Learn how to create a template <VscodeIcon name="link-external" />
1914
</a>
2015
}

packages/tasks/src/components/NotSupportedState.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,7 @@ export function NotSupportedState() {
1111
title="Tasks not available"
1212
description="This Coder server does not support tasks."
1313
action={
14-
<a
15-
href={DOCS_URL}
16-
target="_blank"
17-
rel="noopener noreferrer"
18-
className="text-link"
19-
>
14+
<a href={DOCS_URL} className="text-link">
2015
Learn more <VscodeIcon name="link-external" />
2116
</a>
2217
}

0 commit comments

Comments
 (0)