From 13b2917f748913cbef312824f1050048d00070e5 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sun, 10 May 2026 13:28:00 -0400 Subject: [PATCH] feat(tui): add fullscreen markdown demo --- package.json | 2 + packages/opencode/package.json | 2 + .../opencode/src/cli/cmd/demo-fixtures.ts | 181 +++++++++++ packages/opencode/src/cli/cmd/run/demo.ts | 58 +--- .../src/cli/cmd/tui/context/theme.tsx | 2 + packages/opencode/src/cli/cmd/tui/demo.ts | 285 ++++++++++++++++++ .../src/cli/cmd/tui/routes/session/index.tsx | 91 +++++- packages/opencode/src/cli/cmd/tui/thread.ts | 34 ++- 8 files changed, 600 insertions(+), 55 deletions(-) create mode 100644 packages/opencode/src/cli/cmd/demo-fixtures.ts create mode 100644 packages/opencode/src/cli/cmd/tui/demo.ts diff --git a/package.json b/package.json index 5faf8be920..c8c55bcbf7 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,8 @@ "packageManager": "bun@1.3.13", "scripts": { "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", + "dev:demo": "bun run --cwd packages/opencode --conditions=browser src/index.ts --demo", + "dev:run-demo": "bun run --cwd packages/opencode --conditions=browser src/index.ts run --interactive --demo", "dev:desktop": "bun --cwd packages/desktop dev", "dev:web": "bun --cwd packages/app dev", "dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index f6977296f9..0a7e6590cc 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -13,6 +13,8 @@ "build": "bun run script/build.ts", "fix-node-pty": "bun run script/fix-node-pty.ts", "dev": "bun run --conditions=browser ./src/index.ts", + "dev:demo": "bun run --conditions=browser ./src/index.ts --demo", + "dev:run-demo": "bun run --conditions=browser ./src/index.ts run --interactive --demo", "dev:temporary": "bun run --conditions=browser ./src/temporary.ts", "db": "bun drizzle-kit" }, diff --git a/packages/opencode/src/cli/cmd/demo-fixtures.ts b/packages/opencode/src/cli/cmd/demo-fixtures.ts new file mode 100644 index 0000000000..0da3fb5b5e --- /dev/null +++ b/packages/opencode/src/cli/cmd/demo-fixtures.ts @@ -0,0 +1,181 @@ +export const SAMPLE_MARKDOWN = [ + "# Direct Mode Demo", + "", + "This is a realistic assistant response for direct-mode formatting checks.", + "It mixes **bold**, _italic_, `inline code`, links, code fences, and tables in one streamed reply.", + "", + "## Summary", + "", + "- Restored the final markdown flush so the last block is committed on idle.", + "- Switched markdown scrollback commits back to top-level block boundaries.", + "- Added footer-level regression coverage for split-footer rendering.", + "", + "## Status", + "", + "| Area | Before | After | Notes |", + "| --- | --- | --- | --- |", + "| Direct mode | Missing final rows | Stable | Final markdown block now flushes on idle |", + "| Tables | Dropped in streaming mode | Visible | Block-based commits match the working OpenTUI demo |", + "| Tests | Partial coverage | Broader coverage | Includes a footer-level split render capture |", + "", + "> This sample intentionally includes a wide table so you can spot wrapping and commit bugs quickly.", + "", + "```ts", + "const result = { markdown: true, tables: 2, stable: true }", + "```", + "", + "## Files", + "", + "| File | Change |", + "| --- | --- |", + "| `scrollback.surface.ts` | Align markdown commit logic with the split-footer demo |", + "| `footer.ts` | Keep active surfaces across footer-height-only resizes |", + "| `footer.test.ts` | Capture real split-footer markdown payloads during idle completion |", + "", + "Next step: run `/fmt table` if you want a tighter table-only sample.", +].join("\n") + +export const SAMPLE_TABLE = [ + "# Table Sample", + "", + "| Kind | Example | Notes |", + "| --- | --- | --- |", + "| Pipe | `A\\|B` | Escaped pipes should stay in one cell |", + "| Unicode | `漢字` | Wide characters should remain aligned |", + "| Wrap | `LongTokenWithoutNaturalBreaks_1234567890` | Useful for width stress |", + "| Status | done | Final row should still appear after idle |", +].join("\n") + +export const MARKDOWN_PATTERNS = { + "md-code": [ + "# Interleaved Code", + "", + "Start with a short conclusion before any code appears.", + "", + "```ts", + "export function parse(input: string) {", + " return input.trim().split(/\\s+/)", + "}", + "```", + "", + "Then continue with prose immediately after the code block. This should not inherit code styling or indentation.", + "", + "```tsx", + "", + " ", + "", + "```", + "", + "Final paragraph after a second fence with `inline code`, **bold text**, and _emphasis_ mixed together.", + ].join("\n"), + "md-fence": [ + "# Fence Boundaries", + "", + "The renderer should recover cleanly around multiple fences and nearby paragraphs.", + "", + "```bash", + "bun run test -- --grep markdown", + "```", + "Text directly after a fence.", + "```json", + "{", + ' "status": "ok",', + ' "items": ["one", "two"]', + "}", + "```", + "Trailing paragraph after JSON. The next fence intentionally has no language.", + "```", + "plain fenced text", + "with multiple lines", + "```", + ].join("\n"), + "md-list": [ + "# Lists With Code", + "", + "1. First ordered item with `inline code`.", + "2. Second ordered item before a nested list:", + " - Nested bullet with a long phrase that should wrap without swallowing the marker or changing indentation.", + " - Nested bullet before fenced code:", + "", + " ```ts", + " const nested = true", + " ```", + "", + "3. Third ordered item after the nested fence.", + "", + "- Top-level bullet after ordered list.", + "- Another bullet with a paragraph below.", + "", + " Continuation paragraph should stay associated with the bullet without becoming code.", + ].join("\n"), + "md-table-code": [ + "# Tables And Code", + "", + "| Case | Input | Expected |", + "| --- | --- | --- |", + "| Inline code | `const x = 1` | stays inline |", + "| Escaped pipe | `A\\|B` | one cell |", + "| Long token | `LongTokenWithoutNaturalBreaks_1234567890_abcdefghijklmnopqrstuvwxyz` | wraps or scrolls predictably |", + "", + "A code block follows the table:", + "", + "```ts", + "const rows = [", + ' { case: "inline code", expected: "stays inline" },', + ' { case: "escaped pipe", expected: "one cell" },', + "]", + "```", + "", + "And then another compact table:", + "", + "| A | B |", + "| - | - |", + "| done | yes |", + ].join("\n"), + "md-inline": [ + "# Inline Markdown", + "", + "This paragraph mixes [a normal link](https://opencode.ai), `https://example.com/code-link`, `inline code`, **strong**, _emphasis_, and ~~strikethrough~~.", + "", + "> Blockquote with `inline code` and [a link](https://example.com) should keep quote styling while wrapping.", + "", + "A horizontal rule follows.", + "", + "---", + "", + "After the rule, text should resume normal spacing.", + ].join("\n"), + "md-kitchen": [ + "# Markdown Kitchen Sink", + "", + "This combines headings, paragraphs, lists, blockquotes, tables, inline code, and multiple code fences.", + "", + "## Steps", + "", + "1. Read the response.", + "2. Notice `inline code` before a block.", + "", + "```ts", + "type Result = { ok: boolean; reason?: string }", + "const result: Result = { ok: true }", + "```", + "", + "3. Continue the list after the block.", + "", + "> Quoted note after the list. It should not merge into the previous item.", + "", + "| Feature | Stress |", + "| --- | --- |", + "| Markdown | prose/code/table interleave |", + "| Renderer | wrapping and spacing |", + "", + "```diff", + "- const renderer = oldMarkdown", + "+ const renderer = experimentalMarkdown", + "```", + "", + "Final paragraph with [docs](https://opencode.ai/docs) and `https://example.com/from-code`.", + ].join("\n"), +} as const + +export const MARKDOWN_PATTERN_KINDS = Object.keys(MARKDOWN_PATTERNS) as Array diff --git a/packages/opencode/src/cli/cmd/run/demo.ts b/packages/opencode/src/cli/cmd/run/demo.ts index d0d72ce002..7a5bd05541 100644 --- a/packages/opencode/src/cli/cmd/run/demo.ts +++ b/packages/opencode/src/cli/cmd/run/demo.ts @@ -19,9 +19,11 @@ import type { Event, ToolPart } from "@opencode-ai/sdk/v2" import { createSessionData, reduceSessionData, type SessionData } from "./session-data" import { writeSessionOutput } from "./stream" import type { FooterApi, PermissionReply, QuestionReject, QuestionReply, RunPrompt, StreamCommit } from "./types" +import { MARKDOWN_PATTERN_KINDS, MARKDOWN_PATTERNS, SAMPLE_MARKDOWN, SAMPLE_TABLE } from "../demo-fixtures" const KINDS = [ "markdown", + ...MARKDOWN_PATTERN_KINDS, "table", "text", "reasoning", @@ -51,53 +53,9 @@ function questionKind(value: string | undefined): QuestionKind | undefined { return QUESTIONS.find((item) => item === next) } -const SAMPLE_MARKDOWN = [ - "# Direct Mode Demo", - "", - "This is a realistic assistant response for direct-mode formatting checks.", - "It mixes **bold**, _italic_, `inline code`, links, code fences, and tables in one streamed reply.", - "", - "## Summary", - "", - "- Restored the final markdown flush so the last block is committed on idle.", - "- Switched markdown scrollback commits back to top-level block boundaries.", - "- Added footer-level regression coverage for split-footer rendering.", - "", - "## Status", - "", - "| Area | Before | After | Notes |", - "| --- | --- | --- | --- |", - "| Direct mode | Missing final rows | Stable | Final markdown block now flushes on idle |", - "| Tables | Dropped in streaming mode | Visible | Block-based commits match the working OpenTUI demo |", - "| Tests | Partial coverage | Broader coverage | Includes a footer-level split render capture |", - "", - "> This sample intentionally includes a wide table so you can spot wrapping and commit bugs quickly.", - "", - "```ts", - "const result = { markdown: true, tables: 2, stable: true }", - "```", - "", - "## Files", - "", - "| File | Change |", - "| --- | --- |", - "| `scrollback.surface.ts` | Align markdown commit logic with the split-footer demo |", - "| `footer.ts` | Keep active surfaces across footer-height-only resizes |", - "| `footer.test.ts` | Capture real split-footer markdown payloads during idle completion |", - "", - "Next step: run `/fmt table` if you want a tighter table-only sample.", -].join("\n") - -const SAMPLE_TABLE = [ - "# Table Sample", - "", - "| Kind | Example | Notes |", - "| --- | --- | --- |", - "| Pipe | `A\\|B` | Escaped pipes should stay in one cell |", - "| Unicode | `漢字` | Wide characters should remain aligned |", - "| Wrap | `LongTokenWithoutNaturalBreaks_1234567890` | Useful for width stress |", - "| Status | done | Final row should still appear after idle |", -].join("\n") +function markdownPattern(value: string): keyof typeof MARKDOWN_PATTERNS | undefined { + if (value in MARKDOWN_PATTERNS) return value as keyof typeof MARKDOWN_PATTERNS +} type Ref = { msg: string @@ -1031,6 +989,12 @@ async function emitFmt(state: State, kind: string, body: string, signal?: AbortS return true } + const pattern = markdownPattern(kind) + if (pattern) { + await emitText(state, body || MARKDOWN_PATTERNS[pattern], signal) + return true + } + if (kind === "table") { await emitText(state, body || SAMPLE_TABLE, signal) return true diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index c247640eba..0c4c86b13a 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -962,6 +962,8 @@ function getSyntaxRules(theme: Theme) { style: { foreground: theme.markdownHeading, bold: true, + italic: true, + underline: true, }, }, { diff --git a/packages/opencode/src/cli/cmd/tui/demo.ts b/packages/opencode/src/cli/cmd/tui/demo.ts new file mode 100644 index 0000000000..a3b0648a71 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/demo.ts @@ -0,0 +1,285 @@ +import type { + Agent, + AssistantMessage, + Config, + Message, + Model, + Part, + Path, + Project, + Provider, + Session, +} from "@opencode-ai/sdk/v2" +import type { EventSource } from "./context/sdk" +import { MARKDOWN_PATTERNS, SAMPLE_MARKDOWN, SAMPLE_TABLE } from "../demo-fixtures" + +const sessionID = "demo_tui_markdown" +const userMessageID = "demo_tui_user" +const assistantMessageID = "demo_tui_assistant" +const now = Date.now() + +const markdown = [ + "# Fullscreen TUI Markdown Demo", + "", + "This fake assistant response runs through the fullscreen session timeline without calling an LLM.", + "Use it to compare spacing, wrapping, code fence boundaries, table behavior, and inline markdown rendering.", + "", + "## Baseline", + "", + SAMPLE_MARKDOWN, + "", + "## Table Baseline", + "", + SAMPLE_TABLE, + "", + ...Object.entries(MARKDOWN_PATTERNS).flatMap(([name, value]) => ["## " + name, "", value, ""]), +].join("\n") + +const model = { + id: "demo", + providerID: "demo", + api: { + id: "demo", + url: "https://example.com/demo", + npm: "demo", + }, + name: "Demo", + capabilities: { + temperature: false, + reasoning: true, + attachment: false, + toolcall: true, + input: { + text: true, + audio: false, + image: false, + video: false, + pdf: false, + }, + output: { + text: true, + audio: false, + image: false, + video: false, + pdf: false, + }, + interleaved: true, + }, + cost: { + input: 0, + output: 0, + cache: { + read: 0, + write: 0, + }, + }, + limit: { + context: 128_000, + output: 16_000, + }, + status: "active", + options: {}, + headers: {}, + release_date: "2026-01-01", +} satisfies Model + +const provider = { + id: "demo", + name: "Demo", + source: "custom", + env: [], + options: {}, + models: { + demo: model, + }, +} satisfies Provider + +const agent = { + name: "build", + description: "Demo agent", + mode: "primary", + native: true, + permission: [], + model: { + providerID: "demo", + modelID: "demo", + }, + options: {}, +} satisfies Agent + +function json(data: unknown, status = 200) { + return new Response(JSON.stringify(data), { + status, + headers: { + "content-type": "application/json", + }, + }) +} + +export function createTuiDemo(input: { directory: string }) { + const paths = { + home: process.env.HOME ?? input.directory, + state: input.directory, + config: input.directory, + worktree: input.directory, + directory: input.directory, + } satisfies Path + + const project = { + id: "demo_project", + worktree: input.directory, + vcs: "git", + name: "Markdown Demo", + time: { + created: now, + updated: now, + }, + sandboxes: [], + } satisfies Project + + const session = { + id: sessionID, + slug: "markdown-demo", + projectID: project.id, + directory: input.directory, + title: "Markdown Rendering Demo", + agent: agent.name, + model: { + id: model.id, + providerID: provider.id, + }, + version: "demo", + time: { + created: now, + updated: now + 2, + }, + } satisfies Session + + const user = { + id: userMessageID, + sessionID, + role: "user", + time: { + created: now, + }, + agent: agent.name, + model: { + providerID: provider.id, + modelID: model.id, + }, + } satisfies Message + + const assistant = { + id: assistantMessageID, + sessionID, + role: "assistant", + time: { + created: now + 1, + completed: now + 2, + }, + parentID: userMessageID, + modelID: model.id, + providerID: provider.id, + mode: "demo", + agent: agent.name, + path: { + cwd: input.directory, + root: input.directory, + }, + cost: 0, + tokens: { + input: 120, + output: 3_200, + reasoning: 0, + cache: { + read: 0, + write: 0, + }, + }, + } satisfies AssistantMessage + + const messages = [ + { + info: user, + parts: [ + { + id: "demo_tui_user_text", + sessionID, + messageID: userMessageID, + type: "text", + text: "Show me the fullscreen TUI markdown rendering stress cases.", + time: { + start: now, + end: now, + }, + }, + ], + }, + { + info: assistant, + parts: [ + { + id: "demo_tui_assistant_text", + sessionID, + messageID: assistantMessageID, + type: "text", + text: markdown, + time: { + start: now + 1, + end: now + 2, + }, + }, + ], + }, + ] satisfies Array<{ info: Message; parts: Part[] }> + + const fetch = (async (...args: Parameters) => { + const request = new Request(args[0], args[1]) + const url = new URL(request.url) + const pathname = url.pathname + + if (request.method === "GET" && pathname === "/path") return json(paths) + if (request.method === "GET" && pathname === "/project/current") return json(project) + if (request.method === "GET" && pathname === "/config/providers") { + return json({ providers: [provider], default: { build: "demo/demo" } }) + } + if (request.method === "GET" && pathname === "/provider") { + return json({ all: [provider], default: { build: "demo/demo" }, connected: [provider.id] }) + } + if (request.method === "GET" && pathname === "/experimental/console") { + return json({ consoleManagedProviders: [], switchableOrgCount: 0 }) + } + if (request.method === "GET" && pathname === "/agent") return json([agent]) + if (request.method === "GET" && pathname === "/config") { + return json({ model: "demo/demo", default_agent: agent.name } satisfies Config) + } + if (request.method === "GET" && pathname === "/session") return json([session]) + if (request.method === "GET" && pathname === "/command") return json([]) + if (request.method === "GET" && pathname === "/lsp") return json([]) + if (request.method === "GET" && pathname === "/mcp") return json({}) + if (request.method === "GET" && pathname === "/experimental/resource") return json({}) + if (request.method === "GET" && pathname === "/formatter") return json([]) + if (request.method === "GET" && pathname === "/session/status") return json({ [sessionID]: { type: "idle" } }) + if (request.method === "GET" && pathname === "/provider/auth") return json({}) + if (request.method === "GET" && pathname === "/vcs") return json({ branch: "demo", default_branch: "dev" }) + if (request.method === "GET" && pathname === "/experimental/workspace") return json([]) + if (request.method === "GET" && pathname === "/experimental/workspace/status") return json([]) + if (request.method === "GET" && pathname === `/session/${sessionID}`) return json(session) + if (request.method === "GET" && pathname === `/session/${sessionID}/message`) return json(messages) + if (request.method === "GET" && pathname === `/session/${sessionID}/todo`) return json([]) + if (request.method === "GET" && pathname === `/session/${sessionID}/diff`) return json([]) + if (request.method === "GET" && pathname === `/session/${sessionID}/children`) return json([]) + + return json({ message: `Unhandled demo endpoint: ${request.method} ${pathname}` }, 404) + }) as typeof globalThis.fetch + + const events = { + subscribe: async () => () => {}, + } satisfies EventSource + + return { + sessionID, + fetch, + events, + } +} diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index b0b48ec42c..85d0c62dc4 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -21,7 +21,18 @@ import { useEvent } from "@tui/context/event" import { SplitBorder } from "@tui/component/border" import { Spinner } from "@tui/component/spinner" import { selectedForeground, useTheme } from "@tui/context/theme" -import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers, TextAttributes, RGBA } from "@opentui/core" +import { + BoxRenderable, + ScrollBoxRenderable, + addDefaultParsers, + TextAttributes, + RGBA, + type MarkdownOptions, + type MarkdownRenderable, + TextRenderable, + StyledText, + type TextChunk, +} from "@opentui/core" import { Prompt, type PromptRef } from "@tui/component/prompt" import type { AssistantMessage, @@ -1525,15 +1536,87 @@ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: Ass function TextPart(props: { last: boolean; part: TextPart; message: AssistantMessage }) { const ctx = use() const { theme, syntax } = useTheme() + const text = createMemo(() => props.part.text.trim()) + const diffCache = new Map() + const colorDiffChunks = (text: string) => { + const key = `${theme.diffAdded.toString()}:${theme.diffRemoved.toString()}:${text}` + const cached = diffCache.get(key) + if (cached) return cached + + let line: "added" | "removed" | undefined + let start = true + const chunks = (text.match(/[^\n]+|\n/g) ?? [text]).map((part): TextChunk => { + if (start && part !== "\n") { + line = part.startsWith("+") ? "added" : part.startsWith("-") ? "removed" : undefined + start = false + } + const next = { + __isChunk: true, + text: part, + ...(line === "added" ? { fg: theme.diffAdded } : {}), + ...(line === "removed" ? { fg: theme.diffRemoved } : {}), + } satisfies TextChunk + if (part === "\n") { + line = undefined + start = true + } + return next + }) + diffCache.set(key, chunks) + if (diffCache.size > 20) diffCache.delete(diffCache.keys().next().value!) + return chunks + } + const configureMarkdown = (node: MarkdownRenderable | undefined) => { + if (!node) return + const renderNode: NonNullable = (token, context) => { + if (token.type === "hr") { + return new BoxRenderable(node.ctx, { + width: "100%", + height: 1, + border: ["top"], + borderColor: theme.border, + flexShrink: 0, + }) + } + + const needsCodeTopGap = token.type === "code" && !text().startsWith(token.raw.trimStart()) + if (token.type === "code" && token.lang?.trim().toLowerCase() === "diff") { + const renderable = new TextRenderable(node.ctx, { + content: new StyledText(colorDiffChunks(token.text)), + width: "100%", + flexShrink: 0, + }) + if (needsCodeTopGap) renderable.marginTop = 1 + return renderable + } + + const renderable = context.defaultRender() + if (needsCodeTopGap && renderable) { + renderable.marginTop = typeof renderable.marginTop === "number" ? Math.max(renderable.marginTop, 1) : 1 + } + return renderable + } + + // OpenTUI Solid constructs elements with only `{ id }`, so constructor-only + // MarkdownOptions need to be installed on the renderable directly. + const target = node as unknown as { + _internalBlockMode: "top-level" + _renderNode: typeof renderNode + } + target._internalBlockMode = "top-level" + target._renderNode = renderNode + } return ( - + diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 7230dae16a..ee5b7d2bda 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -111,6 +111,10 @@ export const TuiThreadCommand = cmd({ .option("agent", { type: "string", describe: "agent to use", + }) + .option("demo", { + type: "boolean", + describe: "open a fake fullscreen TUI session for renderer debugging", }), handler: async (args) => { // Keep ENABLE_PROCESSED_INPUT cleared even if other code flips it. @@ -130,7 +134,6 @@ export const TuiThreadCommand = cmd({ // Resolve relative --project paths from PWD, then use the real cwd after // chdir so the thread and worker share the same directory key. const next = resolveThreadDirectory(args.project) - const file = await target() try { process.chdir(next) } catch { @@ -138,6 +141,31 @@ export const TuiThreadCommand = cmd({ return } const cwd = Filesystem.resolve(process.cwd()) + const config = TuiConfig.get() + + if (args.demo) { + const { createTuiDemo } = await import("./demo") + const { tui } = await import("./app") + const demo = createTuiDemo({ directory: cwd }) + await tui({ + url: "http://opencode.demo", + config: await config, + directory: cwd, + fetch: demo.fetch, + events: demo.events, + args: { + continue: false, + sessionID: demo.sessionID, + agent: args.agent, + model: args.model, + prompt: await input(args.prompt), + fork: false, + }, + }) + return + } + + const file = await target() const env = sanitizedProcessEnv({ [OPENCODE_PROCESS_ROLE]: "worker", [OPENCODE_RUN_ID]: ensureRunID(), @@ -187,8 +215,6 @@ export const TuiThreadCommand = cmd({ } const prompt = await input(args.prompt) - const config = await TuiConfig.get() - const network = resolveNetworkOptionsNoConfig(args) const external = process.argv.includes("--port") || @@ -236,7 +262,7 @@ export const TuiThreadCommand = cmd({ const server = await client.call("snapshot", undefined) return [tui, server] }, - config, + config: await config, directory: cwd, fetch: transport.fetch, events: transport.events,