feat(ui): support question tool requests
Add question queue hydration, inline answering UI, and unify pending requests with permissions.
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import type { PermissionRequestLike } from "../../types/permission"
|
||||
import { getPermissionCallId, getPermissionMessageId } from "../../types/permission"
|
||||
import type { QuestionRequest } from "../../types/question"
|
||||
import { getQuestionCallId, getQuestionMessageId } from "../../types/question"
|
||||
import type { Message, MessageInfo, ClientPart } from "../../types/message"
|
||||
import type { Session } from "../../types/session"
|
||||
import { messageStoreBus } from "./bus"
|
||||
@@ -192,6 +194,65 @@ export function reconcilePendingPermissionsV2(instanceId: string, sessionId?: st
|
||||
}
|
||||
}
|
||||
|
||||
function extractQuestionMessageId(request: QuestionRequest): string | undefined {
|
||||
return getQuestionMessageId(request)
|
||||
}
|
||||
|
||||
function extractQuestionCallId(request: QuestionRequest): string | undefined {
|
||||
return getQuestionCallId(request)
|
||||
}
|
||||
|
||||
export function upsertQuestionV2(instanceId: string, request: QuestionRequest): void {
|
||||
if (!request) return
|
||||
const store = messageStoreBus.getOrCreate(instanceId)
|
||||
const messageId = extractQuestionMessageId(request)
|
||||
let partId: string | undefined = undefined
|
||||
const callId = extractQuestionCallId(request)
|
||||
if (callId) {
|
||||
partId = resolvePartIdFromCallId(store, messageId, callId)
|
||||
}
|
||||
store.upsertQuestion({
|
||||
request,
|
||||
messageId,
|
||||
partId,
|
||||
enqueuedAt: (request as any).time?.created ?? Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
export function reconcilePendingQuestionsV2(instanceId: string, sessionId?: string): void {
|
||||
const store = messageStoreBus.getOrCreate(instanceId)
|
||||
const pending = store.state.questions.queue
|
||||
if (!pending || pending.length === 0) return
|
||||
|
||||
for (const entry of pending) {
|
||||
if (!entry || entry.partId) continue
|
||||
const request = entry.request
|
||||
if (!request) continue
|
||||
|
||||
const questionSessionId = request.sessionID
|
||||
if (sessionId && questionSessionId && questionSessionId !== sessionId) {
|
||||
continue
|
||||
}
|
||||
|
||||
const messageId = entry.messageId ?? extractQuestionMessageId(request)
|
||||
const callId = extractQuestionCallId(request)
|
||||
const resolvedPartId = resolvePartIdFromCallId(store, messageId, callId)
|
||||
if (!resolvedPartId) continue
|
||||
|
||||
store.upsertQuestion({
|
||||
...entry,
|
||||
messageId,
|
||||
partId: resolvedPartId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function removeQuestionV2(instanceId: string, requestId: string): void {
|
||||
if (!requestId) return
|
||||
const store = messageStoreBus.getOrCreate(instanceId)
|
||||
store.removeQuestion(requestId)
|
||||
}
|
||||
|
||||
export function removePermissionV2(instanceId: string, permissionId: string): void {
|
||||
if (!permissionId) return
|
||||
const store = messageStoreBus.getOrCreate(instanceId)
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
PartUpdateInput,
|
||||
PendingPartEntry,
|
||||
PermissionEntry,
|
||||
QuestionEntry,
|
||||
ReplaceMessageIdOptions,
|
||||
ScrollSnapshot,
|
||||
SessionRecord,
|
||||
@@ -40,6 +41,11 @@ function createInitialState(instanceId: string): InstanceMessageState {
|
||||
active: null,
|
||||
byMessage: {},
|
||||
},
|
||||
questions: {
|
||||
queue: [],
|
||||
active: null,
|
||||
byMessage: {},
|
||||
},
|
||||
usage: {},
|
||||
scrollState: {},
|
||||
latestTodos: {},
|
||||
@@ -193,6 +199,9 @@ export interface InstanceMessageStore {
|
||||
upsertPermission: (entry: PermissionEntry) => void
|
||||
removePermission: (permissionId: string) => void
|
||||
getPermissionState: (messageId?: string, partId?: string) => { entry: PermissionEntry; active: boolean } | null
|
||||
upsertQuestion: (entry: QuestionEntry) => void
|
||||
removeQuestion: (requestId: string) => void
|
||||
getQuestionState: (messageId?: string, partId?: string) => { entry: QuestionEntry; active: boolean } | null
|
||||
setSessionRevert: (sessionId: string, revert?: SessionRecord["revert"] | null) => void
|
||||
getSessionRevert: (sessionId: string) => SessionRecord["revert"] | undefined | null
|
||||
rebuildUsage: (sessionId: string, infos: Iterable<MessageInfo>) => void
|
||||
@@ -757,6 +766,18 @@ export function createInstanceMessageStore(instanceId: string, hooks?: MessageSt
|
||||
})
|
||||
}
|
||||
|
||||
const questionMap = state.questions.byMessage[options.oldId]
|
||||
if (questionMap) {
|
||||
setState("questions", "byMessage", options.newId, questionMap)
|
||||
setState("questions", (prev) => {
|
||||
const next = { ...prev }
|
||||
const nextByMessage = { ...next.byMessage }
|
||||
delete nextByMessage[options.oldId]
|
||||
next.byMessage = nextByMessage
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const pending = state.pendingParts[options.oldId]
|
||||
if (pending) {
|
||||
setState("pendingParts", options.newId, pending)
|
||||
@@ -832,6 +853,60 @@ export function createInstanceMessageStore(instanceId: string, hooks?: MessageSt
|
||||
return { entry, active }
|
||||
}
|
||||
|
||||
function upsertQuestion(entry: QuestionEntry) {
|
||||
const messageKey = entry.messageId ?? "__global__"
|
||||
const partKey = entry.partId ?? entry.request?.id ?? "__global__"
|
||||
|
||||
setState(
|
||||
"questions",
|
||||
produce((draft) => {
|
||||
draft.byMessage[messageKey] = draft.byMessage[messageKey] ?? {}
|
||||
draft.byMessage[messageKey][partKey] = entry
|
||||
const existingIndex = draft.queue.findIndex((item) => item.request.id === entry.request.id)
|
||||
if (existingIndex === -1) {
|
||||
draft.queue.push(entry)
|
||||
} else {
|
||||
draft.queue[existingIndex] = entry
|
||||
}
|
||||
if (!draft.active || draft.active.request.id === entry.request.id) {
|
||||
draft.active = entry
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function removeQuestion(requestId: string) {
|
||||
setState(
|
||||
"questions",
|
||||
produce((draft) => {
|
||||
draft.queue = draft.queue.filter((item) => item.request.id !== requestId)
|
||||
if (draft.active?.request.id === requestId) {
|
||||
draft.active = draft.queue[0] ?? null
|
||||
}
|
||||
Object.keys(draft.byMessage).forEach((messageKey) => {
|
||||
const partEntries = draft.byMessage[messageKey]
|
||||
Object.keys(partEntries).forEach((partKey) => {
|
||||
if (partEntries[partKey].request.id === requestId) {
|
||||
delete partEntries[partKey]
|
||||
}
|
||||
})
|
||||
if (Object.keys(partEntries).length === 0) {
|
||||
delete draft.byMessage[messageKey]
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function getQuestionState(messageId?: string, partId?: string) {
|
||||
const messageKey = messageId ?? "__global__"
|
||||
const partKey = partId ?? "__global__"
|
||||
const entry = state.questions.byMessage[messageKey]?.[partKey]
|
||||
if (!entry) return null
|
||||
const active = state.questions.active?.request.id === entry.request.id
|
||||
return { entry, active }
|
||||
}
|
||||
|
||||
function pruneMessagesAfterRevert(sessionId: string, revertMessageId: string) {
|
||||
const session = state.sessions[sessionId]
|
||||
if (!session) return
|
||||
@@ -873,6 +948,14 @@ export function createInstanceMessageStore(instanceId: string, hooks?: MessageSt
|
||||
return next
|
||||
})
|
||||
|
||||
setState("questions", "byMessage", (prev) => {
|
||||
const next = { ...prev }
|
||||
removedIds.forEach((id) => {
|
||||
if (next[id]) delete next[id]
|
||||
})
|
||||
return next
|
||||
})
|
||||
|
||||
withUsageState(sessionId, (draft) => {
|
||||
removedIds.forEach((id) => removeUsageEntry(draft, id))
|
||||
})
|
||||
@@ -948,6 +1031,14 @@ export function createInstanceMessageStore(instanceId: string, hooks?: MessageSt
|
||||
return next
|
||||
})
|
||||
|
||||
setState("questions", "byMessage", (prev) => {
|
||||
const next = { ...prev }
|
||||
messageIds.forEach((id) => {
|
||||
if (next[id]) delete next[id]
|
||||
})
|
||||
return next
|
||||
})
|
||||
|
||||
setState("usage", (prev) => {
|
||||
const next = { ...prev }
|
||||
delete next[sessionId]
|
||||
@@ -1012,9 +1103,13 @@ export function createInstanceMessageStore(instanceId: string, hooks?: MessageSt
|
||||
replaceMessageId,
|
||||
setMessageInfo,
|
||||
getMessageInfo,
|
||||
upsertPermission,
|
||||
removePermission,
|
||||
getPermissionState,
|
||||
upsertPermission,
|
||||
removePermission,
|
||||
getPermissionState,
|
||||
upsertQuestion,
|
||||
removeQuestion,
|
||||
getQuestionState,
|
||||
|
||||
setSessionRevert,
|
||||
getSessionRevert,
|
||||
rebuildUsage,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ClientPart } from "../../types/message"
|
||||
import type { PermissionRequestLike } from "../../types/permission"
|
||||
import type { QuestionRequest } from "../../types/question"
|
||||
|
||||
export type MessageStatus = "sending" | "sent" | "streaming" | "complete" | "error"
|
||||
export type MessageRole = "user" | "assistant"
|
||||
@@ -59,6 +60,19 @@ export interface InstancePermissionState {
|
||||
byMessage: Record<string, Record<string, PermissionEntry>>
|
||||
}
|
||||
|
||||
export interface QuestionEntry {
|
||||
request: QuestionRequest
|
||||
messageId?: string
|
||||
partId?: string
|
||||
enqueuedAt: number
|
||||
}
|
||||
|
||||
export interface InstanceQuestionState {
|
||||
queue: QuestionEntry[]
|
||||
active: QuestionEntry | null
|
||||
byMessage: Record<string, Record<string, QuestionEntry>>
|
||||
}
|
||||
|
||||
export interface ScrollSnapshot {
|
||||
scrollTop: number
|
||||
atBottom: boolean
|
||||
@@ -103,6 +117,7 @@ export interface InstanceMessageState {
|
||||
pendingParts: Record<string, PendingPartEntry[]>
|
||||
sessionRevisions: Record<string, number>
|
||||
permissions: InstancePermissionState
|
||||
questions: InstanceQuestionState
|
||||
usage: Record<string, SessionUsageState>
|
||||
scrollState: Record<string, ScrollSnapshot>
|
||||
latestTodos: Record<string, LatestTodoSnapshot | undefined>
|
||||
|
||||
Reference in New Issue
Block a user