Skip to content

feat: task lists #6906

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
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
7 changes: 7 additions & 0 deletions core/config/profile/doLoadConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { TeamAnalytics } from "../../control-plane/TeamAnalytics.js";
import ContinueProxy from "../../llm/llms/stubs/ContinueProxy";
import { getConfigDependentToolDefinitions } from "../../tools";
import { encodeMCPToolUri } from "../../tools/callTool";
import { taskListTool } from "../../tools/definitions/taskListTool";
import { getMCPToolName } from "../../tools/mcpToolName";
import { GlobalContext } from "../../util/GlobalContext";
import { getConfigJsonPath, getConfigYamlPath } from "../../util/paths";
Expand Down Expand Up @@ -261,6 +262,12 @@ export default async function doLoadConfig(options: {
}),
);

if (!newConfig.experimental?.enableTaskLists) {
newConfig.tools = newConfig.tools.filter(
(tool) => tool.function.name !== taskListTool.function.name,
);
}

// Detect duplicate tool names
const counts: Record<string, number> = {};
newConfig.tools.forEach((tool) => {
Expand Down
5 changes: 5 additions & 0 deletions core/config/sharedConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const sharedConfigSchema = z
onlyUseSystemMessageTools: z.boolean(),
codebaseToolCallingOnly: z.boolean(),
enableStaticContextualization: z.boolean(),
enableTaskLists: z.boolean(),

// `ui` in `ContinueConfig`
showSessionTabs: z.boolean(),
Expand Down Expand Up @@ -196,5 +197,9 @@ export function modifyAnyConfigWithSharedConfig<
sharedConfig.enableStaticContextualization;
}

if (sharedConfig.enableTaskLists !== undefined) {
configCopy.experimental.enableTaskLists = sharedConfig.enableTaskLists;
}

return configCopy;
}
122 changes: 122 additions & 0 deletions core/context/taskList/TaskManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import fs from "fs";
import { v4 as uuidv4 } from "uuid";
import { TaskInfo } from "../..";
import type { FromCoreProtocol, ToCoreProtocol } from "../../protocol";
import type { IMessenger } from "../../protocol/messenger";
import { getTaskListsFilePath } from "../../util/paths";

export enum TaskStatus {
Pending = "pending",
Completed = "completed",
}

export interface TaskEvent {
type: "add" | "update" | "remove";
tasks: TaskInfo[];
}

export class TaskManager {
private taskMap = new Map<TaskInfo["task_id"], TaskInfo>();
private taskListsFilePath: string;

constructor(
private messenger: IMessenger<ToCoreProtocol, FromCoreProtocol>,
sessionId: string,
) {
this.taskListsFilePath = getTaskListsFilePath(sessionId);
if (fs.existsSync(this.taskListsFilePath)) {
this.taskMap = new Map(
Object.entries(
JSON.parse(fs.readFileSync(this.taskListsFilePath, "utf8")),
),
);
}
}

async save() {
void fs.writeFileSync(
this.taskListsFilePath,
JSON.stringify(Object.fromEntries(this.taskMap), null, 2),
);
}

private emitEvent(eventType: TaskEvent["type"]): void {
void this.save();
this.messenger.send("taskEvent", {
type: eventType,
tasks: this.list(),
});
}

add(name: string, description: string) {
const taskId = uuidv4();
const task: TaskInfo = {
task_id: taskId,
name,
description,
status: TaskStatus.Pending,
metadata: {
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
};
this.taskMap.set(taskId, task);

this.emitEvent("add");

return taskId;
}

update(taskId: TaskInfo["task_id"], name: string, description: string) {
const previousTask = this.taskMap.get(taskId);
if (!previousTask) {
throw new Error(`Task with id "${taskId}" not found`);
}

const updatedTask: TaskInfo = {
...previousTask,
name,
description,
metadata: {
...previousTask.metadata,
updatedAt: new Date().toISOString(),
},
};

this.taskMap.set(taskId, updatedTask);

this.emitEvent("update");
}

remove(taskId: TaskInfo["task_id"]) {
const task = this.taskMap.get(taskId);
if (!task) {
return;
}

this.taskMap.delete(taskId);

this.emitEvent("remove");
}

list() {
return Array.from(this.taskMap.values());
}

setTaskStatus(taskId: TaskInfo["task_id"], status: TaskStatus) {
if (!this.taskMap.has(taskId)) {
throw new Error(`Task with id "${taskId}" not found`);
}
this.taskMap.set(taskId, {
...this.taskMap.get(taskId)!,
status,
});
}

getTaskById(taskId: TaskInfo["task_id"]) {
if (!this.taskMap.has(taskId)) {
throw new Error(`Task with id "${taskId}" not found`);
}
return this.taskMap.get(taskId)!;
}
}
16 changes: 16 additions & 0 deletions core/context/taskList/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { FromCoreProtocol, ToCoreProtocol } from "../../protocol";
import type { IMessenger } from "../../protocol/messenger";
import { TaskManager } from "./TaskManager";

export async function getSessionTaskManager(
messenger: IMessenger<ToCoreProtocol, FromCoreProtocol>,
) {
const sessionId = await messenger.request("getCurrentSessionId", undefined);
return new TaskManager(messenger, sessionId);
}

export async function fetchTaskList(
messenger: IMessenger<ToCoreProtocol, FromCoreProtocol>,
) {
return (await getSessionTaskManager(messenger)).list();
}
6 changes: 6 additions & 0 deletions core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import {
} from "./config/onboarding";
import { createNewWorkspaceBlockFile } from "./config/workspace/workspaceBlocks";
import { MCPManagerSingleton } from "./context/mcp/MCPManagerSingleton";
import { fetchTaskList } from "./context/taskList";
import { setMdmLicenseKey } from "./control-plane/mdm/mdm";
import { ApplyAbortManager } from "./edit/applyAbortManager";
import { streamDiffLines } from "./edit/streamDiffLines";
Expand Down Expand Up @@ -920,6 +921,10 @@ export class Core {
const isValid = setMdmLicenseKey(licenseKey);
return isValid;
});

