Skip to content

Commit 6a3b2f3

Browse files
authored
add run --replay mode (anomalyco#30239)
1 parent d85f8cd commit 6a3b2f3

16 files changed

Lines changed: 1292 additions & 58 deletions

‎bun.lock‎

Lines changed: 15 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@
3838
"@types/cross-spawn": "6.0.6",
3939
"@octokit/rest": "22.0.0",
4040
"@hono/zod-validator": "0.4.2",
41-
"@opentui/core": "0.3.0",
42-
"@opentui/keymap": "0.3.0",
43-
"@opentui/solid": "0.3.0",
41+
"@opentui/core": "0.3.1",
42+
"@opentui/keymap": "0.3.1",
43+
"@opentui/solid": "0.3.1",
4444
"ulid": "3.0.1",
4545
"@kobalte/core": "0.13.11",
4646
"@types/luxon": "3.7.1",

‎packages/opencode/src/cli/cmd/run.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ export const RunCommand = effectCmd({
221221
.option("replay", {
222222
type: "boolean",
223223
default: false,
224-
describe: "replay visible session history on interactive resume",
224+
describe: "replay interactive session history on resume and after resize",
225225
})
226226
.option("replay-limit", {
227227
type: "number",

‎packages/opencode/src/cli/cmd/run/footer.ts‎

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ export class RunFooter implements FooterApi {
171171
private queue: StreamCommit[] = []
172172
private pending = false
173173
private flushing: Promise<void> = Promise.resolve()
174+
private flushError: unknown
174175
// Fixed portion of footer height above the textarea.
175176
private base: number
176177
private rows = TEXTAREA_MIN_ROWS
@@ -204,6 +205,15 @@ export class RunFooter implements FooterApi {
204205
private requestExitHandler: (() => boolean) | undefined
205206
private scrollback: RunScrollbackStream
206207

208+
private createScrollback(wrote: boolean): RunScrollbackStream {
209+
return new RunScrollbackStream(this.renderer, this.options.theme, {
210+
diffStyle: this.options.diffStyle,
211+
wrote,
212+
sessionID: this.options.sessionID,
213+
treeSitterClient: this.options.treeSitterClient,
214+
})
215+
}
216+
207217
constructor(
208218
private renderer: CliRenderer,
209219
private options: RunFooterOptions,
@@ -257,12 +267,7 @@ export class RunFooter implements FooterApi {
257267
this.queuedPrompts = queuedPrompts
258268
this.setQueuedPrompts = setQueuedPrompts
259269
this.base = Math.max(1, renderer.footerHeight - TEXTAREA_MIN_ROWS)
260-
this.scrollback = new RunScrollbackStream(renderer, options.theme, {
261-
diffStyle: options.diffStyle,
262-
wrote: options.wrote,
263-
sessionID: options.sessionID,
264-
treeSitterClient: options.treeSitterClient,
265-
})
270+
this.scrollback = this.createScrollback(options.wrote ?? false)
266271

267272
this.renderer.on(CliRenderEvents.DESTROY, this.handleDestroy)
268273

@@ -465,7 +470,9 @@ export class RunFooter implements FooterApi {
465470
},
466471
),
467472
)
468-
.catch(() => {})
473+
.catch((error) => {
474+
this.flushError = error
475+
})
469476
}
470477

471478
private present(view: FooterView): void {
@@ -523,6 +530,12 @@ export class RunFooter implements FooterApi {
523530
}
524531

525532
return this.flushing.then(async () => {
533+
if (this.flushError !== undefined) {
534+
const error = this.flushError
535+
this.flushError = undefined
536+
throw error
537+
}
538+
526539
if (this.isGone) {
527540
return
528541
}
@@ -535,6 +548,15 @@ export class RunFooter implements FooterApi {
535548
})
536549
}
537550

551+
public resetForReplay(wrote: boolean): void {
552+
if (this.isGone) {
553+
return
554+
}
555+
556+
this.scrollback.destroy()
557+
this.scrollback = this.createScrollback(wrote)
558+
}
559+
538560
public close(): void {
539561
if (this.closed) {
540562
return
@@ -936,6 +958,8 @@ export class RunFooter implements FooterApi {
936958
},
937959
),
938960
)
939-
.catch(() => {})
961+
.catch((error) => {
962+
this.flushError = error
963+
})
940964
}
941965
}

‎packages/opencode/src/cli/cmd/run/runtime.lifecycle.ts‎

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
//
99
// Also wires SIGINT so Ctrl-c clears a live prompt draft first, then falls
1010
// back to the usual two-press exit sequence through RunFooter.requestExit().
11-
import { createCliRenderer, type CliRenderer, type ScrollbackWriter } from "@opentui/core"
11+
import { CliRenderEvents, createCliRenderer, type CliRenderer, type ScrollbackWriter } from "@opentui/core"
1212
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
1313
import { Session as SessionApi } from "@/session/session"
1414
import { registerOpencodeKeymap } from "@/cli/cmd/tui/keymap"
@@ -75,6 +75,8 @@ export type LifecycleInput = {
7575

7676
export type Lifecycle = {
7777
footer: FooterApi
78+
onResize(fn: () => void): () => void
79+
resetForReplay(input: { sessionTitle?: string; sessionID?: string; history: RunPrompt[] }): Promise<void>
7880
close(input: { showExit: boolean; sessionTitle?: string; sessionID?: string; history?: RunPrompt[] }): Promise<void>
7981
}
8082

@@ -307,6 +309,46 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
307309

308310
return {
309311
footer,
312+
onResize(fn) {
313+
let width = renderer.terminalWidth
314+
let height = renderer.terminalHeight
315+
const resize = () => {
316+
if (width === renderer.terminalWidth && height === renderer.terminalHeight) {
317+
return
318+
}
319+
320+
width = renderer.terminalWidth
321+
height = renderer.terminalHeight
322+
fn()
323+
}
324+
renderer.on(CliRenderEvents.RESIZE, resize)
325+
return () => renderer.off(CliRenderEvents.RESIZE, resize)
326+
},
327+
async resetForReplay(next) {
328+
if (closed || renderer.isDestroyed || footer.isClosed) {
329+
throw new Error("runtime closed")
330+
}
331+
332+
await footer.idle()
333+
if (closed || renderer.isDestroyed || footer.isClosed) {
334+
throw new Error("runtime closed")
335+
}
336+
337+
footer.resetForReplay(true)
338+
renderer.resetSplitFooterForReplay({ clearSavedLines: true })
339+
const splash = splashInfo(next.sessionTitle ?? input.sessionTitle, next.history)
340+
renderer.writeToScrollback(
341+
entrySplash({
342+
...splashMeta({
343+
title: splash.title,
344+
session_id: next.sessionID ?? input.getSessionID?.() ?? input.sessionID,
345+
}),
346+
theme: theme.splash,
347+
showSession: splash.showSession,
348+
}),
349+
)
350+
renderer.requestRender()
351+
},
310352
close,
311353
}
312354
} catch (error) {

‎packages/opencode/src/cli/cmd/run/runtime.queue.ts‎

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,14 @@ export async function runPromptQueue(input: QueueInput): Promise<void> {
162162
continue
163163
}
164164

165-
state.active = prompt
165+
const sent =
166+
prompt.mode === "shell"
167+
? prompt
168+
: {
169+
...prompt,
170+
messageID: prompt.messageID ?? queued?.messageID ?? MessageID.ascending(),
171+
}
172+
state.active = sent
166173

167174
emit(
168175
{
@@ -185,18 +192,24 @@ export async function runPromptQueue(input: QueueInput): Promise<void> {
185192
break
186193
}
187194

188-
if (prompt.mode !== "shell") {
189-
const commit = { kind: "user", text: prompt.text, phase: "start", source: "system" } as const
195+
if (sent.mode !== "shell") {
196+
const commit = {
197+
kind: "user",
198+
text: sent.text,
199+
phase: "start",
200+
source: "system",
201+
messageID: sent.messageID,
202+
} as const
190203
input.trace?.write("ui.commit", commit)
191204
input.footer.append(commit)
192205
}
193-
input.onSend?.(prompt)
206+
input.onSend?.(sent)
194207

195208
if (state.closed) {
196209
break
197210
}
198211

199-
const task = input.run(prompt, ctrl.signal).then(
212+
const task = input.run(sent, ctrl.signal).then(
200213
() => ({ type: "done" as const }),
201214
(error) => ({ type: "error" as const, error }),
202215
)

0 commit comments

Comments
 (0)