Skip to content
Closed
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
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,40 @@ The extension renders a persistent widget above the editor:

### `TaskCreate`

Create a structured task. Used proactively for complex multi-step work.
Create one structured task, or create multiple tasks in one call with `tasks`. Used proactively for complex multi-step work.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `subject` | string | yes | Brief imperative title |
| `description` | string | yes | Detailed context and acceptance criteria |
| `subject` | string | yes* | Brief imperative title |
| `description` | string | yes* | Detailed context and acceptance criteria |
| `activeForm` | string | no | Present continuous form for spinner (e.g., "Running tests") |
| `agentType` | string | no | Agent type for subagent execution (e.g., `"general-purpose"`, `"Explore"`) |
| `metadata` | object | no | Arbitrary key-value pairs |
| `tasks` | array | no* | Bulk task list. Each item accepts `subject`, `description`, `activeForm`, `agentType`, and `metadata`. |

\* Provide either top-level `subject` + `description`, or a non-empty `tasks` array.

```
→ Task #1 created successfully: Fix authentication bug
```

Bulk example:

```json
{
"tasks": [
{ "subject": "Design API", "description": "Define endpoints and data contracts" },
{ "subject": "Implement API", "description": "Build endpoints after design is approved" }
]
}
```

```
→ Created 2 tasks successfully:
Task #1: Design API
Task #2: Implement API
```

### `TaskList`

List all tasks with status, owner, and blocked-by info.
Expand Down
47 changes: 41 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,9 +426,12 @@ NOTE that you should not use this tool if there is only one trivial task to do.

## Task Fields

TaskCreate supports either a single task using top-level fields, or multiple tasks using the \`tasks\` array.

- **subject**: A brief, actionable title in imperative form (e.g., "Fix authentication bug in login flow")
- **description**: Detailed description of what needs to be done, including context and acceptance criteria
- **activeForm** (optional): Present continuous form shown in the spinner when the task is in_progress (e.g., "Fixing authentication bug"). If omitted, the spinner shows the subject instead.
- **tasks** (optional): Array of task objects with the same fields for bulk creation. Use this when creating multiple related tasks at once.

All tasks are created with status \`pending\`.

Expand All @@ -445,20 +448,52 @@ All tasks are created with status \`pending\`.
"Use TaskList to check for available work after completing a task.",
],
parameters: Type.Object({
subject: Type.String({ description: "A brief title for the task" }),
description: Type.String({ description: "A detailed description of what needs to be done" }),
subject: Type.Optional(Type.String({ description: "A brief title for the task" })),
description: Type.Optional(Type.String({ description: "A detailed description of what needs to be done" })),
activeForm: Type.Optional(Type.String({ description: "Present continuous form shown in spinner when in_progress (e.g., 'Running tests')" })),
agentType: Type.Optional(Type.String({ description: "Agent type for subagent execution (e.g., 'general-purpose', 'Explore'). Tasks with agentType can be started via TaskExecute." })),
metadata: Type.Optional(Type.Record(Type.String(), Type.Any(), { description: "Arbitrary metadata to attach to the task" })),
tasks: Type.Optional(Type.Array(Type.Object({
subject: Type.String({ description: "A brief title for the task" }),
description: Type.String({ description: "A detailed description of what needs to be done" }),
activeForm: Type.Optional(Type.String({ description: "Present continuous form shown in spinner when in_progress (e.g., 'Running tests')" })),
agentType: Type.Optional(Type.String({ description: "Agent type for subagent execution (e.g., 'general-purpose', 'Explore'). Tasks with agentType can be started via TaskExecute." })),
metadata: Type.Optional(Type.Record(Type.String(), Type.Any(), { description: "Arbitrary metadata to attach to the task" })),
}), { description: "Create multiple tasks in one call. Each task accepts subject, description, activeForm, agentType, and metadata." })),
}),

execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
autoClear.resetBatchCountdown();
const meta = params.metadata ?? {};
if (params.agentType) meta.agentType = params.agentType;
const task = store.create(params.subject, params.description, params.activeForm, Object.keys(meta).length > 0 ? meta : undefined);
const inputTasks = params.tasks ?? (
params.subject && params.description
? [{
subject: params.subject,
description: params.description,
activeForm: params.activeForm,
agentType: params.agentType,
metadata: params.metadata,
}]
: []
);

if (inputTasks.length === 0) {
throw new Error("TaskCreate requires either subject + description, or a non-empty tasks array");
}

const created = inputTasks.map((input) => {
const meta = { ...(input.metadata ?? {}) };
if (input.agentType) meta.agentType = input.agentType;
return store.create(input.subject, input.description, input.activeForm, Object.keys(meta).length > 0 ? meta : undefined);
});

widget.update();
return Promise.resolve(textResult(`Task #${task.id} created successfully: ${task.subject}`));
if (created.length === 1) {
const task = created[0];
return Promise.resolve(textResult(`Task #${task.id} created successfully: ${task.subject}`));
}
return Promise.resolve(textResult(
`Created ${created.length} tasks successfully:\n${created.map(task => `Task #${task.id}: ${task.subject}`).join("\n")}`,
));
},
});

Expand Down
26 changes: 26 additions & 0 deletions test/subagent-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,32 @@ describe("Standalone operation (no subagents extension)", () => {
expect(result.content[0].text).toContain("Write tests");
});

it("TaskCreate can create multiple tasks in one call", async () => {
const result = await mock.executeTool("TaskCreate", {
tasks: [
{ subject: "Design API", description: "Define endpoints", agentType: "Explore" },
{ subject: "Implement API", description: "Build endpoints", activeForm: "Building endpoints" },
],
});

expect(result.content[0].text).toContain("Created 2 tasks successfully");
expect(result.content[0].text).toContain("Task #1: Design API");
expect(result.content[0].text).toContain("Task #2: Implement API");

const first = await mock.executeTool("TaskGet", { taskId: "1" });
expect(first.content[0].text).toContain("Metadata: {\"agentType\":\"Explore\"}");

const list = await mock.executeTool("TaskList", {});
expect(list.content[0].text).toContain("#1 [pending] Design API");
expect(list.content[0].text).toContain("#2 [pending] Implement API");
});

it("TaskCreate rejects calls without single-task fields or bulk tasks", async () => {
await expect(mock.executeTool("TaskCreate", {})).rejects.toThrow(
"TaskCreate requires either subject + description, or a non-empty tasks array",
);
});

it("TaskList works without subagents", async () => {
await mock.executeTool("TaskCreate", { subject: "A", description: "desc" });
await mock.executeTool("TaskCreate", { subject: "B", description: "desc" });
Expand Down
Loading