From 98db70c62e8a6be6f52013183e1154f4c62b09a7 Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Mon, 20 Apr 2026 10:05:31 +0200 Subject: [PATCH] whitespace + imports --- packages/opencode/src/cli/cmd/run.ts | 12 +- .../src/cli/cmd/run/footer.prompt.tsx | 2 +- .../opencode/src/cli/cmd/run/prompt.shared.ts | 2 +- .../src/cli/cmd/run/runtime.lifecycle.ts | 2 +- .../opencode/src/cli/cmd/run/runtime.queue.ts | 2 +- .../src/cli/cmd/run/scrollback.surface.ts | 23 +++- .../opencode/src/cli/cmd/run/session-data.ts | 10 +- packages/opencode/src/cli/cmd/run/splash.ts | 4 +- .../opencode/src/cli/cmd/run/subagent-data.ts | 2 +- packages/opencode/src/cli/cmd/run/tool.ts | 40 +++---- packages/opencode/src/cli/cmd/run/trace.ts | 2 +- .../opencode/test/cli/run/entry.body.test.ts | 4 +- packages/opencode/test/cli/run/footer.test.ts | 78 ++++++++++++- .../test/cli/run/footer.view.test.tsx | 8 +- .../test/cli/run/permission.shared.test.ts | 2 +- .../test/cli/run/prompt.shared.test.ts | 4 +- .../test/cli/run/question.shared.test.ts | 2 +- .../test/cli/run/runtime.boot.test.ts | 6 +- .../test/cli/run/runtime.queue.test.ts | 4 +- .../test/cli/run/scrollback.surface.test.ts | 110 +++++++++++++++++- .../test/cli/run/session-data.test.ts | 57 ++++++++- .../test/cli/run/session.shared.test.ts | 2 +- packages/opencode/test/cli/run/stream.test.ts | 4 +- .../test/cli/run/stream.transport.test.ts | 4 +- .../test/cli/run/subagent-data.test.ts | 4 +- packages/opencode/test/cli/run/theme.test.ts | 2 +- .../test/cli/run/variant.shared.test.ts | 6 +- 27 files changed, 324 insertions(+), 74 deletions(-) diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 5d032c29e0..1c30ccae8c 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -16,13 +16,13 @@ import path from "path" import { pathToFileURL } from "url" import { UI } from "../ui" import { cmd } from "./cmd" -import { Flag } from "../../flag/flag" +import { Flag } from "@/flag/flag" import { bootstrap } from "../bootstrap" import { EOL } from "os" -import { Filesystem } from "../../util" +import { Filesystem } from "@/util" import { createOpencodeClient, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2" -import { Agent } from "../../agent/agent" -import { Permission } from "../../permission" +import { Agent } from "@/agent/agent" +import { Permission } from "@/permission" import { AppRuntime } from "@/effect/app-runtime" import type { RunDemo } from "./run/types" @@ -715,7 +715,7 @@ export const RunCommand = cmd({ const model = pick(args.model) const { runInteractiveLocalMode } = await runtimeTask const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => { - const { Server } = await import("../../server/server") + const { Server } = await import("@/server/server") const request = new Request(input, init) return Server.Default().app.fetch(request) }) as typeof globalThis.fetch @@ -744,7 +744,7 @@ export const RunCommand = cmd({ await bootstrap(directory ?? root, async () => { const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => { - const { Server } = await import("../../server/server") + const { Server } = await import("@/server/server") const request = new Request(input, init) return Server.Default().app.fetch(request) }) as typeof globalThis.fetch diff --git a/packages/opencode/src/cli/cmd/run/footer.prompt.tsx b/packages/opencode/src/cli/cmd/run/footer.prompt.tsx index 829d8a4853..f49fa7aa22 100644 --- a/packages/opencode/src/cli/cmd/run/footer.prompt.tsx +++ b/packages/opencode/src/cli/cmd/run/footer.prompt.tsx @@ -21,7 +21,7 @@ import { onMount, type Accessor, } from "solid-js" -import * as Locale from "../../../util/locale" +import * as Locale from "@/util/locale" import { createPromptHistory, isExitCommand, diff --git a/packages/opencode/src/cli/cmd/run/prompt.shared.ts b/packages/opencode/src/cli/cmd/run/prompt.shared.ts index 6a09f6133b..83bc27f1a7 100644 --- a/packages/opencode/src/cli/cmd/run/prompt.shared.ts +++ b/packages/opencode/src/cli/cmd/run/prompt.shared.ts @@ -12,7 +12,7 @@ // The leader-key cycle (promptCycle) uses a two-step pattern: first press // arms the leader, second press within the timeout fires the action. import type { KeyBinding } from "@opentui/core" -import * as Keybind from "../../../util/keybind" +import * as Keybind from "@/util/keybind" import type { FooterKeybinds, RunPrompt } from "./types" const HISTORY_LIMIT = 200 diff --git a/packages/opencode/src/cli/cmd/run/runtime.lifecycle.ts b/packages/opencode/src/cli/cmd/run/runtime.lifecycle.ts index 068cbe70ec..4bdad700d8 100644 --- a/packages/opencode/src/cli/cmd/run/runtime.lifecycle.ts +++ b/packages/opencode/src/cli/cmd/run/runtime.lifecycle.ts @@ -9,7 +9,7 @@ // Also wires SIGINT so Ctrl-c during a turn triggers the two-press exit // sequence through RunFooter.requestExit(). import { createCliRenderer, type CliRenderer, type ScrollbackWriter } from "@opentui/core" -import * as Locale from "../../../util/locale" +import * as Locale from "@/util/locale" import { withRunSpan } from "./otel" import { entrySplash, exitSplash, splashMeta } from "./splash" import { resolveRunTheme } from "./theme" diff --git a/packages/opencode/src/cli/cmd/run/runtime.queue.ts b/packages/opencode/src/cli/cmd/run/runtime.queue.ts index 22e7158f54..c35f184563 100644 --- a/packages/opencode/src/cli/cmd/run/runtime.queue.ts +++ b/packages/opencode/src/cli/cmd/run/runtime.queue.ts @@ -9,7 +9,7 @@ // and tracks per-turn wall-clock duration for the footer status line. // // Resolves when the footer closes and all in-flight work finishes. -import * as Locale from "../../../util/locale" +import * as Locale from "@/util/locale" import { isExitCommand } from "./prompt.shared" import type { FooterApi, FooterEvent, RunPrompt } from "./types" diff --git a/packages/opencode/src/cli/cmd/run/scrollback.surface.ts b/packages/opencode/src/cli/cmd/run/scrollback.surface.ts index 93ed7c1985..b885126949 100644 --- a/packages/opencode/src/cli/cmd/run/scrollback.surface.ts +++ b/packages/opencode/src/cli/cmd/run/scrollback.surface.ts @@ -51,10 +51,9 @@ function commitMarkdownBlocks(input: { return false } - const prev = input.renderable._blockStates[input.startBlock - 1] const next = input.renderable._blockStates[input.endBlockExclusive] - const start = Math.max(0, first.renderable.y - (prev?.marginBottom ?? 0)) - const end = last.renderable.y + last.renderable.height + (next ? 0 : (last.marginBottom ?? 0)) + const start = first.renderable.y + const end = next ? next.renderable.y : last.renderable.y + last.renderable.height + (last.marginBottom ?? 0) input.surface.commitRows(start, end, { trailingNewline: input.trailingNewline, @@ -62,6 +61,18 @@ function commitMarkdownBlocks(input: { return true } +function wantsSpacer(prev: StreamCommit | undefined, next: StreamCommit): boolean { + if (!prev) { + return false + } + + if (sameEntryGroup(prev, next)) { + return false + } + + return !(prev.kind === "tool" && prev.phase === "start") +} + export class RunScrollbackStream { private tail: StreamCommit | undefined private active: ActiveEntry | undefined @@ -238,14 +249,14 @@ export class RunScrollbackStream { const body = entryBody(commit) if (body.type === "none") { if (entryDone(commit)) { - await this.finishActive(entryFlags(commit).trailingNewline) + await this.finishActive(false) } this.tail = commit return } - if (this.wrote && !same) { + if (this.wrote && wantsSpacer(this.tail, commit)) { this.renderer.writeToScrollback(spacerWriter()) } @@ -256,7 +267,7 @@ export class RunScrollbackStream { ) { await this.writeStreaming(commit, body) if (entryDone(commit)) { - await this.finishActive(entryFlags(commit).trailingNewline) + await this.finishActive(false) } this.wrote = true this.tail = commit diff --git a/packages/opencode/src/cli/cmd/run/session-data.ts b/packages/opencode/src/cli/cmd/run/session-data.ts index eb17e910db..952eaa3ced 100644 --- a/packages/opencode/src/cli/cmd/run/session-data.ts +++ b/packages/opencode/src/cli/cmd/run/session-data.ts @@ -25,7 +25,7 @@ // event arrives, the queue entry is removed and the footer falls back // to the next pending request or to the prompt view. import type { Event, Part, PermissionRequest, QuestionRequest, ToolPart } from "@opencode-ai/sdk/v2" -import * as Locale from "../../../util/locale" +import * as Locale from "@/util/locale" import { toolView } from "./tool" import type { FooterOutput, FooterPatch, FooterView, StreamCommit } from "./types" @@ -492,11 +492,19 @@ function flushPart(data: SessionData, commits: SessionCommit[], partID: string, if (sent === 0) { chunk = chunk.replace(/^\n+/, "") + // Some models emit a standalone whitespace token before real content. + // Keep buffering until we have visible text so scrollback doesn't get a blank row. + if (!chunk.trim()) { + return + } if (kind === "reasoning" && chunk) { chunk = `Thinking: ${chunk.replace(/\[REDACTED\]/g, "")}` } if (kind === "assistant" && chunk) { chunk = stripEcho(data, msg, chunk) + if (!chunk.trim()) { + return + } } } diff --git a/packages/opencode/src/cli/cmd/run/splash.ts b/packages/opencode/src/cli/cmd/run/splash.ts index 257ce75fcf..ac12ff7498 100644 --- a/packages/opencode/src/cli/cmd/run/splash.ts +++ b/packages/opencode/src/cli/cmd/run/splash.ts @@ -19,8 +19,8 @@ import { type ScrollbackSnapshot, type ScrollbackWriter, } from "@opentui/core" -import * as Locale from "../../../util/locale" -import { logo } from "../../logo" +import * as Locale from "@/util/locale" +import { logo } from "@/cli/logo" import type { RunEntryTheme } from "./theme" export const SPLASH_TITLE_LIMIT = 50 diff --git a/packages/opencode/src/cli/cmd/run/subagent-data.ts b/packages/opencode/src/cli/cmd/run/subagent-data.ts index 89cbe72ddb..628b2084aa 100644 --- a/packages/opencode/src/cli/cmd/run/subagent-data.ts +++ b/packages/opencode/src/cli/cmd/run/subagent-data.ts @@ -1,5 +1,5 @@ import type { Event, Part, PermissionRequest, QuestionRequest, ToolPart } from "@opencode-ai/sdk/v2" -import * as Locale from "../../../util/locale" +import * as Locale from "@/util/locale" import { bootstrapSessionData, createSessionData, diff --git a/packages/opencode/src/cli/cmd/run/tool.ts b/packages/opencode/src/cli/cmd/run/tool.ts index 4582e5bf7b..a41f4a855f 100644 --- a/packages/opencode/src/cli/cmd/run/tool.ts +++ b/packages/opencode/src/cli/cmd/run/tool.ts @@ -18,26 +18,26 @@ import os from "os" import path from "path" import stripAnsi from "strip-ansi" import type { ToolPart } from "@opencode-ai/sdk/v2" -import type * as Tool from "../../../tool/tool" -import type { ApplyPatchTool } from "../../../tool/apply_patch" -import type { BashTool } from "../../../tool/bash" -import type { CodeSearchTool } from "../../../tool/codesearch" -import type { EditTool } from "../../../tool/edit" -import type { GlobTool } from "../../../tool/glob" -import type { GrepTool } from "../../../tool/grep" -import type { InvalidTool } from "../../../tool/invalid" -import type { LspTool } from "../../../tool/lsp" -import type { PlanExitTool } from "../../../tool/plan" -import type { QuestionTool } from "../../../tool/question" -import type { ReadTool } from "../../../tool/read" -import type { SkillTool } from "../../../tool/skill" -import type { TaskTool } from "../../../tool/task" -import type { TodoWriteTool } from "../../../tool/todo" -import type { WebFetchTool } from "../../../tool/webfetch" -import type { WebSearchTool } from "../../../tool/websearch" -import type { WriteTool } from "../../../tool/write" -import { LANGUAGE_EXTENSIONS } from "../../../lsp/language" -import * as Locale from "../../../util/locale" +import type * as Tool from "@/tool/tool" +import type { ApplyPatchTool } from "@/tool/apply_patch" +import type { BashTool } from "@/tool/bash" +import type { CodeSearchTool } from "@/tool/codesearch" +import type { EditTool } from "@/tool/edit" +import type { GlobTool } from "@/tool/glob" +import type { GrepTool } from "@/tool/grep" +import type { InvalidTool } from "@/tool/invalid" +import type { LspTool } from "@/tool/lsp" +import type { PlanExitTool } from "@/tool/plan" +import type { QuestionTool } from "@/tool/question" +import type { ReadTool } from "@/tool/read" +import type { SkillTool } from "@/tool/skill" +import type { TaskTool } from "@/tool/task" +import type { TodoWriteTool } from "@/tool/todo" +import type { WebFetchTool } from "@/tool/webfetch" +import type { WebSearchTool } from "@/tool/websearch" +import type { WriteTool } from "@/tool/write" +import { LANGUAGE_EXTENSIONS } from "@/lsp/language" +import * as Locale from "@/util/locale" import type { RunDiffStyle, RunEntryBody, StreamCommit, ToolSnapshot } from "./types" export type ToolView = { diff --git a/packages/opencode/src/cli/cmd/run/trace.ts b/packages/opencode/src/cli/cmd/run/trace.ts index 75ba79fd00..8770d33444 100644 --- a/packages/opencode/src/cli/cmd/run/trace.ts +++ b/packages/opencode/src/cli/cmd/run/trace.ts @@ -13,7 +13,7 @@ // active based on the env var, and subsequent calls return the cached result. import fs from "fs" import path from "path" -import { Global } from "../../../global" +import { Global } from "@/global" export type Trace = { write(type: string, data?: unknown): void diff --git a/packages/opencode/test/cli/run/entry.body.test.ts b/packages/opencode/test/cli/run/entry.body.test.ts index e55aed38be..1aaf0cf48b 100644 --- a/packages/opencode/test/cli/run/entry.body.test.ts +++ b/packages/opencode/test/cli/run/entry.body.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" -import { entryBody, entryCanStream, entryDone } from "../../../src/cli/cmd/run/entry.body" -import type { StreamCommit } from "../../../src/cli/cmd/run/types" +import { entryBody, entryCanStream, entryDone } from "@/cli/cmd/run/entry.body" +import type { StreamCommit } from "@/cli/cmd/run/types" function commit(input: Partial & Pick): StreamCommit { return input diff --git a/packages/opencode/test/cli/run/footer.test.ts b/packages/opencode/test/cli/run/footer.test.ts index 62b087cec9..a3d214dd8b 100644 --- a/packages/opencode/test/cli/run/footer.test.ts +++ b/packages/opencode/test/cli/run/footer.test.ts @@ -1,7 +1,7 @@ import { afterEach, expect, test } from "bun:test" import { MockTreeSitterClient, createTestRenderer, type TestRenderer } from "@opentui/core/testing" -import { RunFooter } from "../../../src/cli/cmd/run/footer" -import { RUN_THEME_FALLBACK } from "../../../src/cli/cmd/run/theme" +import { RunFooter } from "@/cli/cmd/run/footer" +import { RUN_THEME_FALLBACK } from "@/cli/cmd/run/theme" const decoder = new TextDecoder() const active: Array<{ footer?: RunFooter; renderer: TestRenderer }> = [] @@ -149,3 +149,77 @@ test("run footer keeps active streamed assistant content across width resize", a lib.commitSplitFooterSnapshot = originalCommitSplitFooterSnapshot } }) + +test("run footer keeps tool start rows tight with following reasoning", async () => { + const out = await createTestRenderer({ + width: 80, + height: 24, + screenMode: "split-footer", + footerHeight: 6, + externalOutputMode: "capture-stdout", + consoleMode: "disabled", + }) + const footer = createFooter(out.renderer) + active.push({ footer, renderer: out.renderer }) + const lib = Reflect.get(out.renderer, "lib") as { + commitSplitFooterSnapshot: (...args: unknown[]) => unknown + } + const originalCommitSplitFooterSnapshot = lib.commitSplitFooterSnapshot.bind(lib) + const payloads: string[] = [] + + lib.commitSplitFooterSnapshot = (...args) => { + const snapshot = args[1] as { + getRealCharBytes(addLineBreaks?: boolean): Uint8Array + } + payloads.push(decoder.decode(snapshot.getRealCharBytes(true))) + return originalCommitSplitFooterSnapshot(...args) + } + + try { + footer.append({ + kind: "tool", + source: "tool", + messageID: "msg-tool", + partID: "part-tool", + tool: "glob", + phase: "start", + text: "running glob", + toolState: "running", + part: { + id: "part-tool", + type: "tool", + tool: "glob", + callID: "call-tool", + messageID: "msg-tool", + sessionID: "session-1", + state: { + status: "running", + input: { + pattern: "**/run.ts", + }, + time: { + start: Date.now(), + }, + }, + }, + }) + footer.append({ + kind: "reasoning", + source: "reasoning", + messageID: "msg-reasoning", + partID: "part-reasoning", + phase: "progress", + text: "Thinking: Found it.", + }) + + await footer.idle() + + const rows = payloads + .map((item) => item.replace(/ +/g, " ").trim()) + .filter(Boolean) + + expect(rows).toEqual(['✱ Glob "**/run.ts"', "_Thinking:_ Found it."]) + } finally { + lib.commitSplitFooterSnapshot = originalCommitSplitFooterSnapshot + } +}) diff --git a/packages/opencode/test/cli/run/footer.view.test.tsx b/packages/opencode/test/cli/run/footer.view.test.tsx index c9ee24732e..6998089d27 100644 --- a/packages/opencode/test/cli/run/footer.view.test.tsx +++ b/packages/opencode/test/cli/run/footer.view.test.tsx @@ -2,10 +2,10 @@ import { expect, test } from "bun:test" import { testRender } from "@opentui/solid" import { createSignal } from "solid-js" -import { RunEntryContent } from "../../../src/cli/cmd/run/scrollback.writer" -import { RunFooterView } from "../../../src/cli/cmd/run/footer.view" -import { RUN_THEME_FALLBACK } from "../../../src/cli/cmd/run/theme" -import type { StreamCommit } from "../../../src/cli/cmd/run/types" +import { RunEntryContent } from "@/cli/cmd/run/scrollback.writer" +import { RunFooterView } from "@/cli/cmd/run/footer.view" +import { RUN_THEME_FALLBACK } from "@/cli/cmd/run/theme" +import type { StreamCommit } from "@/cli/cmd/run/types" test("run footer view loads", () => { expect(typeof RunFooterView).toBe("function") diff --git a/packages/opencode/test/cli/run/permission.shared.test.ts b/packages/opencode/test/cli/run/permission.shared.test.ts index 480d07aa61..03be4220f2 100644 --- a/packages/opencode/test/cli/run/permission.shared.test.ts +++ b/packages/opencode/test/cli/run/permission.shared.test.ts @@ -8,7 +8,7 @@ import { permissionInfo, permissionReject, permissionRun, -} from "../../../src/cli/cmd/run/permission.shared" +} from "@/cli/cmd/run/permission.shared" function req(input: Partial = {}): PermissionRequest { return { diff --git a/packages/opencode/test/cli/run/prompt.shared.test.ts b/packages/opencode/test/cli/run/prompt.shared.test.ts index 0a56f5bcea..63c295d7a4 100644 --- a/packages/opencode/test/cli/run/prompt.shared.test.ts +++ b/packages/opencode/test/cli/run/prompt.shared.test.ts @@ -8,8 +8,8 @@ import { promptInfo, promptKeys, pushPromptHistory, -} from "../../../src/cli/cmd/run/prompt.shared" -import type { FooterKeybinds, RunPrompt } from "../../../src/cli/cmd/run/types" +} from "@/cli/cmd/run/prompt.shared" +import type { FooterKeybinds, RunPrompt } from "@/cli/cmd/run/types" const keybinds: FooterKeybinds = { leader: "ctrl+x", diff --git a/packages/opencode/test/cli/run/question.shared.test.ts b/packages/opencode/test/cli/run/question.shared.test.ts index a152f81b08..27c8cecdb3 100644 --- a/packages/opencode/test/cli/run/question.shared.test.ts +++ b/packages/opencode/test/cli/run/question.shared.test.ts @@ -10,7 +10,7 @@ import { questionStoreCustom, questionSubmit, questionSync, -} from "../../../src/cli/cmd/run/question.shared" +} from "@/cli/cmd/run/question.shared" function req(input: Partial = {}): QuestionRequest { return { diff --git a/packages/opencode/test/cli/run/runtime.boot.test.ts b/packages/opencode/test/cli/run/runtime.boot.test.ts index 7dafaa1d0c..abfc75785b 100644 --- a/packages/opencode/test/cli/run/runtime.boot.test.ts +++ b/packages/opencode/test/cli/run/runtime.boot.test.ts @@ -1,12 +1,12 @@ import { afterEach, describe, expect, mock, spyOn, test } from "bun:test" -import { TuiConfig } from "../../../src/cli/cmd/tui/config/tui" +import { TuiConfig } from "@/cli/cmd/tui/config/tui" import { resolveDiffStyle, resolveFooterKeybinds, resolveModelInfo, resolveSessionInfo, -} from "../../../src/cli/cmd/run/runtime.boot" -import type { RunInput } from "../../../src/cli/cmd/run/types" +} from "@/cli/cmd/run/runtime.boot" +import type { RunInput } from "@/cli/cmd/run/types" describe("run runtime boot", () => { afterEach(() => { diff --git a/packages/opencode/test/cli/run/runtime.queue.test.ts b/packages/opencode/test/cli/run/runtime.queue.test.ts index 746f7f6a33..886ac309f0 100644 --- a/packages/opencode/test/cli/run/runtime.queue.test.ts +++ b/packages/opencode/test/cli/run/runtime.queue.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" -import { runPromptQueue } from "../../../src/cli/cmd/run/runtime.queue" -import type { FooterApi, FooterEvent, RunPrompt, StreamCommit } from "../../../src/cli/cmd/run/types" +import { runPromptQueue } from "@/cli/cmd/run/runtime.queue" +import type { FooterApi, FooterEvent, RunPrompt, StreamCommit } from "@/cli/cmd/run/types" function footer() { const prompts = new Set<(input: RunPrompt) => void>() diff --git a/packages/opencode/test/cli/run/scrollback.surface.test.ts b/packages/opencode/test/cli/run/scrollback.surface.test.ts index 62525ea71d..95a48fbb85 100644 --- a/packages/opencode/test/cli/run/scrollback.surface.test.ts +++ b/packages/opencode/test/cli/run/scrollback.surface.test.ts @@ -1,7 +1,7 @@ import { afterEach, expect, test } from "bun:test" import { MockTreeSitterClient, createTestRenderer, type TestRenderer } from "@opentui/core/testing" -import { RunScrollbackStream } from "../../../src/cli/cmd/run/scrollback.surface" -import { RUN_THEME_FALLBACK } from "../../../src/cli/cmd/run/theme" +import { RunScrollbackStream } from "@/cli/cmd/run/scrollback.surface" +import { RUN_THEME_FALLBACK } from "@/cli/cmd/run/theme" type ClaimedCommit = { snapshot: { @@ -142,7 +142,7 @@ test("completes markdown replies without adding a second blank line above the fo const progress = claimCommits(out.renderer) try { expect(progress).toHaveLength(1) - expect(progress[0]!.snapshot.height).toBe(4) + expect(progress[0]!.snapshot.height).toBe(5) const rendered = decoder.decode(progress[0]!.snapshot.getRealCharBytes(true)) expect(rendered).toContain("Markdown Sample") expect(rendered).toContain("Item 2") @@ -165,6 +165,109 @@ test("completes markdown replies without adding a second blank line above the fo } }) +test("streamed assistant final leaves newline ownership to the next entry", 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: false, + }) + + await scrollback.append({ + kind: "assistant", + text: "hello", + phase: "progress", + source: "assistant", + messageID: "msg-1", + partID: "part-1", + }) + destroyCommits(claimCommits(out.renderer)) + + await scrollback.append({ + kind: "assistant", + text: "", + phase: "final", + source: "assistant", + messageID: "msg-1", + partID: "part-1", + }) + + const final = claimCommits(out.renderer) + try { + expect(final).toHaveLength(1) + expect(final[0]!.trailingNewline).toBe(false) + } finally { + destroyCommits(final) + } +}) + +test("preserves blank rows between streamed markdown block commits", async () => { + const out = await createTestRenderer({ + 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: false, + }) + + await scrollback.append({ + kind: "assistant", + text: "# Title\n\nPara 1\n\n", + phase: "progress", + source: "assistant", + messageID: "msg-1", + partID: "part-1", + }) + + const first = claimCommits(out.renderer) + expect(first).toHaveLength(1) + + await scrollback.append({ + kind: "assistant", + text: "> Quote", + phase: "progress", + source: "assistant", + messageID: "msg-1", + partID: "part-1", + }) + + const second = claimCommits(out.renderer) + expect(second).toHaveLength(0) + + await scrollback.complete() + + const final = claimCommits(out.renderer) + try { + expect(final).toHaveLength(1) + + const rendered = [...first, ...final] + .map((item) => decoder.decode(item.snapshot.getRealCharBytes(true)).replace(/ +\n/g, "\n")) + .join("") + expect(rendered).toContain("# Title\n\nPara 1\n\n> Quote") + } finally { + destroyCommits(first) + destroyCommits(final) + } +}) + test("coalesces same-line tool progress into one snapshot", async () => { const out = await createTestRenderer({ width: 80, @@ -345,7 +448,6 @@ test("renders promoted task-result markdown without leading blank rows", async ( expect(commits.length).toBeGreaterThan(0) const rendered = commits.map((item) => decoder.decode(item.snapshot.getRealCharBytes(true))).join("") expect(rendered.startsWith("\n")).toBe(false) - expect(rendered.split("\n")[0]?.trim()).toBe("Location: `/tmp/run.ts`") expect(rendered).toContain("Summary:") expect(rendered).toContain("Local interactive mode") } finally { diff --git a/packages/opencode/test/cli/run/session-data.test.ts b/packages/opencode/test/cli/run/session-data.test.ts index fae50c2237..e16174e4c4 100644 --- a/packages/opencode/test/cli/run/session-data.test.ts +++ b/packages/opencode/test/cli/run/session-data.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import type { Event } from "@opencode-ai/sdk/v2" -import { createSessionData, flushInterrupted, reduceSessionData } from "../../../src/cli/cmd/run/session-data" +import { createSessionData, flushInterrupted, reduceSessionData } from "@/cli/cmd/run/session-data" function reduce(data: ReturnType, event: unknown, thinking = true) { return reduceSessionData({ @@ -77,6 +77,61 @@ describe("run session data", () => { ]) }) + test("buffers whitespace-only initial assistant chunks until real content arrives", () => { + let data = createSessionData() + + data = reduce(data, assistant("msg-1")).data + data = reduce(data, { + type: "message.part.updated", + properties: { + part: { + id: "txt-1", + messageID: "msg-1", + sessionID: "session-1", + type: "text", + text: "", + time: { start: Date.now() }, + }, + }, + }).data + + let out = reduce(data, { + type: "message.part.delta", + properties: { + sessionID: "session-1", + messageID: "msg-1", + partID: "txt-1", + field: "text", + delta: " ", + }, + }) + + expect(out.commits).toEqual([]) + + data = out.data + out = reduce(data, { + type: "message.part.delta", + properties: { + sessionID: "session-1", + messageID: "msg-1", + partID: "txt-1", + field: "text", + delta: "Found", + }, + }) + + expect(out.commits).toEqual([ + { + kind: "assistant", + text: " Found", + phase: "progress", + source: "assistant", + messageID: "msg-1", + partID: "txt-1", + }, + ]) + }) + test("drops user text when the delayed role resolves to user", () => { let data = createSessionData() diff --git a/packages/opencode/test/cli/run/session.shared.test.ts b/packages/opencode/test/cli/run/session.shared.test.ts index dd8f1f587a..9498551594 100644 --- a/packages/opencode/test/cli/run/session.shared.test.ts +++ b/packages/opencode/test/cli/run/session.shared.test.ts @@ -5,7 +5,7 @@ import { sessionVariant, type RunSession, type SessionMessages, -} from "../../../src/cli/cmd/run/session.shared" +} from "@/cli/cmd/run/session.shared" const model = { providerID: "openai", diff --git a/packages/opencode/test/cli/run/stream.test.ts b/packages/opencode/test/cli/run/stream.test.ts index f66f85b3b6..0fab6743c9 100644 --- a/packages/opencode/test/cli/run/stream.test.ts +++ b/packages/opencode/test/cli/run/stream.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" -import { writeSessionOutput } from "../../../src/cli/cmd/run/stream" -import type { FooterApi, FooterEvent, StreamCommit } from "../../../src/cli/cmd/run/types" +import { writeSessionOutput } from "@/cli/cmd/run/stream" +import type { FooterApi, FooterEvent, StreamCommit } from "@/cli/cmd/run/types" function footer() { const events: FooterEvent[] = [] diff --git a/packages/opencode/test/cli/run/stream.transport.test.ts b/packages/opencode/test/cli/run/stream.transport.test.ts index 3121d448e4..550d6a13b8 100644 --- a/packages/opencode/test/cli/run/stream.transport.test.ts +++ b/packages/opencode/test/cli/run/stream.transport.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test" import type { OpencodeClient } from "@opencode-ai/sdk/v2" -import { createSessionTransport } from "../../../src/cli/cmd/run/stream.transport" -import type { FooterApi, FooterEvent, RunFilePart, StreamCommit } from "../../../src/cli/cmd/run/types" +import { createSessionTransport } from "@/cli/cmd/run/stream.transport" +import type { FooterApi, FooterEvent, RunFilePart, StreamCommit } from "@/cli/cmd/run/types" function defer() { let resolve!: (value: T | PromiseLike) => void diff --git a/packages/opencode/test/cli/run/subagent-data.test.ts b/packages/opencode/test/cli/run/subagent-data.test.ts index 7e80db899b..9bcae15fbb 100644 --- a/packages/opencode/test/cli/run/subagent-data.test.ts +++ b/packages/opencode/test/cli/run/subagent-data.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { entryBody } from "../../../src/cli/cmd/run/entry.body" +import { entryBody } from "@/cli/cmd/run/entry.body" import { bootstrapSubagentData, clearFinishedSubagents, @@ -7,7 +7,7 @@ import { reduceSubagentData, snapshotSelectedSubagentData, snapshotSubagentData, -} from "../../../src/cli/cmd/run/subagent-data" +} from "@/cli/cmd/run/subagent-data" function visible(commits: Array[0]>) { return commits.flatMap((item) => { diff --git a/packages/opencode/test/cli/run/theme.test.ts b/packages/opencode/test/cli/run/theme.test.ts index f2ebbc709e..304e8a623c 100644 --- a/packages/opencode/test/cli/run/theme.test.ts +++ b/packages/opencode/test/cli/run/theme.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import { RGBA, SyntaxStyle } from "@opentui/core" -import { opaqueSyntaxStyle } from "../../../src/cli/cmd/run/theme" +import { opaqueSyntaxStyle } from "@/cli/cmd/run/theme" describe("run theme", () => { test("flattens subtle syntax alpha against the run background", () => { diff --git a/packages/opencode/test/cli/run/variant.shared.test.ts b/packages/opencode/test/cli/run/variant.shared.test.ts index b74abd12f5..644a5a8a51 100644 --- a/packages/opencode/test/cli/run/variant.shared.test.ts +++ b/packages/opencode/test/cli/run/variant.shared.test.ts @@ -3,15 +3,15 @@ import { NodeFileSystem } from "@effect/platform-node" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { describe, expect, test } from "bun:test" import { Effect, FileSystem, Layer } from "effect" -import { Global } from "../../../src/global" +import { Global } from "@/global" import { createVariantRuntime, cycleVariant, formatModelLabel, pickVariant, resolveVariant, -} from "../../../src/cli/cmd/run/variant.shared" -import type { SessionMessages } from "../../../src/cli/cmd/run/session.shared" +} from "@/cli/cmd/run/variant.shared" +import type { SessionMessages } from "@/cli/cmd/run/session.shared" import { testEffect } from "../../lib/effect" const model = {