refactor: Drop ThreadWithTasks (#142)

* refactor: Drop ThreadWithTasks

* cr
This commit is contained in:
Brace Sproul 2025-06-12 13:19:18 -07:00 • committed by GitHub
parent 0db1a02d13
commit 76d412275a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 387 additions and 137 deletions

View file

@ -19,7 +19,7 @@ import {
getCompletedPlanItems,
getCurrentPlanItem,
} from "../utils/current-task.js";
import { getActivePlanItems } from "../utils/task-plan.js";
import { getActivePlanItems } from "@open-swe/shared/open-swe/tasks";
const logger = createLogger(LogLevel.INFO, "DiagnoseError");

View file

@ -13,7 +13,7 @@ import {
completeTask,
getActivePlanItems,
getActiveTask,
} from "../utils/task-plan.js";
} from "@open-swe/shared/open-swe/tasks";
const logger = createLogger(LogLevel.INFO, "GenerateConclusionNode");

View file

@ -16,7 +16,7 @@ import { stopSandbox } from "../utils/sandbox.js";
import { createLogger, LogLevel } from "../utils/logger.js";
import { getCurrentPlanItem } from "../utils/current-task.js";
import { getMessageContentString } from "@open-swe/shared/messages";
import { getActivePlanItems } from "../utils/task-plan.js";
import { getActivePlanItems } from "@open-swe/shared/open-swe/tasks";
import { SANDBOX_ROOT_DIR } from "@open-swe/shared/constants";
const logger = createLogger(LogLevel.INFO, "GenerateMessageNode");

View file

