fix(tui): show queued prompt admissions (#34771)
This commit is contained in:
@@ -194,6 +194,19 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||||||
case "session.next.prompted": {
|
case "session.next.prompted": {
|
||||||
setStore("session", "status", event.data.sessionID, "running")
|
setStore("session", "status", event.data.sessionID, "running")
|
||||||
message.update(event.data.sessionID, (draft, index) => {
|
message.update(event.data.sessionID, (draft, index) => {
|
||||||
|
const position = index.get(event.data.messageID)
|
||||||
|
const existing = position === undefined ? undefined : draft[position]
|
||||||
|
if (existing?.type === "user") {
|
||||||
|
existing.text = event.data.prompt.text
|
||||||
|
existing.files = event.data.prompt.files
|
||||||
|
existing.agents = event.data.prompt.agents
|
||||||
|
existing.time.created = event.data.timestamp
|
||||||
|
if (existing.metadata?.queued === true) {
|
||||||
|
delete existing.metadata.queued
|
||||||
|
if (Object.keys(existing.metadata).length === 0) existing.metadata = undefined
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
message.append(draft, index, {
|
message.append(draft, index, {
|
||||||
id: event.data.messageID,
|
id: event.data.messageID,
|
||||||
type: "user",
|
type: "user",
|
||||||
@@ -206,6 +219,17 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "session.next.prompt.admitted":
|
case "session.next.prompt.admitted":
|
||||||
|
message.update(event.data.sessionID, (draft, index) => {
|
||||||
|
message.append(draft, index, {
|
||||||
|
id: event.data.messageID,
|
||||||
|
type: "user",
|
||||||
|
text: event.data.prompt.text,
|
||||||
|
files: event.data.prompt.files,
|
||||||
|
agents: event.data.prompt.agents,
|
||||||
|
metadata: { queued: true },
|
||||||
|
time: { created: event.data.timestamp },
|
||||||
|
})
|
||||||
|
})
|
||||||
break
|
break
|
||||||
case "session.next.context.updated":
|
case "session.next.context.updated":
|
||||||
message.update(event.data.sessionID, (draft, index) => {
|
message.update(event.data.sessionID, (draft, index) => {
|
||||||
@@ -590,15 +614,21 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||||||
return position === undefined ? undefined : messages?.[position]
|
return position === undefined ? undefined : messages?.[position]
|
||||||
},
|
},
|
||||||
async refresh(sessionID: string) {
|
async refresh(sessionID: string) {
|
||||||
|
const live = [...(store.session.message[sessionID] ?? [])]
|
||||||
setStore("session", "message", sessionID, [])
|
setStore("session", "message", sessionID, [])
|
||||||
messageIndex.set(sessionID, new Map())
|
messageIndex.set(sessionID, new Map())
|
||||||
const loaded = mutable(
|
const loaded = mutable(
|
||||||
(await sdk.api.message.list({ sessionID, limit: 200, order: "desc" })).data,
|
(await sdk.api.message.list({ sessionID, limit: 200, order: "desc" })).data,
|
||||||
).toReversed()
|
).toReversed()
|
||||||
const live = store.session.message[sessionID] ?? []
|
const loadedIDs = new Set(loaded.map((message) => message.id))
|
||||||
const liveByID = new Map(live.map((message) => [message.id, message]))
|
const liveByID = new Map(live.map((message) => [message.id, message]))
|
||||||
const messages = [...loaded.map((message) => liveByID.get(message.id) ?? message), ...live]
|
const messages = [
|
||||||
.filter((message, index, messages) => messages.findIndex((item) => item.id === message.id) === index)
|
...loaded.map((message) => {
|
||||||
|
if (message.type === "user") return message
|
||||||
|
return liveByID.get(message.id) ?? message
|
||||||
|
}),
|
||||||
|
...live.filter((message) => !loadedIDs.has(message.id)),
|
||||||
|
]
|
||||||
.toSorted((a, b) => a.time.created - b.time.created)
|
.toSorted((a, b) => a.time.created - b.time.created)
|
||||||
messageIndex.set(sessionID, new Map(messages.map((message, index) => [message.id, index])))
|
messageIndex.set(sessionID, new Map(messages.map((message, index) => [message.id, index])))
|
||||||
setStore("session", "message", sessionID, messages)
|
setStore("session", "message", sessionID, messages)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { useData } from "../../context/data"
|
|||||||
import { SplitBorder } from "../../ui/border"
|
import { SplitBorder } from "../../ui/border"
|
||||||
import { useTuiPaths, useTuiTerminalEnvironment } from "../../context/runtime"
|
import { useTuiPaths, useTuiTerminalEnvironment } from "../../context/runtime"
|
||||||
import { Spinner } from "../../component/spinner"
|
import { Spinner } from "../../component/spinner"
|
||||||
import { createSyntaxStyleMemo, generateSubtleSyntax, useTheme } from "../../context/theme"
|
import { createSyntaxStyleMemo, generateSubtleSyntax, selectedForeground, useTheme } from "../../context/theme"
|
||||||
import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers, TextAttributes, RGBA } from "@opentui/core"
|
import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers, TextAttributes, RGBA } from "@opentui/core"
|
||||||
import { Prompt, type PromptRef } from "../../component/prompt"
|
import { Prompt, type PromptRef } from "../../component/prompt"
|
||||||
import type {
|
import type {
|
||||||
@@ -1319,11 +1319,15 @@ function RevertMessage(props: {
|
|||||||
|
|
||||||
function UserMessage(props: { message: SessionMessageUser }) {
|
function UserMessage(props: { message: SessionMessageUser }) {
|
||||||
const ctx = use()
|
const ctx = use()
|
||||||
|
const data = useData()
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
const files = createMemo(() => props.message.files ?? [])
|
const files = createMemo(() => props.message.files ?? [])
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const [hover, setHover] = createSignal(false)
|
const [hover, setHover] = createSignal(false)
|
||||||
const color = createMemo(() => local.agent.color(useData().session.get(ctx.sessionID)?.agent ?? "build"))
|
const color = createMemo(() => local.agent.color(data.session.get(ctx.sessionID)?.agent ?? "build"))
|
||||||
|
const queued = createMemo(() => props.message.metadata?.queued === true)
|
||||||
|
const queuedFg = createMemo(() => selectedForeground(theme, color()))
|
||||||
|
const metadataVisible = createMemo(() => queued() || ctx.showTimestamps())
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const renderer = useRenderer()
|
const renderer = useRenderer()
|
||||||
|
|
||||||
@@ -1356,7 +1360,7 @@ function UserMessage(props: { message: SessionMessageUser }) {
|
|||||||
<Show when={files().length}>
|
<Show when={files().length}>
|
||||||
<box
|
<box
|
||||||
flexDirection="row"
|
flexDirection="row"
|
||||||
paddingBottom={ctx.showTimestamps() ? 1 : 0}
|
paddingBottom={metadataVisible() ? 1 : 0}
|
||||||
paddingTop={1}
|
paddingTop={1}
|
||||||
gap={1}
|
gap={1}
|
||||||
flexWrap="wrap"
|
flexWrap="wrap"
|
||||||
@@ -1379,9 +1383,18 @@ function UserMessage(props: { message: SessionMessageUser }) {
|
|||||||
</For>
|
</For>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={ctx.showTimestamps()}>
|
<Show
|
||||||
|
when={queued()}
|
||||||
|
fallback={
|
||||||
|
<Show when={ctx.showTimestamps()}>
|
||||||
|
<text fg={theme.textMuted}>
|
||||||
|
<span style={{ fg: theme.textMuted }}>{Locale.todayTimeOrDateTime(props.message.time.created)}</span>
|
||||||
|
</text>
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
>
|
||||||
<text fg={theme.textMuted}>
|
<text fg={theme.textMuted}>
|
||||||
<span style={{ fg: theme.textMuted }}>{Locale.todayTimeOrDateTime(props.message.time.created)}</span>
|
<span style={{ bg: color(), fg: queuedFg(), bold: true }}> QUEUED </span>
|
||||||
</text>
|
</text>
|
||||||
</Show>
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ export function createSessionRows(sessionID: Accessor<string>) {
|
|||||||
if (event.data.sessionID === sessionID()) appendMessage(event.data.messageID)
|
if (event.data.sessionID === sessionID()) appendMessage(event.data.messageID)
|
||||||
}
|
}
|
||||||
const subscriptions = [
|
const subscriptions = [
|
||||||
|
data.on("session.next.prompt.admitted", message),
|
||||||
data.on("session.next.prompted", message),
|
data.on("session.next.prompted", message),
|
||||||
data.on("session.next.context.updated", message),
|
data.on("session.next.context.updated", message),
|
||||||
data.on("session.next.synthetic", message),
|
data.on("session.next.synthetic", message),
|
||||||
|
|||||||
@@ -778,9 +778,17 @@ test("settles pending tools when a live failure arrives", async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test("renders admitted prompts only after they become model-visible", async () => {
|
test("renders admitted prompts immediately with queued marker and clears when promoted", async () => {
|
||||||
const events = createEventStream()
|
const events = createEventStream()
|
||||||
const calls = createFetch(undefined, events)
|
const sessionID = "session-1"
|
||||||
|
const messageID = "msg_user_1"
|
||||||
|
const calls = createFetch((url) => {
|
||||||
|
if (url.pathname === `/api/session/${sessionID}/message`)
|
||||||
|
return json({
|
||||||
|
data: [{ id: messageID, type: "user", text: "hello", time: { created: 0 } }],
|
||||||
|
cursor: {},
|
||||||
|
})
|
||||||
|
}, events)
|
||||||
let sync!: ReturnType<typeof useData>
|
let sync!: ReturnType<typeof useData>
|
||||||
let ready!: () => void
|
let ready!: () => void
|
||||||
const mounted = new Promise<void>((resolve) => {
|
const mounted = new Promise<void>((resolve) => {
|
||||||
@@ -813,38 +821,44 @@ test("renders admitted prompts only after they become model-visible", async () =
|
|||||||
id: "evt_admitted_1",
|
id: "evt_admitted_1",
|
||||||
type: "session.next.prompt.admitted",
|
type: "session.next.prompt.admitted",
|
||||||
data: {
|
data: {
|
||||||
sessionID: "session-1",
|
sessionID,
|
||||||
messageID: "msg_user_1",
|
messageID,
|
||||||
timestamp: 0,
|
timestamp: 0,
|
||||||
prompt: { text: "hello" },
|
prompt: { text: "hello" },
|
||||||
delivery: "steer",
|
delivery: "steer",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expect(sync.session.message.list("session-1") ?? []).toEqual([])
|
await wait(() => sync.session.message.list(sessionID)?.length === 1)
|
||||||
|
const admitted = sync.session.message.list(sessionID)?.[0]
|
||||||
|
expect(admitted).toMatchObject({ id: messageID, type: "user", text: "hello", metadata: { queued: true } })
|
||||||
|
|
||||||
|
await sync.session.message.refresh(sessionID)
|
||||||
|
expect(sync.session.message.list(sessionID)?.[0]?.metadata?.queued).toBeUndefined()
|
||||||
|
|
||||||
emitEvent(events, {
|
emitEvent(events, {
|
||||||
id: "evt_prompted_1",
|
id: "evt_prompted_1",
|
||||||
type: "session.next.prompted",
|
type: "session.next.prompted",
|
||||||
data: {
|
data: {
|
||||||
sessionID: "session-1",
|
sessionID,
|
||||||
messageID: "msg_user_1",
|
messageID,
|
||||||
timestamp: 0,
|
timestamp: 0,
|
||||||
prompt: { text: "hello" },
|
prompt: { text: "hello" },
|
||||||
delivery: "steer",
|
delivery: "steer",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await wait(() => sync.session.message.list("session-1")?.length === 1)
|
await wait(() => received.at(-1) === "session.next.prompted")
|
||||||
expect(received.slice(-2)).toEqual(["session.next.prompt.admitted", "session.next.prompted"])
|
expect(received.slice(-2)).toEqual(["session.next.prompt.admitted", "session.next.prompted"])
|
||||||
unsubscribe()
|
unsubscribe()
|
||||||
const message = sync.session.message.list("session-1")?.[0]
|
const message = sync.session.message.list(sessionID)?.[0]
|
||||||
expect(message?.type).toBe("user")
|
expect(message?.type).toBe("user")
|
||||||
if (message?.type !== "user") return
|
if (message?.type !== "user") return
|
||||||
expect(message).toMatchObject({ id: "msg_user_1", text: "hello" })
|
expect(message).toMatchObject({ id: messageID, text: "hello" })
|
||||||
expect(sync.session.message.ids("session-1")).toEqual(["msg_user_1"])
|
expect(message.metadata?.queued).toBeUndefined()
|
||||||
|
expect(sync.session.message.ids(sessionID)).toEqual([messageID])
|
||||||
expect(sync.session.message.ids("missing")).toEqual([])
|
expect(sync.session.message.ids("missing")).toEqual([])
|
||||||
expect(sync.session.message.get("session-1", "msg_user_1")).toBe(message)
|
expect(sync.session.message.get(sessionID, messageID)).toBe(message)
|
||||||
expect(sync.session.message.get("session-1", "missing")).toBeUndefined()
|
expect(sync.session.message.get(sessionID, "missing")).toBeUndefined()
|
||||||
expect(received).toHaveLength(3)
|
expect(received).toHaveLength(3)
|
||||||
} finally {
|
} finally {
|
||||||
app.renderer.destroy()
|
app.renderer.destroy()
|
||||||
|
|||||||
Reference in New Issue
Block a user