Skip to content

Commit cfb3851

Browse files
fix(web): show task progress only in the composer
1 parent 4977016 commit cfb3851

6 files changed

Lines changed: 17 additions & 289 deletions

File tree

‎apps/web/src/components/ChatView.tsx‎

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,6 @@ import {
102102
deriveTimelineEntries,
103103
deriveActiveWorkStartedAt,
104104
deriveActivePlanState,
105-
deriveTurnPlans,
106105
findLatestProposedPlan,
107106
deriveWorkLogEntries,
108107
hasActionableProposedPlan,
@@ -2368,7 +2367,6 @@ function ChatViewContent(props: ChatViewProps) {
23682367
[threadActivities],
23692368
);
23702369
const workLogEntries = useMemo(() => deriveWorkLogEntries(threadActivities), [threadActivities]);
2371-
const turnPlans = useMemo(() => deriveTurnPlans(threadActivities), [threadActivities]);
23722370
// Native subagent fold: memoized by activity-list identity, shared by the
23732371
// Agents surface, live strip, and workflow cards. v2Projection is null
23742372
// until orchestration-v2 lands (source precedence lives in the derive).
@@ -2438,7 +2436,7 @@ function ChatViewContent(props: ChatViewProps) {
24382436
// Current step for the in-chat working row: only for the running turn's own
24392437
// plan (deriveActivePlanState falls back to older turns' plans, which must
24402438
// not label fresh work). Falls back to the first pending step so an
2441-
// all-pending freshly written plan labels the row, matching the chip and
2439+
// all-pending freshly written plan labels the row, matching the composer and
24422440
// the server's planProgress.
24432441
const workingStepLabel = useMemo(() => {
24442442
if (!activePlan || activePlan.turnId !== (activeLatestTurn?.turnId ?? null)) {
@@ -2835,13 +2833,8 @@ function ChatViewContent(props: ChatViewProps) {
28352833
]);
28362834
const timelineEntries = useMemo(
28372835
() =>
2838-
deriveTimelineEntries(
2839-
timelineMessages,
2840-
activeThread?.proposedPlans ?? [],
2841-
workLogEntries,
2842-
turnPlans,
2843-
),
2844-
[activeThread?.proposedPlans, timelineMessages, turnPlans, workLogEntries],
2836+
deriveTimelineEntries(timelineMessages, activeThread?.proposedPlans ?? [], workLogEntries),
2837+
[activeThread?.proposedPlans, timelineMessages, workLogEntries],
28452838
);
28462839
const [dockedDraftHeroThreadKey, setDockedDraftHeroThreadKey] = useState<string | null>(null);
28472840
const draftHeroDockRequested =

‎apps/web/src/components/chat/MessagesTimeline.logic.ts‎

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
workEntryIndicatesToolNeutralStatus,
77
workLogEntryIsToolLike,
88
type TimelineEntry,
9-
type TurnPlanEntry,
109
type WorkLogEntry,
1110
} from "../../session-logic";
1211
import { type ChatMessage, type ProposedPlan, type TurnDiffSummary } from "../../types";
@@ -232,12 +231,6 @@ export type MessagesTimelineRow =
232231
createdAt: string;
233232
proposedPlan: ProposedPlan;
234233
}
235-
| {
236-
kind: "turn-plan";
237-
id: string;
238-
createdAt: string;
239-
turnPlan: TurnPlanEntry;
240-
}
241234
| {
242235
kind: "working";
243236
id: string;
@@ -513,9 +506,6 @@ function timelineEntryTurnId(entry: TimelineEntry): TurnId | null {
513506
if (entry.kind === "message") {
514507
return entry.message.role === "assistant" ? (entry.message.turnId ?? null) : null;
515508
}
516-
if (entry.kind === "turn-plan") {
517-
return entry.turnPlan.turnId;
518-
}
519509
if (entry.kind === "proposed-plan") {
520510
return entry.proposedPlan.turnId;
521511
}
@@ -726,7 +716,7 @@ export function deriveMessagesTimelineRows(input: {
726716
entry.entry.toolLifecycleStatus === "inProgress"
727717
);
728718
}
729-
if (entry.kind === "proposed-plan" || entry.kind === "turn-plan") return true;
719+
if (entry.kind === "proposed-plan") return true;
730720
return false;
731721
});
732722

@@ -937,16 +927,6 @@ export function deriveMessagesTimelineRows(input: {
937927
continue;
938928
}
939929

940-
if (timelineEntry.kind === "turn-plan") {
941-
nextRows.push({
942-
kind: "turn-plan",
943-
id: timelineEntry.id,
944-
createdAt: timelineEntry.createdAt,
945-
turnPlan: timelineEntry.turnPlan,
946-
});
947-
continue;
948-
}
949-
950930
const assistantTurnStillInProgress =
951931
timelineEntry.message.role === "assistant" &&
952932
unsettledTurnId !== null &&
@@ -1028,13 +1008,6 @@ function isRowUnchanged(a: MessagesTimelineRow, b: MessagesTimelineRow): boolean
10281008
case "proposed-plan":
10291009
return a.proposedPlan === (b as typeof a).proposedPlan;
10301010

1031-
case "turn-plan": {
1032-
const bp = b as typeof a;
1033-
// Plans rewrite in place: compare the snapshot's identity fields so an
1034-
// unchanged plan keeps its row reference (virtualization stability).
1035-
return a.createdAt === bp.createdAt && a.turnPlan.plan === bp.turnPlan.plan;
1036-
}
1037-
10381011
case "work": {
10391012
const bw = b as typeof a;
10401013
return (

‎apps/web/src/components/chat/MessagesTimeline.tsx‎

Lines changed: 1 addition & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -983,8 +983,7 @@ const TimelineRowContent = memo(function TimelineRowContent({ row }: { row: Time
983983
!row.showAssistantMeta) ||
984984
row.kind === "work" ||
985985
row.kind === "work-live" ||
986-
row.kind === "work-toggle" ||
987-
row.kind === "turn-plan"
986+
row.kind === "work-toggle"
988987
? "pb-2"
989988
: "pb-4",
990989
row.kind === "message" && row.message.role === "assistant" ? "group/assistant" : null,
@@ -1008,7 +1007,6 @@ const TimelineRowContent = memo(function TimelineRowContent({ row }: { row: Time
10081007
<AssistantTimelineRow row={row} />
10091008
) : null}
10101009
{row.kind === "proposed-plan" ? <ProposedPlanTimelineRow row={row} /> : null}
1011-
{row.kind === "turn-plan" ? <TurnPlanTimelineRow row={row} /> : null}
10121010
{row.kind === "working" ? <WorkingTimelineRow row={row} /> : null}
10131011
</div>
10141012
);
@@ -1324,110 +1322,6 @@ function ProposedPlanTimelineRow({
13241322
);
13251323
}
13261324

1327-
/**
1328-
* Inline folded plan chip: one row per turn that produced plan/todo steps.
1329-
* Collapsed by default, with a segment bar and the current step label,
1330-
* and expands in place to the full step list. Replaces the old plan sidebar.
1331-
*/
1332-
const TurnPlanTimelineRow = memo(function TurnPlanTimelineRow({
1333-
row,
1334-
}: {
1335-
row: Extract<TimelineRow, { kind: "turn-plan" }>;
1336-
}) {
1337-
const [expanded, setExpanded] = useState(false);
1338-
const { steps } = row.turnPlan.plan;
1339-
const completedCount = steps.filter((step) => step.status === "completed").length;
1340-
const allDone = completedCount === steps.length;
1341-
// Label priority: the in-progress step, else the next pending step (plan
1342-
// just created), else the last step (plan finished, rendered muted).
1343-
const label =
1344-
steps.find((step) => step.status === "inProgress")?.step ??
1345-
steps.find((step) => step.status === "pending")?.step ??
1346-
steps.at(-1)?.step ??
1347-
"Plan";
1348-
const Chevron = expanded ? ChevronDownIcon : ChevronRightIcon;
1349-
1350-
return (
1351-
<div className="min-w-0 py-0.5">
1352-
<button
1353-
type="button"
1354-
className="flex min-h-6 w-full min-w-0 cursor-pointer items-center gap-1.5 rounded-md px-0.5 py-0.5 text-left text-[12px] leading-5 transition-colors duration-150 hover:bg-accent/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70"
1355-
aria-expanded={expanded}
1356-
onClick={() => setExpanded((value) => !value)}
1357-
>
1358-
<span className="flex h-5 w-6 shrink-0 items-center justify-center text-muted-foreground/65">
1359-
<Chevron
1360-
className={cn("size-4 shrink-0", expanded ? "-translate-x-0.5" : "-translate-x-1")}
1361-
/>
1362-
</span>
1363-
{steps.length > 1 && steps.length <= 10 ? (
1364-
<span aria-hidden className="flex shrink-0 items-center gap-0.5">
1365-
{steps.map((step) => (
1366-
<span
1367-
key={step.step}
1368-
className={cn(
1369-
"h-[3px] w-2.5 rounded-full",
1370-
step.status === "completed"
1371-
? "bg-success"
1372-
: step.status === "inProgress"
1373-
? "bg-primary"
1374-
: "bg-muted-foreground/25",
1375-
)}
1376-
/>
1377-
))}
1378-
</span>
1379-
) : null}
1380-
<span
1381-
className={cn(
1382-
"min-w-0 truncate",
1383-
allDone ? "text-muted-foreground/65" : "font-medium text-foreground/85",
1384-
)}
1385-
>
1386-
{label}
1387-
</span>
1388-
{steps.length > 1 ? (
1389-
<span className="shrink-0 text-muted-foreground/50 tabular-nums">
1390-
{completedCount}/{steps.length}
1391-
</span>
1392-
) : null}
1393-
</button>
1394-
{expanded ? (
1395-
<div className="mt-0.5 space-y-px pl-8">
1396-
{steps.map((step) => (
1397-
<div key={step.step} className="flex items-baseline gap-2 text-[12px] leading-5">
1398-
<span
1399-
className={cn(
1400-
"w-3 shrink-0 text-center font-mono text-[10px]",
1401-
step.status === "completed"
1402-
? "text-success"
1403-
: step.status === "inProgress"
1404-
? "text-primary"
1405-
: "text-muted-foreground/40",
1406-
)}
1407-
aria-hidden
1408-
>
1409-
{step.status === "completed" ? "✓" : step.status === "inProgress" ? "●" : "○"}
1410-
</span>
1411-
<span
1412-
className={cn(
1413-
"min-w-0",
1414-
step.status === "completed"
1415-
? "text-muted-foreground/55"
1416-
: step.status === "inProgress"
1417-
? "text-foreground/90"
1418-
: "text-muted-foreground/70",
1419-
)}
1420-
>
1421-
{step.step}
1422-
</span>
1423-
</div>
1424-
))}
1425-
</div>
1426-
) : null}
1427-
</div>
1428-
);
1429-
});
1430-
14311325
function WorkingTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "working" }> }) {
14321326
const { workingStepLabel } = use(TimelineRowActivityCtx);
14331327
return (

‎apps/web/src/session-logic.test.ts‎

Lines changed: 5 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { describe, expect, it } from "vite-plus/test";
1111
import {
1212
deriveActiveWorkStartedAt,
1313
deriveActivePlanState,
14-
deriveTurnPlans,
1514
derivePendingApprovals,
1615
derivePendingUserInputs,
1716
deriveTimelineEntries,
@@ -495,73 +494,6 @@ describe("deriveActivePlanState", () => {
495494
{ durationMs: 3_000, step: "Check", status: "completed" },
496495
]);
497496
});
498-
});
499-
500-
describe("deriveTurnPlans", () => {
501-
it("keeps one entry per turn, anchored at the first snapshot with the latest steps", () => {
502-
const activities: OrchestrationThreadActivity[] = [
503-
makeActivity({
504-
id: "plan-1a",
505-
createdAt: "2026-02-23T00:00:01.000Z",
506-
kind: "turn.plan.updated",
507-
summary: "Plan updated",
508-
tone: "info",
509-
turnId: "turn-1",
510-
payload: {
511-
plan: [{ step: "Inspect code", status: "inProgress" }],
512-
},
513-
}),
514-
makeActivity({
515-
id: "plan-1b",
516-
createdAt: "2026-02-23T00:00:05.000Z",
517-
kind: "turn.plan.updated",
518-
summary: "Plan updated",
519-
tone: "info",
520-
turnId: "turn-1",
521-
payload: {
522-
plan: [{ step: "Inspect code", status: "completed" }],
523-
},
524-
}),
525-
makeActivity({
526-
id: "plan-2a",
527-
createdAt: "2026-02-23T00:01:00.000Z",
528-
kind: "turn.plan.updated",
529-
summary: "Plan updated",
530-
tone: "info",
531-
turnId: "turn-2",
532-
payload: {
533-
plan: [{ step: "Ship it", status: "pending" }],
534-
},
535-
}),
536-
];
537-
538-
const turnPlans = deriveTurnPlans(activities);
539-
expect(turnPlans).toHaveLength(2);
540-
expect(turnPlans[0]).toMatchObject({
541-
id: "turn-plan:turn-1",
542-
createdAt: "2026-02-23T00:00:01.000Z",
543-
turnId: "turn-1",
544-
});
545-
expect(turnPlans[0]?.plan.steps).toEqual([
546-
{ durationMs: 4_000, step: "Inspect code", status: "completed" },
547-
]);
548-
expect(turnPlans[1]?.plan.steps).toEqual([{ step: "Ship it", status: "pending" }]);
549-
});
550-
551-
it("skips activities without parseable steps", () => {
552-
const activities: OrchestrationThreadActivity[] = [
553-
makeActivity({
554-
id: "plan-bad",
555-
createdAt: "2026-02-23T00:00:01.000Z",
556-
kind: "turn.plan.updated",
557-
summary: "Plan updated",
558-
tone: "info",
559-
turnId: "turn-1",
560-
payload: { plan: [] },
561-
}),
562-
];
563-
expect(deriveTurnPlans(activities)).toEqual([]);
564-
});
565497

566498
it("tracks repeated step labels independently", () => {
567499
const activities: OrchestrationThreadActivity[] = [
@@ -609,7 +541,7 @@ describe("deriveTurnPlans", () => {
609541
}),
610542
];
611543

612-
expect(deriveTurnPlans(activities)[0]?.plan.steps).toEqual([
544+
expect(deriveActivePlanState(activities, TurnId.make("turn-1"))?.steps).toEqual([
613545
{ durationMs: 4_000, step: "Check", status: "completed" },
614546
{ durationMs: 6_000, step: "Check", status: "completed" },
615547
]);
@@ -661,13 +593,13 @@ describe("deriveTurnPlans", () => {
661593
}),
662594
];
663595

664-
expect(deriveTurnPlans(activities)[0]?.plan.steps).toEqual([
596+
expect(deriveActivePlanState(activities, TurnId.make("turn-1"))?.steps).toEqual([
665597
{ durationMs: 5_000, step: "First", status: "completed" },
666598
{ durationMs: 5_000, step: "Second", status: "completed" },
667599
]);
668600
});
669601

670-
it("drops a turn's chip when a later snapshot clears the plan", () => {
602+
it("clears the active plan when a later snapshot has no steps", () => {
671603
const activities: OrchestrationThreadActivity[] = [
672604
makeActivity({
673605
id: "plan-set",
@@ -688,7 +620,7 @@ describe("deriveTurnPlans", () => {
688620
payload: { plan: [] },
689621
}),
690622
];
691-
expect(deriveTurnPlans(activities)).toEqual([]);
623+
expect(deriveActivePlanState(activities, TurnId.make("turn-1"))).toBeNull();
692624
});
693625
});
694626

@@ -937,8 +869,7 @@ describe("deriveWorkLogEntries", () => {
937869
makeActivity({ id: "after", kind: "tool.completed", summary: "Ran tests", sequence: 3 }),
938870
];
939871
expect(deriveWorkLogEntries(activities).map((entry) => entry.id)).toEqual(["before", "after"]);
940-
expect(deriveTurnPlans(activities)).toHaveLength(1);
941-
expect(deriveTurnPlans(activities)[0]?.plan.steps).toMatchObject([
872+
expect(deriveActivePlanState(activities, TurnId.make("turn-1"))?.steps).toMatchObject([
942873
{ step: "Verify the composer", status: "completed" },
943874
]);
944875
});

0 commit comments

Comments
 (0)