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
21 changes: 20 additions & 1 deletion packages/tui/src/component/prompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ import { promptOffsetWidth } from "../../prompt/display"
import { createStore, produce, unwrap } from "solid-js/store"
import { usePromptHistory, type PromptInfo } from "../../prompt/history"
import { computePromptTraits } from "../../prompt/traits"
import { expandPastedTextPlaceholders, expandTrackedPastedText } from "../../prompt/part"
import { expandPastedTextPlaceholders, expandTrackedPastedText, stripPromptPartIDs } from "../../prompt/part"
import { queuedMessages } from "../../prompt/queued"
import { usePromptStash } from "../../prompt/stash"
import { DialogStash } from "../dialog-stash"
import { type AutocompleteRef, Autocomplete } from "./autocomplete"
Expand Down Expand Up @@ -411,6 +412,24 @@ export function Prompt(props: PromptProps) {
}, 5000)

if (store.interrupt >= 2) {
// Aborting drops queued follow-ups, so hand their text back unless a new draft started.
const queued =
store.prompt.input || store.prompt.parts.length > 0
? []
: queuedMessages(sync.data.message[props.sessionID] ?? [])
if (queued.length > 0)
ref.set(
queued
.flatMap((message) => sync.data.part[message.id] ?? [])
.reduce(
(agg, part) => {
if (part.type === "text" && !part.synthetic) agg.input += part.text
if (part.type === "file") agg.parts.push(stripPromptPartIDs(part))
return agg
},
{ input: "", parts: [] as PromptInfo["parts"] },
),
)
void sdk.client.session.abort({
sessionID: props.sessionID,
})
Expand Down
14 changes: 14 additions & 0 deletions packages/tui/src/prompt/queued.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* User messages submitted while a turn is still running. The server holds them until the
* turn finishes, so they are only queued while an assistant message is still incomplete.
*/
export function queuedMessages<
Message extends { id: string; role: string; time: { created: number; completed?: number } },
>(messages: Message[]) {
const completed = messages.findLast((message) => message.role === "assistant" && message.time.completed)?.id
const pending = messages.findLast(
(message) => message.role === "assistant" && !message.time.completed && (!completed || message.id > completed),
)?.id
if (!pending) return []
return messages.filter((message) => message.role === "user" && message.id > pending)
}
29 changes: 29 additions & 0 deletions packages/tui/test/prompt/queued.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, test } from "bun:test"
import { queuedMessages } from "../../src/prompt/queued"

const assistant = (id: string, completed?: number) => ({ id, role: "assistant", time: { created: 1, completed } })
const user = (id: string) => ({ id, role: "user", time: { created: 1 } })

describe("prompt queued", () => {
test("returns user messages sent while a turn is still running", () => {
expect(
queuedMessages([user("msg_1"), assistant("msg_2", 2), user("msg_3"), assistant("msg_4"), user("msg_5")]).map(
(message) => message.id,
),
).toEqual(["msg_5"])
})

test("returns every queued message when several stack up", () => {
expect(
queuedMessages([user("msg_1"), assistant("msg_2"), user("msg_3"), user("msg_4")]).map((message) => message.id),
).toEqual(["msg_3", "msg_4"])
})

test("returns nothing when the session is idle", () => {
expect(queuedMessages([user("msg_1"), assistant("msg_2", 2)])).toEqual([])
})

test("ignores an abandoned turn that a later turn already completed", () => {
expect(queuedMessages([user("msg_1"), assistant("msg_2"), assistant("msg_3", 3)])).toEqual([])
})
})
Loading