on("taskList/list", ({ data }) => {
return fetchTaskList(this.messenger);
});
}

private async handleToolCall(toolCall: ToolCall) {
Expand Down Expand Up @@ -958,6 +963,7 @@ export class Core {
toolCallId: toolCall.id,
onPartialOutput,
codeBaseIndexer: this.codeBaseIndexer,
messenger: this.messenger,
});

return result;
Expand Down
23 changes: 23 additions & 0 deletions core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import {
PromptTemplates,
} from "@continuedev/config-yaml";
import Parser from "web-tree-sitter";
import { TaskStatus } from "./context/taskList/TaskManager";
import { CodebaseIndexer } from "./indexing/CodebaseIndexer";
import { LLMConfigurationStatuses } from "./llm/constants";
import type { FromCoreProtocol, ToCoreProtocol } from "./protocol";
import type { IMessenger } from "./protocol/messenger";

declare global {
interface Window {
Expand Down Expand Up @@ -1066,6 +1069,7 @@ export interface ToolExtras {
contextItems: ContextItem[];
}) => void;
config: ContinueConfig;
messenger?: IMessenger<ToCoreProtocol, FromCoreProtocol>;
codeBaseIndexer?: CodebaseIndexer;
}

Expand Down Expand Up @@ -1556,6 +1560,11 @@ export interface ExperimentalConfig {
* gather context for the model where necessary.
*/
enableStaticContextualization?: boolean;

/**
* If enabled, task lists will be available.
*/
enableTaskLists?: boolean;
}

export interface AnalyticsConfig {
Expand Down Expand Up @@ -1800,6 +1809,20 @@ export interface MessageOption {
precompiled: boolean;
}

// Task Manager

type TaskMetadata = {
createdAt: string;
updatedAt: string;
};

export type TaskInfo = {
task_id: string;
name: string;
description: string;
status: TaskStatus;
metadata: TaskMetadata;
};
/* LSP-specific interfaces. */

// See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#symbolKind.
Expand Down
2 changes: 2 additions & 0 deletions core/protocol/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
SiteIndexingConfig,
SlashCommandDescWithSource,
StreamDiffLinesPayload,
TaskInfo,
ToolCall,
} from "../";
import { AutocompleteCodeSnippet } from "../autocomplete/snippets/types";
Expand Down Expand Up @@ -265,4 +266,5 @@ export type ToCoreFromIdeOrWebviewProtocol = {
"process/markAsBackgrounded": [{ toolCallId: string }, void];
"process/isBackgrounded": [{ toolCallId: string }, boolean];
"mdm/setLicenseKey": [{ licenseKey: string }, boolean];
"taskList/list": [undefined, TaskInfo[]];
};
3 changes: 3 additions & 0 deletions core/protocol/passThrough.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ export const WEBVIEW_TO_CORE_PASS_THROUGH: (keyof ToCoreFromWebviewProtocol)[] =
"isItemTooBig",
"process/markAsBackgrounded",
"process/isBackgrounded",
"controlPlane/getFreeTrialStatus",
"taskList/list",
];

