Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 3 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,14 +139,6 @@
"experimental"
]
},
"coder.experimental.tasks": {
"markdownDescription": "Enable the experimental [Tasks](https://coder.com/docs/ai-coder/tasks) panel in VS Code. When enabled, a sidebar panel lets you run and manage AI coding agents in Coder workspaces. This feature is under active development and may change. Requires a Coder deployment with Tasks support.",
"type": "boolean",
"default": false,
"tags": [
"experimental"
]
},
"coder.sshFlags": {
"markdownDescription": "Additional flags to pass to the `coder ssh` command when establishing SSH connections. Enter each flag as a separate array item; values are passed verbatim and in order. See the [CLI ssh reference](https://coder.com/docs/reference/cli/ssh) for available flags.\n\nNote: `--network-info-dir` and `--ssh-host-prefix` are ignored (managed internally). Prefer `#coder.proxyLogDirectory#` over `--log-dir`/`-l` for full functionality.",
"type": "array",
Expand Down Expand Up @@ -219,7 +211,7 @@
"id": "coder.tasksPanel",
"name": "Coder Tasks",
"icon": "media/tasks-logo.svg",
"when": "coder.authenticated && coder.tasksEnabled"
"when": "coder.authenticated"
}
]
},
Expand Down Expand Up @@ -426,7 +418,7 @@
},
{
"command": "coder.tasks.refresh",
"when": "coder.authenticated && coder.tasksEnabled && view == coder.tasksPanel",
"when": "coder.authenticated && view == coder.tasksPanel",
"group": "navigation@1"
}
],
Expand Down Expand Up @@ -474,7 +466,7 @@
"@peculiar/x509": "^1.14.3",
"@repo/shared": "workspace:*",
"axios": "1.13.6",
"date-fns": "^4.1.0",
"date-fns": "catalog:",
"eventsource": "^4.1.0",
"find-process": "^2.1.0",
"jsonc-parser": "^3.3.1",
Expand Down
7 changes: 6 additions & 1 deletion packages/shared/src/tasks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ export interface TaskPreset {

/** Result of fetching task logs: either logs or an error/unavailable state. */
export type TaskLogs =
| { status: "ok"; logs: readonly TaskLogEntry[] }
| {
status: "ok";
logs: readonly TaskLogEntry[];
snapshot?: boolean;
snapshotAt?: string;
}
| { status: "not_available" }
| { status: "error" };

Expand Down
11 changes: 11 additions & 0 deletions packages/shared/src/tasks/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,14 @@ export function isAgentStarting(task: Task): boolean {
export function isWorkspaceStarting(task: Task): boolean {
return isBuildingWorkspace(task) || isAgentStarting(task);
}

/** Label for the log preview header, matching the Coder dashboard pattern. */
export function logPreviewLabel(count: number): string {
if (count === 0) {
return "AI chat messages";
}
if (count === 1) {
return "Last message of AI chat";
}
return `Last ${count} messages of AI chat`;
}
1 change: 1 addition & 0 deletions packages/tasks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@tanstack/react-query": "catalog:",
"@vscode-elements/react-elements": "catalog:",
"@vscode/codicons": "catalog:",
"date-fns": "catalog:",
"react": "catalog:",
"react-dom": "catalog:"
},
Expand Down
42 changes: 37 additions & 5 deletions packages/tasks/src/components/AgentChatHistory.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import {
logPreviewLabel,
type TaskLogEntry,
type TaskLogs,
} from "@repo/shared";
import { formatDistanceToNowStrict } from "date-fns";

import { LogViewer, LogViewerPlaceholder } from "./LogViewer";

import type { TaskLogEntry, TaskLogs } from "@repo/shared";
import type { ReactNode } from "react";

interface AgentChatHistoryProps {
taskLogs: TaskLogs;
Expand All @@ -18,22 +25,47 @@ function LogEntry({
<div className={`log-entry log-entry-${log.type}`}>
{isGroupStart && (
<div className="log-entry-role">
{log.type === "input" ? "You" : "Agent"}
{log.type === "input" ? "[User]" : "[Agent]"}
</div>
)}
{log.content}
</div>
);
}

function chatHistoryHeader(taskLogs: TaskLogs): ReactNode {
if (taskLogs.status !== "ok" || taskLogs.snapshot !== true) {
return "Chat history";
}
const label = logPreviewLabel(taskLogs.logs.length);
if (taskLogs.snapshotAt === undefined) {
return label;
}
const relativeTime = formatDistanceToNowStrict(
new Date(taskLogs.snapshotAt),
{ addSuffix: true },
);
return (
<>
{label}{" "}
<span className="snapshot-info">
<span className="codicon codicon-info" />
<span className="snapshot-info-tooltip">
Snapshot taken {relativeTime}
</span>
</span>
</>
);
}

export function AgentChatHistory({
taskLogs,
isThinking,
}: AgentChatHistoryProps) {
const logs = taskLogs.status === "ok" ? taskLogs.logs : [];

return (
<LogViewer header="Agent chat history">
<LogViewer header={chatHistoryHeader(taskLogs)}>
{logs.length === 0 ? (
<LogViewerPlaceholder error={taskLogs.status === "error"}>
{getEmptyMessage(taskLogs.status)}
Expand All @@ -57,9 +89,9 @@ export function AgentChatHistory({
function getEmptyMessage(status: TaskLogs["status"]): string {
switch (status) {
case "not_available":
return "Logs not available in current task state";
return "Messages are not available yet";
case "error":
return "Failed to load logs";
return "Failed to load messages";
case "ok":
return "No messages yet";
}
Expand Down
2 changes: 1 addition & 1 deletion packages/tasks/src/components/CreateTaskSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export function CreateTaskSection({ templates }: CreateTaskSectionProps) {
onSubmit={handleSubmit}
loading={isPending}
actionIcon="send"
actionLabel="Send"
actionLabel="Create task"
actionEnabled={canSubmit === true}
/>
{error && <div className="create-task-error">{error.message}</div>}
Expand Down
2 changes: 1 addition & 1 deletion packages/tasks/src/components/LogViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useFollowScroll } from "../hooks/useFollowScroll";
import type { ReactNode } from "react";

interface LogViewerProps {
header: string;
header: ReactNode;
children: ReactNode;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/tasks/src/components/NoTemplateState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const DOCS_URL = "https://coder.com/docs/admin/templates";
export function NoTemplateState() {
return (
<StatePanel
title="No Task template found"
title="No task templates found"
action={
<a href={DOCS_URL} className="text-link">
Learn how to create a template <VscodeIcon name="link-external" />
Expand Down
10 changes: 5 additions & 5 deletions packages/tasks/src/components/TaskMessageInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,20 @@ function getPlaceholder(task: Task): string {
return "Waiting for the agent to start...";
case "error":
case "unknown":
return "Task is in an error state and cannot receive messages";
return "This task encountered an error";
case "active":
break;
}

switch (task.current_state?.state) {
case "working":
return "Agent is working — you can pause or wait for it to finish...";
return "Agent is working...";
case "complete":
return "Task completed — send a follow-up to continue...";
return "Send a follow-up to continue...";
case "failed":
return "Task failed — send a message to retry...";
return "Send a message to retry...";
default:
return "Send a message to the agent...";
return "Send a message...";
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/tasks/src/components/useTaskMenuItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export function useTaskMenuItems({
menuItems.push({ separator: true });

menuItems.push({
label: "Delete",
label: "Delete Task",
icon: "trash",
onClick: () =>
run("deleting", () => api.deleteTask({ taskId: task.id, taskName })),
Expand Down
17 changes: 16 additions & 1 deletion packages/tasks/src/hooks/useScrollableHeight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,24 @@ export function useScrollableHeight(
}
});

// Observe content child so the layout recalculates when
// the content is replaced (e.g. loading state -> form).
function observeContent() {
if (scroll?.firstElementChild) {
observer.observe(scroll.firstElementChild);
}
}

observer.observe(host);
observer.observe(scroll);
observeContent();

const mutations = new MutationObserver(observeContent);
mutations.observe(scroll, { childList: true });

return () => observer.disconnect();
return () => {
observer.disconnect();
mutations.disconnect();
};
}, [hostRef, scrollRef]);
}
36 changes: 35 additions & 1 deletion packages/tasks/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -478,12 +478,46 @@ vscode-icon.disabled {
}

.log-viewer-header {
position: relative;
padding: 6px 8px;
font-size: 0.8em;
font-size: 0.9em;
color: var(--vscode-descriptionForeground);
border-bottom: 1px solid var(--vscode-input-border);
}

.snapshot-info {
margin-left: 3px;
}

.snapshot-info .codicon-info {
font-size: 1em;
color: var(--vscode-descriptionForeground);
vertical-align: middle;
cursor: pointer;
}

.snapshot-info-tooltip {
display: none;
position: absolute;
bottom: calc(100% + 4px);
left: 8px;
right: 8px;
width: fit-content;
padding: 6px 10px;
white-space: nowrap;
font-size: 1em;
color: var(--vscode-editorHoverWidget-foreground);
background: var(--vscode-editorHoverWidget-background);
border: 1px solid var(--vscode-editorHoverWidget-border);
border-radius: 3px;
z-index: 10;
pointer-events: none;
}

.snapshot-info:hover .snapshot-info-tooltip {
display: block;
}

.log-viewer-content {
flex: 1;
min-height: 0;
Expand Down
16 changes: 11 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ catalog:
"@vscode/codicons": ^0.0.44
babel-plugin-react-compiler: ^1.0.0
coder: github:coder/coder#main
date-fns: ^4.1.0
react: ^19.2.4
react-dom: ^19.2.4
typescript: ^5.9.3
Expand Down
1 change: 0 additions & 1 deletion src/core/contextManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ const CONTEXT_DEFAULTS = {
"coder.authenticated": false,
"coder.isOwner": false,
"coder.loaded": false,
"coder.tasksEnabled": false,
"coder.workspace.connected": false,
"coder.workspace.updatable": false,
} as const;
Expand Down
16 changes: 0 additions & 16 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,22 +66,6 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
const secretsManager = serviceContainer.getSecretsManager();
const contextManager = serviceContainer.getContextManager();

const syncTasksFlag = () => {
const enabled =
vscode.workspace
.getConfiguration()
.get<boolean>("coder.experimental.tasks") === true;
contextManager.set("coder.tasksEnabled", enabled);
};
syncTasksFlag();
ctx.subscriptions.push(
vscode.workspace.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration("coder.experimental.tasks")) {
syncTasksFlag();
}
}),
);

// Migrate auth storage from old flat format to new label-based format
await migrateAuthStorage(serviceContainer);

Expand Down
Loading