Skip to content

Commit 508db63

Browse files
committed
Refactoring
1 parent 59fac6e commit 508db63

38 files changed

+572
-807
lines changed

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,7 @@
496496
"@eslint/js": "^9.39.2",
497497
"@eslint/markdown": "^7.5.1",
498498
"@tanstack/react-query": "catalog:",
499+
"@testing-library/jest-dom": "^6.9.1",
499500
"@testing-library/react": "^16.3.2",
500501
"@tsconfig/node20": "^20.1.9",
501502
"@types/mocha": "^10.0.10",
@@ -527,7 +528,7 @@
527528
"eslint-plugin-import-x": "^4.16.1",
528529
"eslint-plugin-package-json": "^0.88.2",
529530
"eslint-plugin-react": "^7.37.5",
530-
"eslint-plugin-react-hooks": "catalog:",
531+
"eslint-plugin-react-hooks": "^7.0.1",
531532
"globals": "^17.3.0",
532533
"jsdom": "^28.0.0",
533534
"jsonc-eslint-parser": "^2.4.2",
@@ -544,7 +545,7 @@
544545
"extensionPack": [
545546
"ms-vscode-remote.remote-ssh"
546547
],
547-
"packageManager": "pnpm@10.29.2+sha512.bef43fa759d91fd2da4b319a5a0d13ef7a45bb985a3d7342058470f9d2051a3ba8674e629672654686ef9443ad13a82da2beb9eeb3e0221c87b8154fff9d74b8",
548+
"packageManager": "pnpm@10.29.2",
548549
"engines": {
549550
"vscode": "^1.95.0",
550551
"node": ">= 20"

packages/shared/src/tasks/api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,10 @@ export interface TaskActionParams {
4646
const deleteTask = defineRequest<TaskActionParams, void>("deleteTask");
4747
const pauseTask = defineRequest<TaskActionParams, void>("pauseTask");
4848
const resumeTask = defineRequest<TaskActionParams, void>("resumeTask");
49+
const downloadLogs = defineRequest<{ taskId: string }, void>("downloadLogs");
4950

5051
const viewInCoder = defineCommand<{ taskId: string }>("viewInCoder");
5152
const viewLogs = defineCommand<{ taskId: string }>("viewLogs");
52-
const downloadLogs = defineRequest<{ taskId: string }, void>("downloadLogs");
5353
const sendTaskMessage = defineCommand<{
5454
taskId: string;
5555
message: string;

packages/shared/src/tasks/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,13 @@ export type LogsStatus = "ok" | "not_available" | "error";
3535
/**
3636
* Full details for a selected task, including logs and action availability.
3737
*/
38-
export interface TaskDetails extends TaskActions {
38+
export interface TaskDetails extends TaskPermissions {
3939
task: Task;
4040
logs: TaskLogEntry[];
4141
logsStatus: LogsStatus;
4242
}
4343

44-
export interface TaskActions {
44+
export interface TaskPermissions {
4545
canPause: boolean;
4646
pauseDisabled: boolean;
4747
canResume: boolean;

packages/shared/src/tasks/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Task, TaskActions, TaskStatus } from "./types";
1+
import type { Task, TaskPermissions, TaskStatus } from "./types";
22

33
export function getTaskLabel(task: Task): string {
44
return task.display_name || task.name || task.id;
@@ -23,7 +23,7 @@ const RESUMABLE_STATUSES: readonly TaskStatus[] = [
2323
"unknown",
2424
];
2525

26-
export function getTaskActions(task: Task): TaskActions {
26+
export function getTaskPermissions(task: Task): TaskPermissions {
2727
const hasWorkspace = task.workspace_id !== null;
2828
const status = task.status;
2929
return {

packages/tasks/src/App.tsx

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,40 @@
1-
import { TasksApi } from "@repo/shared";
1+
import { TasksApi, type InitResponse } from "@repo/shared";
2+
import { getState, setState } from "@repo/webview-shared";
23
import { useIpc } from "@repo/webview-shared/react";
34
import {
45
VscodeCollapsible,
56
VscodeProgressRing,
67
VscodeScrollable,
78
} from "@vscode-elements/react-elements";
8-
import { useEffect, useRef } from "react";
9+
import { useEffect, useRef, useState } from "react";
910

10-
import {
11-
CreateTaskSection,
12-
ErrorState,
13-
NoTemplateState,
14-
NotSupportedState,
15-
TaskList,
16-
} from "./components";
11+
import { CreateTaskSection } from "./components/CreateTaskSection";
12+
import { ErrorState } from "./components/ErrorState";
13+
import { NoTemplateState } from "./components/NoTemplateState";
14+
import { NotSupportedState } from "./components/NotSupportedState";
15+
import { TaskList } from "./components/TaskList";
1716
import { useCollapsibleToggle } from "./hooks/useCollapsibleToggle";
1817
import { useScrollableHeight } from "./hooks/useScrollableHeight";
19-
import { useTasksData } from "./hooks/useTasksData";
18+
import { useTasksQuery } from "./hooks/useTasksQuery";
19+
20+
interface PersistedState extends InitResponse {
21+
createExpanded: boolean;
22+
historyExpanded: boolean;
23+
}
2024

2125
type CollapsibleElement = React.ComponentRef<typeof VscodeCollapsible>;
2226
type ScrollableElement = React.ComponentRef<typeof VscodeScrollable>;
2327

2428
export default function App() {
25-
const {
26-
tasks,
27-
templates,
28-
tasksSupported,
29-
isLoading,
30-
error,
31-
refetch,
32-
initialCreateExpanded,
33-
initialHistoryExpanded,
34-
persistUiState,
35-
} = useTasksData();
29+
const [restored] = useState(() => getState<PersistedState>());
30+
const { tasks, templates, tasksSupported, data, isLoading, error, refetch } =
31+
useTasksQuery(restored);
3632

3733
const [createRef, createOpen, setCreateOpen] =
38-
useCollapsibleToggle<CollapsibleElement>(initialCreateExpanded);
39-
const [historyRef, historyOpen, _setHistoryOpen] =
40-
useCollapsibleToggle<CollapsibleElement>(initialHistoryExpanded);
34+
useCollapsibleToggle<CollapsibleElement>(restored?.createExpanded ?? true);
35+
const [historyRef, historyOpen] = useCollapsibleToggle<CollapsibleElement>(
36+
restored?.historyExpanded ?? true,
37+
);
4138

4239
const createScrollRef = useRef<ScrollableElement>(null);
4340
const historyScrollRef = useRef<ScrollableElement>(null);
@@ -50,11 +47,14 @@ export default function App() {
5047
}, [onNotification, setCreateOpen]);
5148

5249
useEffect(() => {
53-
persistUiState({
54-
createExpanded: createOpen,
55-
historyExpanded: historyOpen,
56-
});
57-
}, [createOpen, historyOpen, persistUiState]);
50+
if (data) {
51+
setState<PersistedState>({
52+
...data,
53+
createExpanded: createOpen,
54+
historyExpanded: historyOpen,
55+
});
56+
}
57+
}, [data, createOpen, historyOpen]);
5858

5959
if (isLoading) {
6060
return (

packages/tasks/src/components/ActionMenu.tsx

Lines changed: 30 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
// VscodeContextMenu is data-driven with { label, value, separator }[] and lacks
2-
// support for icons, per-item danger styling, loading spinners, and disabled states.
31
import {
42
VscodeIcon,
53
VscodeProgressRing,
64
} from "@vscode-elements/react-elements";
7-
import { useState, useRef, useEffect, useCallback } from "react";
5+
import { useState, useRef, useEffect } from "react";
6+
7+
import { isEscape } from "../utils/keys";
88

99
interface ActionMenuAction {
10-
separator?: false;
1110
label: string;
1211
icon: string;
1312
onClick: () => void;
@@ -16,56 +15,27 @@ interface ActionMenuAction {
1615
loading?: boolean;
1716
}
1817

19-
interface ActionMenuSeparator {
20-
separator: true;
21-
}
22-
23-
export type ActionMenuItem = ActionMenuAction | ActionMenuSeparator;
18+
export type ActionMenuItem =
19+
| { separator: true }
20+
| ({ separator?: false } & ActionMenuAction);
2421

2522
interface ActionMenuProps {
2623
items: ActionMenuItem[];
2724
}
2825

26+
/*
27+
* VscodeContextMenu is data-driven with { label, value, separator }[] and lacks
28+
* support for icons, per-item danger styling, loading spinners, and disabled states.
29+
*/
2930
export function ActionMenu({ items }: ActionMenuProps) {
30-
const [position, setPosition] = useState<{
31-
top: number;
32-
right: number;
33-
} | null>(null);
31+
const [isOpen, setIsOpen] = useState(false);
3432
const menuRef = useRef<HTMLDivElement>(null);
35-
const buttonRef = useRef<HTMLDivElement>(null);
36-
const dropdownRef = useRef<HTMLDivElement>(null);
37-
38-
function toggle() {
39-
setPosition((prev) => {
40-
if (prev) {
41-
return null;
42-
}
43-
const rect = buttonRef.current?.getBoundingClientRect();
44-
if (!rect) {
45-
return null;
46-
}
47-
return { top: rect.bottom, right: window.innerWidth - rect.right };
48-
});
49-
}
50-
51-
const isOpen = position !== null;
5233

53-
const dropdownRefCallback = useCallback((node: HTMLDivElement | null) => {
54-
dropdownRef.current = node;
55-
node?.focus();
56-
}, []);
57-
58-
function onKeyDown(event: React.KeyboardEvent) {
59-
if (event.key === "Escape") {
60-
setPosition(null);
61-
}
62-
}
34+
const close = () => setIsOpen(false);
6335

6436
useEffect(() => {
6537
if (!isOpen) return;
6638

67-
const close = () => setPosition(null);
68-
6939
function onMouseDown(event: MouseEvent) {
7040
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
7141
close();
@@ -83,21 +53,26 @@ export function ActionMenu({ items }: ActionMenuProps) {
8353

8454
return (
8555
<div className="action-menu" ref={menuRef}>
86-
<div ref={buttonRef}>
87-
<VscodeIcon
88-
actionIcon
89-
name="ellipsis"
90-
label="More actions"
91-
onClick={toggle}
92-
/>
93-
</div>
94-
{position && (
56+
<VscodeIcon
57+
actionIcon
58+
name="ellipsis"
59+
label="More actions"
60+
onClick={() => setIsOpen((prev) => !prev)}
61+
/>
62+
{isOpen && (
9563
<div
96-
ref={dropdownRefCallback}
64+
ref={(node) => {
65+
if (!node || !menuRef.current) {
66+
return;
67+
}
68+
const rect = menuRef.current.getBoundingClientRect();
69+
node.style.top = `${rect.bottom + 4}px`;
70+
node.style.right = `${window.innerWidth - rect.right}px`;
71+
node.focus({ preventScroll: true });
72+
}}
9773
className="action-menu-dropdown"
98-
style={position}
9974
tabIndex={-1}
100-
onKeyDown={onKeyDown}
75+
onKeyDown={(e) => isEscape(e) && close()}
10176
>
10277
{items.map((item, index) =>
10378
item.separator ? (
@@ -119,7 +94,7 @@ export function ActionMenu({ items }: ActionMenuProps) {
11994
.join(" ")}
12095
onClick={() => {
12196
item.onClick();
122-
setPosition(null);
97+
close();
12398
}}
12499
disabled={item.disabled === true || item.loading === true}
125100
>

packages/tasks/src/components/CreateTaskSection.tsx

Lines changed: 9 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { useMutation } from "@tanstack/react-query";
22
import {
3-
VscodeIcon,
43
VscodeOption,
5-
VscodeProgressRing,
64
VscodeSingleSelect,
75
} from "@vscode-elements/react-elements";
86
import { useState } from "react";
97

108
import { useTasksApi } from "../hooks/useTasksApi";
119

10+
import { PromptInput } from "./PromptInput";
11+
1212
import type { CreateTaskParams, TaskTemplate } from "@repo/shared";
1313

1414
interface CreateTaskSectionProps {
@@ -22,7 +22,7 @@ export function CreateTaskSection({ templates }: CreateTaskSectionProps) {
2222
const [presetId, setPresetId] = useState("");
2323

2424
const { mutate, isPending, error } = useMutation({
25-
mutationFn: (vars: CreateTaskParams) => api.createTask(vars),
25+
mutationFn: (params: CreateTaskParams) => api.createTask(params),
2626
onSuccess: () => setPrompt(""),
2727
});
2828

@@ -40,38 +40,14 @@ export function CreateTaskSection({ templates }: CreateTaskSectionProps) {
4040
}
4141
};
4242

43-
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
44-
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
45-
e.preventDefault();
46-
handleSubmit();
47-
}
48-
};
49-
5043
return (
5144
<div className="create-task-section">
52-
<div className="prompt-input-container">
53-
<textarea
54-
className="prompt-input"
55-
placeholder="Prompt your AI agent to start a task..."
56-
value={prompt}
57-
onChange={(e) => setPrompt(e.target.value)}
58-
onKeyDown={handleKeyDown}
59-
disabled={isPending}
60-
/>
61-
<div className="prompt-send-button">
62-
{isPending ? (
63-
<VscodeProgressRing />
64-
) : (
65-
<VscodeIcon
66-
actionIcon
67-
name="send"
68-
label="Send"
69-
onClick={() => void handleSubmit()}
70-
className={canSubmit ? "" : "disabled"}
71-
/>
72-
)}
73-
</div>
74-
</div>
45+
<PromptInput
46+
value={prompt}
47+
onChange={setPrompt}
48+
onSubmit={handleSubmit}
49+
loading={isPending}
50+
/>
7551
{error && <div className="create-task-error">{error.message}</div>}
7652
<div className="create-task-options">
7753
<div className="option-row">

0 commit comments

Comments
 (0)