// Message types to pass through from core to webview
Expand All @@ -93,4 +95,5 @@ export const CORE_TO_WEBVIEW_PASS_THROUGH: (keyof ToWebviewFromCoreProtocol)[] =
"didCloseFiles",
"toolCallPartialOutput",
"freeTrialExceeded",
"taskEvent",
];
2 changes: 2 additions & 0 deletions core/protocol/webview.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ConfigResult } from "@continuedev/config-yaml";
import { SerializedOrgWithProfiles } from "../config/ProfileLifecycleManager.js";
import { TaskEvent } from "../context/taskList/TaskManager.js";
import { ControlPlaneSessionInfo } from "../control-plane/AuthTypes.js";
import type {
BrowserSerializedContinueConfig,
Expand Down Expand Up @@ -44,4 +45,5 @@ export type ToWebviewFromIdeOrCoreProtocol = {
sessionUpdate: [{ sessionInfo: ControlPlaneSessionInfo | undefined }, void];
toolCallPartialOutput: [{ toolCallId: string; contextItems: any[] }, void];
freeTrialExceeded: [undefined, void];
taskEvent: [TaskEvent, void];
};
1 change: 1 addition & 0 deletions core/tools/builtIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export enum BuiltInToolNames {
RequestRule = "request_rule",
FetchUrlContent = "fetch_url_content",
CodebaseTool = "codebase",
TaskList = "task_list",

// excluded from allTools for now
ViewRepoMap = "view_repo_map",
Expand Down
3 changes: 3 additions & 0 deletions core/tools/callTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { readFileImpl } from "./implementations/readFile";
import { requestRuleImpl } from "./implementations/requestRule";
import { runTerminalCommandImpl } from "./implementations/runTerminalCommand";
import { searchWebImpl } from "./implementations/searchWeb";
import { taskListImpl } from "./implementations/taskList";
import { viewDiffImpl } from "./implementations/viewDiff";
import { viewRepoMapImpl } from "./implementations/viewRepoMap";
import { viewSubdirectoryImpl } from "./implementations/viewSubdirectory";
Expand Down Expand Up @@ -169,6 +170,8 @@ export async function callBuiltInTool(
return await requestRuleImpl(args, extras);
case BuiltInToolNames.CodebaseTool:
return await codebaseToolImpl(args, extras);
case BuiltInToolNames.TaskList:
return await taskListImpl(args, extras);
case BuiltInToolNames.ViewRepoMap:
return await viewRepoMapImpl(args, extras);
case BuiltInToolNames.ViewSubdirectory:
Expand Down
64 changes: 64 additions & 0 deletions core/tools/definitions/taskListTool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Tool } from "../..";
import { BUILT_IN_GROUP_NAME, BuiltInToolNames } from "../builtIn";

export const taskListTool: Tool = {
type: "function",
displayTitle: "Task List Manager",
wouldLikeTo: "manage tasks",
isCurrently: "managing tasks",
hasAlready: "managed tasks",
readonly: false,
isInstant: false,
group: BUILT_IN_GROUP_NAME,
function: {
name: BuiltInToolNames.TaskList,
description: `A task management tool for organizing and tracking work.
Helps break down complex workflows into manageable tasks that can be tracked systematically.

Operations:
- add: Create new tasks with names and descriptions
- list: View all tasks and their status
- update: Modify existing task details
- run_task: Begin working on a specific task

Best practices:
- Create meaningful units of work
- Use descriptive names
- Include detailed descriptions with requirements
- Execute tasks one at a time

Parameters:
- action: Operation to perform (add/list/update/run_task)
- name: Task name (for add/update)
- description: Task details and requirements (for add/update)
- task_id: Internal identifier (for update/run_task)

Task IDs and status tracking are managed automatically.`,
parameters: {
type: "object",
required: ["action"],
properties: {
action: {
type: "string",
enum: ["add", "list", "update", "run_task"],
description: "The specific action to perform on the task list.",
},
name: {
type: "string",
description:
"A short, descriptive name for the task. Required when adding or updating a task. Should clearly indicate the task purpose.",
},
description: {
type: "string",
description:
"A detailed description of the task including context, requirements, and acceptance criteria. Required when adding or updating a task.",
},
task_id: {
type: "string",
description:
"The unique identifier of the task to operate on. Required when updating or running a specific task. Should not be exposed to users.",
},
},
},
},
};
Loading
Loading