Skip to content

Commit 61b81eb

Browse files
committed
fix missing whitespace after first scrollback message
1 parent 3fb8f53 commit 61b81eb

4 files changed

Lines changed: 97 additions & 3 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ type RunFooterOptions = {
6363
findFiles: (query: string) => Promise<string[]>
6464
agents: RunAgent[]
6565
resources: RunResource[]
66+
wrote?: boolean
6667
sessionID: () => string | undefined
6768
agentLabel: string
6869
modelLabel: string
@@ -207,6 +208,7 @@ export class RunFooter implements FooterApi {
207208
this.interruptHint = printableBinding(options.keybinds.interrupt, options.keybinds.leader) || "esc"
208209
this.scrollback = new RunScrollbackStream(renderer, options.theme, {
209210
diffStyle: options.diffStyle,
211+
wrote: options.wrote,
210212
sessionID: options.sessionID,
211213
treeSitterClient: options.treeSitterClient,
212214
})

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
189189
session_id: input.sessionID,
190190
})
191191
const footerTask = import("./footer")
192-
queueSplash(
192+
const wrote = queueSplash(
193193
renderer,
194194
state,
195195
"entry",
@@ -219,6 +219,7 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
219219
first: input.first,
220220
history: input.history,
221221
theme,
222+
wrote,
222223
keybinds: input.keybinds,
223224
diffStyle: input.diffStyle,
224225
onPermissionReply: input.onPermissionReply,

packages/opencode/src/cli/cmd/run/scrollback.surface.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export class RunScrollbackStream {
7272
private diffStyle: RunDiffStyle | undefined
7373
private sessionID?: () => string | undefined
7474
private treeSitterClient: TreeSitterClient | undefined
75+
private wrote: boolean
7576

7677
constructor(
7778
private renderer: CliRenderer,
@@ -86,6 +87,7 @@ export class RunScrollbackStream {
8687
this.diffStyle = options.diffStyle
8788
this.sessionID = options.sessionID
8889
this.treeSitterClient = options.treeSitterClient ?? getTreeSitterClient()
90+
this.wrote = options.wrote ?? false
8991
}
9092

9193
private createEntry(commit: StreamCommit, body: ActiveBody): ActiveEntry {
@@ -131,6 +133,8 @@ export class RunScrollbackStream {
131133

132134
surface.root.add(renderable)
133135

136+
const rows = separatorRows(this.rendered, commit, body)
137+
134138
return {
135139
body,
136140
commit,
@@ -139,7 +143,7 @@ export class RunScrollbackStream {
139143
content: "",
140144
committedRows: 0,
141145
committedBlocks: 0,
142-
pendingSpacerRows: separatorRows(this.rendered, commit, body),
146+
pendingSpacerRows: rows || (!this.rendered && this.wrote ? 1 : 0),
143147
rendered: false,
144148
}
145149
}
@@ -158,6 +162,7 @@ export class RunScrollbackStream {
158162
}
159163

160164
this.renderer.writeToScrollback(spacerWriter())
165+
this.wrote = false
161166
}
162167

163168
private flushPendingSpacer(active: ActiveEntry): void {
@@ -317,7 +322,8 @@ export class RunScrollbackStream {
317322
this.markRendered(await this.finishActive(false))
318323
}
319324

320-
this.writeSpacer(separatorRows(this.rendered, commit, body))
325+
const rows = separatorRows(this.rendered, commit, body)
326+
this.writeSpacer(rows || (!this.rendered && this.wrote ? 1 : 0))
321327

322328
this.renderer.writeToScrollback(
323329
entryWriter({

packages/opencode/test/cli/run/scrollback.surface.test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,6 +1008,91 @@ test("streamed assistant blocks defer their spacer until first render", async ()
10081008
}
10091009
})
10101010

1011+
test("first entry after prior scrollback gets a spacer", async () => {
1012+
const out = await createTestRenderer({
1013+
width: 80,
1014+
screenMode: "split-footer",
1015+
footerHeight: 6,
1016+
externalOutputMode: "capture-stdout",
1017+
consoleMode: "disabled",
1018+
})
1019+
active.push(out.renderer)
1020+
1021+
const treeSitterClient = new MockTreeSitterClient({ autoResolveTimeout: 0 })
1022+
treeSitterClient.setMockResult({ highlights: [] })
1023+
1024+
const scrollback = new RunScrollbackStream(out.renderer, RUN_THEME_FALLBACK, {
1025+
treeSitterClient,
1026+
wrote: true,
1027+
})
1028+
1029+
await scrollback.append({
1030+
kind: "user",
1031+
text: "use subagent to explore run.ts",
1032+
phase: "start",
1033+
source: "system",
1034+
})
1035+
1036+
const commits = claimCommits(out.renderer)
1037+
try {
1038+
expect(commits).toHaveLength(2)
1039+
expect(renderCommit(commits[0]!).trim()).toBe("")
1040+
expect(renderCommit(commits[1]!).trim()).toBe("› use subagent to explore run.ts")
1041+
} finally {
1042+
destroyCommits(commits)
1043+
}
1044+
})
1045+
1046+
test("first streamed entry after prior scrollback gets a spacer", async () => {
1047+
const out = await createTestRenderer({
1048+
width: 80,
1049+
screenMode: "split-footer",
1050+
footerHeight: 6,
1051+
externalOutputMode: "capture-stdout",
1052+
consoleMode: "disabled",
1053+
})
1054+
active.push(out.renderer)
1055+
1056+
const treeSitterClient = new MockTreeSitterClient({ autoResolveTimeout: 0 })
1057+
treeSitterClient.setMockResult({ highlights: [] })
1058+
1059+
const scrollback = new RunScrollbackStream(out.renderer, RUN_THEME_FALLBACK, {
1060+
treeSitterClient,
1061+
wrote: true,
1062+
})
1063+
1064+
for (const chunk of ["Exploring", " run.ts", " via", " a codebase-aware", " subagent next."]) {
1065+
await scrollback.append({
1066+
kind: "assistant",
1067+
text: chunk,
1068+
phase: "progress",
1069+
source: "assistant",
1070+
messageID: "msg-1",
1071+
partID: "part-1",
1072+
})
1073+
}
1074+
1075+
const progress = claimCommits(out.renderer)
1076+
try {
1077+
expect(progress).toHaveLength(0)
1078+
} finally {
1079+
destroyCommits(progress)
1080+
}
1081+
1082+
await scrollback.complete()
1083+
1084+
const commits = claimCommits(out.renderer)
1085+
try {
1086+
expect(commits).toHaveLength(2)
1087+
expect(renderCommit(commits[0]!).trim()).toBe("")
1088+
expect(renderCommit(commits[1]!).replace(/\n/g, " ")).toContain(
1089+
"Exploring run.ts via a codebase-aware subagent next.",
1090+
)
1091+
} finally {
1092+
destroyCommits(commits)
1093+
}
1094+
})
1095+
10111096
test("coalesces same-line tool progress into one snapshot", async () => {
10121097
const out = await createTestRenderer({
10131098
width: 80,

0 commit comments

Comments
 (0)