Skip to content
Draft
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
4 changes: 2 additions & 2 deletions packages/cli/src/lib/load-agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type {
CustomAgentFile,
ValidCustomAgentFile,
} from "@getpochi/common/vscode-webui-bridge";
import { isValidCustomAgentFile } from "@getpochi/common/vscode-webui-bridge";
import { isValidCustomAgent } from "@getpochi/common/vscode-webui-bridge";
import type { CustomAgent } from "@getpochi/tools";
import { uniqueBy } from "remeda";

Expand Down Expand Up @@ -74,7 +74,7 @@ export async function loadAgents(
// Filter out invalid agents for CLI usage
const validAgents = uniqueBy(allAgents, (agent) => agent.name).filter(
(agent): agent is ValidCustomAgentFile => {
if (isValidCustomAgentFile(agent)) {
if (isValidCustomAgent(agent)) {
return true;
}
logger.warn(
Expand Down
3 changes: 3 additions & 0 deletions packages/common/src/base/built-in-custom-agents/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { plannerAgent } from "./planner-agent";

export const builtInCustomAgents = [plannerAgent];
104 changes: 104 additions & 0 deletions packages/common/src/base/built-in-custom-agents/planner-agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import type { CustomAgent } from "@getpochi/tools";

export const plannerAgent: CustomAgent = {
name: "planner",
description: `
Use this agent to create detailed, actionable implementation plans for new features, refactoring, or bug fixes.
This agent ONLY produces a plan and does NOT modify any source code. Code implementation should only begin after the user approves the generated plan.
`.trim(),
tools: ["readFile", "globFiles", "listFiles", "searchFiles", "writeToFile"],
systemPrompt: `
You are the Plan agent, specialized in architecting technical solutions and providing methodical, step-by-step implementation roadmaps.

## Your Role

Your SOLE goal is to transform high-level requirements into a concrete technical plan.

**CRITICAL CONSTRAINT**: You MUST NOT modify any source code files or implement any logic. Your task is complete once the plan is architected and saved. Any actual code changes will be performed in a subsequent step ONLY after the user has reviewed and approved your plan.

You should:

1. **Understand Context & Constraints**: Analyze the codebase to understand how the new feature fits into the existing architecture.
2. **Design the Solution**: Determine the best technical approach, considering project conventions and best practices.
3. **Break Down Implementation**: Deconstruct the task into atomic, manageable steps.
4. **Define Verification**: Specify how each change and the overall feature should be tested.
5. **Persist the Plan**: Save the final roadmap to the designated location for future reference.

## Planning Strategies

### For New Feature Development

- Identify necessary new files, components, or modules.
- Map out data flow and interface changes.
- Determine required changes to existing services or stores.
- Consider UI/UX consistency with existing patterns.

### For Code Refactoring

- Identify the target code and its dependencies.
- Plan incremental changes to ensure the system remains functional.
- Define clear entry and exit points for the refactored logic.
- Prioritize maintainability and testability.

### For Bug Fixes

- Locate the root cause and analyze the surrounding context.
- Plan the fix and any necessary regression tests.
- Consider if similar bugs exist elsewhere in the codebase.

## Important Reminders

- **DO NOT implement the plan.** You are a planning agent, not a coding agent.
- **DO NOT modify any source code files.** Your only write operation should be creating the plan file.
- Be specific and technical. Avoid vague instructions.
- Respect the project's existing style, libraries, and architectural patterns.
- Ensure the plan is actionable for a developer who may not have full context.
- Always include the correct file paths and line number references where applicable.

## Output Format & Persistence

### 1. Identify Task ID

Find the current **Task ID** from the **System Information** section in the **system-reminder** (look for "Task ID: [UUID]").

### 2. Save the Plan

**CRITICAL:** You MUST use the 'writeToFile' tool to save the plan to '.pochi/plans/{taskId}.md'. The content of the file should follow this markdown structure:

\`\`\`markdown
# Implementation Plan - {Feature Name}

## Problem Analysis
{Brief description of the problem and current state}

## Proposed Solution
{High-level architectural design and technical approach}

## Implementation Steps

### Step 1: {Step Title}
- **Objective**: {What this step achieves}
- **Files**:
- \`path/to/file1.ts\`
- **Changes**:
- {Detailed description of changes}

### Step 2: {Step Title}
...

## Verification Plan
### Automated Tests
- [ ] {Test Case 1}
### Manual Verification
- [ ] {Verification Step 1}
\`\`\`

### 3. Final Report

In your final response via 'attemptCompletion', provide:

- **Plan UI**: **MUST** include exactly this line at the end (not in a code block):
<plan-card task-id="{taskId}"></plan-card>
- **Instruction to Main Agent**: **CRITICAL**: Explicitly state: "The plan has been created. STOP execution now and ask the user to review the plan before proceeding."
`.trim(),
};
6 changes: 6 additions & 0 deletions packages/common/src/base/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ export const Environment = z.object({
.string()
.optional()
.describe("Custom rules provided by the user."),
taskId: z
.string()
.optional()
.describe(
"The task id of the current task. It is useful for tools that require task id.",
),
})
.describe("General information about the environment."),
todos: z.array(Todo).optional().describe("Todos in current task"),
Expand Down
2 changes: 2 additions & 0 deletions packages/common/src/base/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export { WebsiteTaskCreateEvent } from "./event";

export { toErrorMessage } from "./error";

export { builtInCustomAgents } from "./built-in-custom-agents";

export const PochiProviderOptions = z.object({
taskId: z.string(),
client: z.string(),
Expand Down
3 changes: 2 additions & 1 deletion packages/common/src/base/prompts/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ Operating System: ${info.os}
Default Shell: ${info.shell}
Home Directory: ${info.homedir}
Current Working Directory: ${info.cwd}
Current Time: ${currentTime}`;
Current Time: ${currentTime}
Current Task ID: ${info.taskId ?? "N/A"}`;
return prompt;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/vscode-webui-bridge/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export type {
DiffCheckpointOptions,
CreateWorktreeOptions,
} from "./types/git";
export { isValidCustomAgentFile } from "./types/custom-agent";
export { isValidCustomAgent } from "./types/custom-agent";
export {
prefixTaskDisplayId,
prefixWorktreeName,
Expand Down
4 changes: 2 additions & 2 deletions packages/common/src/vscode-webui-bridge/types/custom-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ export interface InvalidCustomAgentFile extends Partial<CustomAgent> {

export type CustomAgentFile = ValidCustomAgentFile | InvalidCustomAgentFile;

export const isValidCustomAgentFile = (
agent: CustomAgentFile,
export const isValidCustomAgent = (
agent: CustomAgent | CustomAgentFile,
): agent is ValidCustomAgentFile => {
return (
(agent as ValidCustomAgentFile).name !== undefined &&
Expand Down
7 changes: 5 additions & 2 deletions packages/common/src/vscode-webui-bridge/webview-stub.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { CustomAgent } from "@getpochi/tools";
import type { ThreadAbortSignalSerialization } from "@quilted/threads";
import type { ThreadSignalSerialization } from "@quilted/threads/signals";
import type { Environment } from "../base";
Expand Down Expand Up @@ -243,9 +244,11 @@ const VSCodeHostStub = {
);
},
readCustomAgents: async (): Promise<
ThreadSignalSerialization<CustomAgentFile[]>
ThreadSignalSerialization<(CustomAgent | CustomAgentFile)[]>
> => {
return Promise.resolve({} as ThreadSignalSerialization<CustomAgentFile[]>);
return Promise.resolve(
{} as ThreadSignalSerialization<(CustomAgent | CustomAgentFile)[]>,
);
},

openTaskInPanel: async (): Promise<void> => {},
Expand Down
7 changes: 5 additions & 2 deletions packages/common/src/vscode-webui-bridge/webview.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { PreviewReturnType } from "@getpochi/tools";
import type { CustomAgent, PreviewReturnType } from "@getpochi/tools";
import type { ThreadAbortSignalSerialization } from "@quilted/threads";
import type { ThreadSignalSerialization } from "@quilted/threads/signals";
import type { Environment } from "../base";
Expand Down Expand Up @@ -53,6 +53,7 @@ export interface VSCodeHostApi {
readEnvironment(options: {
isSubTask?: boolean;
webviewKind: "sidebar" | "pane";
taskId?: string;
}): Promise<Environment>;

previewToolCall(
Expand Down Expand Up @@ -167,7 +168,9 @@ export interface VSCodeHostApi {
workspacePath: string | null;
}>;

readCustomAgents(): Promise<ThreadSignalSerialization<CustomAgentFile[]>>;
readCustomAgents(): Promise<
ThreadSignalSerialization<(CustomAgent | CustomAgentFile)[]>
>;

executeBashCommand: (
command: string,
Expand Down
20 changes: 16 additions & 4 deletions packages/vscode-webui/src/components/message/markdown.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { PlanCard, useReplaceJobIdsInContent } from "@/features/chat";
import { FileBadge, IssueBadge } from "@/features/tools";
import { CustomHtmlTags } from "@/lib/constants";
import { cn } from "@/lib/utils";
import { isKnownProgrammingLanguage } from "@/lib/utils/languages";
import { isVSCodeEnvironment, vscodeHost } from "@/lib/vscode";
import { Bot } from "lucide-react";
import {
type DetailedHTMLProps,
type HTMLAttributes,
Expand All @@ -20,7 +22,6 @@ import {
import { CodeBlock } from "./code-block";
import { customStripTagsPlugin } from "./custom-strip-tags-plugin";
import "./markdown.css";
import { useReplaceJobIdsInContent } from "@/features/chat";
import { useTranslation } from "react-i18next";
import type { ExtraProps, Options } from "react-markdown";

Expand Down Expand Up @@ -399,6 +400,7 @@ export function MessageMarkdown({
...defaultSchema.attributes,
workflow: ["path", "id"],
"custom-agent": ["path", "id"],
"plan-card": ["task-id"],
issue: ["id", "url", "title"],
...mathSanitizeConfig.attributes,
},
Expand All @@ -422,9 +424,19 @@ export function MessageMarkdown({
},
"custom-agent": (props: WorkflowComponentProps) => {
const { id, path } = props;
return (
<FileBadge label={id.replaceAll("user-content-", "/")} path={path} />
);
const cleanId = id.replaceAll("user-content-", "/");
if (!path) {
return (
<span className="mx-px inline-flex items-center gap-1 rounded-sm border border-border bg-muted px-1.5 py-0.5 align-baseline font-medium text-muted-foreground text-sm/4">
<Bot className="size-3" />
{cleanId}
</span>
);
}
return <FileBadge label={cleanId} path={path} />;
},
"plan-card": (props: { "task-id"?: string }) => {
return <PlanCard taskId={props["task-id"] || ""} />;
},
issue: (props: IssueComponentProps) => {
const { id, url, title } = props;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { useSelectedModels } from "@/features/settings";
import { useLatest } from "@/lib/hooks/use-latest";
import { cn } from "@/lib/utils";
import { resolveModelFromId } from "@/lib/utils/resolve-model-from-id";
import { isValidCustomAgentFile } from "@getpochi/common/vscode-webui-bridge";
import { isValidCustomAgent } from "@getpochi/common/vscode-webui-bridge";
import { threadSignal } from "@quilted/threads/signals";
import {
type SuggestionMatch,
Expand Down Expand Up @@ -820,7 +820,7 @@ export const debouncedListSlashCommand = debounceWithCachedValue(
]);
const options: SlashCandidate[] = [
...customAgents.value
.filter((x) => isValidCustomAgentFile(x))
.filter((x) => isValidCustomAgent(x))
.map((x) => ({
type: "custom-agent" as const,
id: x.name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { useTaskInputDraft } from "@/lib/hooks/use-task-input-draft";
import { useWorktrees } from "@/lib/hooks/use-worktrees";
import { vscodeHost } from "@/lib/vscode";
import type { GitWorktree } from "@getpochi/common/vscode-webui-bridge";
import { Loader2, PaperclipIcon } from "lucide-react";
import { ClipboardList, Loader2, PaperclipIcon } from "lucide-react";
import type React from "react";
import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
Expand Down Expand Up @@ -111,11 +111,11 @@ export const CreateTaskInput: React.FC<CreateTaskInputProps> = ({
useDebounceState(isCreatingTask, 300);

const handleSubmitImpl = useCallback(
async (
e?: React.FormEvent<HTMLFormElement>,
shouldCreateWorktree?: boolean,
) => {
e?.preventDefault();
async (options?: {
shouldCreateWorktree?: boolean;
shouldCreatePlan?: boolean;
}) => {
const { shouldCreateWorktree, shouldCreatePlan } = options || {};

if (isCreatingTask) return;

Expand All @@ -125,11 +125,16 @@ export const CreateTaskInput: React.FC<CreateTaskInputProps> = ({
// If no valid model is selected, submission is not allowed.
if (!selectedModel) return;

const content = input.trim();
let content = input.trim();

// Disallow empty submissions
if (content.length === 0 && files.length === 0) return;

if (shouldCreatePlan) {
// Use the planner custom agent to create a plan
content = `<custom-agent id="planner" path="">newTask:planner</custom-agent> ${content}`;
}

// Set isCreatingTask state true
// Show loading and freeze input
setIsCreatingTask(true);
Expand Down Expand Up @@ -208,18 +213,24 @@ export const CreateTaskInput: React.FC<CreateTaskInputProps> = ({

const handleSubmit = useCallback(
async (e: React.FormEvent<HTMLFormElement>) => {
handleSubmitImpl(e);
e.preventDefault();
handleSubmitImpl();
},
[handleSubmitImpl],
);

const handleCtrlSubmit = useCallback(
async (e: React.FormEvent<HTMLFormElement>) => {
handleSubmitImpl(e, true);
e.preventDefault();
handleSubmitImpl({ shouldCreateWorktree: true });
},
[handleSubmitImpl],
);

const handleCreatePlan = useCallback(async () => {
handleSubmitImpl({ shouldCreatePlan: true });
}, [handleSubmitImpl]);

return (
<>
<ChatInputForm
Expand Down Expand Up @@ -307,6 +318,30 @@ export const CreateTaskInput: React.FC<CreateTaskInputProps> = ({
{t("chat.attachmentTooltip")}
</HoverCardContent>
</HoverCard>
<HoverCard>
<HoverCardTrigger asChild>
<span>
<Button
variant="ghost"
size="icon"
onClick={handleCreatePlan}
className="button-focus relative h-6 w-6 p-0"
>
<span className="size-4">
<ClipboardList className="size-4 translate-y-[1.5px] scale-105" />
</span>
</Button>
</span>
</HoverCardTrigger>
<HoverCardContent
side="top"
align="start"
sideOffset={6}
className="!w-auto max-w-sm bg-background px-3 py-1.5 text-xs"
>
{t("chat.createPlanTooltip")}
</HoverCardContent>
</HoverCard>
{!!debouncedIsCreatingTask && (
<span className="p-1">
<Loader2 className="size-4 animate-spin" />
Expand Down
Loading
Loading