refactor(server): canonicalize service API (#31049)

This commit is contained in:
Dax
2026-06-06 23:27:28 -04:00
committed by GitHub
parent d9a6e74fcf
commit 93316f1fae
388 changed files with 7103 additions and 4092 deletions
@@ -0,0 +1,72 @@
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
exports[`TUI inline tool wrapping snapshots consecutive grep, glob, and read rows at a narrow width 1`] = `
" ✱ Grep "OPENCODE.*DB|database|sqlite|drizzle|dev.*db|data.
*dir|xdg|APPDATA" in packages/opencode/src (151 matches)
✱ Glob "**/*db*" in packages/opencode (6 matches)
→ Read packages/opencode/src/storage/db.ts [offset=1, limit=130]
→ Read packages/opencode/src/index.ts [offset=1, limit=100]
✱ Grep "export const OPENCODE_DB|OPENCODE_DB|OPENCODE_DEV|Global\\.
Path\\.data|data =" in packages/opencode/src (115 matches)"
`;
exports[`TUI inline tool wrapping snapshots expanded tool errors under the tool text 1`] = `
" ✱ Grep "OPENCODE.*DB|database|sqlite|drizzle|dev.*db|data.
*dir|xdg|APPDATA" in packages/opencode/src (151 matches)
✱ Glob "**/*db*" in packages/opencode (6 matches)
→ Read packages/opencode/src/storage/db.ts [offset=1, limit=130]
→ Read packages/opencode/src/index.ts [offset=1, limit=100]
No LSP server available for this file type.
✱ Grep "export const OPENCODE_DB|OPENCODE_DB|OPENCODE_DEV|Global\\.
Path\\.data|data =" in packages/opencode/src (115 matches)"
`;
exports[`TUI inline tool wrapping keeps separation after a shell output block 1`] = `
"
# List files
$ ls
file.ts
✱ Grep "OPENCODE.*DB|database|sqlite|drizzle|dev.*db|data.
*dir|xdg|APPDATA" in packages/opencode/src (151 matches)
✱ Glob "**/*db*" in packages/opencode (6 matches)
→ Read packages/opencode/src/storage/db.ts [offset=1, limit=130]
→ Read packages/opencode/src/index.ts [offset=1, limit=100]
✱ Grep "export const OPENCODE_DB|OPENCODE_DB|OPENCODE_DEV|Global\\.
Path\\.data|data =" in packages/opencode/src (115 matches)"
`;
exports[`TUI inline tool wrapping keeps separation after a padded user message 1`] = `
"
Check whether the next tool remains separated.
✱ Grep "OPENCODE.*DB|database|sqlite|drizzle|dev.*db|data.
*dir|xdg|APPDATA" in packages/opencode/src (151 matches)
✱ Glob "**/*db*" in packages/opencode (6 matches)
→ Read packages/opencode/src/storage/db.ts [offset=1, limit=130]
→ Read packages/opencode/src/index.ts [offset=1, limit=100]
✱ Grep "export const OPENCODE_DB|OPENCODE_DB|OPENCODE_DEV|Global\\.
Path\\.data|data =" in packages/opencode/src (115 matches)"
`;
exports[`TUI inline tool wrapping separates a contiguous subagent group from inline tools 1`] = `
" ✱ Grep "Task" (2 matches)
⠙ Explore Task — Inspect active task spacing
✓ General Task — Confirm completed task spacing
↳ 1 toolcall · 501ms
→ Read src/cli/cmd/tui/routes/session/index.tsx"
`;
exports[`TUI inline tool wrapping separates a subagent group after an expanded read 1`] = `
" → Read src/cli/cmd/tui/routes/session/index.tsx
↳ Loaded src/cli/cmd/tui/routes/session/tools.tsx
✓ Explore Task — Inspect active task spacing
↳ 1 toolcall · 501ms"
`;
@@ -0,0 +1,147 @@
/** @jsxImportSource @opentui/solid */
import { TextareaRenderable } from "@opentui/core"
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
import { testRender, useRenderer } from "@opentui/solid"
import { expect, test } from "bun:test"
import { mkdir } from "node:fs/promises"
import path from "node:path"
import { onCleanup } from "solid-js"
import { tmpdir } from "../../fixture/fixture"
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
import type { TuiKeybind } from "../../../src/config/keybind"
import { TestTuiEnvironmentProvider } from "../../fixture/tui-environment"
async function wait(fn: () => boolean, timeout = 2000) {
const start = Date.now()
while (!fn()) {
if (Date.now() - start > timeout) throw new Error("timed out waiting for condition")
await Bun.sleep(10)
}
}
async function mountPrompt(input: {
root: string
keybinds: Partial<TuiKeybind.Keybinds>
onConfirm: (value: string) => void
}) {
const state = path.join(input.root, "state")
await mkdir(state, { recursive: true })
await Bun.write(path.join(state, "kv.json"), "{}")
const [
{ DialogProvider },
{ DialogPrompt },
{ KVProvider },
{ ThemeProvider },
{ TuiConfigProvider },
{ ToastProvider },
{ OpencodeKeymapProvider, registerOpencodeKeymap },
] = await Promise.all([
import("../../../src/ui/dialog"),
import("../../../src/ui/dialog-prompt"),
import("../../../src/context/kv"),
import("../../../src/context/theme"),
import("../../../src/config"),
import("../../../src/ui/toast"),
import("../../../src/keymap"),
])
function Harness() {
const renderer = useRenderer()
const keymap = createDefaultOpenTuiKeymap(renderer)
const resolvedConfig = createTuiResolvedConfig({
keybinds: input.keybinds,
leader_timeout: 1000,
})
const off = registerOpencodeKeymap(keymap, renderer, resolvedConfig)
onCleanup(off)
return (
<TestTuiEnvironmentProvider
directory={input.root}
paths={{
home: input.root,
state,
worktree: input.root,
}}
>
<OpencodeKeymapProvider keymap={keymap}>
<TuiConfigProvider config={resolvedConfig}>
<KVProvider>
<ThemeProvider mode="dark">
<ToastProvider>
<DialogProvider>
<DialogPrompt title="Rename Session" value="draft" onConfirm={input.onConfirm} />
</DialogProvider>
</ToastProvider>
</ThemeProvider>
</KVProvider>
</TuiConfigProvider>
</OpencodeKeymapProvider>
</TestTuiEnvironmentProvider>
)
}
const app = await testRender(() => <Harness />, { kittyKeyboard: true })
return {
app,
async cleanup() {
app.renderer.destroy()
},
}
}
test("dialog prompt submit wins when return is also input newline", async () => {
await using tmp = await tmpdir()
const confirmed: string[] = []
const prompt = await mountPrompt({
root: tmp.path,
keybinds: {
input_submit: "super+return",
input_newline: "return,shift+return,alt+return,ctrl+j",
},
onConfirm: (value) => confirmed.push(value),
})
try {
await wait(() => prompt.app.renderer.currentFocusedEditor instanceof TextareaRenderable)
const textarea = prompt.app.renderer.currentFocusedEditor
if (!(textarea instanceof TextareaRenderable)) throw new Error("expected focused dialog textarea")
prompt.app.mockInput.pressEnter()
expect(confirmed).toEqual(["draft"])
expect(textarea.plainText).toBe("draft")
} finally {
await prompt.cleanup()
}
})
test("dialog prompt submit can be rebound separately from input submit", async () => {
await using tmp = await tmpdir()
const confirmed: string[] = []
const prompt = await mountPrompt({
root: tmp.path,
keybinds: {
input_submit: "return",
"dialog.prompt.submit": "ctrl+y",
},
onConfirm: (value) => confirmed.push(value),
})
try {
await wait(() => prompt.app.renderer.currentFocusedEditor instanceof TextareaRenderable)
const textarea = prompt.app.renderer.currentFocusedEditor
if (!(textarea instanceof TextareaRenderable)) throw new Error("expected focused dialog textarea")
prompt.app.mockInput.pressEnter()
expect(confirmed).toEqual([])
expect(textarea.plainText).toBe("draft")
prompt.app.mockInput.pressKey("y", { ctrl: true })
expect(confirmed).toEqual(["draft"])
} finally {
await prompt.cleanup()
}
})
@@ -0,0 +1,200 @@
/** @jsxImportSource @opentui/solid */
import { describe, expect, test } from "bun:test"
import { RGBA } from "@opentui/core"
import { testRender } from "@opentui/solid"
import type { JSX } from "solid-js"
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
import { KVProvider } from "../../../src/context/kv"
import { ThemeProvider } from "../../../src/context/theme"
import { TuiConfigProvider } from "../../../src/config"
import { DiffViewerFileTree } from "../../../src/feature-plugins/system/diff-viewer-file-tree"
import { TestTuiEnvironmentProvider } from "../../fixture/tui-environment"
import {
allExpandedFileTreeDirectories,
buildFileTree,
} from "../../../src/feature-plugins/system/diff-viewer-file-tree-utils"
const theme = {
background: RGBA.fromHex("#000000"),
backgroundPanel: RGBA.fromHex("#111111"),
backgroundElement: RGBA.fromHex("#333333"),
primary: RGBA.fromHex("#00ffff"),
secondary: RGBA.fromHex("#0088ff"),
selectedListItemText: RGBA.fromHex("#ffffff"),
text: RGBA.fromHex("#ffffff"),
textMuted: RGBA.fromHex("#888888"),
error: RGBA.fromHex("#ff0000"),
}
describe("DiffViewerFileTree", () => {
test.skip("renders sorted hierarchical file rows", async () => {
const app = await testRender(
() =>
withTheme(() => (
<DiffViewerFileTree
width={32}
files={[
{ file: "z-file.ts" },
{ file: "b/file.ts" },
{ file: "a/zeta.ts" },
{ file: "b/alpha.ts" },
{ file: "a/alpha.ts" },
]}
loading={false}
error={undefined}
theme={theme}
focused={true}
/>
)),
{ width: 40, height: 20 },
)
try {
await renderOnceSettled(app)
const lines = visibleLines(app.captureCharFrame())
expect(lines).toEqual([
"▾ a",
"│ ├─ alpha.ts ?",
"│ └─ zeta.ts ?",
"├─ ▾ b",
"│ ├─ alpha.ts ?",
"│ └─ file.ts ?",
])
} finally {
app.renderer.destroy()
}
})
test("keeps loading and error quiet while rendering an empty settled state", async () => {
const loading = await renderFrame(() => (
<DiffViewerFileTree width={32} files={[]} loading={true} error={undefined} theme={theme} />
))
const failed = await renderFrame(() => (
<DiffViewerFileTree width={32} files={[]} loading={false} error={new Error("nope")} theme={theme} />
))
const empty = await renderFrame(() => (
<DiffViewerFileTree width={32} files={[]} loading={false} error={undefined} theme={theme} />
))
expect(loading).not.toContain("Loading diff...")
expect(loading).not.toContain("No files")
expect(failed).not.toContain("Failed to load diff")
expect(failed).not.toContain("No files")
expect(empty).toContain("No files")
})
test("does not render text markers for highlighted rows", async () => {
const files = [{ file: "src/config/tui.ts" }, { file: "README.md" }]
const src = buildFileTree(files).nodes.find((node) => node.kind === "directory" && node.name === "src")!
const focused = visibleLines(
await renderFrame(() => (
<DiffViewerFileTree
width={32}
files={files}
loading={false}
error={undefined}
theme={theme}
focused
highlightedNode={src.id}
/>
)),
)
const unfocused = visibleLines(
await renderFrame(() => (
<DiffViewerFileTree width={32} files={files} loading={false} error={undefined} theme={theme} />
)),
)
expect(focused).toContain("▾ src/config")
expect(unfocused).toContain("▾ src/config")
expect(focused.some((line) => line.includes("*"))).toBe(false)
expect(unfocused.some((line) => line.includes("*"))).toBe(false)
})
test("renders collapsed and expanded directory rows", async () => {
const files = [{ file: "src/config/tui.ts" }, { file: "README.md" }]
const tree = buildFileTree(files)
const src = tree.nodes.find((node) => node.kind === "directory" && node.name === "src")!
const collapsed = allExpandedFileTreeDirectories(tree)
collapsed.delete(src.id)
expect(
visibleLines(
await renderFrame(() => (
<DiffViewerFileTree
width={32}
files={files}
loading={false}
error={undefined}
theme={theme}
expandedNodes={collapsed}
/>
)),
),
).toEqual(["▸ src/config"])
expect(
visibleLines(
await renderFrame(() => (
<DiffViewerFileTree
files={files}
width={32}
loading={false}
error={undefined}
theme={theme}
expandedNodes={allExpandedFileTreeDirectories(tree)}
/>
)),
),
).toEqual(["▾ src/config", "│ └─ tui.ts ?"])
})
})
async function renderFrame(component: () => JSX.Element) {
const app = await testRender(() => withTheme(component), { width: 40, height: 10 })
try {
await renderOnceSettled(app)
return await captureSettledFrame(app)
} finally {
app.renderer.destroy()
}
}
async function renderOnceSettled(app: Awaited<ReturnType<typeof testRender>>) {
await app.renderOnce()
await new Promise((resolve) => setTimeout(resolve, 25))
await app.renderOnce()
}
async function captureSettledFrame(app: Awaited<ReturnType<typeof testRender>>) {
for (let attempt = 0; attempt < 5; attempt++) {
const frame = app.captureCharFrame()
if (frame.trim().length > 0) return frame
await new Promise((resolve) => setTimeout(resolve, 25))
await app.renderOnce()
}
return app.captureCharFrame()
}
function withTheme(component: () => JSX.Element) {
return (
<TestTuiEnvironmentProvider>
<TuiConfigProvider config={createTuiResolvedConfig()}>
<KVProvider>
<ThemeProvider mode="dark">{component()}</ThemeProvider>
</KVProvider>
</TuiConfigProvider>
</TestTuiEnvironmentProvider>
)
}
function visibleLines(frame: string) {
return frame
.split("\n")
.map((line) => line.trimEnd())
.map((line) => line.replace(/^ ?│ ?/, "").replace(/[ │]*$/, ""))
.map((line) => (line.startsWith(" ") ? line.slice(1) : line))
.filter((line) => line.length > 0 && !/^┌|^└|^─+$/.test(line))
}
@@ -0,0 +1,225 @@
/** @jsxImportSource @opentui/solid */
import { expect, test } from "bun:test"
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
import type { Renderable, ScrollBoxRenderable } from "@opentui/core"
import { testRender, useRenderer } from "@opentui/solid"
import type { TuiPluginApi, TuiPluginMeta, TuiRouteCurrent, TuiRouteDefinition } from "@opencode-ai/plugin/tui"
import type { Session } from "@opencode-ai/sdk/v2"
import { KVProvider } from "../../../src/context/kv"
import { ThemeProvider } from "../../../src/context/theme"
import { TuiConfigProvider } from "../../../src/config"
import { TuiKeybind } from "../../../src/config/keybind"
import { OpencodeKeymapProvider } from "../../../src/keymap"
import diffViewerPlugin from "../../../src/feature-plugins/system/diff-viewer"
import { createTuiPluginApi } from "../../fixture/tui-plugin"
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
import { TestTuiEnvironmentProvider } from "../../fixture/tui-environment"
test("closing the diff viewer returns to the route it opened from", async () => {
const viewer = await renderDiffViewer([])
try {
expect(viewer.current()).toEqual({
name: "diff",
params: { mode: "git", sessionID: "session-1", returnRoute: startRoute },
})
expect(viewer.vcsDiffInput()).toEqual({ directory: "/repo/session", mode: "git", context: 12 })
expect(viewer.commands.has("diff.close")).toBe(true)
viewer.commands.get("diff.close")!.run?.({} as never)
expect(viewer.current()).toEqual(startRoute)
} finally {
viewer.app.renderer.destroy()
}
})
test("brackets navigate diff hunks", async () => {
const viewer = await renderDiffViewer(
[
{
file: "src/file.ts",
additions: 3,
deletions: 3,
status: "modified",
patch: `--- a/src/file.ts
+++ b/src/file.ts
@@ -1,3 +1,3 @@
const first = true
-const oldFirst = true
+const newFirst = true
const afterFirst = true
@@ -20,3 +20,3 @@
const second = true
-const oldSecond = true
+const newSecond = true
const afterSecond = true
@@ -40,3 +40,3 @@
const third = true
-const oldThird = true
+const newThird = true
const afterThird = true`,
},
],
12,
)
try {
await viewer.app.waitForFrame((frame) => frame.includes("const first"))
await viewer.app.waitFor(() => Boolean(findRenderable(viewer.app.renderer.root, "diff-viewer-patches")))
await viewer.app.flush()
const scroll = findRenderable(viewer.app.renderer.root, "diff-viewer-patches") as ScrollBoxRenderable
const initial = scroll.scrollTop
expect(TuiKeybind.defaultValue("diff_next_hunk")).toBe("]")
expect(TuiKeybind.defaultValue("diff_previous_hunk")).toBe("[")
viewer.commands.get("diff.next_hunk")!.run?.({} as never)
await viewer.app.renderOnce()
const first = scroll.scrollTop
expect(first).toBeGreaterThan(initial)
viewer.commands.get("diff.next_hunk")!.run?.({} as never)
await viewer.app.renderOnce()
const second = scroll.scrollTop
expect(second).toBeGreaterThan(first)
viewer.commands.get("diff.previous_hunk")!.run?.({} as never)
await viewer.app.renderOnce()
expect(scroll.scrollTop).toBe(first)
viewer.commands.get("diff.next_hunk")!.run?.({} as never)
await viewer.app.renderOnce()
expect(scroll.scrollTop).toBe(second)
scroll.scrollTo(initial)
viewer.commands.get("diff.next_hunk")!.run?.({} as never)
await viewer.app.renderOnce()
expect(scroll.scrollTop).toBe(first)
} finally {
viewer.app.renderer.destroy()
}
})
async function renderDiffViewer(vcsDiff: unknown[], height = 20) {
const commands = new Map<
string,
NonNullable<Parameters<TuiPluginApi["keymap"]["registerLayer"]>[0]["commands"]>[number]
>()
let current = startRoute
let renderDiff: TuiRouteDefinition["render"] | undefined
let vcsDiffInput: unknown
const config = createTuiResolvedConfig()
function Harness() {
const renderer = useRenderer()
const keymap = createDefaultOpenTuiKeymap(renderer)
const registerLayer = keymap.registerLayer.bind(keymap)
keymap.registerLayer = (layer) => {
layer.commands?.forEach((command) => commands.set(command.name, command))
return registerLayer(layer)
}
const base = createTuiPluginApi({
keymap,
client: {
vcs: {
diff: async (input: unknown) => {
vcsDiffInput = input
return { data: vcsDiff }
},
},
session: { diff: async () => ({ data: [] }) },
} as unknown as TuiPluginApi["client"],
state: {
session: {
get: () => session,
},
},
})
const api = {
...base,
route: {
register(routes) {
renderDiff = routes.find((route) => route.name === "diff")?.render
return () => {}
},
navigate(name, params) {
current = params ? { name, params } : { name }
},
get current() {
return current
},
},
} satisfies TuiPluginApi
void diffViewerPlugin.tui(api, undefined, pluginMeta)
commands.get("diff.open")?.run?.({} as never)
return (
<TestTuiEnvironmentProvider>
<OpencodeKeymapProvider keymap={keymap}>
<TuiConfigProvider config={config}>
<KVProvider>
<ThemeProvider mode="dark">
{renderDiff?.({ params: "params" in current ? current.params : undefined })}
</ThemeProvider>
</KVProvider>
</TuiConfigProvider>
</OpencodeKeymapProvider>
</TestTuiEnvironmentProvider>
)
}
const app = await testRender(() => <Harness />, { width: 80, height })
await waitForCommand(app, commands, "diff.close")
return {
app,
commands,
current: () => current,
vcsDiffInput: () => vcsDiffInput,
}
}
const startRoute: TuiRouteCurrent = { name: "session", params: { sessionID: "session-1" } }
function findRenderable(root: Renderable, id: string): Renderable | undefined {
if (root.id === id) return root
return root
.getChildren()
.map((child) => findRenderable(child, id))
.find(Boolean)
}
const session = {
id: "session-1",
slug: "session-1",
projectID: "project-1",
directory: "/repo/session",
title: "Session",
version: "1",
time: {
created: 0,
updated: 0,
},
} satisfies Session
async function waitForCommand(
app: Awaited<ReturnType<typeof testRender>>,
commands: Map<string, unknown>,
command: string,
) {
for (let attempt = 0; attempt < 10; attempt++) {
await app.renderOnce()
if (commands.has(command)) return
await new Promise((resolve) => setTimeout(resolve, 25))
}
}
const pluginMeta = {
id: "diff-viewer",
source: "internal",
spec: "diff-viewer",
target: "diff-viewer",
first_time: 0,
last_time: 0,
time_changed: 0,
load_count: 1,
fingerprint: "test",
state: "same",
} satisfies TuiPluginMeta
@@ -0,0 +1,276 @@
import { afterEach, describe, expect, test } from "bun:test"
import { createSignal, For, Show } from "solid-js"
import type { ScrollBoxRenderable } from "@opentui/core"
import { testRender, type JSX } from "@opentui/solid"
import {
formatCompletedSubagentDetail,
formatSubagentRetry,
formatSubagentTitle,
formatSubagentToolcalls,
InlineToolRow,
parseApplyPatchFiles,
parseDiagnostics,
parseQuestionAnswers,
parseQuestions,
parseTodos,
toolDisplay,
} from "../../../src/routes/session"
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined
afterEach(() => {
testSetup?.renderer.destroy()
testSetup = undefined
})
type ToolFixture = { icon: string; label: string; error?: string }
const tools: readonly ToolFixture[] = [
{
icon: "✱",
label:
'Grep "OPENCODE.*DB|database|sqlite|drizzle|dev.*db|data.*dir|xdg|APPDATA" in packages/opencode/src (151 matches)',
},
{
icon: "✱",
label: 'Glob "**/*db*" in packages/opencode (6 matches)',
},
{
icon: "→",
label: "Read packages/opencode/src/storage/db.ts [offset=1, limit=130]",
},
{
icon: "→",
label: "Read packages/opencode/src/index.ts [offset=1, limit=100]",
error: "No LSP server available for this file type.",
},
{
icon: "✱",
label:
'Grep "export const OPENCODE_DB|OPENCODE_DB|OPENCODE_DEV|Global\\.Path\\.data|data =" in packages/opencode/src (115 matches)',
},
] as const
function ShellOutput() {
return (
<box id="tool-block-shell" marginTop={1} paddingTop={1} paddingBottom={1} paddingLeft={2} gap={1}>
<text paddingLeft={3}># List files</text>
<box gap={1}>
<text>$ ls</text>
<text>file.ts</text>
</box>
</box>
)
}
function UserMessage() {
return (
<box id="message-user">
<box paddingTop={1} paddingBottom={1} paddingLeft={2}>
<text>Check whether the next tool remains separated.</text>
</box>
</box>
)
}
function Fixture(props: { errorExpanded?: boolean; before?: "shell" | "user" }) {
return (
<box flexDirection="column" width={72}>
<box flexDirection="column">
{props.before === "shell" && <ShellOutput />}
{props.before === "user" && <UserMessage />}
<For each={tools}>
{(item) => (
<InlineToolRow
icon={item.icon}
complete={true}
pending=""
failed={Boolean(item.error)}
error={item.error}
errorExpanded={props.errorExpanded}
separateAfter={(id) => id === "message-user"}
>
{item.label}
</InlineToolRow>
)}
</For>
</box>
</box>
)
}
function SubagentGroupFixture() {
return (
<box flexDirection="column" width={72}>
<InlineToolRow id="tool-inline-before" icon="✱" complete={true} pending="">
Grep "Task" (2 matches)
</InlineToolRow>
<InlineToolRow id="tool-inline-subagent-one" icon="⠙" complete={true} pending="" subagent={true}>
Explore Task Inspect active task spacing
</InlineToolRow>
<InlineToolRow id="tool-inline-subagent-two" icon="✓" complete={true} pending="" subagent={true}>
{"General Task — Confirm completed task spacing\n↳ 1 toolcall · 501ms"}
</InlineToolRow>
<InlineToolRow id="tool-inline-after" icon="→" complete={true} pending="">
Read src/cli/cmd/tui/routes/session/index.tsx
</InlineToolRow>
</box>
)
}
function LoadedReadBeforeSubagentFixture() {
return (
<box flexDirection="column" width={72}>
<InlineToolRow id="tool-inline-read" icon="→" complete={true} pending="">
Read src/cli/cmd/tui/routes/session/index.tsx
</InlineToolRow>
<box id="tool-inline-loaded-read-child" paddingLeft={3}>
<text paddingLeft={3}> Loaded src/cli/cmd/tui/routes/session/tools.tsx</text>
</box>
<InlineToolRow id="tool-inline-subagent-after-read" icon="✓" complete={true} pending="" subagent={true}>
{"Explore Task — Inspect active task spacing\n↳ 1 toolcall · 501ms"}
</InlineToolRow>
</box>
)
}
function StickyScrollFixture(props: { separated: boolean; scroll: (scroll: ScrollBoxRenderable) => void }) {
return (
<scrollbox ref={props.scroll} stickyScroll={true} stickyStart="bottom" height={3} width={72}>
<box height={1}>
<text>First row</text>
</box>
<box height={1}>
<text>Second row</text>
</box>
<Show when={props.separated}>
<box id="text-before-tool">
<text>Assistant text</text>
</box>
</Show>
<InlineToolRow icon="→" complete={true} pending="">
Read src/cli/cmd/tui/routes/session/index.tsx
</InlineToolRow>
</scrollbox>
)
}
async function renderFrame(component: () => JSX.Element, options: { width: number; height: number }) {
testSetup = await testRender(component, options)
await testSetup.renderOnce()
return testSetup
.captureCharFrame()
.split("\n")
.map((line) => line.trimEnd())
.join("\n")
.trimEnd()
}
describe("TUI inline tool wrapping", () => {
test("falls back for unknown tool names", () => {
expect(toolDisplay("bash")).toBe("bash")
expect(toolDisplay("plugin_tool")).toBe("generic")
})
test("filters malformed nested tool wire data", () => {
expect(
parseApplyPatchFiles([
null,
{ type: "add" },
{ type: "add", relativePath: "a.ts", filePath: "a.ts", patch: "diff", deletions: 0 },
]),
).toEqual([
{ type: "add", relativePath: "a.ts", filePath: "a.ts", patch: "diff", deletions: 0, movePath: undefined },
])
expect(parseTodos([null, { status: "pending" }, { status: "pending", content: "Safe" }])).toEqual([
{ status: "pending", content: "Safe" },
])
expect(parseQuestions([{}, { question: 1 }, { question: "Continue?" }])).toEqual([{ question: "Continue?" }])
expect(parseQuestionAnswers([null, ["yes", 1], "no"])).toEqual([[], ["yes"], []])
expect(parseQuestionAnswers({})).toBeUndefined()
})
test("ignores diagnostics with malformed nested ranges", () => {
expect(
parseDiagnostics(
{
"a.ts": [
{ severity: 1, message: "missing range" },
{ severity: 1, message: "bad line", range: { start: { line: "0", character: 1 } } },
{ severity: 1, message: "valid", range: { start: { line: 2, character: 3 } } },
],
},
"a.ts",
),
).toEqual([{ message: "valid", range: { start: { line: 2, character: 3 } } }])
})
test("formats completed subagent toolcall details", () => {
expect(formatCompletedSubagentDetail(0, "501ms")).toBe("501ms")
expect(formatCompletedSubagentDetail(1, "501ms")).toBe("1 toolcall · 501ms")
expect(formatCompletedSubagentDetail(2, "501ms")).toBe("2 toolcalls · 501ms")
expect(formatSubagentToolcalls(0)).toBe("0 toolcalls")
})
test("keeps background state attached to the subagent identity", () => {
expect(formatSubagentTitle("Explore", "Inspect renderer", false)).toBe("Explore Task — Inspect renderer")
expect(formatSubagentTitle("Explore", "Inspect renderer", true)).toBe(
"Explore Task (background) — Inspect renderer",
)
})
test("keeps retry status ahead of wrapping messages", () => {
expect(formatSubagentRetry(2, "Rate limited by provider")).toBe("Retrying (attempt 2) · Rate limited by provider")
})
test("snapshots consecutive grep, glob, and read rows at a narrow width", async () => {
expect(await renderFrame(() => <Fixture />, { width: 72, height: 12 })).toMatchSnapshot()
})
test("snapshots expanded tool errors under the tool text", async () => {
expect(await renderFrame(() => <Fixture errorExpanded />, { width: 72, height: 12 })).toMatchSnapshot()
})
test("keeps separation after a shell output block", async () => {
expect(await renderFrame(() => <Fixture before="shell" />, { width: 72, height: 16 })).toMatchSnapshot()
})
test("keeps separation after a padded user message", async () => {
expect(await renderFrame(() => <Fixture before="user" />, { width: 72, height: 14 })).toMatchSnapshot()
})
test("separates a contiguous subagent group from inline tools", async () => {
expect(await renderFrame(() => <SubagentGroupFixture />, { width: 72, height: 10 })).toMatchSnapshot()
})
test("separates a subagent group after an expanded read", async () => {
expect(await renderFrame(() => <LoadedReadBeforeSubagentFixture />, { width: 72, height: 8 })).toMatchSnapshot()
})
test("updates sticky-bottom geometry when a text separator mounts and unmounts", async () => {
const [separated, setSeparated] = createSignal(false)
let scroll: ScrollBoxRenderable | undefined
testSetup = await testRender(
() => <StickyScrollFixture separated={separated()} scroll={(value) => (scroll = value)} />,
{
width: 72,
height: 3,
},
)
await testSetup.renderOnce()
expect(scroll?.scrollHeight).toBe(3)
expect(scroll?.scrollTop).toBe(Math.max(0, scroll!.scrollHeight - scroll!.viewport.height))
setSeparated(true)
await testSetup.renderOnce()
expect(scroll?.scrollHeight).toBe(5)
expect(scroll?.scrollTop).toBe(Math.max(0, scroll!.scrollHeight - scroll!.viewport.height))
setSeparated(false)
await testSetup.renderOnce()
expect(scroll?.scrollHeight).toBe(3)
expect(scroll?.scrollTop).toBe(Math.max(0, scroll!.scrollHeight - scroll!.viewport.height))
})
})
@@ -0,0 +1,98 @@
import { describe, expect, test } from "bun:test"
// Regression test for the prompt submit race in
// packages/tui/src/component/prompt/index.tsx (`submit`).
//
// Before the fix, two concurrent `submit()` calls (e.g. a double-pressed
// Enter, or the input's native onSubmit racing another dispatch) each
// passed the `if (!store.prompt.input) return false` guard, each
// `await sdk.client.session.create(...)`, and each only captured
// `inputText = store.prompt.input` AFTER that await. The first invocation
// finished, sent the prompt, and cleared the store; the second invocation,
// now past its await, read the cleared store and sent an empty prompt to a
// second freshly-created session - leaving an orphaned session with the
// user's actual text and a phantom session visible to the user containing
// only an assistant reply.
//
// `submitMirror` below has the exact shape of the production `submit()`
// after the fix: an in-flight `submitting` guard wraps the original body.
// Two concurrent invocations must result in exactly one submission carrying
// the user's text, with no empty-text submission.
type Store = { input: string }
type SubmitResult = { sessionID: string; text: string }
type Harness = {
store: Store
submissions: SubmitResult[]
createSession(): Promise<string>
sendPrompt(sessionID: string, text: string): Promise<void>
}
function createHarness(opts: { sessionCreateDelayMs: number }): Harness {
let sessionCounter = 0
const submissions: SubmitResult[] = []
return {
store: { input: "" },
submissions,
async createSession() {
sessionCounter += 1
const id = `ses_${sessionCounter}`
await Bun.sleep(opts.sessionCreateDelayMs)
return id
},
async sendPrompt(sessionID, text) {
submissions.push({ sessionID, text })
},
}
}
function createSubmit() {
let submitting = false
return async function submit(h: Harness) {
if (submitting) return false
submitting = true
try {
if (!h.store.input) return false
const sessionID = await h.createSession()
const inputText = h.store.input
await h.sendPrompt(sessionID, inputText)
h.store.input = ""
return true
} finally {
submitting = false
}
}
}
describe("Prompt.submit race", () => {
test("concurrent submits must not lose the user's text", async () => {
const submit = createSubmit()
const h = createHarness({ sessionCreateDelayMs: 5 })
h.store.input = "Hello there."
// Two invocations back-to-back, mimicking a double-Enter.
await Promise.all([submit(h), submit(h)])
// Every submission that did make it through must carry the actual user
// text, and no submission may have an empty text payload.
expect(h.submissions.every((s) => s.text === "Hello there.")).toBe(true)
expect(h.submissions.some((s) => s.text === "")).toBe(false)
})
test("a sequential second submit after clear is a no-op, not a phantom session", async () => {
const submit = createSubmit()
const h = createHarness({ sessionCreateDelayMs: 1 })
h.store.input = "Hello there."
await submit(h)
// After the first submission completes, the store is cleared; a second
// Enter on an empty input must not create a phantom session.
await submit(h)
expect(h.submissions).toHaveLength(1)
expect(h.submissions[0].text).toBe("Hello there.")
})
})
+573
View File
@@ -0,0 +1,573 @@
/** @jsxImportSource @opentui/solid */
import { expect, test } from "bun:test"
import { testRender } from "@opentui/solid"
import type { Event, GlobalEvent } from "@opencode-ai/sdk/v2"
import { onMount } from "solid-js"
import { ProjectProvider } from "../../../src/context/project"
import { SDKProvider } from "../../../src/context/sdk"
import { SyncProviderV2, useSyncV2 } from "../../../src/context/sync-v2"
import { createEventSource, createFetch, directory, json } from "../../fixture/tui-sdk"
import { TestTuiEnvironmentProvider } from "../../fixture/tui-environment"
async function wait(fn: () => boolean, timeout = 2000) {
const start = Date.now()
while (!fn()) {
if (Date.now() - start > timeout) throw new Error("timed out waiting for condition")
await Bun.sleep(10)
}
}
function global(payload: Event): GlobalEvent {
return { directory, project: "proj_test", payload }
}
function emitTwice(events: ReturnType<typeof createEventSource>, payload: Event) {
const event = global(payload)
events.emit(event)
events.emit(event)
}
test("sync v2 settles pending tools when a live failure arrives", async () => {
const events = createEventSource()
const calls = createFetch()
let sync!: ReturnType<typeof useSyncV2>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
ready = resolve
})
function Probe() {
sync = useSyncV2()
onMount(ready)
return <box />
}
const app = await testRender(() => (
<TestTuiEnvironmentProvider>
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
<ProjectProvider>
<SyncProviderV2>
<Probe />
</SyncProviderV2>
</ProjectProvider>
</SDKProvider>
</TestTuiEnvironmentProvider>
))
try {
await mounted
emitTwice(events, {
id: "evt_agent_1",
type: "session.next.agent.switched",
properties: { sessionID: "session-1", messageID: "msg_agent_1", timestamp: 0, agent: "build" },
})
emitTwice(events, {
id: "evt_model_1",
type: "session.next.model.switched",
properties: {
sessionID: "session-1",
messageID: "msg_model_1",
timestamp: 0,
model: { id: "model-1", providerID: "provider-1" },
},
})
emitTwice(events, {
id: "evt_step_started_1",
type: "session.next.step.started",
properties: {
sessionID: "session-1",
assistantMessageID: "msg_explicit_assistant_9",
timestamp: 1,
agent: "build",
model: { id: "model-1", providerID: "provider-1" },
},
})
emitTwice(events, {
id: "evt_input_1",
type: "session.next.tool.input.started",
properties: {
sessionID: "session-1",
assistantMessageID: "msg_explicit_assistant_9",
timestamp: 2,
callID: "call-1",
name: "bash",
},
})
emitTwice(events, {
id: "evt_called_1",
type: "session.next.tool.called",
properties: {
sessionID: "session-1",
timestamp: 2,
assistantMessageID: "msg_explicit_assistant_9",
callID: "call-1",
tool: "bash",
input: {},
provider: { executed: false, metadata: { fake: { call: true } } },
},
})
emitTwice(events, {
id: "evt_failed_1",
type: "session.next.tool.failed",
properties: {
sessionID: "session-1",
timestamp: 3,
assistantMessageID: "msg_explicit_assistant_9",
callID: "call-1",
error: { type: "unknown", message: "aborted" },
provider: { executed: false, metadata: { fake: { result: true } } },
},
})
await wait(() => {
const assistant = sync.session.message.fromSession("session-1")[0]
return (
assistant?.type === "assistant" &&
assistant.content[0]?.type === "tool" &&
assistant.content[0].state.status === "error"
)
})
const assistant = sync.session.message.fromSession("session-1")[0]
expect(assistant?.type).toBe("assistant")
if (assistant?.type !== "assistant") return
expect(assistant.id).toBe("msg_explicit_assistant_9")
const tool = assistant.content[0]
expect(tool?.type).toBe("tool")
if (tool?.type !== "tool") return
expect(tool.state.status).toBe("error")
if (tool.state.status !== "error") return
expect(tool.state.error).toEqual({ type: "unknown", message: "aborted" })
expect(tool.state.input).toEqual({})
expect(tool.state.structured).toEqual({})
expect(tool.state.content).toEqual([])
expect(tool.provider).toEqual({
executed: false,
metadata: { fake: { call: true } },
resultMetadata: { fake: { result: true } },
})
expect(sync.session.message.fromSession("session-1").map((message) => message.type)).toEqual([
"assistant",
"model-switched",
"agent-switched",
])
} finally {
app.renderer.destroy()
}
})
test("sync v2 renders admitted prompts only after promotion", async () => {
const events = createEventSource()
const calls = createFetch()
let sync!: ReturnType<typeof useSyncV2>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
ready = resolve
})
function Probe() {
sync = useSyncV2()
onMount(ready)
return <box />
}
const app = await testRender(() => (
<TestTuiEnvironmentProvider>
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
<ProjectProvider>
<SyncProviderV2>
<Probe />
</SyncProviderV2>
</ProjectProvider>
</SDKProvider>
</TestTuiEnvironmentProvider>
))
try {
await mounted
emitTwice(events, {
id: "evt_admitted_1",
type: "session.next.prompt.admitted",
properties: {
sessionID: "session-1",
messageID: "msg_user_1",
timestamp: 0,
prompt: { text: "hello" },
delivery: "steer",
},
})
expect(sync.session.message.fromSession("session-1")).toEqual([])
emitTwice(events, {
id: "evt_promoted_1",
type: "session.next.prompt.promoted",
properties: {
sessionID: "session-1",
messageID: "msg_user_1",
timestamp: 1,
prompt: { text: "hello" },
timeCreated: 0,
},
})
await wait(() => sync.session.message.fromSession("session-1").length === 1)
const message = sync.session.message.fromSession("session-1")[0]
expect(message?.type).toBe("user")
if (message?.type !== "user") return
expect(message).toMatchObject({ id: "msg_user_1", text: "hello" })
} finally {
app.renderer.destroy()
}
})
test("sync v2 renders a promoted prompt when admission was missed", async () => {
const events = createEventSource()
const calls = createFetch()
let sync!: ReturnType<typeof useSyncV2>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
ready = resolve
})
function Probe() {
sync = useSyncV2()
onMount(ready)
return <box />
}
const app = await testRender(() => (
<TestTuiEnvironmentProvider>
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
<ProjectProvider>
<SyncProviderV2>
<Probe />
</SyncProviderV2>
</ProjectProvider>
</SDKProvider>
</TestTuiEnvironmentProvider>
))
try {
await mounted
emitTwice(events, {
id: "evt_promoted_1",
type: "session.next.prompt.promoted",
properties: {
sessionID: "session-1",
messageID: "msg_user_1",
timestamp: 1,
prompt: { text: "hello" },
timeCreated: 0,
},
})
await wait(() => sync.session.message.fromSession("session-1").length === 1)
expect(sync.session.message.fromSession("session-1")[0]?.id).toBe("msg_user_1")
} finally {
app.renderer.destroy()
}
})
test("sync v2 projects live context updates with their message ID", async () => {
const events = createEventSource()
const calls = createFetch()
let sync!: ReturnType<typeof useSyncV2>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
ready = resolve
})
function Probe() {
sync = useSyncV2()
onMount(ready)
return <box />
}
const app = await testRender(() => (
<TestTuiEnvironmentProvider>
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
<ProjectProvider>
<SyncProviderV2>
<Probe />
</SyncProviderV2>
</ProjectProvider>
</SDKProvider>
</TestTuiEnvironmentProvider>
))
try {
await mounted
emitTwice(events, {
id: "evt_context_1",
type: "session.next.context.updated",
properties: {
sessionID: "session-1",
messageID: "msg_context_1",
timestamp: 1,
text: "Updated context",
},
})
await wait(() => sync.session.message.fromSession("session-1").length === 1)
expect(sync.session.message.fromSession("session-1")[0]).toMatchObject({
id: "msg_context_1",
type: "system",
text: "Updated context",
})
} finally {
app.renderer.destroy()
}
})
test("sync v2 preserves live events while snapshot hydration is in flight", async () => {
const events = createEventSource()
const response = Promise.withResolvers<Response>()
const calls = createFetch((url) => {
if (url.pathname === "/api/session/session-1/message") return response.promise
return undefined
})
let sync!: ReturnType<typeof useSyncV2>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
ready = resolve
})
function Probe() {
sync = useSyncV2()
onMount(ready)
return <box />
}
const app = await testRender(() => (
<TestTuiEnvironmentProvider>
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
<ProjectProvider>
<SyncProviderV2>
<Probe />
</SyncProviderV2>
</ProjectProvider>
</SDKProvider>
</TestTuiEnvironmentProvider>
))
try {
await mounted
const hydration = sync.session.message.sync("session-1")
emitTwice(events, {
id: "evt_agent_1",
type: "session.next.agent.switched",
properties: { sessionID: "session-1", messageID: "msg_agent_1", timestamp: 0, agent: "build" },
})
response.resolve(json({ data: [] }))
await hydration
expect(sync.session.message.fromSession("session-1").map((message) => [message.id, message.type])).toEqual([
["msg_agent_1", "agent-switched"],
])
} finally {
app.renderer.destroy()
}
})
test("sync v2 replaces stale cached rows while preserving in-flight live rows", async () => {
const events = createEventSource()
const response = Promise.withResolvers<Response>()
const calls = createFetch((url) => {
if (url.pathname === "/api/session/session-1/message") return response.promise
return undefined
})
let sync!: ReturnType<typeof useSyncV2>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
ready = resolve
})
function Probe() {
sync = useSyncV2()
onMount(ready)
return <box />
}
const app = await testRender(() => (
<TestTuiEnvironmentProvider>
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
<ProjectProvider>
<SyncProviderV2>
<Probe />
</SyncProviderV2>
</ProjectProvider>
</SDKProvider>
</TestTuiEnvironmentProvider>
))
try {
await mounted
emitTwice(events, {
id: "evt_promoted_1",
type: "session.next.prompt.promoted",
properties: {
sessionID: "session-1",
messageID: "msg_user_1",
timestamp: 1,
prompt: { text: "stale" },
timeCreated: 0,
},
})
await wait(() => sync.session.message.fromSession("session-1")[0]?.id === "msg_user_1")
const hydration = sync.session.message.sync("session-1")
emitTwice(events, {
id: "evt_agent_1",
type: "session.next.agent.switched",
properties: { sessionID: "session-1", messageID: "msg_agent_1", timestamp: 2, agent: "build" },
})
await wait(() => sync.session.message.fromSession("session-1")[0]?.id === "msg_agent_1")
response.resolve(
json({
data: [{ id: "msg_user_1", type: "user", text: "fresh", time: { created: 0 } }],
}),
)
await hydration
expect(sync.session.message.fromSession("session-1").map((message) => [message.id, message.type])).toEqual([
["msg_agent_1", "agent-switched"],
["msg_user_1", "user"],
])
expect(sync.session.message.fromSession("session-1")[1]).toMatchObject({ text: "fresh" })
} finally {
app.renderer.destroy()
}
})
test("sync v2 preserves snapshot order and metadata for in-flight updates", async () => {
const events = createEventSource()
const response = Promise.withResolvers<Response>()
const calls = createFetch((url) => {
if (url.pathname === "/api/session/session-1/message") return response.promise
return undefined
})
let sync!: ReturnType<typeof useSyncV2>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
ready = resolve
})
function Probe() {
sync = useSyncV2()
onMount(ready)
return <box />
}
const app = await testRender(() => (
<TestTuiEnvironmentProvider>
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
<ProjectProvider>
<SyncProviderV2>
<Probe />
</SyncProviderV2>
</ProjectProvider>
</SDKProvider>
</TestTuiEnvironmentProvider>
))
try {
await mounted
emitTwice(events, {
id: "evt_step_older",
type: "session.next.step.started",
properties: {
sessionID: "session-1",
assistantMessageID: "msg_assistant_older",
timestamp: 0,
agent: "build",
model: { id: "model", providerID: "provider" },
},
})
emitTwice(events, {
id: "evt_step_1",
type: "session.next.step.started",
properties: {
sessionID: "session-1",
assistantMessageID: "msg_assistant_old",
timestamp: 1,
agent: "build",
model: { id: "model", providerID: "provider" },
},
})
await wait(() => sync.session.message.fromSession("session-1")[0]?.id === "msg_assistant_old")
const hydration = sync.session.message.sync("session-1")
emitTwice(events, {
id: "evt_text_1",
type: "session.next.text.started",
properties: {
sessionID: "session-1",
assistantMessageID: "msg_assistant_old",
timestamp: 2,
textID: "text-1",
},
})
emitTwice(events, {
id: "evt_text_older",
type: "session.next.text.started",
properties: {
sessionID: "session-1",
assistantMessageID: "msg_assistant_older",
timestamp: 2,
textID: "text-older",
},
})
await wait(() => {
const messages = sync.session.message.fromSession("session-1")
return messages.every((message) => message.type !== "assistant" || message.content[0]?.type === "text")
})
response.resolve(
json({
data: [
{
id: "msg_assistant_new",
type: "assistant",
agent: "build",
model: { id: "model", providerID: "provider" },
content: [],
time: { created: 3 },
},
{
id: "msg_assistant_old",
type: "assistant",
metadata: { source: "snapshot" },
agent: "build",
model: { id: "model", providerID: "provider" },
content: [],
time: { created: 1 },
},
],
}),
)
await hydration
emitTwice(events, {
id: "evt_step_late_duplicate",
type: "session.next.step.started",
properties: {
sessionID: "session-1",
assistantMessageID: "msg_assistant_old",
timestamp: 1,
agent: "build",
model: { id: "model", providerID: "provider" },
},
})
expect(sync.session.message.fromSession("session-1").map((message) => message.id)).toEqual([
"msg_assistant_new",
"msg_assistant_old",
"msg_assistant_older",
])
expect(JSON.parse(JSON.stringify(sync.session.message.fromSession("session-1")[1]))).toMatchObject({
metadata: { source: "snapshot" },
content: [{ type: "text", id: "text-1", text: "" }],
})
expect(JSON.parse(JSON.stringify(sync.session.message.fromSession("session-1")[2]))).toMatchObject({
content: [{ type: "text", id: "text-older", text: "" }],
})
} finally {
app.renderer.destroy()
}
})
@@ -0,0 +1,36 @@
import { describe, expect, test } from "bun:test"
import { reasoningSummary } from "../../../src/context/thinking"
describe("reasoningSummary", () => {
test("extracts a leading summary title and leaves markdown body", () => {
expect(reasoningSummary("**Continuing Quality Review**\n\nDetails.\n\n**Next section**\n\nMore.")).toEqual({
title: "Continuing Quality Review",
body: "Details.\n\n**Next section**\n\nMore.",
})
})
test("extracts a completed title before its streamed body arrives", () => {
expect(reasoningSummary("**Continuing Quality Review**")).toEqual({
title: "Continuing Quality Review",
body: "",
})
})
test("preserves markdown-significant indentation in the extracted body", () => {
expect(reasoningSummary("**Continuing Quality Review**\n\n const value = true\n")).toEqual({
title: "Continuing Quality Review",
body: " const value = true",
})
})
test("does not consume ordinary leading bold content", () => {
expect(reasoningSummary("**Important:** keep this in the body.")).toEqual({
title: null,
body: "**Important:** keep this in the body.",
})
})
test("leaves content without a leading title in its body", () => {
expect(reasoningSummary("Details only.")).toEqual({ title: null, body: "Details only." })
})
})
@@ -0,0 +1,148 @@
/** @jsxImportSource @opentui/solid */
import { describe, expect, test } from "bun:test"
import { testRender } from "@opentui/solid"
import type { Event, GlobalEvent } from "@opencode-ai/sdk/v2"
import { onMount } from "solid-js"
import { ProjectProvider, useProject } from "../../../src/context/project"
import { SDKProvider } from "../../../src/context/sdk"
import { useEvent } from "../../../src/context/event"
import { createEventSource, createFetch, directory } from "../../fixture/tui-sdk"
import { TestTuiEnvironmentProvider } from "../../fixture/tui-environment"
const projectID = "proj_test"
async function wait(fn: () => boolean, timeout = 2000) {
const start = Date.now()
while (!fn()) {
if (Date.now() - start > timeout) throw new Error("timed out waiting for condition")
await Bun.sleep(10)
}
}
function event(payload: Event, input: { directory: string; project?: string; workspace?: string }): GlobalEvent {
return {
directory: input.directory,
project: input.project,
workspace: input.workspace,
payload,
}
}
function vcs(branch: string): Event {
return {
id: `evt_vcs_${branch}`,
type: "vcs.branch.updated",
properties: {
branch,
},
}
}
function update(version: string): Event {
return {
id: `evt_update_${version}`,
type: "installation.update-available",
properties: {
version,
},
}
}
async function mount() {
const events = createEventSource()
const calls = createFetch()
const seen: Event[] = []
const workspaces: Array<string | undefined> = []
let project!: ReturnType<typeof useProject>
let done!: () => void
const ready = new Promise<void>((resolve) => {
done = resolve
})
const app = await testRender(() => (
<TestTuiEnvironmentProvider>
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
<ProjectProvider>
<Probe
onReady={async (ctx) => {
project = ctx.project
await project.sync()
done()
}}
seen={seen}
workspaces={workspaces}
/>
</ProjectProvider>
</SDKProvider>
</TestTuiEnvironmentProvider>
))
await ready
return { app, emit: events.emit, project, seen, workspaces }
}
function Probe(props: {
seen: Event[]
workspaces: Array<string | undefined>
onReady: (ctx: { project: ReturnType<typeof useProject> }) => void
}) {
const project = useProject()
const event = useEvent()
onMount(() => {
event.subscribe((evt, { workspace }) => {
props.seen.push(evt)
props.workspaces.push(workspace)
})
props.onReady({ project })
})
return <box />
}
describe("useEvent", () => {
test("delivers events for the current project", async () => {
const { app, emit, seen, workspaces } = await mount()
try {
emit(event(vcs("main"), { directory: "/tmp/other", project: projectID, workspace: "ws_a" }))
await wait(() => seen.length === 1)
expect(seen).toEqual([vcs("main")])
expect(workspaces).toEqual(["ws_a"])
} finally {
app.renderer.destroy()
}
})
test("delivers current project events regardless of active workspace", async () => {
const { app, emit, project, seen } = await mount()
try {
project.workspace.set("ws_a")
emit(event(vcs("ws"), { directory: "/tmp/other", project: projectID, workspace: "ws_b" }))
await wait(() => seen.length === 1)
expect(seen).toEqual([vcs("ws")])
} finally {
app.renderer.destroy()
}
})
test("delivers truly global events even when a workspace is active", async () => {
const { app, emit, project, seen } = await mount()
try {
project.workspace.set("ws_a")
emit(event(update("1.2.3"), { directory: "global" }))
await wait(() => seen.length === 1)
expect(seen).toEqual([update("1.2.3")])
} finally {
app.renderer.destroy()
}
})
})