add run --replay mode (#30239)
This commit is contained in:
@@ -14,13 +14,14 @@
|
||||
// 4. runs the prompt queue until the footer closes.
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { MessageID } from "@/session/schema"
|
||||
import { createRunDemo } from "./demo"
|
||||
import { resolveModelInfo, resolveRunTuiConfig, resolveSessionInfo } from "./runtime.boot"
|
||||
import { createRuntimeLifecycle } from "./runtime.lifecycle"
|
||||
import { recordRunSpanError, setRunSpanAttributes, withRunSpan } from "./otel"
|
||||
import { trace } from "./trace"
|
||||
import { cycleVariant, formatModelLabel, resolveSavedVariant, resolveVariant, saveVariant } from "./variant.shared"
|
||||
import type { RunInput, RunPrompt, RunProvider } from "./types"
|
||||
import type { LocalReplayAnchor, LocalReplayRow, RunInput, RunPrompt, RunProvider, StreamCommit } from "./types"
|
||||
|
||||
/** @internal Exported for testing */
|
||||
export { pickVariant, resolveVariant } from "./variant.shared"
|
||||
@@ -114,6 +115,7 @@ type RuntimeState = {
|
||||
activeVariant: string | undefined
|
||||
sessionID: string
|
||||
history: RunPrompt[]
|
||||
localRows: LocalReplayRow[]
|
||||
sessionTitle?: string
|
||||
agent: string | undefined
|
||||
switching?: Promise<void>
|
||||
@@ -139,6 +141,9 @@ function variantsFor(providers: RunProvider[], model: RunInput["model"]) {
|
||||
return Object.keys(providers.find((item) => item.id === model.providerID)?.models?.[model.modelID]?.variants ?? {})
|
||||
}
|
||||
|
||||
const REPLAY_RESIZE_DELAY = 250
|
||||
const LOCAL_REPLAY_ROW_LIMIT = 100
|
||||
|
||||
async function resolveExitTitle(
|
||||
ctx: BootContext,
|
||||
input: RunRuntimeInput,
|
||||
@@ -196,6 +201,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
|
||||
activeVariant: resolveVariant(ctx.variant, session.variant, savedVariant, []),
|
||||
sessionID: ctx.sessionID,
|
||||
history: [...session.history],
|
||||
localRows: [],
|
||||
sessionTitle: ctx.sessionTitle,
|
||||
agent: ctx.agent,
|
||||
}
|
||||
@@ -374,6 +380,9 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
|
||||
},
|
||||
})
|
||||
const footer = shell.footer
|
||||
const rememberLocal = (commit: StreamCommit, after?: LocalReplayAnchor) => {
|
||||
state.localRows = [...state.localRows, { commit, after }].slice(-LOCAL_REPLAY_ROW_LIMIT)
|
||||
}
|
||||
|
||||
const loadCatalog = async (): Promise<void> => {
|
||||
if (footer.isClosed) {
|
||||
@@ -510,6 +519,36 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
|
||||
return next
|
||||
}
|
||||
|
||||
let replayResizeTimer: ReturnType<typeof setTimeout> | undefined
|
||||
const offResize = input.replay
|
||||
? shell.onResize(() => {
|
||||
if (replayResizeTimer) {
|
||||
clearTimeout(replayResizeTimer)
|
||||
}
|
||||
|
||||
replayResizeTimer = setTimeout(() => {
|
||||
replayResizeTimer = undefined
|
||||
if (footer.isClosed || !state.stream) {
|
||||
return
|
||||
}
|
||||
|
||||
void state.stream
|
||||
.then((item) =>
|
||||
item.handle.replayOnResize({
|
||||
localRows: () => state.localRows,
|
||||
reset: () =>
|
||||
shell.resetForReplay({
|
||||
sessionTitle: state.sessionTitle,
|
||||
sessionID: state.sessionID,
|
||||
history: state.history,
|
||||
}),
|
||||
}),
|
||||
)
|
||||
.catch(() => {})
|
||||
}, REPLAY_RESIZE_DELAY)
|
||||
})
|
||||
: () => {}
|
||||
|
||||
const runQueue = async () => {
|
||||
let includeFiles = true
|
||||
if (state.demo) {
|
||||
@@ -525,6 +564,15 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
|
||||
onSend: (prompt) => {
|
||||
state.shown = true
|
||||
state.history.push(prompt)
|
||||
if (prompt.mode !== "shell") {
|
||||
rememberLocal({
|
||||
kind: "user",
|
||||
text: prompt.text,
|
||||
phase: "start",
|
||||
source: "system",
|
||||
messageID: prompt.messageID,
|
||||
})
|
||||
}
|
||||
},
|
||||
onNewSession: createSession
|
||||
? async () => {
|
||||
@@ -545,6 +593,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
|
||||
state.sessionTitle = created.sessionTitle
|
||||
state.agent = created.agent ?? state.agent
|
||||
state.history = []
|
||||
state.localRows = []
|
||||
includeFiles = true
|
||||
state.demo = input.demo
|
||||
? createRunDemo({
|
||||
@@ -598,12 +647,15 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
|
||||
status: "failed to start new session",
|
||||
},
|
||||
})
|
||||
footer.append({
|
||||
const commit = {
|
||||
kind: "error",
|
||||
text: error instanceof Error ? error.message : String(error),
|
||||
phase: "start",
|
||||
source: "system",
|
||||
})
|
||||
messageID: MessageID.ascending(),
|
||||
} as const
|
||||
rememberLocal(commit)
|
||||
footer.append(commit)
|
||||
}
|
||||
}
|
||||
: undefined,
|
||||
@@ -614,6 +666,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
|
||||
|
||||
await state.switching?.catch(() => {})
|
||||
|
||||
let outputAnchor: LocalReplayAnchor | undefined
|
||||
return withRunSpan(
|
||||
"RunInteractive.turn",
|
||||
{
|
||||
@@ -644,8 +697,16 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
|
||||
prompt,
|
||||
files: input.files,
|
||||
includeFiles,
|
||||
onVisibleOutput: (anchor) => {
|
||||
outputAnchor = anchor
|
||||
},
|
||||
signal,
|
||||
})
|
||||
if (prompt.messageID) {
|
||||
state.localRows = state.localRows.filter(
|
||||
(row) => row.commit.kind !== "user" || row.commit.messageID !== prompt.messageID,
|
||||
)
|
||||
}
|
||||
includeFiles = false
|
||||
} catch (error) {
|
||||
if (signal.aborted || footer.isClosed) {
|
||||
@@ -656,7 +717,15 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
|
||||
const text =
|
||||
(await state.stream?.then((item) => item.mod).catch(() => undefined))?.formatUnknownError(error) ??
|
||||
(error instanceof Error ? error.message : String(error))
|
||||
footer.append({ kind: "error", text, phase: "start", source: "system" })
|
||||
const commit = {
|
||||
kind: "error",
|
||||
text,
|
||||
phase: "start",
|
||||
source: "system",
|
||||
messageID: prompt.messageID,
|
||||
} as const
|
||||
rememberLocal(commit, outputAnchor)
|
||||
footer.append(commit)
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -683,6 +752,10 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
|
||||
try {
|
||||
await runQueue()
|
||||
} finally {
|
||||
if (replayResizeTimer) {
|
||||
clearTimeout(replayResizeTimer)
|
||||
}
|
||||
offResize()
|
||||
await state.stream?.then((item) => item.handle.close()).catch(() => {})
|
||||
}
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user