From cd1f051e7efa7ef0aae87330352b852cf874030c Mon Sep 17 00:00:00 2001 From: Kerem Proulx Date: Sat, 18 Apr 2026 22:45:39 -0400 Subject: [PATCH 1/4] fix(tui): decode paste bytes after opentui PasteEvent shape change opentui 0.1.100 changed PasteEvent from `{ text: string }` to `{ bytes: Uint8Array }`, which broke every onPaste handler with `undefined is not an object (evaluating 'text.split')`. Decode via `stripAnsiSequences(decodePasteBytes(event.bytes))` to match the built-in Textarea handler. --- src/tui/components/chat/input-area.tsx | 6 ++-- src/tui/components/commands/api-key-input.tsx | 6 ++-- src/tui/components/commands/web-wizard.tsx | 35 ++++++++++++++----- .../components/model-picker/ModelPicker.tsx | 21 +++++------ src/tui/components/shared/approval-prompt.tsx | 6 ++-- .../components/shared/use-paste-extmarks.ts | 10 ++++-- 6 files changed, 53 insertions(+), 31 deletions(-) diff --git a/src/tui/components/chat/input-area.tsx b/src/tui/components/chat/input-area.tsx index ef21dad1c..9dd1942b9 100644 --- a/src/tui/components/chat/input-area.tsx +++ b/src/tui/components/chat/input-area.tsx @@ -9,6 +9,7 @@ import React, { useState, useEffect, useRef } from "react"; import { useKeyboard } from "@opentui/react"; +import { decodePasteBytes, stripAnsiSequences } from "@opentui/core"; import { useTheme } from "../../theme"; import { PromptInput, type PromptInputRef } from "../shared/prompt-input"; import { InputProvider, useInput } from "../../context/input"; @@ -16,7 +17,6 @@ import type { PendingApproval } from "../../../core/operator"; import { type OperatorMode, OPERATOR_MODES } from "../../../core/operator"; import { useAgent } from "../../context/agent"; import { useDimensions } from "../../context/dimensions"; -import { getPasteText } from "../../utils/paste"; const PROVIDER_DISPLAY_NAMES: Record = { anthropic: "Anthropic", @@ -465,8 +465,8 @@ function ApprovalInputArea({ value={redirectInput} onInput={setRedirectInput} onPaste={(event) => { - const cleaned = getPasteText(event).replace(/\r?\n/g, " "); - setRedirectInput(cleaned); + const text = stripAnsiSequences(decodePasteBytes(event.bytes)); + setRedirectInput(text.replace(/\r?\n/g, " ")); }} focused={focusedElement === 2} placeholder="Or type to redirect agent..." diff --git a/src/tui/components/commands/api-key-input.tsx b/src/tui/components/commands/api-key-input.tsx index 0122d8414..e89cd1327 100644 --- a/src/tui/components/commands/api-key-input.tsx +++ b/src/tui/components/commands/api-key-input.tsx @@ -1,11 +1,11 @@ import { useKeyboard } from "@opentui/react"; +import { decodePasteBytes, stripAnsiSequences } from "@opentui/core"; import { useState } from "react"; import Input from "../input"; import { type ProviderType, verifyApiKey } from "../../../core/providers"; import { useTheme } from "../../theme"; import { Dialog } from "../../context/dialog"; import DialogLayout from "../dialog-layout"; -import { getPasteText } from "../../utils/paste"; type VerifyState = "idle" | "verifying" | "error"; @@ -93,8 +93,8 @@ export default function APIKeyInput({ setApiKey(typeof value === "string" ? value : "") } onPaste={(event) => { - const cleaned = getPasteText(event); - setApiKey((prev) => `${prev}${cleaned}`); + const text = stripAnsiSequences(decodePasteBytes(event.bytes)); + setApiKey((prev) => `${prev}${text}`); }} onSubmit={handleSubmit} /> diff --git a/src/tui/components/commands/web-wizard.tsx b/src/tui/components/commands/web-wizard.tsx index 99f694b0f..71f32ce64 100644 --- a/src/tui/components/commands/web-wizard.tsx +++ b/src/tui/components/commands/web-wizard.tsx @@ -1,6 +1,10 @@ import { useState, useEffect, useRef } from "react"; import { useKeyboard } from "@opentui/react"; -import { ScrollBoxRenderable } from "@opentui/core"; +import { + ScrollBoxRenderable, + decodePasteBytes, + stripAnsiSequences, +} from "@opentui/core"; import Input from "../input"; import { useConfig } from "../../context/config"; import { useAgent } from "../../context/agent"; @@ -20,7 +24,6 @@ import { } from "../../utils/command-flags"; import { scrollToChild } from "../../utils/scroll"; import { ModelPickerDialog } from "../model-picker"; -import { getPasteText } from "../../utils/paste"; // Wizard state interface interface WizardState { @@ -595,7 +598,9 @@ export default function WebWizard({ setState((prev) => ({ ...prev, target: v })); }} onPaste={(event) => { - const cleaned = getPasteText(event).replace(/\r?\n/g, " "); + const cleaned = stripAnsiSequences( + decodePasteBytes(event.bytes), + ).replace(/\r?\n/g, " "); setTargetError(null); setState((prev) => ({ ...prev, @@ -663,7 +668,9 @@ export default function WebWizard({ value={state.prompt} onInput={(v) => setState((prev) => ({ ...prev, prompt: v }))} onPaste={(event) => { - const cleaned = getPasteText(event).replace(/\r?\n/g, " "); + const cleaned = stripAnsiSequences( + decodePasteBytes(event.bytes), + ).replace(/\r?\n/g, " "); setState((prev) => ({ ...prev, prompt: prev.prompt + cleaned, @@ -686,7 +693,9 @@ export default function WebWizard({ }} onPaste={(event) => { setThreatModelPreWrapped(false); - const cleaned = getPasteText(event).replace(/\r?\n/g, " "); + const cleaned = stripAnsiSequences( + decodePasteBytes(event.bytes), + ).replace(/\r?\n/g, " "); setState((prev) => ({ ...prev, threatModel: prev.threatModel + cleaned, @@ -711,7 +720,9 @@ export default function WebWizard({ })) } onPaste={(event) => { - const cleaned = getPasteText(event).replace(/\r?\n/g, " "); + const cleaned = stripAnsiSequences( + decodePasteBytes(event.bytes), + ).replace(/\r?\n/g, " "); setState((prev) => ({ ...prev, auth: { @@ -735,7 +746,9 @@ export default function WebWizard({ })) } onPaste={(event) => { - const cleaned = getPasteText(event).replace(/\r?\n/g, " "); + const cleaned = stripAnsiSequences( + decodePasteBytes(event.bytes), + ).replace(/\r?\n/g, " "); setState((prev) => ({ ...prev, auth: { @@ -759,7 +772,9 @@ export default function WebWizard({ })) } onPaste={(event) => { - const cleaned = getPasteText(event).replace(/\r?\n/g, " "); + const cleaned = stripAnsiSequences( + decodePasteBytes(event.bytes), + ).replace(/\r?\n/g, " "); setState((prev) => ({ ...prev, auth: { @@ -783,7 +798,9 @@ export default function WebWizard({ })) } onPaste={(event) => { - const cleaned = getPasteText(event).replace(/\r?\n/g, " "); + const cleaned = stripAnsiSequences( + decodePasteBytes(event.bytes), + ).replace(/\r?\n/g, " "); setState((prev) => ({ ...prev, auth: { diff --git a/src/tui/components/model-picker/ModelPicker.tsx b/src/tui/components/model-picker/ModelPicker.tsx index ad272f858..6077f865e 100644 --- a/src/tui/components/model-picker/ModelPicker.tsx +++ b/src/tui/components/model-picker/ModelPicker.tsx @@ -7,13 +7,16 @@ import { type ReactNode, } from "react"; import { useKeyboard } from "@opentui/react"; -import { ScrollBoxRenderable } from "@opentui/core"; +import { + ScrollBoxRenderable, + decodePasteBytes, + stripAnsiSequences, +} from "@opentui/core"; import { modelSupportsThinking, type ModelInfo } from "../../../core/ai"; import { getAvailableModels } from "../../../core/providers/utils"; import type { Config } from "../../../core/config/config"; import { useTheme } from "../../theme"; import { scrollToChild } from "../../utils/scroll"; -import { getPasteText } from "../../utils/paste"; const providerNames: Record = { anthropic: "Claude", @@ -549,10 +552,9 @@ export function ModelPicker({ setLocalUrl(typeof v === "string" ? v : "") } onPaste={(event) => { - const cleaned = getPasteText(event).replace( - /\r?\n/g, - "", - ); + const cleaned = stripAnsiSequences( + decodePasteBytes(event.bytes), + ).replace(/\r?\n/g, ""); setLocalUrl((prev) => `${prev}${cleaned}`); }} onSubmit={finishEditing} @@ -593,10 +595,9 @@ export function ModelPicker({ setLocalModelName(typeof v === "string" ? v : "") } onPaste={(event) => { - const cleaned = getPasteText(event).replace( - /\r?\n/g, - "", - ); + const cleaned = stripAnsiSequences( + decodePasteBytes(event.bytes), + ).replace(/\r?\n/g, ""); setLocalModelName((prev) => `${prev}${cleaned}`); }} onSubmit={finishEditing} diff --git a/src/tui/components/shared/approval-prompt.tsx b/src/tui/components/shared/approval-prompt.tsx index d95aacf5a..a667ed72f 100644 --- a/src/tui/components/shared/approval-prompt.tsx +++ b/src/tui/components/shared/approval-prompt.tsx @@ -6,10 +6,10 @@ import { useState } from "react"; import { useKeyboard } from "@opentui/react"; +import { decodePasteBytes, stripAnsiSequences } from "@opentui/core"; import { useTheme } from "../../theme"; import { getToolSummary } from "./tool-registry"; import type { PendingApproval } from "../../../core/operator"; -import { getPasteText } from "../../utils/paste"; interface InlineApprovalPromptProps { approval: PendingApproval; @@ -138,8 +138,8 @@ export function ApprovalInputArea({ value={redirectInput} onInput={setRedirectInput} onPaste={(event) => { - const cleaned = getPasteText(event).replace(/\r?\n/g, " "); - setRedirectInput(cleaned); + const text = stripAnsiSequences(decodePasteBytes(event.bytes)); + setRedirectInput(text.replace(/\r?\n/g, " ")); }} focused={focusedElement === 2} placeholder="Tell the agent something else..." diff --git a/src/tui/components/shared/use-paste-extmarks.ts b/src/tui/components/shared/use-paste-extmarks.ts index 522dd618f..6916e204b 100644 --- a/src/tui/components/shared/use-paste-extmarks.ts +++ b/src/tui/components/shared/use-paste-extmarks.ts @@ -1,6 +1,10 @@ import { useRef } from "react"; -import type { PasteEvent, TextareaRenderable } from "@opentui/core"; -import { getPasteText } from "../../utils/paste"; +import { + decodePasteBytes, + stripAnsiSequences, + type PasteEvent, + type TextareaRenderable, +} from "@opentui/core"; interface PasteEntry { fullText: string; @@ -40,7 +44,7 @@ export function usePasteExtmarks( const textarea = textareaRef.current; if (!textarea) return; - const text = getPasteText(event); + const text = stripAnsiSequences(decodePasteBytes(event.bytes)); const lineCount = text.split("\n").length; if ( From 1d98a7895f3132b746ed3f50dd856a260314ec95 Mon Sep 17 00:00:00 2001 From: Kerem Proulx Date: Sat, 18 Apr 2026 23:53:15 -0400 Subject: [PATCH 2/4] chore(deps): pin @opentui/core and @opentui/react to 0.1.99 Previously `^0.1.80`, which silently picked up breaking changes (e.g. the PasteEvent bytes-vs-text flip shipped in 0.1.90). Pin exactly so future bumps are intentional. --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 020550a2b..4c2b7fc22 100644 --- a/package.json +++ b/package.json @@ -90,8 +90,8 @@ "@microsoft/microsoft-graph-client": "^3.0.7", "@modelcontextprotocol/sdk": "^1.0.0", "@openrouter/ai-sdk-provider": "^2.2.3", - "@opentui/core": "^0.1.80", - "@opentui/react": "^0.1.80", + "@opentui/core": "0.1.99", + "@opentui/react": "0.1.99", "@playwright/mcp": "^0.0.54", "ai": "^6.0.105", "glob": "^13.0.0", From 4fb18d29c6290f01829cae4596307a125b2ca0e1 Mon Sep 17 00:00:00 2001 From: Kerem Proulx Date: Sun, 19 Apr 2026 00:01:50 -0400 Subject: [PATCH 3/4] chore: commit bun.lock to lock dependency versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without a committed lockfile, fresh `bun install`s resolve every caret range to latest and can silently pull in breaking upstream changes — this is exactly how opentui 0.1.100 crept in and broke paste handling. Check bun.lock into git so every contributor and CI run gets the same resolved tree. --- bun.lock | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/bun.lock b/bun.lock index 758b743f1..17037e233 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 1, "workspaces": { "": { "name": "@pensar/apex", @@ -15,8 +14,8 @@ "@microsoft/microsoft-graph-client": "^3.0.7", "@modelcontextprotocol/sdk": "^1.0.0", "@openrouter/ai-sdk-provider": "^2.2.3", - "@opentui/core": "^0.1.80", - "@opentui/react": "^0.1.80", + "@opentui/core": "0.1.99", + "@opentui/react": "0.1.99", "@playwright/mcp": "^0.0.54", "ai": "^6.0.105", "glob": "^13.0.0", @@ -367,21 +366,21 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - "@opentui/core": ["@opentui/core@0.1.100", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.100", "@opentui/core-darwin-x64": "0.1.100", "@opentui/core-linux-arm64": "0.1.100", "@opentui/core-linux-x64": "0.1.100", "@opentui/core-win32-arm64": "0.1.100", "@opentui/core-win32-x64": "0.1.100", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-g6Ft3CcOVpytMzq2AZrnHHeMrmvYeLVCsy/8LqZ30VnPv0zfJ+f1TVi/EFrcl4m0GRPdy6yBOVOMcIAWHSZvtg=="], + "@opentui/core": ["@opentui/core@0.1.99", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.99", "@opentui/core-darwin-x64": "0.1.99", "@opentui/core-linux-arm64": "0.1.99", "@opentui/core-linux-x64": "0.1.99", "@opentui/core-win32-arm64": "0.1.99", "@opentui/core-win32-x64": "0.1.99", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-I3+AEgGzqNWIpWX9g2WOscSPwtQDNOm4KlBjxBWCZjLxkF07u77heWXF7OiAdhKLtNUW6TFiyt6yznqAZPdG3A=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.100", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cY70nNjkh53tys4iQ0FDST3CQfN4Rp8zOrT6EW0dH/51KZq2Rg3EhxDpc+qu4zASadR5uuU5i61g8lYyVGeGgw=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.99", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bzVrqeX2vb5iWrc/ftOUOqeUY8XO+qSgoTwj5TXHuwagavgwD3Hpeyjx8+icnTTeM4pao0som1WR9xfye6/X5Q=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.100", "", { "os": "darwin", "cpu": "x64" }, "sha512-RUJa4MPX5BWwXuc5nE+Zc+md8+ITYp5X0ourM4+ReaIIW9pTxSDcIPBba9pkJb3PIADyulUPMmmy5OX+umDJhQ=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.99", "", { "os": "darwin", "cpu": "x64" }, "sha512-VE4FrXBYpkxnvkqcCV1a8aN9jyyMJMihVW+V2NLCtp+4yQsj0AapG5TiUSN76XnmSZRptxDy5rBmEempeoIZbg=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.100", "", { "os": "linux", "cpu": "arm64" }, "sha512-Hznr39cSXg+2sQd+WcTsk67BjyqrZvuTK6f94Uu1ULYZJYCE4KdyNU2NzJSK6ooyosSWXPDsb4kwetTlfypMZg=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.99", "", { "os": "linux", "cpu": "arm64" }, "sha512-viXQsbpS7yHjYkl7+am32JdvG96QU9lvHh1UiZtpOxcNUUqiYmA2ZwZFPD2Bi54jNyj5l2hjH6YkD3DzE2FEWA=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.100", "", { "os": "linux", "cpu": "x64" }, "sha512-mssQIwH2DU1aMGSUnTJgahMeAEfF82n2WKeTGabTRkPrcxD/6ML+JFiV9l8/8YA880fBD2Kh1XXGEhN2ZGB5UA=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.99", "", { "os": "linux", "cpu": "x64" }, "sha512-WLoEFINOSp0tZSR9y4LUuGc7n4Y7H1wcpjUPzQ9vChkYDXrfZltEanzoDWbDcQ4kZQW5tHVC7LrZHpAsRLwFZg=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.100", "", { "os": "win32", "cpu": "arm64" }, "sha512-EuLA6+kiIyW/hbo7k56QTc9/QGxg2bYHQ+XPn1UWPf6tmjzbDUwzhw+y52CNJQZpTXtAQSNtxfiYIbigGBd4TQ=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.99", "", { "os": "win32", "cpu": "arm64" }, "sha512-yWMOLWCEO8HdrctU1dMkgZC8qGkiO4Dwr4/e11tTvVpRmYhDsP/IR89ZjEEtOwnKwFOFuB/MxvflqaEWVQ2g5Q=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.100", "", { "os": "win32", "cpu": "x64" }, "sha512-RyiqbKQ15olR8hK4VsPKL3gHrOIIpqEXhUP0jzXJdQQNusmQKlxHyk5EY3R6hi52IgqjjGxwG+ocVeRb4/VTRg=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.99", "", { "os": "win32", "cpu": "x64" }, "sha512-aYRlsL2w8YRL6vPd7/hrqlNVkXU3QowWb01TOvAcHS8UAsXaGFUr47kSDyjxDi1wg1MzmVduCfsC7T3NoThV1w=="], - "@opentui/react": ["@opentui/react@0.1.100", "", { "dependencies": { "@opentui/core": "0.1.100", "react-reconciler": "^0.32.0" }, "peerDependencies": { "react": ">=19.0.0", "react-devtools-core": "^7.0.1", "ws": "^8.18.0" } }, "sha512-4mIkrioQE/qMB3ItzZyKMhP6iEMiY0pwqHyeG4ANqY1Qe6xLvzYOfzQ0qG0iVVVM+FSo2YfcwMWKaij9iP3/BA=="], + "@opentui/react": ["@opentui/react@0.1.99", "", { "dependencies": { "@opentui/core": "0.1.99", "react-reconciler": "^0.32.0" }, "peerDependencies": { "react": ">=19.0.0", "react-devtools-core": "^7.0.1", "ws": "^8.18.0" } }, "sha512-Z7IWg2QLa2FOuaJEYvl6Uwi9vXobpkbFWFPPUN22aAQSKi1E7aOUJ0oNMvObB8eSnsJzv0bdvTP4CuvtnYEz/g=="], "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], From e317f40591650b0750319f787156b90fba99f33d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 20 Apr 2026 14:27:14 +0000 Subject: [PATCH 4/4] style: fix prettier formatting issues in offSecAgent tools Co-authored-by: Yuvanesh --- bun.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/bun.lock b/bun.lock index 17037e233..619fa2648 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "@pensar/apex",