fix(app): place thinking above queued prompts (#44616)

Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
This commit is contained in:
opencode-agent[bot] 2026-08-24 06:05:22 +00:00 committed by GitHub
parent 6704f34cad
commit e282066cf8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 43 additions and 3 deletions

View file

@ -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 })

View file

@ -8,6 +8,7 @@ export function createTimelineProjection(input: {
sessionMessages: Accessor<SessionMessageInfo[]>
status: Accessor<SessionStatus>
showReasoningSummaries: Accessor<boolean>
pendingUserMessageIDs: Accessor<ReadonlySet<string>>
}) {
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) =>

View file

@ -25,6 +25,7 @@ export type TimelineProjectionInput = {
sessionMessages: SessionMessageInfo[]
status: SessionStatus
showReasoningSummaries: boolean
pendingUserMessageIDs?: ReadonlySet<string>
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<SessionMessageInfo[]>
status: Accessor<SessionStatus>
showReasoningSummaries: Accessor<boolean>
pendingUserMessageIDs?: Accessor<ReadonlySet<string>>
}) {
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<string>,
) {
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: [

View file

@ -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) => {