diff --git a/packages/app/src/session/timeline/controller.tsx b/packages/app/src/session/timeline/controller.tsx index e7db9164a33..e7a676b7e3b 100644 --- a/packages/app/src/session/timeline/controller.tsx +++ b/packages/app/src/session/timeline/controller.tsx @@ -68,6 +68,14 @@ export function createTimelineController(input: { session: TimelineSessionSource input.session.data.info()?.revert?.messageID, ) }) + const pendingUserMessageIDs = createMemo(() => { + const id = input.session.identity.sessionID() + return new Set( + (id ? data.session.pending.list(id) : []).flatMap((item) => + item.type === "user" && item.delivery === "steer" ? [item.id] : [], + ), + ) + }) const titleValue = createMemo(() => input.session.data.info()?.title) const titleLabel = createMemo(() => sessionTitle(titleValue()) ?? language.t("command.session.new")) const parentMessages = createMemo(() => { @@ -97,6 +105,7 @@ export function createTimelineController(input: { session: TimelineSessionSource sessionMessages: projectedMessages, status: input.session.data.status, showReasoningSummaries: settings.general.showReasoningSummaries, + pendingUserMessageIDs, }) const [pending, setPending] = createStore({ rename: false }) diff --git a/packages/app/src/session/timeline/projection.ts b/packages/app/src/session/timeline/projection.ts index 411df468954..361324560ba 100644 --- a/packages/app/src/session/timeline/projection.ts +++ b/packages/app/src/session/timeline/projection.ts @@ -8,6 +8,7 @@ export function createTimelineProjection(input: { sessionMessages: Accessor status: Accessor showReasoningSummaries: Accessor + pendingUserMessageIDs: Accessor> }) { const sessionMessageByID = createMemo( () => new Map(input.sessionMessages().map((message) => [message.id, message] as const)), @@ -75,7 +76,12 @@ export function createTimelineProjection(input: { return result }) const projection = createMemo(() => - Timeline.constructSessionMessageRows(input.sessionMessages(), input.showReasoningSummaries(), input.status()), + Timeline.constructSessionMessageRows( + input.sessionMessages(), + input.showReasoningSummaries(), + input.status(), + input.pendingUserMessageIDs(), + ), ) const activeMessageID = createMemo(() => projection().activeMessageID) const rows = createMemo((previous: TimelineRow.TimelineRow[] | undefined) => diff --git a/packages/session-ui/src/timeline/projection.ts b/packages/session-ui/src/timeline/projection.ts index a7c6684889a..61a74d44e23 100644 --- a/packages/session-ui/src/timeline/projection.ts +++ b/packages/session-ui/src/timeline/projection.ts @@ -25,6 +25,7 @@ export type TimelineProjectionInput = { sessionMessages: SessionMessageInfo[] status: SessionStatus showReasoningSummaries: boolean + pendingUserMessageIDs?: ReadonlySet previousRows?: TimelineRow.TimelineRow[] } @@ -34,6 +35,7 @@ export function createTimelineProjection(input: TimelineProjectionInput) { input.sessionMessages, input.showReasoningSummaries, input.status, + input.pendingUserMessageIDs, ) const rows = reuseTimelineRows(input.previousRows, projection.rows) const rowByKey = new Map(rows.map((row) => [TimelineRow.key(row), row] as const)) @@ -65,6 +67,7 @@ export function createReactiveTimelineProjection(input: { sessionMessages: Accessor status: Accessor showReasoningSummaries: Accessor + pendingUserMessageIDs?: Accessor> }) { const sessionMessageByID = createMemo( () => new Map(input.sessionMessages().map((message) => [message.id, message] as const)), @@ -72,7 +75,12 @@ export function createReactiveTimelineProjection(input: { const userContextByID = createMemo(() => indexUserContext(input.sessionMessages())) const assistantMessagesByParent = createMemo(() => indexAssistantMessages(input.sessionMessages())) const projection = createMemo(() => - Timeline.constructSessionMessageRows(input.sessionMessages(), input.showReasoningSummaries(), input.status()), + Timeline.constructSessionMessageRows( + input.sessionMessages(), + input.showReasoningSummaries(), + input.status(), + input.pendingUserMessageIDs?.(), + ), ) const activeMessageID = createMemo(() => projection().activeMessageID) const rows = createMemo((previous: TimelineRow.TimelineRow[] | undefined) => @@ -119,6 +127,7 @@ export namespace Timeline { messages: SessionMessageInfo[], showReasoning: boolean, status: SessionStatus, + pendingUserMessageIDs?: ReadonlySet, ) { type Turn = { id: string @@ -169,7 +178,7 @@ export namespace Timeline { current = turn }) - const activeMessageID = turns.at(-1)?.id + const activeMessageID = turns.findLast((turn) => !pendingUserMessageIDs?.has(turn.id))?.id ?? turns.at(-1)?.id return { activeMessageID, rows: [ diff --git a/packages/session-ui/src/timeline/rows-current.test.ts b/packages/session-ui/src/timeline/rows-current.test.ts index d93b66c3cee..fc83c1a442f 100644 --- a/packages/session-ui/src/timeline/rows-current.test.ts +++ b/packages/session-ui/src/timeline/rows-current.test.ts @@ -166,6 +166,22 @@ describe("current session timeline rows", () => { ]) }) + test("renders thinking above a queued user message", () => { + const source = [ + { id: "msg_active", type: "user", text: "active", time: { created: 1 } }, + { id: "msg_queued", type: "user", text: "queued", time: { created: 2 } }, + ] satisfies SessionMessageInfo[] + const result = Timeline.constructSessionMessageRows(source, true, { type: "busy" }, new Set(["msg_queued"])) + + expect(result.activeMessageID).toBe("msg_active") + expect(result.rows.map(TimelineRow.key)).toEqual([ + "user-message:msg_active", + "thinking:msg_active", + "turn-gap:msg_queued", + "user-message:msg_queued", + ]) + }) + test("suppresses thinking while a subagent is delegating or running", () => { const statuses = ["streaming", "running"] as const statuses.forEach((status) => {