diff --git a/CHANGELOG.md b/CHANGELOG.md
index 354a302..5922329 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,19 @@
# Changelog
+## 0.1.44-beta.19 - 2026-09-26
+
+### Added
+
+- Allow prompt status scripts to render multiline footers by returning explicit
+ `rows` (up to four); legacy JSON and text output stays single-line.
+- Support Vercel AI Gateway as an adaptive reasoning evaluator provider next to
+ OpenRouter.
+
+### Fixed
+
+- Accept `null` for the `computer_capture` `region` and `windowID` arguments so
+ strict tool transports that require every key can capture the main display.
+
## 0.1.44-beta.18 - 2026-09-23
### Fixed
diff --git a/src/mendcode/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/src/mendcode/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
index 6f3c7a1..4e52f77 100644
--- a/src/mendcode/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
+++ b/src/mendcode/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
@@ -3393,6 +3393,34 @@ export function Prompt(props: PromptProps) {
render?: JSX.Element
bold?: boolean
separatorBefore?: boolean
+ breakBefore?: boolean
+ }
+
+ function PromptStatusLines(props: { segments: PromptStatusSegment[] }) {
+ const rows = createMemo(() => {
+ const result: PromptStatusSegment[][] = []
+ for (const segment of props.segments) {
+ if (!result.length || segment.breakBefore) result.push([])
+ result[result.length - 1].push(segment)
+ }
+ return result
+ })
+ return (
+
+ {(row) => (
+
+ {(segment) => (
+ <>
+
+ {promptStatusSeparator()}
+
+
+ >
+ )}
+
+ )}
+
+ )
}
function PromptStatusSegmentText(props: { segment: PromptStatusSegment }) {
@@ -3682,13 +3710,14 @@ export function Prompt(props: PromptProps) {
if (scriptOutput?.segments?.length) {
const separatorBefore = base.length > 0
const next = scriptOutput.segments
- .filter((item): item is { text: string; fg?: string; bold?: boolean } =>
+ .filter((item) =>
Boolean(item.text.trim() && !isTransientPromptStatusText(item.text)),
)
.map((item, index: number) => ({
text: item.text,
fg: resolvePromptStatusScriptColor(item.fg),
bold: item.bold,
+ breakBefore: item.breakBefore,
separatorBefore: index === 0 ? separatorBefore : false,
}))
if (script?.prepend) base.unshift(...next)
@@ -4570,38 +4599,12 @@ export function Prompt(props: PromptProps) {
>
}>
-
-
- {(segment, index) => (
- <>
-
-
- {promptStatusSeparator()}
-
-
-
- >
- )}
-
-
+
-
-
- {(segment, index) => (
- <>
-
-
- {promptStatusSeparator()}
-
-
-
- >
- )}
-
-
+
@@ -4703,20 +4706,7 @@ export function Prompt(props: PromptProps) {
-
-
- {(segment, index) => (
- <>
-
-
- {promptStatusSeparator()}
-
-
-
- >
- )}
-
-
+
{props.hint ?? }
@@ -4738,20 +4728,7 @@ export function Prompt(props: PromptProps) {
-
-
- {(segment, index) => (
- <>
-
-
- {promptStatusSeparator()}
-
-
-
- >
- )}
-
-
+
{(item) => item.render() as any}
diff --git a/src/mendcode/packages/opencode/src/mend/adaptive-reasoning/jev.ts b/src/mendcode/packages/opencode/src/mend/adaptive-reasoning/jev.ts
index 33d6a55..085e90b 100644
--- a/src/mendcode/packages/opencode/src/mend/adaptive-reasoning/jev.ts
+++ b/src/mendcode/packages/opencode/src/mend/adaptive-reasoning/jev.ts
@@ -3,8 +3,10 @@ import { z } from "zod"
import { AdaptivePolicySchema, type AdaptivePolicy } from "./policy"
import { redactEvaluatorText, type EvaluatorContext } from "./context"
-const endpoint = "https://openrouter.ai/api/alpha/decisions"
-const model = "typesafe/jev-1.13"
+const bindings = {
+ openrouter: { endpoint: "https://openrouter.ai/api/alpha/decisions", model: "typesafe/jev-1.13" },
+ "vercel-ai-gateway": { endpoint: "https://ai-gateway.vercel.sh/v1/evaluate", model: "typesafe-ai/jev" },
+} as const
const efforts = {
low: "A clear next step with little uncertainty or comparison.",
medium: "Focused analysis of connected facts and a bounded decision.",
@@ -29,6 +31,20 @@ const responseSchema = z.object({
}).optional(),
})
+const gatewayResponseSchema = z.object({
+ model: z.literal("typesafe-ai/jev"),
+ answers: z.object({ effort: answer, lease: answer }),
+ usage: z.object({
+ inputTokens: z.number().int().nonnegative().optional(),
+ outputTokens: z.number().int().nonnegative().optional(),
+ cost: z.number().finite().nonnegative().optional(),
+ }).optional(),
+}).transform((data) => ({
+ model: data.model,
+ answers: data.answers,
+ usage: data.usage && { input_tokens: data.usage.inputTokens, output_tokens: data.usage.outputTokens, cost: data.usage.cost },
+}))
+
export type JevDecision = {
effort: string
leaseSteps: 1 | 2 | 5
@@ -44,7 +60,7 @@ export class JevError extends Error {
}
}
-export function jevRequest(context: EvaluatorContext, supportedEfforts: readonly string[], maxLeaseSteps: AdaptivePolicy["maxLeaseSteps"]) {
+export function jevRequest(context: EvaluatorContext, supportedEfforts: readonly string[], maxLeaseSteps: AdaptivePolicy["maxLeaseSteps"], provider: AdaptivePolicy["provider"] = "openrouter") {
const parsed = contextSchema.safeParse(context)
if (!parsed.success || Buffer.byteLength(context.goal) > 8_192 || Buffer.byteLength(context.progress) > 4_096 ||
context.tools.some((tool) => Buffer.byteLength(tool.name) > 128 || Buffer.byteLength(tool.result) > 2_048)) throw new JevError("context")
@@ -55,8 +71,10 @@ export function jevRequest(context: EvaluatorContext, supportedEfforts: readonly
tools: parsed.data.tools.map((tool) => ({ name: redactEvaluatorText(tool.name), result: redactEvaluatorText(tool.result) })),
}
const body = JSON.stringify({
- model, state,
- provider: { only: ["typesafe"], allow_fallbacks: false },
+ model: bindings[provider].model, state,
+ ...(provider === "vercel-ai-gateway"
+ ? { providerOptions: { gateway: { only: ["typesafe-ai"] } } }
+ : { provider: { only: ["typesafe"], allow_fallbacks: false } }),
questions: {
effort: {
type: "choice",
@@ -129,7 +147,8 @@ export async function evaluateJev(input: {
if (!policy.success || policy.data.mode === "off" || !policy.data.remoteProcessing) throw new JevError("consent")
if (!input.apiKey || /\s/.test(input.apiKey)) throw new JevError("credential")
const supportedEfforts = [...input.supportedEfforts]
- const body = jevRequest(input.context, supportedEfforts, policy.data.maxLeaseSteps)
+ const binding = bindings[policy.data.provider]
+ const body = jevRequest(input.context, supportedEfforts, policy.data.maxLeaseSteps, policy.data.provider)
// A known credential cannot cross the boundary even when a caller bypasses projection.
if (body.includes(input.apiKey)) throw new JevError("context")
const controller = new AbortController()
@@ -146,7 +165,7 @@ export async function evaluateJev(input: {
await current()
if (!await withAbort(input.admitAttempt(signal), signal)) throw new JevError("budget")
await current()
- const response = await (input.fetch ?? fetch)(endpoint, {
+ const response = await (input.fetch ?? fetch)(binding.endpoint, {
method: "POST", headers: { authorization: `Bearer ${input.apiKey}`, "content-type": "application/json" },
body, signal, redirect: "error",
})
@@ -160,7 +179,8 @@ export async function evaluateJev(input: {
await delay(ms, undefined, { signal })
continue
}
- const parsed = responseSchema.safeParse(await readResponse(response))
+ const schema = policy.data.provider === "vercel-ai-gateway" ? gatewayResponseSchema : responseSchema
+ const parsed = schema.safeParse(await readResponse(response))
await current()
if (!parsed.success) throw new JevError("response")
const effort = parsed.data.answers.effort.choice
diff --git a/src/mendcode/packages/opencode/src/mend/adaptive-reasoning/policy.ts b/src/mendcode/packages/opencode/src/mend/adaptive-reasoning/policy.ts
index da0429e..873e435 100644
--- a/src/mendcode/packages/opencode/src/mend/adaptive-reasoning/policy.ts
+++ b/src/mendcode/packages/opencode/src/mend/adaptive-reasoning/policy.ts
@@ -10,7 +10,7 @@ export const AdaptivePolicySchema = z.object({
version: z.literal(1),
mode: z.enum(["off", "shadow", "adaptive"]),
remoteProcessing: z.boolean(),
- provider: z.literal("openrouter"),
+ provider: z.enum(["openrouter", "vercel-ai-gateway"]),
maxLeaseSteps: lease,
maxDecisionsPerTurn: z.number().int().min(1).max(20),
failureMode: z.enum(["pause", "baseline"]),
diff --git a/src/mendcode/packages/opencode/src/mend/tui/prompt-status.ts b/src/mendcode/packages/opencode/src/mend/tui/prompt-status.ts
index 3058fc1..d56e72d 100644
--- a/src/mendcode/packages/opencode/src/mend/tui/prompt-status.ts
+++ b/src/mendcode/packages/opencode/src/mend/tui/prompt-status.ts
@@ -96,6 +96,7 @@ export type MendPromptStatusScriptSegment = {
text: string
fg?: string
bold?: boolean
+ breakBefore?: boolean
}
export type MendPromptStatusScriptOutput = {
@@ -398,6 +399,23 @@ function parseScriptOutput(text: string): MendPromptStatusScriptOutput {
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
try {
const parsed = JSON.parse(trimmed) as unknown
+ // Explicit rows opt into multiline output; legacy JSON/TSV stays single-line.
+ if (parsed && typeof parsed === "object" && "rows" in parsed && Array.isArray(parsed.rows)) {
+ const segments: MendPromptStatusScriptSegment[] = []
+ for (const row of parsed.rows.slice(0, 4)) {
+ if (!Array.isArray(row)) continue
+ const items = row.slice(0, 32).flatMap((item): MendPromptStatusScriptSegment[] => {
+ if (!item || typeof item !== "object" || typeof item.text !== "string") return []
+ const text = item.text.replace(/[\x00-\x1f\x7f]/g, " ").slice(0, 240)
+ if (!text.trim()) return []
+ return [{ text, fg: typeof item.fg === "string" ? item.fg : undefined, bold: item.bold === true }]
+ })
+ if (!items.length) continue
+ if (segments.length) items[0].breakBefore = true
+ segments.push(...items)
+ }
+ return { text: segments.map((item) => `${item.breakBefore ? "\n" : ""}${item.text}`).join(""), segments }
+ }
const rawSegments = Array.isArray(parsed)
? parsed
: parsed && typeof parsed === "object" && "segments" in parsed && Array.isArray((parsed as any).segments)
@@ -437,7 +455,7 @@ function parseScriptOutput(text: string): MendPromptStatusScriptOutput {
return { text: normalizeScriptText(trimmed) }
}
-export async function readPromptStatusScript(input: MendPromptStatusScriptInput) {
+export async function readPromptStatusScript(input: MendPromptStatusScriptInput): Promise {
const key = JSON.stringify(input)
const warmKey = warmCacheKey(input)
const now = Date.now()
@@ -478,6 +496,7 @@ export async function readPromptStatusScript(input: MendPromptStatusScriptInput)
MEND_TUI_AGENTS_HINT: input.agentsHint || "",
MEND_TUI_PROMPT_PRESET: input.preset,
MEND_TUI_STATUS_SIDE: input.side,
+ MEND_TUI_STATUS_FORMAT: "rows-v1",
MEND_TUI_STATUS_PREPEND: input.prepend ? "1" : "0",
MEND_TUI_STATUS_REFRESH_KEY: String(input.refreshKey ?? 0),
},
diff --git a/src/mendcode/packages/opencode/src/tool/computer.ts b/src/mendcode/packages/opencode/src/tool/computer.ts
index e2d029f..15ca560 100644
--- a/src/mendcode/packages/opencode/src/tool/computer.ts
+++ b/src/mendcode/packages/opencode/src/tool/computer.ts
@@ -24,18 +24,21 @@ export async function nativeComputerCommand(command: string[], signal: AbortSign
}
const Parameters = Schema.Struct({
- region: Schema.optional(Schema.Struct({ x: Schema.Number, y: Schema.Number, width: Schema.Number, height: Schema.Number })).annotate({ description: "Optional crop in native screen points; cannot be combined with windowID." }),
- windowID: Schema.optional(Schema.Number).annotate({ description: "Optional known macOS window ID; omit to capture the main display." }),
+ region: Schema.optional(Schema.NullOr(Schema.Struct({ x: Schema.Number, y: Schema.Number, width: Schema.Number, height: Schema.Number }))).annotate({ description: "Crop in native screen points, or null/omitted. Use null when capturing a window; never combine two non-null selectors." }),
+ windowID: Schema.optional(Schema.NullOr(Schema.Number)).annotate({ description: "Known positive macOS window ID, or null/omitted. For the main display, set both region and windowID to null. Never use 0 as an absent ID." }),
})
export const ComputerCaptureTool = Tool.define("computer_capture", Effect.succeed({
description: "Take and immediately view a native screenshot on macOS. Captures the main display or a known window ID, returns a PNG attachment plus an absolute file path for read. Resizes the preview to at most 1600 pixels. Requires OS Screen Recording permission; does not grant it automatically. Other operating systems are currently unsupported.",
parameters: Parameters,
execute: (args: typeof Parameters.Type, ctx: Tool.Context) => Effect.gen(function* () {
+ // Strict tool transports may require every key: null represents absence.
+ const region = args.region ?? undefined
+ const windowID = args.windowID ?? undefined
if (process.platform !== "darwin") throw new Error("Native computer capture is currently supported only on macOS. Use a configured computer/browser MCP service on this platform.")
- if (args.windowID !== undefined && (!Number.isSafeInteger(args.windowID) || args.windowID <= 0)) throw new Error("windowID must be a positive integer")
- if (args.region && (args.windowID !== undefined || !Object.values(args.region).every(Number.isSafeInteger) || args.region.width < 1 || args.region.height < 1 || args.region.width > 16384 || args.region.height > 16384)) throw new Error("Use an integer crop with positive dimensions up to 16384, or a window ID, not both")
- yield* ctx.ask({ permission: "computer_capture", patterns: [args.windowID ? `window:${args.windowID}` : "main-display"], always: ["*"], metadata: { surface: args.windowID ?? "main-display" } })
+ if (windowID !== undefined && (!Number.isSafeInteger(windowID) || windowID <= 0)) throw new Error("windowID must be a positive integer")
+ if (region && (windowID !== undefined || !Object.values(region).every(Number.isSafeInteger) || region.width < 1 || region.height < 1 || region.width > 16384 || region.height > 16384)) throw new Error("Use an integer crop with positive dimensions up to 16384, or a window ID, not both")
+ yield* ctx.ask({ permission: "computer_capture", patterns: [windowID ? `window:${windowID}` : "main-display"], always: ["*"], metadata: { surface: windowID ?? "main-display" } })
const directory = path.join(Global.Path.cache, "computer", ctx.sessionID)
const id = crypto.randomUUID()
const filePath = path.join(directory, `${id}.png`)
@@ -43,13 +46,13 @@ export const ComputerCaptureTool = Tool.define("computer_capture", Effect.succee
yield* Effect.promise(async () => {
const displays = await nativeComputerCommand(["/usr/sbin/system_profiler", "SPDisplaysDataType", "-json"], ctx.abort).catch(() => "{}")
const displayCount = (JSON.parse(displays).SPDisplaysDataType ?? []).reduce((sum: number, gpu: { spdisplays_ndrvs?: unknown[] }) => sum + (gpu.spdisplays_ndrvs?.length ?? 0), 0)
- if (args.windowID === undefined && !args.region && displayCount === 1) {
+ if (windowID === undefined && !region && displayCount === 1) {
const value = await nativeComputerCommand(["/usr/bin/osascript", "-e", FRONTMOST], ctx.abort).catch(() => "")
if (/^\d+$/.test(value)) observedPID = Number(value)
}
await mkdir(directory, { recursive: true, mode: 0o700 })
if ((await readdir(directory)).filter((name) => name.endsWith(".png")).length >= 64) throw new Error("This session reached its 64 screenshot artifact limit. Archive or remove its cached captures before continuing.")
- await nativeComputerCommand(["/usr/sbin/screencapture", "-x", ...(args.region ? ["-R", `${args.region.x},${args.region.y},${args.region.width},${args.region.height}`] : args.windowID ? ["-l", String(args.windowID)] : ["-m"]), filePath], ctx.abort)
+ await nativeComputerCommand(["/usr/sbin/screencapture", "-x", ...(region ? ["-R", `${region.x},${region.y},${region.width},${region.height}`] : windowID ? ["-l", String(windowID)] : ["-m"]), filePath], ctx.abort)
await nativeComputerCommand(["/usr/bin/sips", "-Z", "1600", filePath], ctx.abort)
if (observedPID !== undefined) {
const after = await nativeComputerCommand(["/usr/bin/osascript", "-e", FRONTMOST], ctx.abort).catch(() => "")
diff --git a/src/mendcode/packages/opencode/test/mend/adaptive-reasoning-jev.test.ts b/src/mendcode/packages/opencode/test/mend/adaptive-reasoning-jev.test.ts
index a5cece6..cc26d2f 100644
--- a/src/mendcode/packages/opencode/test/mend/adaptive-reasoning-jev.test.ts
+++ b/src/mendcode/packages/opencode/test/mend/adaptive-reasoning-jev.test.ts
@@ -41,6 +41,21 @@ describe("Jev isolated HTTP contract", () => {
} finally { await server.stop(true) }
})
+ test("uses the Pi Gateway binding without OpenRouter routing or model fallback", async () => {
+ const input = { ...base(), policy: { ...base().policy, provider: "vercel-ai-gateway" as const } }
+ const decision = await evaluateJev({ ...input, fetch: async (url, init) => {
+ expect(url).toBe("https://ai-gateway.vercel.sh/v1/evaluate")
+ const body = JSON.parse(String(init.body))
+ expect(body.model).toBe("typesafe-ai/jev")
+ expect(body.provider).toBeUndefined()
+ expect(body.providerOptions).toEqual({ gateway: { only: ["typesafe-ai"] } })
+ return Response.json({ model: "typesafe-ai/jev", answers: result.answers, usage: { inputTokens: 8, outputTokens: 2 } })
+ } })
+ expect(decision.effort).toBe("high")
+ expect(decision.usage).toEqual({ inputTokens: 8, outputTokens: 2, cost: undefined })
+ await expect(evaluateJev({ ...input, fetch: async () => Response.json(result) })).rejects.toMatchObject({ code: "response" })
+ })
+
test("off, missing consent, missing credential, stale revision and denied budget send nothing", async () => {
let calls = 0
const transport = async () => { calls++; return Response.json(result) }
diff --git a/src/mendcode/packages/opencode/test/mend/tui-prompt-chrome.test.ts b/src/mendcode/packages/opencode/test/mend/tui-prompt-chrome.test.ts
index eb227cb..35ce37b 100644
--- a/src/mendcode/packages/opencode/test/mend/tui-prompt-chrome.test.ts
+++ b/src/mendcode/packages/opencode/test/mend/tui-prompt-chrome.test.ts
@@ -399,6 +399,21 @@ describe("mend tui prompt chrome", () => {
expect(result.text).toBe("Build")
})
+ test("preserves explicit status rows, colors and bounded content", async () => {
+ const rows = [[{ text: "ASTRA", fg: "accent", bold: true }], [], [{ text: "quota\nā" }], [{ text: "GRAFT" }], [{ text: "hidden" }]]
+ const result = await readPromptStatusScript({
+ command: `printf '%s' '${JSON.stringify({ rows })}'`,
+ root: process.cwd(), promptMode: "custom", model: "", provider: "",
+ preset: "top-bottom", side: "left", prepend: false, timeoutMs: 1000,
+ })
+ expect(result.text).toBe("ASTRA\nquota ā\nGRAFT")
+ expect(result.segments).toEqual([
+ { text: "ASTRA", fg: "accent", bold: true },
+ { text: "quota ā", fg: undefined, bold: false, breakBefore: true },
+ { text: "GRAFT", fg: undefined, bold: false, breakBefore: true },
+ ])
+ })
+
test("calculates the current prompt cache ratio and hides cache misses", () => {
expect(resolvePromptCachePercent({ input: 5000, cache: { read: 5000, write: 0 } })).toBe(50)
expect(resolvePromptCachePercent({ input: 100, cache: { read: 100, write: 50 } })).toBe(40)
diff --git a/src/mendcode/packages/opencode/test/tool/computer.test.ts b/src/mendcode/packages/opencode/test/tool/computer.test.ts
index 3465e19..e494c42 100644
--- a/src/mendcode/packages/opencode/test/tool/computer.test.ts
+++ b/src/mendcode/packages/opencode/test/tool/computer.test.ts
@@ -65,6 +65,23 @@ describe("native computer boundary", () => {
expect(os.state.commands).toHaveLength(0)
}))
+ it.instance("accepts nullable selectors from strict tool transports", () => Effect.gen(function* () {
+ if (process.platform !== "darwin") return
+ const capture = yield* Tool.init(yield* ComputerCaptureTool)
+ using os = desktop()
+ const ctx = context()
+ yield* capture.execute({ region: null, windowID: null }, ctx)
+ yield* capture.execute({ region: null, windowID: 42 }, ctx)
+ yield* capture.execute({ region: { x: 0, y: 0, width: 100, height: 80 }, windowID: null }, ctx)
+ const commands = os.state.commands.filter((command) => command[0] === "/usr/sbin/screencapture")
+ expect(commands[0]).toContain("-m")
+ expect(commands[1]).toContain("42")
+ expect(commands[2]).toContain("0,0,100,80")
+ const count = os.state.commands.length
+ expect((yield* Effect.exit(capture.execute({ region: null, windowID: 0 }, ctx)))._tag).toBe("Failure")
+ expect(os.state.commands).toHaveLength(count)
+ }))
+
it.instance("binds capture to session, consumes once and rejects foreground changes", () => Effect.gen(function* () {
if (process.platform !== "darwin") return
const capture = yield* Tool.init(yield* ComputerCaptureTool)