Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions packages/ui/src/stores/message-v2/instance-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,60 @@ describe("message-v2 permission state", () => {
assert.equal(store.getPermissionState(undefined, "permission-2")?.active, true)
})
})

describe("message-v2 question state", () => {
it("keeps one question attachment when a duplicate moves from global to a tool part", () => {
const store = createInstanceMessageStore("instance-1")

store.upsertQuestion({
request: { id: "question-1", sessionID: "session-1", questions: [] },
enqueuedAt: 1_000,
})
store.upsertQuestion({
request: { id: "question-1", sessionID: "session-1", questions: [] },
messageId: "message-1",
partId: "part-1",
enqueuedAt: 2_000,
})

assert.equal(store.state.questions.queue.length, 1)
assert.equal(store.getQuestionState(undefined, "question-1"), null)
assert.equal(store.getQuestionState("message-1", "part-1")?.entry.request.id, "question-1")
assert.equal(store.getQuestionState("message-1", "part-1")?.active, true)
})

it("recalculates the active question after removing the first queue entry", () => {
const store = createInstanceMessageStore("instance-1")

store.upsertQuestion({ request: { id: "question-1", sessionID: "session-1", questions: [] }, enqueuedAt: 1_000 })
store.upsertQuestion({ request: { id: "question-2", sessionID: "session-1", questions: [] }, enqueuedAt: 2_000 })
store.removeQuestion("question-1")

assert.equal(store.state.questions.active?.request.id, "question-2")
assert.equal(store.getQuestionState(undefined, "question-2")?.active, true)
})

it("preserves original enqueuedAt when a question is upserted with a newer timestamp", () => {
const store = createInstanceMessageStore("instance-1")

store.upsertQuestion({ request: { id: "question-1", sessionID: "session-1", questions: [] }, enqueuedAt: 1_000 })
store.upsertQuestion({ request: { id: "question-2", sessionID: "session-1", questions: [] }, enqueuedAt: 1_500 })
store.upsertQuestion({
request: { id: "question-1", sessionID: "session-1", questions: [] },
messageId: "message-1",
partId: "part-1",
enqueuedAt: 2_000,
})

// Queue stays ordered by original enqueue time, not the newer upsert time
assert.equal(store.state.questions.queue.length, 2)
assert.equal(store.state.questions.queue[0].request.id, "question-1")
assert.equal(store.state.questions.queue[0].enqueuedAt, 1_000)
assert.equal(store.state.questions.queue[1].request.id, "question-2")
assert.equal(store.state.questions.queue[1].enqueuedAt, 1_500)
assert.equal(store.state.questions.active?.request.id, "question-1")

// Resolved question-1 is reachable at its tool part location
assert.equal(store.getQuestionState("message-1", "part-1")?.active, true)
})
})
30 changes: 26 additions & 4 deletions packages/ui/src/stores/message-v2/instance-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -983,13 +983,36 @@ export function createInstanceMessageStore(instanceId: string, hooks?: MessageSt
return { entry, active }
}

function upsertQuestion(entry: QuestionEntry) {
function mergeQuestionEntry(entry: QuestionEntry): QuestionEntry {
const existing = state.questions.queue.find((item) => item.request.id === entry.request.id)
if (!existing) return entry
return {
...entry,
messageId: entry.messageId ?? existing.messageId,
partId: entry.partId ?? existing.partId,
enqueuedAt: Math.min(existing.enqueuedAt, entry.enqueuedAt),
}
}

function upsertQuestion(input: QuestionEntry) {
const entry = mergeQuestionEntry(input)
const messageKey = entry.messageId ?? "__global__"
const partKey = entry.partId ?? entry.request?.id ?? "__global__"

setState(
"questions",
produce((draft) => {
Object.keys(draft.byMessage).forEach((existingMessageKey) => {
const partEntries = draft.byMessage[existingMessageKey]
Object.keys(partEntries).forEach((existingPartKey) => {
if (partEntries[existingPartKey].request.id === entry.request.id) {
delete partEntries[existingPartKey]
}
})
if (Object.keys(partEntries).length === 0) {
delete draft.byMessage[existingMessageKey]
}
})
draft.byMessage[messageKey] = draft.byMessage[messageKey] ?? {}
draft.byMessage[messageKey][partKey] = entry
const existingIndex = draft.queue.findIndex((item) => item.request.id === entry.request.id)
Expand All @@ -998,9 +1021,8 @@ export function createInstanceMessageStore(instanceId: string, hooks?: MessageSt
} else {
draft.queue[existingIndex] = entry
}
if (!draft.active || draft.active.request.id === entry.request.id) {
draft.active = entry
}
draft.queue.sort((left, right) => left.enqueuedAt - right.enqueuedAt)
draft.active = draft.queue[0] ?? null
}),
)
}
Expand Down
Loading