Skip to content

Commit e20a7bd

Browse files
committed
Self-review
1 parent bfd85e1 commit e20a7bd

File tree

19 files changed

+294
-306
lines changed

19 files changed

+294
-306
lines changed

packages/shared/src/tasks/api.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@ const sendTaskMessage = defineRequest<TaskIdParams & { message: string }, void>(
5454
"sendTaskMessage",
5555
);
5656

57-
const viewInCoder = defineCommand<{ taskId: string }>("viewInCoder");
58-
const viewLogs = defineCommand<{ taskId: string }>("viewLogs");
59-
const closeWorkspaceLogs = defineCommand("closeWorkspaceLogs");
57+
const viewInCoder = defineCommand<TaskIdParams>("viewInCoder");
58+
const viewLogs = defineCommand<TaskIdParams>("viewLogs");
59+
const closeWorkspaceLogs = defineCommand<void>("closeWorkspaceLogs");
6060

6161
const taskUpdated = defineNotification<Task>("taskUpdated");
6262
const tasksUpdated = defineNotification<Task[]>("tasksUpdated");

packages/shared/src/tasks/types.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,18 @@ export interface TaskPreset {
2929
isDefault: boolean;
3030
}
3131

32-
/** Status of log fetching */
33-
export type LogsStatus = "ok" | "not_available" | "error";
32+
/** Result of fetching task logs: either logs or an error/unavailable state. */
33+
export type TaskLogs =
34+
| { status: "ok"; logs: TaskLogEntry[] }
35+
| { status: "not_available" }
36+
| { status: "error" };
3437

3538
/**
3639
* Full details for a selected task, including logs and action availability.
3740
*/
3841
export interface TaskDetails extends TaskPermissions {
3942
task: Task;
40-
logs: TaskLogEntry[];
41-
logsStatus: LogsStatus;
43+
logs: TaskLogs;
4244
}
4345

