Explorar o código

fix missing whitespace after first scrollback message

Simon Klee hai 16 horas
pai
achega
61b81ebc13

+ 2 - 0
packages/opencode/src/cli/cmd/run/footer.ts

@@ -63,6 +63,7 @@ type RunFooterOptions = {
   findFiles: (query: string) => Promise<string[]>
   agents: RunAgent[]
   resources: RunResource[]
+  wrote?: boolean
   sessionID: () => string | undefined
   agentLabel: string
   modelLabel: string
@@ -207,6 +208,7 @@ export class RunFooter implements FooterApi {
     this.interruptHint = printableBinding(options.keybinds.interrupt, options.keybinds.leader) || "esc"
     this.scrollback = new RunScrollbackStream(renderer, options.theme, {
       diffStyle: options.diffStyle,
+      wrote: options.wrote,
       sessionID: options.sessionID,
       treeSitterClient: options.treeSitterClient,
     })

+ 2 - 1
packages/opencode/src/cli/cmd/run/runtime.lifecycle.ts

@@ -189,7 +189,7 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
         session_id: input.sessionID,
       })
       const footerTask = import("./footer")
-      queueSplash(
+      const wrote = queueSplash(
         renderer,
         state,
         "entry",
@@ -219,6 +219,7 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
         first: input.first,
         history: input.history,
         theme,
+        wrote,
         keybinds: input.keybinds,
         diffStyle: input.diffStyle,
         onPermissionReply: input.onPermissionReply,

+ 8 - 2
packages/opencode/src/cli/cmd/run/scrollback.surface.ts

@@ -72,6 +72,7 @@ export class RunScrollbackStream {
   private diffStyle: RunDiffStyle | undefined
   private sessionID?: () => string | undefined
   private treeSitterClient: TreeSitterClient | undefined
+  private wrote: boolean
 
   constructor(
     private renderer: CliRenderer,
@@ -86,6 +87,7 @@ export class RunScrollbackStream {
     this.diffStyle = options.diffStyle
     this.sessionID = options.sessionID
     this.treeSitterClient = options.treeSitterClient ?? getTreeSitterClient()
+    this.wrote = options.wrote ?? false
   }
 
   private createEntry(commit: StreamCommit, body: ActiveBody): ActiveEntry {
@@ -131,6 +133,8 @@ export class RunScrollbackStream {
 
     surface.root.add(renderable)
 
+    const rows = separatorRows(this.rendered, commit, body)
+
     return {
       body,
       commit,
@@ -139,7 +143,7 @@ export class RunScrollbackStream {
       content: "",
       committedRows: 0,
       committedBlocks: 0,
-      pendingSpacerRows: separatorRows(this.rendered, commit, body),
+      pendingSpacerRows: rows || (!this.rendered && this.wrote ? 1 : 0),
       rendered: false,
     }
   }
@@ -158,6 +162,7 @@ export class RunScrollbackStream {
     }
 
     this.renderer.writeToScrollback(spacerWriter())
+    this.wrote = false
   }
 
   private flushPendingSpacer(active: ActiveEntry): void {
@@ -317,7 +322,8 @@ export class RunScrollbackStream {
       this.markRendered(await this.finishActive(false))
     }
 
-    this.writeSpacer(separatorRows(this.rendered, commit, body))
+    const rows = separatorRows(this.rendered, commit, body)
+    this.writeSpacer(rows || (!this.rendered && this.wrote ? 1 : 0))
 
     this.renderer.writeToScrollback(
       entryWriter({

+ 85 - 0
packages/opencode/test/cli/run/scrollback.surface.test.ts

@@ -1008,6 +1008,91 @@ test("streamed assistant blocks defer their spacer until first render", async ()
   }
 })
 
+test("first entry after prior scrollback gets a spacer", async () => {
+  const out = await createTestRenderer({
+    width: 80,
+    screenMode: "split-footer",
+    footerHeight: 6,
+    externalOutputMode: "capture-stdout",
+    consoleMode: "disabled",
+  })
+  active.push(out.renderer)
+
+  const treeSitterClient = new MockTreeSitterClient({ autoResolveTimeout: 0 })
+  treeSitterClient.setMockResult({ highlights: [] })
+
+  const scrollback = new RunScrollbackStream(out.renderer, RUN_THEME_FALLBACK, {
+    treeSitterClient,
+    wrote: true,
+  })
+
+  await scrollback.append({
+    kind: "user",
+    text: "use subagent to explore run.ts",
+    phase: "start",
+    source: "system",
+  })
+
+  const commits = claimCommits(out.renderer)
+  try {
+    expect(commits).toHaveLength(2)
+    expect(renderCommit(commits[0]!).trim()).toBe("")
+    expect(renderCommit(commits[1]!).trim()).toBe("› use subagent to explore run.ts")
+  } finally {
+    destroyCommits(commits)
+  }
+})
+
+test("first streamed entry after prior scrollback gets a spacer", async () => {
+  const out = await createTestRenderer({
+    width: 80,
+    screenMode: "split-footer",
+    footerHeight: 6,
+    externalOutputMode: "capture-stdout",
+    consoleMode: "disabled",
+  })
+  active.push(out.renderer)
+
+  const treeSitterClient = new MockTreeSitterClient({ autoResolveTimeout: 0 })
+  treeSitterClient.setMockResult({ highlights: [] })
+
+  const scrollback = new RunScrollbackStream(out.renderer, RUN_THEME_FALLBACK, {
+    treeSitterClient,
+    wrote: true,
+  })
+
+  for (const chunk of ["Exploring", " run.ts", " via", " a codebase-aware", " subagent next."]) {
+    await scrollback.append({
+      kind: "assistant",
+      text: chunk,
+      phase: "progress",
+      source: "assistant",
+      messageID: "msg-1",
+      partID: "part-1",
+    })
+  }
+
+  const progress = claimCommits(out.renderer)
+  try {
+    expect(progress).toHaveLength(0)
+  } finally {
+    destroyCommits(progress)
+  }
+
+  await scrollback.complete()
+
+  const commits = claimCommits(out.renderer)
+  try {
+    expect(commits).toHaveLength(2)
+    expect(renderCommit(commits[0]!).trim()).toBe("")
+    expect(renderCommit(commits[1]!).replace(/\n/g, " ")).toContain(
+      "Exploring run.ts via a codebase-aware subagent next.",
+    )
+  } finally {
+    destroyCommits(commits)
+  }
+})
+
 test("coalesces same-line tool progress into one snapshot", async () => {
   const out = await createTestRenderer({
     width: 80,