@ -6,7 +6,7 @@ import {
HumanResponse,
} from "@langchain/langgraph/prebuilt";
import { startSandbox } from "../utils/sandbox.js";
import { createNewTask } from "../utils/task-plan.js";
import { createNewTask } from "@open-swe/shared/open-swe/tasks";
import { getUserRequest } from "../utils/user-request.js";
import {
PLAN_INTERRUPT_ACTION_TITLE,

View file

@ -19,7 +19,7 @@ import { getUserRequest } from "../utils/user-request.js";
import { ToolMessage } from "@langchain/core/messages";
import { daytonaClient, deleteSandbox } from "../utils/sandbox.js";
import { getGitHubTokensFromConfig } from "../utils/github-tokens.js";
import { getActivePlanItems } from "../utils/task-plan.js";
import { getActivePlanItems } from "@open-swe/shared/open-swe/tasks";
const logger = createLogger(LogLevel.INFO, "Open PR");

View file

@ -16,7 +16,7 @@ import {
completePlanItem,
getActivePlanItems,
getActiveTask,
} from "../utils/task-plan.js";
} from "@open-swe/shared/open-swe/tasks";
import {
getCurrentPlanItem,
getRemainingPlanItems,

View file

@ -19,7 +19,7 @@ import {
completePlanItem,
getActivePlanItems,
getActiveTask,
} from "../utils/task-plan.js";
} from "@open-swe/shared/open-swe/tasks";
import { getCompletedPlanItems } from "../utils/current-task.js";
const taskSummarySysPrompt = `You are operating as a terminal-based agentic coding assistant built by LangChain. It wraps LLM models to enable natural language interaction with a local codebase. You are expected to be precise, safe, and helpful.

View file

@ -6,7 +6,10 @@ import {
} from "@open-swe/shared/open-swe/types";
import { loadModel, Task } from "../utils/load-model.js";
import { z } from "zod";
import { getActiveTask, updateTaskPlanItems } from "../utils/task-plan.js";
import {
getActiveTask,
updateTaskPlanItems,
} from "@open-swe/shared/open-swe/tasks";
import {
BaseMessage,
isAIMessage,

View file

@ -6,10 +6,12 @@ import {
PanelRightOpen,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { useThreads, ThreadWithTasks } from "@/providers/Thread";
import { useThreads } from "@/providers/Thread";
import { useQueryState, parseAsString } from "nuqs";
import { useState, useCallback } from "react";
import { ThreadItem } from "./thread-item";
import { Thread } from "@langchain/langgraph-sdk";
import { GraphState } from "@open-swe/shared/open-swe/types";
const THREADS_PER_PAGE = 10;
@ -18,13 +20,12 @@ interface TaskListSidebarProps {
}
export default function TaskListSidebar({ onCollapse }: TaskListSidebarProps) {
const [taskId, setTaskId] = useQueryState("taskId", parseAsString);
const [threadId, setThreadId] = useQueryState("threadId", parseAsString);
const [currentPage, setCurrentPage] = useState(0);
const { threads, threadsLoading, handleThreadClick } = useThreads();
const onThreadClick = useCallback(
(thread: ThreadWithTasks) => {
(thread: Thread<GraphState>) => {
handleThreadClick(thread, threadId, setThreadId);
},
[handleThreadClick, threadId, setThreadId],

View file

@ -1,10 +1,12 @@
"use client";
import { Archive, ChevronLeft, ChevronRight } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useThreads, ThreadWithTasks } from "@/providers/Thread";
import { useThreads } from "@/providers/Thread";
import { useQueryState, parseAsString } from "nuqs";
import { useState, useCallback } from "react";
import { ThreadItem } from "./thread-item";
import { Thread } from "@langchain/langgraph-sdk";
import { GraphState } from "@open-swe/shared/open-swe/types";
const THREADS_PER_PAGE = 5;
@ -17,7 +19,7 @@ export default function TaskList() {
const isDashboardMode = !taskId;
const onThreadClick = useCallback(
(thread: ThreadWithTasks) => {
(thread: Thread<GraphState>) => {
handleThreadClick(thread, threadId, setThreadId);
setTaskId(null);
},

View file

@ -1,21 +1,19 @@
"use client";
import { memo } from "react";
import {
formatDistanceToNow,
differenceInHours,
differenceInMinutes,
format,
} from "date-fns";
import { differenceInHours, differenceInMinutes, format } from "date-fns";
import { GitBranch, ArrowRight, ListTodo } from "lucide-react";
import { ThreadWithTasks, useThreads } from "@/providers/Thread";
import { useThreads } from "@/providers/Thread";
import { cn } from "@/lib/utils";
import { StatusIndicator } from "@/components/status-indicator";
import { GitHubSVG } from "./icons/github";
import { useQueryState } from "nuqs";
import { Thread } from "@langchain/langgraph-sdk";
import { GraphState } from "@open-swe/shared/open-swe/types";
import { getThreadTasks, getThreadTitle } from "@/lib/thread";
interface ThreadItemProps {
thread: ThreadWithTasks;
onClick: (thread: ThreadWithTasks) => void;
thread: Thread<GraphState>;
onClick: (thread: Thread<GraphState>) => void;
variant?: "sidebar" | "dashboard";
className?: string;
}
@ -55,6 +53,8 @@ export const ThreadItem = memo(function ThreadItem({
const displayDate = formatRelativeDate(thread.created_at);
const { totalTasks, completedTasks } = getThreadTasks(thread);
return (
<div
className={cn(
@ -74,7 +74,7 @@ export const ThreadItem = memo(function ThreadItem({
<div className="flex w-full items-center gap-1.5">
<StatusIndicator status={thread.status} />
<h4 className="w-full truncate text-xs leading-tight font-medium text-gray-900">
{thread.threadTitle}
{getThreadTitle(thread)}
</h4>
</div>
@ -85,10 +85,14 @@ export const ThreadItem = memo(function ThreadItem({
height="16"
className="flex-shrink-0"
/>
<span className="max-w-[90px] truncate">{thread.repository}</span>
<span className="max-w-[90px] truncate">
{thread.values.targetRepository.repo}
</span>
<span>/</span>
<GitBranch className="size-2.5 flex-shrink-0" />
<span className="max-w-[70px] truncate">{thread.branch}</span>
<span className="max-w-[70px] truncate">
{thread.values.targetRepository.branch}
</span>
</div>
<span>•</span>
@ -101,7 +105,7 @@ export const ThreadItem = memo(function ThreadItem({
<div className="ml-1 flex items-center gap-1">
<ListTodo className="size-4 flex-shrink-0" />
<span>
{thread.completedTasksCount}/{thread.totalTasksCount} tasks
{completedTasks}/{totalTasks} tasks
</span>
</div>
</>
@ -111,7 +115,7 @@ export const ThreadItem = memo(function ThreadItem({
<div className="flex items-center gap-1 text-xs text-gray-500">
<ListTodo className="size-4 flex-shrink-0" />
<span>
{thread.completedTasksCount}/{thread.totalTasksCount} tasks
{completedTasks}/{totalTasks} tasks
</span>
</div>
)}

View file

@ -1,12 +1,13 @@
import { useEffect, useRef } from "react";
import { ThreadPoller, PollConfig } from "@/lib/polling/thread-poller";
import { ThreadWithTasks } from "@/providers/Thread";
import { GraphState } from "@open-swe/shared/open-swe/types";
import { Thread } from "@langchain/langgraph-sdk";
interface UseThreadPollingProps {
threads: ThreadWithTasks[];
getThread: (threadId: string) => Promise<ThreadWithTasks | null>;
threads: Thread<GraphState>[];
getThread: (threadId: string) => Promise<Thread<GraphState> | null>;
onUpdate: (
updatedThreads: ThreadWithTasks[],
updatedThreads: Thread<GraphState>[],
changedThreadIds: string[],
) => void;

View file

@ -1,9 +1,11 @@
import { ThreadWithTasks } from "@/providers/Thread";
import { Thread } from "@langchain/langgraph-sdk";
import { GraphState } from "@open-swe/shared/open-swe/types";
import { getThreadTasks, getThreadTitle } from "../thread";
export interface PollConfig {
interval: number;
onUpdate: (
updatedThreads: ThreadWithTasks[],
updatedThreads: Thread<GraphState>[],
changedThreadIds: string[],
) => void;
}
@ -12,13 +14,13 @@ export class ThreadPoller {
private config: PollConfig;
private isPolling: boolean = false;
private intervalId: NodeJS.Timeout | null = null;
private threads: ThreadWithTasks[];
private getThreadFn: (threadId: string) => Promise<ThreadWithTasks | null>;
private threads: Thread<GraphState>[];
private getThreadFn: (threadId: string) => Promise<Thread<GraphState> | null>;
constructor(
config: PollConfig,
threads: ThreadWithTasks[],
getThreadFn: (threadId: string) => Promise<ThreadWithTasks | null>,
threads: Thread<GraphState>[],
getThreadFn: (threadId: string) => Promise<Thread<GraphState> | null>,
) {
this.config = config;
this.threads = threads;
@ -49,7 +51,7 @@ export class ThreadPoller {
const currentThreads = this.threads;
const threadsToPool = currentThreads.slice(0, 10);
const updatedThreads: ThreadWithTasks[] = [];
const updatedThreads: Thread<GraphState>[] = [];
const changedThreadIds: string[] = [];
const errors: string[] = [];
@ -79,19 +81,24 @@ export class ThreadPoller {
}
private hasThreadChanged(
current: ThreadWithTasks,
updated: ThreadWithTasks,
current: Thread<GraphState>,
updated: Thread<GraphState>,
): boolean {
const currentTaskCounts = getThreadTasks(current);
const updatedTaskCounts = getThreadTasks(updated);
const currentTargetRepo = current.values?.targetRepository;
const updatedTargetRepo = updated.values?.targetRepository;
return (
current.completedTasksCount !== updated.completedTasksCount ||
current.totalTasksCount !== updated.totalTasksCount ||
currentTaskCounts.completedTasks !== updatedTaskCounts.completedTasks ||
currentTaskCounts.totalTasks !== updatedTaskCounts.totalTasks ||
current.status !== updated.status ||
current.threadTitle !== updated.threadTitle ||
current.repository !== updated.repository ||
current.branch !== updated.branch ||
JSON.stringify(current.tasks) !== JSON.stringify(updated.tasks) ||
JSON.stringify(current.proposedPlan) !==
JSON.stringify(updated.proposedPlan)
getThreadTitle(current) !== getThreadTitle(updated) ||
currentTargetRepo.repo !== updatedTargetRepo.repo ||
currentTargetRepo.branch !== updatedTargetRepo.branch ||
JSON.stringify(current.values?.plan) !==
JSON.stringify(updated.values?.plan) ||
JSON.stringify(current.values?.proposedPlan) !==
JSON.stringify(updated.values?.proposedPlan)
);
}
}

View file

@ -0,0 +1,32 @@
import { Thread } from "@langchain/langgraph-sdk";
import { getMessageContentString } from "@open-swe/shared/messages";
import { GraphState } from "@open-swe/shared/open-swe/types";
import { getActivePlanItems } from "@open-swe/shared/open-swe/tasks";
export function getThreadTitle(thread: Thread<GraphState>): string {
const messages = thread?.values.messages;
if (!messages?.length || !messages[0]?.content) {
return `Thread ${thread.thread_id.substring(0, 8)}`;
}
const threadTitle = getMessageContentString(messages[0].content);
return threadTitle;
}
export function getThreadTasks(thread: Thread<GraphState>): {
totalTasks: number;
completedTasks: number;
} {
if (!thread.values.plan) {
return {
totalTasks: 0,
completedTasks: 0,
};
}
const activePlanItems = getActivePlanItems(thread.values.plan);
const totalTasks = activePlanItems.length;
const completedTasks = activePlanItems.filter((p) => p.completed).length;
return {
totalTasks,
completedTasks,
};
}

View file

@ -12,31 +12,20 @@ import {
useTransition,
} from "react";
import { createClient } from "./client";
import { getMessageContentString } from "@open-swe/shared/messages";
import { TaskPlan, GraphState } from "@open-swe/shared/open-swe/types";
import { useThreadPolling } from "@/hooks/useThreadPolling";
export interface ThreadWithTasks extends Thread {
threadTitle: string;
repository: string;
branch: string;
completedTasksCount: number;
totalTasksCount: number;
tasks: TaskPlan | undefined;
proposedPlan: string[];
}
interface ThreadContextType {
threads: ThreadWithTasks[];
setThreads: Dispatch<SetStateAction<ThreadWithTasks[]>>;
threads: Thread<GraphState>[];
setThreads: Dispatch<SetStateAction<Thread<GraphState>[]>>;
threadsLoading: boolean;
setThreadsLoading: Dispatch<SetStateAction<boolean>>;
refreshThreads: () => Promise<void>;
getThread: (threadId: string) => Promise<ThreadWithTasks | null>;
getThread: (threadId: string) => Promise<Thread<GraphState> | null>;
isPending: boolean;
recentlyUpdatedThreads: Set<string>;
handleThreadClick: (
thread: ThreadWithTasks,
thread: Thread<GraphState>,
currentThreadId: string | null,
setThreadId: (id: string) => void,
) => void;
@ -115,7 +104,7 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
const assistantId: string | undefined =
process.env.NEXT_PUBLIC_ASSISTANT_ID ?? "";
const [threads, setThreads] = useState<ThreadWithTasks[]>([]);
const [threads, setThreads] = useState<Thread<GraphState>[]>([]);
const [threadsLoading, setThreadsLoading] = useState(false);
const [isPending, startTransition] = useTransition();
const [recentlyUpdatedThreads, setRecentlyUpdatedThreads] = useState<
@ -123,23 +112,13 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
>(new Set());
const getThread = useCallback(
async (threadId: string): Promise<ThreadWithTasks | null> => {
async (threadId: string): Promise<Thread<GraphState> | null> => {
if (!apiUrl || !assistantId) return null;
const client = createClient(apiUrl);
try {
const thread = await client.threads.get(threadId);
// Get the comprehensive state data which contains the plan
let stateData: { values: GraphState } | null = null;
try {
stateData = await client.threads.getState(threadId);
} catch (stateError) {
console.error("Failed to get state data:", stateError);
}
return enhanceThreadWithTasks(thread, stateData);
const thread = await client.threads.get<GraphState>(threadId);
return thread;
} catch (error) {
console.error("Failed to fetch thread:", threadId, error);
return null;
@ -148,42 +127,6 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
[apiUrl, assistantId],
);
const enhanceThreadWithTasks = (
thread: Thread,
stateData?: { values: GraphState } | null,
): ThreadWithTasks => {
const stateValues = stateData?.values;
const threadValues = thread.values as GraphState;
const plan: TaskPlan | undefined = stateValues?.plan || threadValues?.plan;
const proposedPlan: string[] =
stateValues?.proposedPlan || threadValues?.proposedPlan || [];
const targetRepository =
stateValues?.targetRepository || threadValues?.targetRepository;
const messages = stateValues?.messages || threadValues?.messages;
const firstMessageContent = messages?.[0]?.content;
const threadTitle = firstMessageContent
? getMessageContentString(firstMessageContent)
: `Thread ${thread.thread_id.substring(0, 8)}`;
const { totalTasksCount, completedTasksCount } = getTaskCounts(
plan,
proposedPlan,
);
return {
...thread,
threadTitle,
repository: targetRepository?.repo || "Unknown Repository",
branch: targetRepository?.branch || "main",
completedTasksCount,
totalTasksCount,
tasks: plan,
proposedPlan,
};
};
const refreshThreads = useCallback(async (): Promise<void> => {
if (!apiUrl || !assistantId) return;
@ -196,42 +139,25 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
metadata: getThreadSearchMetadata(assistantId),
};
let threadsResponse = await client.threads.search(searchParams);
let threadsResponse =
await client.threads.search<GraphState>(searchParams);
if (threadsResponse.length === 0) {
const altMetadata = assistantId.includes("-")
? { assistant_id: assistantId }
: { graph_id: assistantId };
threadsResponse = await client.threads.search({
threadsResponse = await client.threads.search<GraphState>({
limit: 100,
metadata: altMetadata,
});
}
const enhancedThreads: ThreadWithTasks[] = [];
for (const thread of threadsResponse) {
try {
const fullThread = await client.threads.get(thread.thread_id);
let stateData: { values: GraphState } | null = null;
try {
stateData = await client.threads.getState(thread.thread_id);
} catch (stateError) {
console.error("Failed to get state data:", stateError);
}
enhancedThreads.push(enhanceThreadWithTasks(fullThread, stateData));
} catch (error) {
console.error(`Failed to enhance thread ${thread.thread_id}:`, error);
}
}
enhancedThreads.sort(
threadsResponse.sort(
(a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
);
setThreads(enhancedThreads);
setThreads(threadsResponse);
} catch (error) {
console.error("Failed to fetch threads:", error);
} finally {
@ -244,7 +170,7 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
}, [refreshThreads]);
const handlePollingUpdate = useCallback(
(updatedThreads: ThreadWithTasks[], changedThreadIds: string[]) => {
(updatedThreads: Thread<GraphState>[], changedThreadIds: string[]) => {
setThreads((currentThreads) => {
const updatedMap = new Map(updatedThreads.map((t) => [t.thread_id, t]));
return currentThreads.map(
@ -271,7 +197,7 @@ export function ThreadProvider({ children }: { children: ReactNode }) {
const handleThreadClick = useCallback(
(
thread: ThreadWithTasks,
thread: Thread<GraphState>,
currentThreadId: string | null,
setThreadId: (id: string) => void,
) => {

View file

@ -0,0 +1,274 @@
import { v4 as uuidv4 } from "uuid";
import { PlanItem, Task, TaskPlan, PlanRevision } from "./types.js";
/**
* Creates a new task with the provided plan items.
* Can either add to an existing TaskPlan or create a brand new one.
*
* @param request The original user request text that initiated this task
* @param planItems The plan items to include in the new task
* @param existingTaskPlan Optional existing TaskPlan to add the new task to
* @param parentTaskId Optional ID of a parent task if this task is derived from another
* @returns The updated TaskPlan with the new task added
*/
export function createNewTask(
request: string,
planItems: PlanItem[],
existingTaskPlan?: TaskPlan,
parentTaskId?: string,
): TaskPlan {
// Create the initial plan revision
const initialRevision: PlanRevision = {
revisionIndex: 0,
plans: planItems,
createdAt: Date.now(),
createdBy: "agent",
};
// Create the new task
const newTask: Task = {
id: uuidv4(),
taskIndex: existingTaskPlan ? existingTaskPlan.tasks.length : 0,
request,
createdAt: Date.now(),
completed: false,
planRevisions: [initialRevision],
activeRevisionIndex: 0,
parentTaskId,
};
// If there's an existing task plan, add the new task to it
if (existingTaskPlan) {
return {
tasks: [...existingTaskPlan.tasks, newTask],
activeTaskIndex: existingTaskPlan.tasks.length, // Set the new task as active
};
}
// Otherwise create a new task plan with just this task
return {
tasks: [newTask],
activeTaskIndex: 0,
};
}
/**
* Updates the plan items for an existing task by creating a new revision.
*
* @param taskPlan The current task plan
* @param taskId The ID of the task to update
* @param planItems The new plan items
* @param createdBy Who created this revision ('agent' or 'user')
* @returns The updated TaskPlan with the new revision
* @throws Error if the task ID doesn't exist
*/
export function updateTaskPlanItems(
taskPlan: TaskPlan,
taskId: string,
planItems: PlanItem[],
createdBy: "agent" | "user" = "agent",
): TaskPlan {
// Find the task to update
const taskIndex = taskPlan.tasks.findIndex((task) => task.id === taskId);
if (taskIndex === -1) {
throw new Error(`Task with ID ${taskId} not found`);
}
const task = taskPlan.tasks[taskIndex];
// Create a new revision with the updated plan items
const newRevision: PlanRevision = {
revisionIndex: task.planRevisions.length,
plans: planItems,
createdAt: Date.now(),
createdBy,
};
// Create an updated task with the new revision
const updatedTask: Task = {
...task,
planRevisions: [...task.planRevisions, newRevision],
activeRevisionIndex: task.planRevisions.length, // Set the new revision as active
};
// Create a new array of tasks with the updated task
const updatedTasks = [...taskPlan.tasks];
updatedTasks[taskIndex] = updatedTask;
// Return the updated task plan
return {
...taskPlan,
tasks: updatedTasks,
};
}
/**
* Helper function to get the active task from a TaskPlan
*
* @param taskPlan The task plan
* @returns The currently active task
* @throws Error if there are no tasks
*/
export function getActiveTask(taskPlan: TaskPlan): Task {
if (taskPlan.tasks.length === 0) {
throw new Error("No tasks available");
}
return taskPlan.tasks[taskPlan.activeTaskIndex];
}
/**
* Helper function to get the active plan items for the active task
*
* @param taskPlan The task plan
* @returns The currently active plan items
* @throws Error if there are no tasks or no plan revisions
*/
export function getActivePlanItems(taskPlan: TaskPlan): PlanItem[] {
const activeTask = getActiveTask(taskPlan);
if (activeTask.planRevisions.length === 0) {
throw new Error("No plan revisions available for the active task");
}
return activeTask.planRevisions[activeTask.activeRevisionIndex].plans;
}
/**
* Marks a specific plan item as completed and adds a summary.
* This operation modifies the current active revision directly and does NOT create a new revision.
*
* @param taskPlan The current task plan
* @param taskId The ID of the task containing the plan item
* @param planItemIndex The `index` property of the plan item to mark as completed
* @param summary Optional summary of the completed plan item. If undefined, existing summary is preserved.
* @returns The updated TaskPlan
* @throws Error if the task or plan item is not found, or if no active revision exists.
*/
export function completePlanItem(
taskPlan: TaskPlan,
taskId: string,
planItemIndex: number,
summary?: string,
): TaskPlan {
const taskIndexInPlan = taskPlan.tasks.findIndex(
(task) => task.id === taskId,
);
if (taskIndexInPlan === -1) {
throw new Error(`Task with ID ${taskId} not found in task plan`);
}
const originalTask = taskPlan.tasks[taskIndexInPlan];
const activeRevisionIndex = originalTask.activeRevisionIndex;
// Ensure there's a planRevisions array and the activeRevisionIndex is valid
if (
!originalTask.planRevisions ||
activeRevisionIndex < 0 ||
activeRevisionIndex >= originalTask.planRevisions.length
) {
throw new Error(
`Invalid active revision index (${activeRevisionIndex}) for task ${taskId}`,
);
}
const originalActiveRevision =
originalTask.planRevisions[activeRevisionIndex];
// This check should be covered by the index check, but reinforces intent
if (!originalActiveRevision) {
// This case implies an issue with activeRevisionIndex or planRevisions structure
throw new Error(
`Active revision (index ${activeRevisionIndex}) not found for task ${taskId}`,
);
}
const planItemToUpdateActualIndexInPlansArray =
originalActiveRevision.plans.findIndex(
(item) => item.index === planItemIndex,
);
if (planItemToUpdateActualIndexInPlansArray === -1) {
throw new Error(
`Plan item with .index ${planItemIndex} not found in active revision (index ${activeRevisionIndex}) of task ${taskId}`,
);
}
// Create a new 'plans' array with the specific item updated
const updatedPlansForRevision = originalActiveRevision.plans.map((item) => {
if (item.index === planItemIndex) {
const newSummary = summary !== undefined ? summary : item.summary;
return { ...item, completed: true, summary: newSummary };
}
return item;
});
// Create a new 'PlanRevision' object for the active revision, with the updated 'plans'
const updatedActiveRevision: PlanRevision = {
...originalActiveRevision,
plans: updatedPlansForRevision,
};
// Create a new 'planRevisions' array, replacing the active revision with the updated one
const updatedPlanRevisions = [...originalTask.planRevisions];
updatedPlanRevisions[activeRevisionIndex] = updatedActiveRevision;
// Create a new 'Task' object with the updated 'planRevisions'
const updatedTask: Task = {
...originalTask,
planRevisions: updatedPlanRevisions,
};
// Create a new 'tasks' array for the TaskPlan, replacing the updated task
const updatedTasksArray = [...taskPlan.tasks];
updatedTasksArray[taskIndexInPlan] = updatedTask;
// Return the new TaskPlan object
return {
...taskPlan,
tasks: updatedTasksArray,
};
}
/**
* Marks a task as completed
*
* @param taskPlan The current task plan
* @param taskId The ID of the task to mark as completed
* @param summary Optional summary of the completed task
* @returns The updated TaskPlan
*/
export function completeTask(
taskPlan: TaskPlan,
taskId: string,
summary?: string,
): TaskPlan {
const taskIndex = taskPlan.tasks.findIndex((task) => task.id === taskId);
if (taskIndex === -1) {
throw new Error(`Task with ID ${taskId} not found`);
}
const task = taskPlan.tasks[taskIndex];
// Create an updated task marked as completed
const updatedTask: Task = {
...task,
completed: true,
completedAt: Date.now(),
summary,
};
// Create a new array of tasks with the updated task
const updatedTasks = [...taskPlan.tasks];
updatedTasks[taskIndex] = updatedTask;
// Return the updated task plan
return {
...taskPlan,
tasks: updatedTasks,
};
}