4446
export interface TaskPermissions {

packages/shared/src/tasks/utils.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,6 @@ export function getTaskLabel(task: Task): string {
44
return task.display_name || task.name || task.id;
55
}
66

7-
/** Whether the agent is actively working (status is active and state is working). */
8-
export function isTaskWorking(task: Task): boolean {
9-
return task.status === "active" && task.current_state?.state === "working";
10-
}
11-
127
const PAUSABLE_STATUSES: readonly TaskStatus[] = [
138
"active",
149
"initializing",
@@ -42,6 +37,11 @@ export function getTaskPermissions(task: Task): TaskPermissions {
4237
};
4338
}
4439

40+
/** Whether the agent is actively working (status is active and state is working). */
41+
export function isTaskWorking(task: Task): boolean {
42+
return task.status === "active" && task.current_state?.state === "working";
43+
}
44+
4545
/**
4646
* Task statuses where logs won't change (stable/terminal states).
4747
* "complete" is a TaskState (sub-state of active), checked separately.
@@ -64,7 +64,9 @@ export function isBuildingWorkspace(task: Task): boolean {
6464

6565
/** Whether the workspace is running but the agent hasn't reached "ready" yet. */
6666
export function isAgentStarting(task: Task): boolean {
67-
if (task.workspace_status !== "running") return false;
67+
if (task.workspace_status !== "running") {
68+
return false;
69+
}
6870
const lc = task.workspace_agent_lifecycle;
6971
return lc === "created" || lc === "starting";
7072
}

packages/tasks/src/App.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { TasksApi, type InitResponse } from "@repo/shared";
1+
import { type InitResponse } from "@repo/shared";
22
import { getState, setState } from "@repo/webview-shared";
3-
import { useIpc } from "@repo/webview-shared/react";
43
import {
54
VscodeCollapsible,
65
VscodeProgressRing,
@@ -17,6 +16,7 @@ import { TaskList } from "./components/TaskList";
1716
import { useCollapsibleToggle } from "./hooks/useCollapsibleToggle";
1817
import { useScrollableHeight } from "./hooks/useScrollableHeight";
1918
import { useSelectedTask } from "./hooks/useSelectedTask";
19+
import { useTasksApi } from "./hooks/useTasksApi";
2020
import { useTasksQuery } from "./hooks/useTasksQuery";
2121

2222
interface PersistedState extends InitResponse {
@@ -46,10 +46,10 @@ export default function App() {
4646
useScrollableHeight(createRef, createScrollRef);
4747
useScrollableHeight(historyRef, historyScrollRef);
4848

49-
const { onNotification } = useIpc();
49+
const { onShowCreateForm } = useTasksApi();
5050
useEffect(() => {
51-
return onNotification(TasksApi.showCreateForm, () => setCreateOpen(true));
52-
}, [onNotification, setCreateOpen]);
51+
return onShowCreateForm(() => setCreateOpen(true));
52+
}, [onShowCreateForm, setCreateOpen]);
5353

5454
useEffect(() => {
5555
if (data) {

packages/tasks/src/components/AgentChatHistory.tsx

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { LogViewer, LogViewerPlaceholder } from "./LogViewer";
22

3-
import type { LogsStatus, TaskLogEntry } from "@repo/shared";
3+
import type { TaskLogEntry, TaskLogs } from "@repo/shared";
44

55
interface AgentChatHistoryProps {
6-
logs: TaskLogEntry[];
7-
logsStatus: LogsStatus;
6+
taskLogs: TaskLogs;
87
isThinking: boolean;
98
}
109

@@ -28,38 +27,35 @@ function LogEntry({
2827
}
2928

3029
export function AgentChatHistory({
31-
logs,
32-
logsStatus,
30+
taskLogs,
3331
isThinking,
3432
}: AgentChatHistoryProps) {
35-
const isEmpty = logs.length === 0 && (logsStatus !== "ok" || !isThinking);
33+
const logs = taskLogs.status === "ok" ? taskLogs.logs : [];
3634

3735
return (
3836
<LogViewer header="Agent chat history">
39-
{isEmpty ? (
40-
<LogViewerPlaceholder error={logsStatus === "error"}>
41-
{getEmptyMessage(logsStatus)}
37+
{logs.length === 0 ? (
38+
<LogViewerPlaceholder error={taskLogs.status === "error"}>
39+
{getEmptyMessage(taskLogs.status)}
4240
</LogViewerPlaceholder>
4341
) : (
44-
<>
45-
{logs.map((log, index) => (
46-
<LogEntry
47-
key={log.id}
48-
log={log}
49-
isGroupStart={index === 0 || log.type !== logs[index - 1].type}
50-
/>
51-
))}
52-
{isThinking && (
53-
<div className="log-entry log-entry-thinking">Thinking...</div>
54-
)}
55-
</>
42+
logs.map((log, index) => (
43+
<LogEntry
44+
key={log.id}
45+
log={log}
46+
isGroupStart={index === 0 || log.type !== logs[index - 1].type}
47+
/>
48+
))
49+
)}
50+
{isThinking && (
51+
<div className="log-entry log-entry-thinking">Thinking...</div>
5652
)}
5753
</LogViewer>
5854
);
5955
}
6056

61-
function getEmptyMessage(logsStatus: LogsStatus): string {
62-
switch (logsStatus) {
57+
function getEmptyMessage(status: TaskLogs["status"]): string {
58+
switch (status) {
6359
case "not_available":
6460
return "Logs not available in current task state";
6561
case "error":

packages/tasks/src/components/LogViewer.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,11 @@ export function LogViewerPlaceholder({
2727
children,
2828
error,
2929
}: {
30-
children: ReactNode;
30+
children: string;
3131
error?: boolean;
3232
}) {
3333
return (
34-
<div
35-
className={
36-
error ? "log-viewer-empty log-viewer-error" : "log-viewer-empty"
37-
}
38-
>
34+
<div className={`log-viewer-empty${error ? " log-viewer-error" : ""}`}>
3935
{children}
4036
</div>
4137
);

packages/tasks/src/components/TaskDetailView.tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import {
44
type TaskDetails,
55
} from "@repo/shared";
66

7-
import { useWorkspaceLogs } from "../hooks/useWorkspaceLogs";
8-
97
import { AgentChatHistory } from "./AgentChatHistory";
108
import { ErrorBanner } from "./ErrorBanner";
119
import { TaskDetailHeader } from "./TaskDetailHeader";
@@ -18,24 +16,19 @@ interface TaskDetailViewProps {
1816
}
1917

2018
export function TaskDetailView({ details, onBack }: TaskDetailViewProps) {
21-
const { task, logs, logsStatus } = details;
19+
const { task, logs } = details;
2220

2321
const starting = isWorkspaceStarting(task);
24-
const workspaceLines = useWorkspaceLogs(starting);
2522
const isThinking = isTaskWorking(task);
2623

2724
return (
2825
<div className="task-detail-view">
2926
<TaskDetailHeader task={task} onBack={onBack} />
3027
{task.status === "error" && <ErrorBanner task={task} />}
3128
{starting ? (
32-
<WorkspaceLogs task={task} lines={workspaceLines} />
29+
<WorkspaceLogs task={task} />
3330
) : (
34-
<AgentChatHistory
35-
logs={logs}
36-
logsStatus={logsStatus}
37-
isThinking={isThinking}
38-
/>
31+
<AgentChatHistory taskLogs={logs} isThinking={isThinking} />
3932
)}
4033
<TaskMessageInput task={task} />
4134
</div>

packages/tasks/src/components/WorkspaceLogs.tsx

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { isBuildingWorkspace, type Task } from "@repo/shared";
22

3+
import { useWorkspaceLogs } from "../hooks/useWorkspaceLogs";
4+
35
import { LogViewer, LogViewerPlaceholder } from "./LogViewer";
46

5-
export function WorkspaceLogs({
6-
task,
7-
lines,
8-
}: {
9-
task: Task;
10-
lines: string[];
11-
}) {
7+
function LogLine({ children }: { children: string }) {
8+
return <div className="log-entry">{children}</div>;
9+
}
10+
11+
export function WorkspaceLogs({ task }: { task: Task }) {
12+
const lines = useWorkspaceLogs();
1213
const header = isBuildingWorkspace(task)
1314
? "Building workspace..."
1415
: "Running startup scripts...";
@@ -18,11 +19,7 @@ export function WorkspaceLogs({
1819
{lines.length === 0 ? (
1920
<LogViewerPlaceholder>Waiting for logs...</LogViewerPlaceholder>
2021
) : (
21-
lines.map((line, i) => (
22-
<div key={i} className="log-entry">
23-
{line}
24-
</div>
25-
))
22+
lines.map((line, i) => <LogLine key={i}>{line}</LogLine>)
2623
)}
2724
</LogViewer>
2825
);

packages/tasks/src/hooks/useSelectedTask.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
1-
import {
2-
TasksApi,
3-
isStableTask,
4-
type Task,
5-
type TaskDetails,
6-
} from "@repo/shared";
7-
import { useIpc } from "@repo/webview-shared/react";
1+
import { isStableTask, type Task, type TaskDetails } from "@repo/shared";
82
import { skipToken, useQuery, useQueryClient } from "@tanstack/react-query";
93
import { useEffect, useState } from "react";
104

@@ -20,7 +14,6 @@ const QUERY_KEY = "task-details";
2014
export function useSelectedTask(tasks: readonly Task[]) {
2115
const api = useTasksApi();
2216
const queryClient = useQueryClient();
23-
const { onNotification } = useIpc();
2417
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
2518

2619
// Auto-deselect when the selected task disappears from the list
@@ -48,14 +41,14 @@ export function useSelectedTask(tasks: readonly Task[]) {
4841

4942
// Keep selected task in sync with push updates between polls
5043
useEffect(() => {
51-
return onNotification(TasksApi.taskUpdated, (updatedTask) => {
44+
return api.onTaskUpdated((updatedTask) => {
5245
if (updatedTask.id !== selectedTaskId) return;
5346
queryClient.setQueryData<TaskDetails>(
5447
[QUERY_KEY, selectedTaskId],
5548
(prev) => (prev ? { ...prev, task: updatedTask } : undefined),
5649
);
5750
});
58-
}, [onNotification, selectedTaskId, queryClient]);
51+
}, [api.onTaskUpdated, selectedTaskId, queryClient]);
5952

6053
const deselectTask = () => {
6154
setSelectedTaskId(null);

packages/tasks/src/hooks/useTasksApi.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,13 @@
1212
import {
1313
TasksApi,
1414
type CreateTaskParams,
15+
type Task,
1516
type TaskActionParams,
1617
} from "@repo/shared";
17-
import { logger } from "@repo/webview-shared/logger";
1818
import { useIpc } from "@repo/webview-shared/react";
1919

2020
export function useTasksApi() {
21-
const { request, command } = useIpc();
22-
23-
function safeCommand<P>(
24-
definition: { method: string; _types?: { params: P } },
25-
...args: P extends void ? [] : [params: P]
26-
): void {
27-
try {
28-
command(definition, ...args);
29-
} catch (err) {
30-
logger.error(`Command ${definition.method} failed`, err);
31-
}
32-
}
21+
const { request, command, onNotification } = useIpc();
3322

3423
return {
3524
// Requests
@@ -53,8 +42,19 @@ export function useTasksApi() {
5342
request(TasksApi.sendTaskMessage, { taskId, message }),
5443

5544
// Commands
56-
viewInCoder: (taskId: string) =>
57-
safeCommand(TasksApi.viewInCoder, { taskId }),
58-
viewLogs: (taskId: string) => safeCommand(TasksApi.viewLogs, { taskId }),
45+
viewInCoder: (taskId: string) => command(TasksApi.viewInCoder, { taskId }),
46+
viewLogs: (taskId: string) => command(TasksApi.viewLogs, { taskId }),
47+
closeWorkspaceLogs: () => command(TasksApi.closeWorkspaceLogs),
48+
49+
// Notifications
50+
onTaskUpdated: (cb: (task: Task) => void) =>
51+
onNotification(TasksApi.taskUpdated, cb),
52+
onTasksUpdated: (cb: (tasks: Task[]) => void) =>
53+
onNotification(TasksApi.tasksUpdated, cb),
54+
onWorkspaceLogsAppend: (cb: (lines: string[]) => void) =>
55+
onNotification(TasksApi.workspaceLogsAppend, cb),
56+
onRefresh: (cb: () => void) => onNotification(TasksApi.refresh, cb),
57+
onShowCreateForm: (cb: () => void) =>
58+
onNotification(TasksApi.showCreateForm, cb),
5959
};
6060
}

0 commit comments

Comments
 (0)