feat: Thread title proposed plan (#494)

* init thread title from proposed plan task

* add proposed plan thread title to threadCard component

* remove comments

* refactor

* abstract computeThreadTitle

* self CR, refactor

* self CR, refactor, rm unused import from un-needed memo

* Update apps/web/src/lib/thread.ts

Co-authored-by: Brace Sproul <braceasproul@gmail.com>

* CR: useThreadMetadata remove increased polling speed

* CR: revert inlineMD, rm wrapped <p> pass className to basicMD, import getActiveTask

* CR: revert getActiveTaskPlan to previous first task logic

---------

Co-authored-by: Brace Sproul <braceasproul@gmail.com>
This commit is contained in:
Dylan Boudro 2025-07-23 16:57:10 -04:00 • committed by GitHub
parent 6f31717637
commit fac83e1a55
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 28 additions and 15 deletions

View file

@ -327,9 +327,9 @@ export function TasksSidebar({
<div className="min-w-0 flex-1">
<>
<div className="mb-1 flex items-start justify-between gap-2">
<p className="text-sm leading-relaxed text-gray-900 dark:text-gray-100">
<BasicMarkdownText>{item.plan}</BasicMarkdownText>
</p>
<BasicMarkdownText className="text-sm leading-relaxed text-gray-900 dark:text-gray-100">
{item.plan}
</BasicMarkdownText>
</div>
<div className="flex items-center justify-between">

View file

@ -20,6 +20,7 @@ import { cn } from "@/lib/utils";
import { TaskPlan } from "@open-swe/shared/open-swe/types";
import { getActivePlanItems } from "@open-swe/shared/open-swe/tasks";
import { InlineMarkdownText } from "../thread/markdown-text";
import { computeThreadTitle } from "@/lib/thread";
interface ThreadCardProps {
thread: ThreadMetadata;
@ -36,6 +37,7 @@ export function ThreadCard({
}: ThreadCardProps) {
const router = useRouter();
const threadTitle = computeThreadTitle(taskPlan, thread.title);
const isStatusLoading = statusLoading && !status;
const displayStatus = status || ("idle" as ThreadUIStatus);
@ -48,7 +50,6 @@ export function ThreadCard({
const planItems = getActivePlanItems(taskPlan);
const sortedPlanItems = [...planItems].sort((a, b) => a.index - b.index);
// Find the current task (lowest index among uncompleted tasks)
const currentTaskIndex = sortedPlanItems
.filter((item) => !item.completed)
.reduce(
@ -58,8 +59,8 @@ export function ThreadCard({
const displayCurrentIndex =
currentTaskIndex === Number.POSITIVE_INFINITY
? sortedPlanItems.length // All tasks completed
: currentTaskIndex; // +1 for 1-based display
? sortedPlanItems.length
: currentTaskIndex;
return {
currentTaskIndex: displayCurrentIndex,
@ -137,7 +138,7 @@ export function ThreadCard({
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1">
<CardTitle className="text-foreground truncate text-sm">
<InlineMarkdownText>{thread.title}</InlineMarkdownText>
<InlineMarkdownText>{threadTitle}</InlineMarkdownText>
</CardTitle>
<div className="mt-1 flex items-center gap-1">
<GitBranch className="text-muted-foreground h-2 w-2" />

View file

@ -188,12 +188,10 @@ export function ThreadView({
}
}, [plannerStream.values, selectedTab]);
// Extract task plan from programmer stream
useEffect(() => {
if (programmerStream.values?.taskPlan) {
setProgrammerTaskPlan(programmerStream.values.taskPlan);
} else if (realTimeTaskPlan) {
// Fallback to real-time task plan if stream doesn't have it
setProgrammerTaskPlan(realTimeTaskPlan);
}
}, [programmerStream.values, realTimeTaskPlan]);

View file

@ -3,7 +3,7 @@ import { ManagerGraphState } from "@open-swe/shared/open-swe/manager/types";
import { ThreadMetadata } from "@/components/v2/types";
import { useThreadStatus } from "./useThreadStatus";
import { useMemo } from "react";
import { getThreadTitle } from "@/lib/thread";
import { getThreadTitle, computeThreadTitle } from "@/lib/thread";
import { calculateLastActivity } from "@/lib/thread-utils";
/**
@ -18,6 +18,7 @@ export function useThreadMetadata(thread: Thread<ManagerGraphState>): {
status,
isLoading: isStatusLoading,
error: statusError,
taskPlan: realTimeTaskPlan,
} = useThreadStatus(thread.thread_id);
const metadata: ThreadMetadata = useMemo((): ThreadMetadata => {
@ -25,14 +26,14 @@ export function useThreadMetadata(thread: Thread<ManagerGraphState>): {
return {
id: thread.thread_id,
title: getThreadTitle(thread),
title: computeThreadTitle(realTimeTaskPlan, getThreadTitle(thread)),
lastActivity: calculateLastActivity(thread.updated_at),
taskCount: values?.taskPlan?.tasks.length ?? 0,
taskCount: realTimeTaskPlan?.tasks.length ?? 0,
repository: values?.targetRepository
? `${values.targetRepository.owner}/${values.targetRepository.repo}`
: "",
branch: values?.targetRepository?.branch || "main",
taskPlan: values?.taskPlan,
taskPlan: realTimeTaskPlan,
status,
githubIssue: values?.githubIssueId
? {
@ -41,7 +42,7 @@ export function useThreadMetadata(thread: Thread<ManagerGraphState>): {
}
: undefined,
};
}, [thread, status]);
}, [thread, status, realTimeTaskPlan]);
return {
metadata,

View file

@ -1,8 +1,21 @@
import { Thread } from "@langchain/langgraph-sdk";
import { getMessageContentString } from "@open-swe/shared/messages";
import { GraphState } from "@open-swe/shared/open-swe/types";
import { GraphState, TaskPlan } from "@open-swe/shared/open-swe/types";
import { getActivePlanItems } from "@open-swe/shared/open-swe/tasks";
export function computeThreadTitle(
taskPlan: TaskPlan | undefined,
fallbackTitle: string,
): string {
if (taskPlan?.tasks && taskPlan.tasks.length > 0) {
const firstTaskTitle = taskPlan.tasks[0]?.title;
if (firstTaskTitle && firstTaskTitle.trim()) {
return firstTaskTitle;
}
}
return fallbackTitle;
}
export function getThreadTitle<State extends Record<string, any> = GraphState>(
thread: Thread<State>,
): string {