run: replay session history on interactive resume (#26880)
This commit is contained in:
@@ -96,6 +96,17 @@ function assistant(text: string, phase: StreamCommit["phase"] = "progress"): Str
|
||||
}
|
||||
}
|
||||
|
||||
function reasoning(text: string, phase: StreamCommit["phase"] = "progress"): StreamCommit {
|
||||
return {
|
||||
kind: "reasoning",
|
||||
text,
|
||||
phase,
|
||||
source: "reasoning",
|
||||
messageID: "msg-r-1",
|
||||
partID: "part-r-1",
|
||||
}
|
||||
}
|
||||
|
||||
function user(text: string): StreamCommit {
|
||||
return {
|
||||
kind: "user",
|
||||
@@ -392,6 +403,39 @@ test("inserts spacers for new visible groups", async () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("renders replayed user, reasoning, and assistant output after completion", async () => {
|
||||
const out = await setup()
|
||||
|
||||
try {
|
||||
const lines: string[] = []
|
||||
const take = () => {
|
||||
const commits = claim(out.renderer)
|
||||
try {
|
||||
lines.push(...commits.flatMap((commit) => renderRows(commit).flatMap((row) => row.split("\n"))))
|
||||
} finally {
|
||||
destroy(commits)
|
||||
}
|
||||
}
|
||||
|
||||
await out.scrollback.append(user("Hello you"))
|
||||
take()
|
||||
await out.scrollback.append(reasoning("Thinking: **Plan**\n\nSay hello.", "progress"))
|
||||
await out.scrollback.complete()
|
||||
take()
|
||||
await out.scrollback.append(assistant("Hello.", "progress"))
|
||||
await out.scrollback.complete()
|
||||
take()
|
||||
|
||||
const output = lines.join("\n")
|
||||
expect(output).toContain("› Hello you")
|
||||
expect(output).toContain("Thinking:")
|
||||
expect(output).toContain("Plan")
|
||||
expect(output).toContain("Hello.")
|
||||
} finally {
|
||||
out.scrollback.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("coalesces same-line tool progress into one snapshot", async () => {
|
||||
const out = await setup()
|
||||
|
||||
|
||||
156
packages/opencode/test/cli/run/session-replay.test.ts
Normal file
156
packages/opencode/test/cli/run/session-replay.test.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { replaySession } from "@/cli/cmd/run/session-replay"
|
||||
import type { SessionMessages } from "@/cli/cmd/run/session.shared"
|
||||
|
||||
function userMessage(id: string, text: string): SessionMessages[number] {
|
||||
return {
|
||||
info: {
|
||||
id,
|
||||
sessionID: "session-1",
|
||||
role: "user",
|
||||
time: {
|
||||
created: 1,
|
||||
},
|
||||
agent: "build",
|
||||
model: {
|
||||
providerID: "openai",
|
||||
modelID: "gpt-5",
|
||||
},
|
||||
},
|
||||
parts: [
|
||||
{
|
||||
id: `${id}-text`,
|
||||
sessionID: "session-1",
|
||||
messageID: id,
|
||||
type: "text",
|
||||
text,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function assistantInfo(id: string) {
|
||||
return {
|
||||
id,
|
||||
sessionID: "session-1",
|
||||
role: "assistant" as const,
|
||||
time: {
|
||||
created: 2,
|
||||
},
|
||||
parentID: "msg-user-1",
|
||||
modelID: "gpt-5",
|
||||
providerID: "openai",
|
||||
mode: "chat",
|
||||
agent: "build",
|
||||
path: {
|
||||
cwd: "/tmp",
|
||||
root: "/tmp",
|
||||
},
|
||||
cost: 0,
|
||||
tokens: {
|
||||
input: 1,
|
||||
output: 1,
|
||||
reasoning: 0,
|
||||
cache: {
|
||||
read: 0,
|
||||
write: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function assistantMessage(id: string, text: string): SessionMessages[number] {
|
||||
return {
|
||||
info: assistantInfo(id),
|
||||
parts: [
|
||||
{
|
||||
id: `${id}-text`,
|
||||
sessionID: "session-1",
|
||||
messageID: id,
|
||||
type: "text",
|
||||
text,
|
||||
time: {
|
||||
start: 2,
|
||||
end: 3,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function runningToolMessage(id: string): SessionMessages[number] {
|
||||
return {
|
||||
info: assistantInfo(id),
|
||||
parts: [
|
||||
{
|
||||
id: `${id}-tool`,
|
||||
sessionID: "session-1",
|
||||
messageID: id,
|
||||
type: "tool",
|
||||
callID: `${id}-call`,
|
||||
tool: "bash",
|
||||
state: {
|
||||
status: "running",
|
||||
input: {
|
||||
command: "pwd",
|
||||
},
|
||||
time: {
|
||||
start: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
describe("run session replay", () => {
|
||||
test("replays persisted user and assistant history into scrollback commits", () => {
|
||||
const out = replaySession({
|
||||
messages: [userMessage("msg-user-1", "Hello, whats the weather today?"), assistantMessage("msg-1", "What city or ZIP code should I check?")],
|
||||
permissions: [],
|
||||
questions: [],
|
||||
thinking: true,
|
||||
limits: {},
|
||||
})
|
||||
|
||||
expect(out.commits).toEqual([
|
||||
expect.objectContaining({
|
||||
kind: "user",
|
||||
text: "Hello, whats the weather today?",
|
||||
phase: "start",
|
||||
source: "system",
|
||||
messageID: "msg-user-1",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
kind: "assistant",
|
||||
text: "What city or ZIP code should I check?",
|
||||
phase: "progress",
|
||||
source: "assistant",
|
||||
messageID: "msg-1",
|
||||
}),
|
||||
])
|
||||
expect(out.patch).toEqual(
|
||||
expect.objectContaining({
|
||||
phase: "idle",
|
||||
status: "",
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test("keeps the footer in a running state for resumed active tools", () => {
|
||||
const out = replaySession({
|
||||
messages: [runningToolMessage("msg-1")],
|
||||
permissions: [],
|
||||
questions: [],
|
||||
thinking: true,
|
||||
limits: {},
|
||||
})
|
||||
|
||||
expect(out.patch).toEqual(
|
||||
expect.objectContaining({
|
||||
phase: "running",
|
||||
status: "running bash",
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -67,6 +67,22 @@ function idle(sessionID = "session-1") {
|
||||
} satisfies SdkEvent
|
||||
}
|
||||
|
||||
function retry(sessionID: string, attempt: number, message: string) {
|
||||
return {
|
||||
id: `evt-${sessionID}-retry-${attempt}`,
|
||||
type: "session.status",
|
||||
properties: {
|
||||
sessionID,
|
||||
status: {
|
||||
type: "retry",
|
||||
attempt,
|
||||
message,
|
||||
next: 1,
|
||||
},
|
||||
},
|
||||
} satisfies SdkEvent
|
||||
}
|
||||
|
||||
function assistant(id: string) {
|
||||
return {
|
||||
id: `evt-${id}`,
|
||||
@@ -290,12 +306,12 @@ function toolUpdated(part: SessionToolPart): SdkEvent {
|
||||
}
|
||||
}
|
||||
|
||||
function textDelta(messageID: string, partID: string, delta: string): SdkEvent {
|
||||
function textDelta(messageID: string, partID: string, delta: string, sessionID = "session-1"): SdkEvent {
|
||||
return {
|
||||
id: `evt-${partID}-delta`,
|
||||
type: "message.part.delta",
|
||||
properties: {
|
||||
sessionID: "session-1",
|
||||
sessionID,
|
||||
messageID,
|
||||
partID,
|
||||
field: "text",
|
||||
@@ -331,6 +347,7 @@ function footer(fn?: (commit: StreamCommit) => void) {
|
||||
const commits: StreamCommit[] = []
|
||||
const events: FooterEvent[] = []
|
||||
let closed = false
|
||||
let idleCalls = 0
|
||||
|
||||
const api: FooterApi = {
|
||||
get isClosed() {
|
||||
@@ -346,6 +363,7 @@ function footer(fn?: (commit: StreamCommit) => void) {
|
||||
fn?.(next)
|
||||
},
|
||||
idle() {
|
||||
idleCalls += 1
|
||||
return Promise.resolve()
|
||||
},
|
||||
close() {
|
||||
@@ -356,7 +374,7 @@ function footer(fn?: (commit: StreamCommit) => void) {
|
||||
},
|
||||
}
|
||||
|
||||
return { api, commits, events }
|
||||
return { api, commits, events, get idleCalls() { return idleCalls } }
|
||||
}
|
||||
|
||||
function sdk(
|
||||
@@ -398,6 +416,355 @@ function sdk(
|
||||
}
|
||||
|
||||
describe("run stream transport", () => {
|
||||
test("does not replay persisted main-session history during bootstrap by default", async () => {
|
||||
const src = eventFeed()
|
||||
const ui = footer()
|
||||
const transport = await createSessionTransport({
|
||||
sdk: sdk({
|
||||
stream: src.stream,
|
||||
messages: async ({ sessionID }) =>
|
||||
sessionID === "session-1"
|
||||
? ok([
|
||||
assistantMessage({
|
||||
sessionID: "session-1",
|
||||
id: "msg-1",
|
||||
parts: [
|
||||
{
|
||||
...textPart("text-1", "msg-1", "Hello."),
|
||||
time: {
|
||||
start: 1,
|
||||
end: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
: ok([]),
|
||||
}),
|
||||
sessionID: "session-1",
|
||||
thinking: true,
|
||||
limits: () => ({}),
|
||||
footer: ui.api,
|
||||
})
|
||||
|
||||
try {
|
||||
expect(ui.commits).toEqual([])
|
||||
expect(ui.idleCalls).toBe(0)
|
||||
} finally {
|
||||
src.close()
|
||||
await transport.close()
|
||||
}
|
||||
})
|
||||
|
||||
test("replays persisted main-session history during bootstrap when enabled", async () => {
|
||||
const src = eventFeed()
|
||||
const ui = footer()
|
||||
const transport = await createSessionTransport({
|
||||
sdk: sdk({
|
||||
stream: src.stream,
|
||||
messages: async ({ sessionID }) =>
|
||||
sessionID === "session-1"
|
||||
? ok([
|
||||
assistantMessage({
|
||||
sessionID: "session-1",
|
||||
id: "msg-1",
|
||||
parts: [
|
||||
{
|
||||
...textPart("text-1", "msg-1", "Hello."),
|
||||
time: {
|
||||
start: 1,
|
||||
end: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
: ok([]),
|
||||
}),
|
||||
sessionID: "session-1",
|
||||
thinking: true,
|
||||
replay: true,
|
||||
limits: () => ({}),
|
||||
footer: ui.api,
|
||||
})
|
||||
|
||||
try {
|
||||
await waitFor(() => ui.commits.find((item) => item.kind === "assistant" && item.text === "Hello."))
|
||||
expect(ui.idleCalls).toBeGreaterThan(0)
|
||||
} finally {
|
||||
src.close()
|
||||
await transport.close()
|
||||
}
|
||||
})
|
||||
|
||||
test("caps replayed bootstrap history to the configured number of messages", async () => {
|
||||
const src = eventFeed()
|
||||
const ui = footer()
|
||||
const transport = await createSessionTransport({
|
||||
sdk: sdk({
|
||||
stream: src.stream,
|
||||
messages: async ({ sessionID }) =>
|
||||
ok(
|
||||
sessionID === "session-1"
|
||||
? [
|
||||
assistantMessage({
|
||||
sessionID: "session-1",
|
||||
id: "msg-1",
|
||||
parts: [
|
||||
{
|
||||
...textPart("text-1", "msg-1", "Hello."),
|
||||
time: {
|
||||
start: 1,
|
||||
end: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
assistantMessage({
|
||||
sessionID: "session-1",
|
||||
id: "msg-2",
|
||||
parts: [
|
||||
{
|
||||
...textPart("text-2", "msg-2", "World."),
|
||||
time: {
|
||||
start: 3,
|
||||
end: 4,
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
]
|
||||
: [],
|
||||
),
|
||||
}),
|
||||
sessionID: "session-1",
|
||||
thinking: true,
|
||||
replay: true,
|
||||
replayLimit: 1,
|
||||
limits: () => ({}),
|
||||
footer: ui.api,
|
||||
})
|
||||
|
||||
try {
|
||||
await waitFor(() => (ui.commits.length > 0 ? ui.commits : undefined))
|
||||
expect(ui.commits.filter((item) => item.kind === "assistant")).toEqual([
|
||||
expect.objectContaining({
|
||||
text: "World.",
|
||||
}),
|
||||
])
|
||||
} finally {
|
||||
src.close()
|
||||
await transport.close()
|
||||
}
|
||||
})
|
||||
|
||||
test("skips buffered pre-bootstrap deltas already covered by replay history", async () => {
|
||||
const src = eventFeed()
|
||||
const ui = footer()
|
||||
const gate = defer<void>()
|
||||
let transport: Awaited<ReturnType<typeof createSessionTransport>> | undefined
|
||||
const task = createSessionTransport({
|
||||
sdk: sdk({
|
||||
stream: src.stream,
|
||||
messages: async ({ sessionID }) => {
|
||||
if (sessionID !== "session-1") {
|
||||
return ok([])
|
||||
}
|
||||
|
||||
await gate.promise
|
||||
return ok([
|
||||
assistantMessage({
|
||||
sessionID: "session-1",
|
||||
id: "msg-1",
|
||||
parts: [textPart("text-1", "msg-1", "Hello")],
|
||||
}),
|
||||
])
|
||||
},
|
||||
}),
|
||||
sessionID: "session-1",
|
||||
thinking: true,
|
||||
replay: true,
|
||||
limits: () => ({}),
|
||||
footer: ui.api,
|
||||
})
|
||||
|
||||
try {
|
||||
await Promise.resolve()
|
||||
src.push(textDelta("msg-1", "text-1", "lo"))
|
||||
gate.resolve()
|
||||
transport = await task
|
||||
|
||||
await waitFor(() => (ui.commits.length > 0 ? ui.commits : undefined))
|
||||
await Bun.sleep(20)
|
||||
expect(ui.commits.filter((item) => item.kind === "assistant")).toEqual([
|
||||
expect.objectContaining({
|
||||
text: "Hello",
|
||||
}),
|
||||
])
|
||||
} finally {
|
||||
src.close()
|
||||
await transport?.close()
|
||||
}
|
||||
})
|
||||
|
||||
test("applies buffered pre-bootstrap deltas not yet persisted", async () => {
|
||||
const src = eventFeed()
|
||||
const ui = footer()
|
||||
const gate = defer<void>()
|
||||
let transport: Awaited<ReturnType<typeof createSessionTransport>> | undefined
|
||||
const task = createSessionTransport({
|
||||
sdk: sdk({
|
||||
stream: src.stream,
|
||||
messages: async ({ sessionID }) => {
|
||||
if (sessionID !== "session-1") {
|
||||
return ok([])
|
||||
}
|
||||
|
||||
await gate.promise
|
||||
return ok([
|
||||
assistantMessage({
|
||||
sessionID: "session-1",
|
||||
id: "msg-1",
|
||||
parts: [textPart("text-1", "msg-1", "")],
|
||||
}),
|
||||
])
|
||||
},
|
||||
}),
|
||||
sessionID: "session-1",
|
||||
thinking: true,
|
||||
replay: true,
|
||||
limits: () => ({}),
|
||||
footer: ui.api,
|
||||
})
|
||||
|
||||
try {
|
||||
await Promise.resolve()
|
||||
src.push(textDelta("msg-1", "text-1", "Hello"))
|
||||
gate.resolve()
|
||||
transport = await task
|
||||
|
||||
await waitFor(() => (ui.commits.length > 0 ? ui.commits : undefined))
|
||||
await Bun.sleep(20)
|
||||
expect(ui.commits.filter((item) => item.kind === "assistant")).toEqual([
|
||||
expect.objectContaining({
|
||||
text: "Hello",
|
||||
}),
|
||||
])
|
||||
} finally {
|
||||
src.close()
|
||||
await transport?.close()
|
||||
}
|
||||
})
|
||||
|
||||
test("preserves running footer state for resumed active sessions", async () => {
|
||||
const src = eventFeed()
|
||||
const ui = footer()
|
||||
const transport = await createSessionTransport({
|
||||
sdk: sdk({
|
||||
stream: src.stream,
|
||||
messages: async ({ sessionID }) =>
|
||||
sessionID === "session-1"
|
||||
? ok([
|
||||
assistantMessage({
|
||||
sessionID: "session-1",
|
||||
id: "msg-1",
|
||||
parts: [
|
||||
runningTool({
|
||||
sessionID: "session-1",
|
||||
messageID: "msg-1",
|
||||
id: "bash-1",
|
||||
callID: "call-1",
|
||||
tool: "bash",
|
||||
body: {
|
||||
command: "pwd",
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
])
|
||||
: ok([]),
|
||||
}),
|
||||
sessionID: "session-1",
|
||||
thinking: true,
|
||||
replay: true,
|
||||
limits: () => ({}),
|
||||
footer: ui.api,
|
||||
})
|
||||
|
||||
try {
|
||||
const patch = await waitFor(() => {
|
||||
const item = ui.events.findLast((event) => event.type === "stream.patch")
|
||||
return item?.type === "stream.patch" ? item.patch : undefined
|
||||
})
|
||||
|
||||
expect(patch).toEqual(
|
||||
expect.objectContaining({
|
||||
phase: "running",
|
||||
status: "running bash",
|
||||
}),
|
||||
)
|
||||
} finally {
|
||||
src.close()
|
||||
await transport.close()
|
||||
}
|
||||
})
|
||||
|
||||
test("drops completed historical subagent tabs during bootstrap", async () => {
|
||||
const src = eventFeed()
|
||||
const ui = footer()
|
||||
const transport = await createSessionTransport({
|
||||
sdk: sdk({
|
||||
stream: src.stream,
|
||||
messages: async ({ sessionID }) => {
|
||||
if (sessionID !== "session-1") {
|
||||
return ok([])
|
||||
}
|
||||
|
||||
return ok([
|
||||
assistantMessage({
|
||||
sessionID: "session-1",
|
||||
id: "msg-1",
|
||||
parts: [
|
||||
completedTool({
|
||||
sessionID: "session-1",
|
||||
messageID: "msg-1",
|
||||
id: "task-1",
|
||||
callID: "call-1",
|
||||
tool: "task",
|
||||
body: {
|
||||
description: "Explore run folder",
|
||||
subagent_type: "explore",
|
||||
},
|
||||
metadata: {
|
||||
sessionId: "child-1",
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
])
|
||||
},
|
||||
children: async () => ok([child("child-1")]),
|
||||
}),
|
||||
sessionID: "session-1",
|
||||
thinking: true,
|
||||
limits: () => ({}),
|
||||
footer: ui.api,
|
||||
})
|
||||
|
||||
try {
|
||||
const state = await waitFor(() => {
|
||||
const item = ui.events.findLast((event) => event.type === "stream.subagent")
|
||||
return item?.type === "stream.subagent" ? item.state : undefined
|
||||
})
|
||||
|
||||
expect(state.tabs).toEqual([])
|
||||
expect(state.details).toEqual({})
|
||||
} finally {
|
||||
src.close()
|
||||
await transport.close()
|
||||
}
|
||||
})
|
||||
|
||||
test("bootstraps child tabs and resumed blocker input", async () => {
|
||||
const src = eventFeed()
|
||||
const ui = footer()
|
||||
@@ -487,7 +854,7 @@ describe("run stream transport", () => {
|
||||
expect.objectContaining({
|
||||
sessionID: "child-1",
|
||||
label: "Explore",
|
||||
description: "Explore run folder",
|
||||
description: "Pending permission",
|
||||
status: "running",
|
||||
}),
|
||||
])
|
||||
@@ -565,16 +932,16 @@ describe("run stream transport", () => {
|
||||
messages: async ({ sessionID }) => {
|
||||
if (sessionID === "session-1") {
|
||||
return ok([
|
||||
assistantMessage({
|
||||
sessionID: "session-1",
|
||||
id: "msg-1",
|
||||
parts: [
|
||||
completedTool({
|
||||
sessionID: "session-1",
|
||||
messageID: "msg-1",
|
||||
id: "task-1",
|
||||
callID: "call-1",
|
||||
tool: "task",
|
||||
assistantMessage({
|
||||
sessionID: "session-1",
|
||||
id: "msg-1",
|
||||
parts: [
|
||||
runningTool({
|
||||
sessionID: "session-1",
|
||||
messageID: "msg-1",
|
||||
id: "task-1",
|
||||
callID: "call-1",
|
||||
tool: "task",
|
||||
body: {
|
||||
description: "Explore run.ts",
|
||||
subagent_type: "explore",
|
||||
@@ -582,10 +949,10 @@ describe("run stream transport", () => {
|
||||
metadata: {
|
||||
sessionId: "child-1",
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
])
|
||||
}),
|
||||
],
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
return sessionID === "child-1"
|
||||
@@ -711,6 +1078,109 @@ describe("run stream transport", () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("replays child events buffered during bootstrap once the tab is known", async () => {
|
||||
const global = globalFeed()
|
||||
const ui = footer()
|
||||
const gate = defer<void>()
|
||||
let transport: Awaited<ReturnType<typeof createSessionTransport>> | undefined
|
||||
const task = createSessionTransport({
|
||||
sdk: sdk({
|
||||
globalStream: global.stream,
|
||||
messages: async ({ sessionID }) => {
|
||||
if (sessionID !== "session-1") {
|
||||
return ok([])
|
||||
}
|
||||
|
||||
await gate.promise
|
||||
return ok([])
|
||||
},
|
||||
children: async () => ok([]),
|
||||
}),
|
||||
sessionID: "session-1",
|
||||
thinking: true,
|
||||
limits: () => ({}),
|
||||
footer: ui.api,
|
||||
})
|
||||
|
||||
try {
|
||||
await Promise.resolve()
|
||||
global.push(globalEvent(retry("child-1", 1, "retry child")))
|
||||
global.push(
|
||||
globalEvent({
|
||||
id: "evt-child-message",
|
||||
type: "message.updated",
|
||||
properties: {
|
||||
sessionID: "child-1",
|
||||
info: assistantMessage({
|
||||
sessionID: "child-1",
|
||||
id: "msg-child-1",
|
||||
parts: [],
|
||||
}).info,
|
||||
},
|
||||
}),
|
||||
)
|
||||
global.push(globalEvent(textUpdated(textPart("txt-child-1", "msg-child-1", "", "child-1"))))
|
||||
global.push(globalEvent(textDelta("msg-child-1", "txt-child-1", "Hello", "child-1")))
|
||||
global.push(
|
||||
globalEvent(
|
||||
toolUpdated(
|
||||
runningTool({
|
||||
sessionID: "session-1",
|
||||
messageID: "msg-1",
|
||||
id: "task-1",
|
||||
callID: "call-1",
|
||||
tool: "task",
|
||||
body: {
|
||||
description: "Explore run.ts",
|
||||
subagent_type: "explore",
|
||||
},
|
||||
metadata: {
|
||||
sessionId: "child-1",
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
gate.resolve()
|
||||
transport = await task
|
||||
|
||||
await waitFor(() => {
|
||||
const item = ui.events.findLast((event) => event.type === "stream.subagent")
|
||||
return item?.type === "stream.subagent" && item.state.tabs.some((tab) => tab.sessionID === "child-1")
|
||||
? item
|
||||
: undefined
|
||||
})
|
||||
|
||||
transport.selectSubagent("child-1")
|
||||
|
||||
const detail = await waitFor(() => {
|
||||
const item = ui.events.findLast((event) => event.type === "stream.subagent")
|
||||
const next = item?.type === "stream.subagent" ? item.state.details["child-1"] : undefined
|
||||
return next?.commits.some((commit) => commit.kind === "error" && commit.text === "retry child") &&
|
||||
next.commits.some((commit) => commit.kind === "assistant" && commit.text === "Hello")
|
||||
? next
|
||||
: undefined
|
||||
})
|
||||
|
||||
expect(detail).toEqual({
|
||||
sessionID: "child-1",
|
||||
commits: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: "error",
|
||||
text: "retry child",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
kind: "assistant",
|
||||
text: "Hello",
|
||||
}),
|
||||
]),
|
||||
})
|
||||
} finally {
|
||||
global.close()
|
||||
await transport?.close()
|
||||
}
|
||||
})
|
||||
|
||||
test("streams selected subagent output from global events while it is running", async () => {
|
||||
const global = globalFeed()
|
||||
const ui = footer()
|
||||
|
||||
Reference in New Issue
Block a user