diff --git a/bun.lock b/bun.lock index 97d9463..e78e09a 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "@four-bytes/four-opencode-brain", "dependencies": { - "@four-bytes/opencode-plugin-lib": "github:four-bytes/four-opencode-plugin-lib#v0.3.0", + "@four-bytes/opencode-plugin-lib": "github:four-bytes/four-opencode-plugin-lib#v0.5.0", "@opencode-ai/plugin": "1.16.2", "@opentui/core": "0.3.2", "@opentui/solid": "0.3.2", @@ -81,7 +81,7 @@ "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - "@four-bytes/opencode-plugin-lib": ["@four-bytes/opencode-plugin-lib@github:four-bytes/four-opencode-plugin-lib#af1d900", { "peerDependencies": { "@opencode-ai/plugin": ">=1.16.0" } }, "four-bytes-four-opencode-plugin-lib-af1d900", "sha512-zssN7Eo524vy3NLzJ/VyqscwBFWn3t5ri3abxHOTT29x4bKfTqjJpx7EtHNDeoJ7l3oV+hhCLIY8LZq1Lbdv7A=="], + "@four-bytes/opencode-plugin-lib": ["@four-bytes/opencode-plugin-lib@github:four-bytes/four-opencode-plugin-lib#6e3e32c", { "peerDependencies": { "@opencode-ai/plugin": ">=1.16.0", "@opentui/solid": "^0.4.1", "solid-js": "^1.9.13" } }, "four-bytes-four-opencode-plugin-lib-6e3e32c", "sha512-TXCpPGPttPsUMX8fVeAIp24Xk8WvbUxXXwtm5LtXawJmN6/pahs/n131WhXP6y0D/ftt+/ZO1nRU9NPye39hog=="], "@huggingface/jinja": ["@huggingface/jinja@0.5.9", "", {}, "sha512-uWTG+l3VJRsl7EXxYizuL3P+cCPoc3cRqbWWRcQN0FhejRfbdq0RNhCmbY/YDtnTcz9icdLYuLDjsnz4d8JMuw=="], @@ -187,7 +187,7 @@ "@tinyhttp/content-disposition": ["@tinyhttp/content-disposition@2.2.4", "", {}, "sha512-5Kc5CM2Ysn3vTTArBs2vESUt0AQiWZA86yc1TI3B+lxXmtEq133C1nxXNOgnzhrivdPZIh3zLj5gDnZjoLL5GA=="], - "@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="], + "@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="], "ansi-escapes": ["ansi-escapes@6.2.1", "", {}, "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig=="], @@ -205,7 +205,7 @@ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.34", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-IMDedajPifLnHNY0X9n8hKxRTQ6/eTHwr5bDo04WnuqxyKw6LYtQywCuuqPZwhl3aBXMvQpJov42GLCwRRdQzw=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.37", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig=="], "brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], @@ -217,7 +217,7 @@ "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], - "caniuse-lite": ["caniuse-lite@1.0.30001797", "", {}, "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w=="], + "caniuse-lite": ["caniuse-lite@1.0.30001799", "", {}, "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw=="], "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -257,7 +257,7 @@ "effect": ["effect@4.0.0-beta.74", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-Yx+Kh12U+i2FmjwEfKs+ePFmpMd43RPD1oGqc/VraSS9bYzvF0Ff3PojwEFEVEewp8xc92Uxu28gTspU4qyvHA=="], - "electron-to-chromium": ["electron-to-chromium@1.5.368", "", {}, "sha512-7RckJJK4uESJF9PxvfMWd3TGqIiieUTG4HxnKaKuIpGbcr+r2ZEB3g2gAhCP3Fqm42vJSzLfgab9eva/C4/XVw=="], + "electron-to-chromium": ["electron-to-chromium@1.5.372", "", {}, "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA=="], "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], @@ -355,7 +355,7 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "msgpackr": ["msgpackr@2.0.3", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-vFKpMYFTEQujRQxvdS/u6zlfesws0J40K74w6E1fVsYnIa9WKJKB5xIVVON8L7S39hCNrCVGXcPjrYmCb9lT+w=="], + "msgpackr": ["msgpackr@2.0.4", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA=="], "msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="], @@ -423,7 +423,7 @@ "s-js": ["s-js@0.4.9", "", {}, "sha512-RtpOm+cM6O0sHg6IA70wH+UC3FZcND+rccBZpBAHzlUgNO2Bm5BN+FnM8+OBxzXdwpKWFwX11JGF0MFRkhSoIQ=="], - "semver": ["semver@7.8.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-wnilbGyMxzbY7dNOl7jpKbLSjcfeweJWU5j4+u5qW+6/wuGD9KzIGOyZnQVSBM9E7DtWaaH3CyHkppYrKYoxwg=="], + "semver": ["semver@7.8.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA=="], "seroval": ["seroval@1.5.4", "", {}, "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw=="], @@ -469,7 +469,7 @@ "tree-sitter-typescript": ["tree-sitter-typescript@0.23.2", "", { "dependencies": { "node-addon-api": "^8.2.2", "node-gyp-build": "^4.8.2", "tree-sitter-javascript": "^0.23.1" }, "peerDependencies": { "tree-sitter": "^0.21.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-e04JUUKxTT53/x3Uq1zIL45DoYKVfHH4CZqwgZhPg5qYROl5nQjV+85ruFzFGZxu+QeFVbRTPDRnqL9UbU4VeA=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], @@ -515,6 +515,8 @@ "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], + "bun-ffi-structs/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], diff --git a/package.json b/package.json index d92bfd3..4e5c0a6 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "four-bytes" ], "dependencies": { - "@four-bytes/opencode-plugin-lib": "github:four-bytes/four-opencode-plugin-lib#v0.3.0", + "@four-bytes/opencode-plugin-lib": "github:four-bytes/four-opencode-plugin-lib#v0.5.0", "@opencode-ai/plugin": "1.16.2", "@opentui/core": "0.3.2", "@opentui/solid": "0.3.2", diff --git a/src/four-opencode-brain.ts b/src/four-opencode-brain.ts index 595d57c..b70e0cf 100644 --- a/src/four-opencode-brain.ts +++ b/src/four-opencode-brain.ts @@ -53,7 +53,7 @@ function calculateIngestTimeout(fileCount: number): number { } /** Unified status updates — see src/status.ts */ -import { updateStatus, initStatus, initVersion, stopStatusServer, toast } from "./status"; +import { updateStatus, initStatus, initVersion, setSessionId, stopStatusServer, toast } from "./status"; @@ -97,20 +97,20 @@ const _serverPlugin = async (input: PluginInput) => { // Fire-and-forget — don't block plugin readiness (async () => { // Signal TUI we're scanning the directory tree - updateStatus("busy", { text: "scanning files...", total: 0 }); + updateStatus("busy", { text: "scanning files…", total: 0 }); // Quick preliminary file count for toast + timeout calculation let fileCount = 0; try { const walked = await resolveFiles(directory, true); fileCount = walked.files.length; - updateStatus("busy", { text: `scanning files... ${fileCount}`, total: fileCount }); + updateStatus("busy", { text: `scanning files… ${fileCount}`, total: fileCount }); const timeoutS = (calculateIngestTimeout(fileCount) / 1000).toFixed(0); toast( `Indexing ${fileCount} files… (timeout: ${timeoutS}s)`, "info", "Brain 🧠"); - updateStatus("busy", { text: `ingesting... 0/${fileCount}`, current: 0, total: fileCount }); + updateStatus("busy", { text: `ingesting…`, current: 0, total: fileCount }); } catch { toast( `Indexing ${project?.name ?? "project"}…`, "info", "Brain 🧠"); - updateStatus("busy", { text: "ingesting..." }); + updateStatus("busy", { text: "ingesting…" }); } const ingestDb = initBrainDatabase(); @@ -123,7 +123,7 @@ const _serverPlugin = async (input: PluginInput) => { project: directory, progressCallback: ({ current, total }) => { // Update status file every tick so TUI spinner stays live - updateStatus("busy", { text: `ingesting... ${current}/${total}`, current, total }); + updateStatus("busy", { text: `ingesting…`, current, total }); }, }), timeoutMs, @@ -250,7 +250,7 @@ const _serverPlugin = async (input: PluginInput) => { project: toolCtx.directory, progressCallback: ({ current, total }) => { // Update status file every tick so TUI spinner stays live - updateStatus("busy", { text: `ingesting... ${current}/${total}`, current, total }); + updateStatus("busy", { text: `ingesting…`, current, total }); }, }), timeoutMs, @@ -295,7 +295,7 @@ const _serverPlugin = async (input: PluginInput) => { project: s.string().optional().describe("Project name or hash to scope search"), }, execute: async (args, toolCtx) => { - updateStatus("busy", { text: "searching..." }); + updateStatus("busy", { text: "searching…" }); const db = initBrainDatabase(); try { const results = await withTimeout( @@ -691,6 +691,7 @@ const _serverPlugin = async (input: PluginInput) => { output.system.push(brainSystemPrompt()); }, "chat.message": async (_hookInput, output) => { + if (_hookInput?.sessionID) setSessionId(_hookInput.sessionID); if (output.message?.role === "user" && output.message?.content) { const stored = await onChatMessage(input, output.message as { role: string; content: string }); if (stored && output.parts) { diff --git a/src/status.ts b/src/status.ts index c30a674..45d8c7b 100644 --- a/src/status.ts +++ b/src/status.ts @@ -3,7 +3,8 @@ import { createHash } from "crypto"; import { homedir } from "os"; import { join } from "path"; import type { PluginInput } from "@opencode-ai/plugin"; -import { brainBus, type BrainStatusEvent } from "./event-bus"; +import { BusClient } from "@four-bytes/opencode-plugin-lib"; +import type { BrainStatusEvent } from "./event-bus"; export type StatusState = "busy" | "success" | "warning" | "error" | "ready"; @@ -23,15 +24,24 @@ export interface StatusOpts { const _state = { current: {} as Record }; _state.current = { status: "init", statusText: "", version: "" }; let _version = ""; +let _sessionId = ""; +let _channel = "brain/status"; let _client: PluginInput["client"] | null = null; let _server: ReturnType | null = null; let _port = 0; +let _busPromise: Promise | null = null; /** Initialize with client for toast support */ export function initVersion(v: string): void { _version = v; - write({ status: "init", statusText: "initializing..." }); + write({ status: "init", statusText: "initializing…" }); +} + +export function setSessionId(id: string): void { + if (id === _sessionId) return; + _sessionId = id; + _channel = `brain/${id}`; } export function initStatus(client: PluginInput["client"], directory: string): void { @@ -39,6 +49,16 @@ export function initStatus(client: PluginInput["client"], directory: string): vo startStatusServer(directory); } +function getBus(): Promise { + if (!_busPromise) { + _busPromise = BusClient.connect().catch((err) => { + console.warn("[brain] BusClient connect failed:", (err as Error).message); + throw err; + }); + } + return _busPromise; +} + export function startStatusServer(directory: string): void { if (_server) return; @@ -84,7 +104,14 @@ export function stopStatusServer(): void { function write(data: Record): void { _state.current = { ..._state.current, ...data }; - brainBus.emit("status", { ..._state.current, version: _version } as BrainStatusEvent); + const payload = { ..._state.current, version: _version } as BrainStatusEvent; + + // Real-time push via plugin bus (HTTP fallback still serves status endpoint) + getBus() + .then((bus) => bus.publish(_channel, payload)) + .catch((err) => { + console.warn("[brain] Bus publish failed:", (err as Error).message); + }); } /** diff --git a/src/tui.tsx b/src/tui.tsx index e980ed2..62db4b2 100644 --- a/src/tui.tsx +++ b/src/tui.tsx @@ -3,15 +3,17 @@ import { createSignal, onMount, onCleanup } from "solid-js"; import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"; import type { RGBA } from "@opentui/core"; -import { brainBus, type BrainStatusEvent } from "./event-bus"; +import { BusTui } from "@four-bytes/opencode-plugin-lib/tui"; +import { ProgressBar } from "@four-bytes/opencode-plugin-lib/tui-components"; +import type { BrainStatusEvent } from "./event-bus"; import { Spinner } from "./spinner"; import { createHash } from "crypto"; import { homedir } from "os"; import { join } from "path"; -function BrainStatusBar(props: { centered?: boolean; api: TuiPluginApi }) { +function BrainStatusBar(props: { centered?: boolean; api: TuiPluginApi; sessionId?: string }) { const [indicator, setIndicator] = createSignal("•"); - const [status, setStatus] = createSignal("connecting..."); + const [status, setStatus] = createSignal("connecting…"); const [version, setVersion] = createSignal(""); const [current, setCurrent] = createSignal(0); const [total, setTotal] = createSignal(0); @@ -37,15 +39,19 @@ function BrainStatusBar(props: { centered?: boolean; api: TuiPluginApi }) { setFg(theme().error); } else if (data.status === "init") { setBusy(true); - setStatus(data.statusText ?? "initializing..."); + setStatus(data.statusText ?? "initializing…"); + setCurrent(0); + setTotal(0); setFg(theme().warning); } else if (data.status === "busy") { setBusy(true); setCurrent(data.current ?? 0); setTotal(data.total ?? 0); - setStatus(data.statusText ?? "working"); + setStatus(data.statusText ?? "working…"); setFg(pulse % 2 === 0 ? theme().warning : theme().accent); } else { + setCurrent(0); + setTotal(0); setBusy(false); setIndicator("•"); setStatus("ready"); @@ -60,9 +66,30 @@ function BrainStatusBar(props: { centered?: boolean; api: TuiPluginApi }) { }; onMount(() => { - const unsub = brainBus.on("status", handleStatus); + let bus: BusTui | null = null; + let unsub: (() => void) | null = null; + let timer: ReturnType | null = null; - // Resolve port from discovery file, then poll HTTP endpoint + onCleanup(() => { + unsub?.(); + bus?.close(); + if (timer) clearInterval(timer); + }); + + // Real-time WebSocket subscription via plugin bus + BusTui.connect() + .then((b) => { + bus = b; + const channel = `brain/${props.sessionId || "unknown"}`; + unsub = b.subscribe(channel, (envelope) => { + handleStatus(envelope.payload as BrainStatusEvent); + }); + }) + .catch((err) => { + console.warn("[brain TUI] BusTui connect failed:", (err as Error).message); + }); + + // HTTP fallback for when bus is unavailable (cross-process) let statusUrl = ""; const hash = createHash("md5").update(props.api.state.path.directory).digest("hex").slice(0, 12); const portFile = join(homedir(), ".cache", "opencode", "brain", `status-port-${hash}.json`); @@ -92,15 +119,20 @@ function BrainStatusBar(props: { centered?: boolean; api: TuiPluginApi }) { }; poll(); - const timer = setInterval(poll, 200); - onCleanup(() => { unsub(); clearInterval(timer); }); + timer = setInterval(poll, 200); }); const StatusRow = () => ( 🧠 {version()} {busy() ? : {indicator()}} - {connecting() ? "connecting..." : status()} + {connecting() ? "connecting…" : status()} + {current() > 0 && total() > 0 && ( + + )} + {current() > 0 && total() > 0 && ( + + )} ); @@ -123,7 +155,13 @@ function BrainStatusBar(props: { centered?: boolean; api: TuiPluginApi }) { {busy() ? : {indicator()}} - {connecting() ? "connecting..." : status()} + {connecting() ? "connecting…" : status()} + {current() > 0 && total() > 0 && ( + + )} + {current() > 0 && total() > 0 && ( + + )} )} @@ -137,7 +175,7 @@ const tui: TuiPlugin = (api) => { api.slots.register({ order: 60, // below deepseek-meter (55) slots: { - sidebar_content: () => , + sidebar_content: (_ctx: any, props: any) => , home_bottom: () => , }, });