diff --git a/package.json b/package.json index 5a15b205..61a63fbb 100644 --- a/package.json +++ b/package.json @@ -219,7 +219,7 @@ "id": "coder.tasksPanel", "name": "Coder Tasks", "icon": "media/tasks-logo.svg", - "when": "coder.authenticated && coder.tasksEnabled" + "when": "coder.tasksEnabled" } ] }, diff --git a/packages/shared/src/tasks/api.ts b/packages/shared/src/tasks/api.ts index 80ec0c26..a510566e 100644 --- a/packages/shared/src/tasks/api.ts +++ b/packages/shared/src/tasks/api.ts @@ -47,13 +47,13 @@ const deleteTask = defineRequest("deleteTask"); const pauseTask = defineRequest("pauseTask"); const resumeTask = defineRequest("resumeTask"); const downloadLogs = defineRequest<{ taskId: string }, void>("downloadLogs"); +const sendTaskMessage = defineRequest< + { taskId: string; message: string }, + void +>("sendTaskMessage"); const viewInCoder = defineCommand<{ taskId: string }>("viewInCoder"); const viewLogs = defineCommand<{ taskId: string }>("viewLogs"); -const sendTaskMessage = defineCommand<{ - taskId: string; - message: string; -}>("sendTaskMessage"); const taskUpdated = defineNotification("taskUpdated"); const tasksUpdated = defineNotification("tasksUpdated"); @@ -73,10 +73,10 @@ export const TasksApi = { pauseTask, resumeTask, downloadLogs, + sendTaskMessage, // Commands viewInCoder, viewLogs, - sendTaskMessage, // Notifications taskUpdated, tasksUpdated, diff --git a/packages/shared/src/tasks/types.ts b/packages/shared/src/tasks/types.ts index 95654b5f..b30546c7 100644 --- a/packages/shared/src/tasks/types.ts +++ b/packages/shared/src/tasks/types.ts @@ -45,4 +45,5 @@ export interface TaskPermissions { canPause: boolean; pauseDisabled: boolean; canResume: boolean; + canSendMessage: boolean; } diff --git a/packages/shared/src/tasks/utils.ts b/packages/shared/src/tasks/utils.ts index 16f42098..94554a4b 100644 --- a/packages/shared/src/tasks/utils.ts +++ b/packages/shared/src/tasks/utils.ts @@ -26,10 +26,14 @@ const RESUMABLE_STATUSES: readonly TaskStatus[] = [ export function getTaskPermissions(task: Task): TaskPermissions { const hasWorkspace = task.workspace_id !== null; const status = task.status; + const canSendMessage = + task.status === "paused" || + (task.status === "active" && task.current_state?.state !== "working"); return { canPause: hasWorkspace && PAUSABLE_STATUSES.includes(status), pauseDisabled: PAUSE_DISABLED_STATUSES.includes(status), canResume: hasWorkspace && RESUMABLE_STATUSES.includes(status), + canSendMessage, }; } diff --git a/packages/tasks/src/App.tsx b/packages/tasks/src/App.tsx index b507dc5f..9123452f 100644 --- a/packages/tasks/src/App.tsx +++ b/packages/tasks/src/App.tsx @@ -12,9 +12,11 @@ import { CreateTaskSection } from "./components/CreateTaskSection"; import { ErrorState } from "./components/ErrorState"; import { NoTemplateState } from "./components/NoTemplateState"; import { NotSupportedState } from "./components/NotSupportedState"; +import { TaskDetailView } from "./components/TaskDetailView"; import { TaskList } from "./components/TaskList"; import { useCollapsibleToggle } from "./hooks/useCollapsibleToggle"; import { useScrollableHeight } from "./hooks/useScrollableHeight"; +import { useSelectedTask } from "./hooks/useSelectedTask"; import { useTasksQuery } from "./hooks/useTasksQuery"; interface PersistedState extends InitResponse { @@ -30,6 +32,9 @@ export default function App() { const { tasks, templates, tasksSupported, data, isLoading, error, refetch } = useTasksQuery(restored); + const { selectedTask, isLoadingDetails, selectTask, deselectTask } = + useSelectedTask(tasks); + const [createRef, createOpen, setCreateOpen] = useCollapsibleToggle(restored?.createExpanded ?? true); const [historyRef, historyOpen] = useCollapsibleToggle( @@ -37,7 +42,7 @@ export default function App() { ); const createScrollRef = useRef(null); - const historyScrollRef = useRef(null); + const historyScrollRef = useRef(null); useScrollableHeight(createRef, createScrollRef); useScrollableHeight(historyRef, historyScrollRef); @@ -95,14 +100,17 @@ export default function App() { heading="Task History" open={historyOpen} > - - { - // Task detail view will be added in next PR - }} - /> - +
+ {selectedTask ? ( + + ) : isLoadingDetails ? ( +
+ +
+ ) : ( + + )} +
); diff --git a/packages/tasks/src/components/AgentChatHistory.tsx b/packages/tasks/src/components/AgentChatHistory.tsx new file mode 100644 index 00000000..6b1510f5 --- /dev/null +++ b/packages/tasks/src/components/AgentChatHistory.tsx @@ -0,0 +1,80 @@ +import { VscodeScrollable } from "@vscode-elements/react-elements"; + +import { useFollowScroll } from "../hooks/useFollowScroll"; + +import type { LogsStatus, TaskLogEntry } from "@repo/shared"; + +interface AgentChatHistoryProps { + logs: TaskLogEntry[]; + logsStatus: LogsStatus; + isThinking: boolean; +} + +function LogEntry({ + log, + isGroupStart, +}: { + log: TaskLogEntry; + isGroupStart: boolean; +}) { + return ( +
+ {isGroupStart && ( +
+ {log.type === "input" ? "You" : "Agent"} +
+ )} + {log.content} +
+ ); +} + +export function AgentChatHistory({ + logs, + logsStatus, + isThinking, +}: AgentChatHistoryProps) { + const bottomRef = useFollowScroll(); + + return ( +
+
Agent chat history
+ + {logs.length === 0 ? ( +
+ {getEmptyMessage(logsStatus)} +
+ ) : ( + logs.map((log, index) => ( + + )) + )} + {isThinking && ( +
Thinking...
+ )} +
+ +
+ ); +} + +function getEmptyMessage(logsStatus: LogsStatus): string { + switch (logsStatus) { + case "not_available": + return "Logs not available in current task state"; + case "error": + return "Failed to load logs"; + default: + return "No messages yet"; + } +} diff --git a/packages/tasks/src/components/CreateTaskSection.tsx b/packages/tasks/src/components/CreateTaskSection.tsx index 70226a22..8a89925b 100644 --- a/packages/tasks/src/components/CreateTaskSection.tsx +++ b/packages/tasks/src/components/CreateTaskSection.tsx @@ -47,6 +47,9 @@ export function CreateTaskSection({ templates }: CreateTaskSectionProps) { onChange={setPrompt} onSubmit={handleSubmit} loading={isPending} + actionIcon="send" + actionLabel="Send" + actionDisabled={!canSubmit} /> {error &&
{error.message}
}
diff --git a/packages/tasks/src/components/ErrorBanner.tsx b/packages/tasks/src/components/ErrorBanner.tsx new file mode 100644 index 00000000..17b262f3 --- /dev/null +++ b/packages/tasks/src/components/ErrorBanner.tsx @@ -0,0 +1,28 @@ +import { VscodeIcon } from "@vscode-elements/react-elements"; + +import { useTasksApi } from "../hooks/useTasksApi"; + +import type { Task } from "@repo/shared"; + +interface ErrorBannerProps { + task: Task; +} + +export function ErrorBanner({ task }: ErrorBannerProps) { + const api = useTasksApi(); + const message = task.current_state?.message || "Task failed"; + + return ( +
+ + {message} + +
+ ); +} diff --git a/packages/tasks/src/components/PromptInput.tsx b/packages/tasks/src/components/PromptInput.tsx index b454d17d..df5fc483 100644 --- a/packages/tasks/src/components/PromptInput.tsx +++ b/packages/tasks/src/components/PromptInput.tsx @@ -12,6 +12,9 @@ interface PromptInputProps { disabled?: boolean; loading?: boolean; placeholder?: string; + actionIcon: "send" | "debug-pause"; + actionLabel: string; + actionDisabled: boolean; } export function PromptInput({ @@ -21,9 +24,10 @@ export function PromptInput({ disabled = false, loading = false, placeholder = "Prompt your AI agent to start a task...", + actionIcon, + actionLabel, + actionDisabled, }: PromptInputProps) { - const canSubmit = value.trim().length > 0 && !disabled && !loading; - return (