diff --git a/packages/tui/package.json b/packages/tui/package.json
index 4a8d189c5..a3d386f99 100644
--- a/packages/tui/package.json
+++ b/packages/tui/package.json
@@ -23,17 +23,11 @@
"dependencies": {
"chalk": "^5.6.2",
"get-east-asian-width": "^1.3.0",
- "ink": "^7.0.2",
"marked": "^18.0.3",
- "mime-types": "^3.0.1",
- "react": "^19.2.6"
+ "mime-types": "^3.0.1"
},
"devDependencies": {
- "@types/mime-types": "^2.1.4",
- "@types/react": "^19.2.14"
- },
- "peerDependencies": {
- "react": ">=19"
+ "@types/mime-types": "^2.1.4"
},
"optionalDependencies": {
"koffi": "^2.9.0"
diff --git a/packages/tui/src/__tests__/ink-bridge.test.ts b/packages/tui/src/__tests__/ink-bridge.test.ts
deleted file mode 100644
index fb7c2e829..000000000
--- a/packages/tui/src/__tests__/ink-bridge.test.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { describe, expect, test } from "vitest";
-import type { Component } from "../tui.js";
-
-/** Minimal component used to verify the bridge's rendering contract. */
-class HelloComponent implements Component {
- render(_width: number): string[] {
- return ["Hello from Ink bridge!"];
- }
- invalidate() {}
-}
-
-describe("ink-bridge", () => {
- test("LegacyComponentView_renders_Component_lines", () => {
- // The bridge rendering itself requires a TTY — verify the Component
- // protocol works as expected before wiring it into Ink.
- const comp = new HelloComponent();
- const lines = comp.render(80);
- expect(lines).toEqual(["Hello from Ink bridge!"]);
- });
-
- test("Component_render_respects_width", () => {
- const comp = new HelloComponent();
- // Width is ignored by this stub, but callers may pass any positive value.
- expect(comp.render(40)).toHaveLength(1);
- expect(comp.render(120)).toHaveLength(1);
- });
-
- test("startInkRenderer_is_exported_from_package", async () => {
- // Verify the public API surface exists without running it (no TTY in CI).
- const mod = await import("../ink-bridge.js");
- expect(typeof mod.startInkRenderer).toBe("function");
- });
-});
diff --git a/packages/tui/src/__tests__/tui.test.ts b/packages/tui/src/__tests__/tui.test.ts
index 6248dc52b..5667b9c9d 100644
--- a/packages/tui/src/__tests__/tui.test.ts
+++ b/packages/tui/src/__tests__/tui.test.ts
@@ -153,41 +153,3 @@ describe("Container", () => {
}
});
});
-
-describe("TUI useInk", () => {
- it("useInk_sets_flag_read_by_start", () => {
- const tui = new TUI(makeTerminal());
- const anyTui = tui as any;
- assert.equal(anyTui._useInk, false, "defaults to false");
- tui.useInk();
- assert.equal(anyTui._useInk, true, "useInk() sets flag to true");
- });
-
- it("stop_clears_inkHandle_and_skips_terminal_stop_when_ink_active", () => {
- const tui = new TUI(makeTerminal());
- const anyTui = tui as any;
- let stopped = false;
- // Inject a fake Ink handle as if start() had mounted Ink.
- anyTui._inkHandle = {
- stop: () => {
- stopped = true;
- },
- invalidate: () => {},
- };
- // Track whether the legacy terminal.stop() path was taken.
- let terminalStopped = false;
- (anyTui.terminal as any).stop = () => {
- terminalStopped = true;
- };
-
- tui.stop();
-
- assert.equal(stopped, true, "Ink handle stop() must be called");
- assert.equal(anyTui._inkHandle, null, "_inkHandle cleared after stop");
- assert.equal(
- terminalStopped,
- false,
- "legacy terminal.stop() must not be called when Ink is active",
- );
- });
-});
diff --git a/packages/tui/src/index.ts b/packages/tui/src/index.ts
index ff8f8ac13..9d7b5085b 100644
--- a/packages/tui/src/index.ts
+++ b/packages/tui/src/index.ts
@@ -48,8 +48,6 @@ export { TruncatedText } from "./components/truncated-text.js";
export type { EditorComponent } from "./editor-component.js";
// Fuzzy helpers are imported from `@singularity-forge/tui/fuzzy` (package subpath —
// avoids barrel regressions dropping exports). See package.json `"exports"` map.
-// Ink bridge — gradual migration infrastructure
-export { startInkRenderer } from "./ink-bridge.js";
// Keybindings
export {
DEFAULT_EDITOR_KEYBINDINGS,
diff --git a/packages/tui/src/ink-bridge.tsx b/packages/tui/src/ink-bridge.tsx
deleted file mode 100644
index 1041ee00e..000000000
--- a/packages/tui/src/ink-bridge.tsx
+++ /dev/null
@@ -1,123 +0,0 @@
-/**
- * ink-bridge.tsx — Ink render loop adapter for the legacy Component interface.
- *
- * Purpose: host the existing Component tree inside Ink's React render loop,
- * enabling gradual migration to native Ink components without rewriting
- * the entire interactive mode at once.
- *
- * Consumer: TUI class (replaces the custom differential renderer in a future step).
- */
-import React, { useEffect, useState } from "react";
-import { Box, Text, render, useInput, useWindowSize } from "ink";
-import type { Component } from "./tui.js";
-
-/**
- * Renders a legacy Component tree inside Ink.
- *
- * Purpose: bridge the existing string-line rendering protocol into Ink's
- * React tree so that components can be migrated one-by-one.
- *
- * Consumer: InkApp below.
- */
-function LegacyComponentView({
- component,
- width,
-}: {
- component: Component;
- width: number;
-}) {
- const lines = component.render(width);
- return (
-
- {lines.map((line, i) => (
- // biome-ignore lint/suspicious/noArrayIndexKey: stable index is fine for sequential lines
- {line}
- ))}
-
- );
-}
-
-/**
- * Root Ink app that drives the legacy Component render loop.
- *
- * Purpose: accept keyboard input from Ink and route it to the active
- * component, then trigger a re-render so the updated state is displayed.
- * Invalidation is event-driven: external callers invoke the returned
- * invalidate() handle, which fires the tick signal registered here.
- *
- * Consumer: startInkRenderer.
- */
-function InkApp({
- root,
- onInput,
- onRegisterTick,
-}: {
- root: Component;
- onInput: (data: string) => void;
- onRegisterTick: (tick: () => void) => void;
-}) {
- const [, tick] = useState(0);
- const { columns } = useWindowSize();
-
- // Register the tick function so that startInkRenderer's invalidate() can
- // trigger a React re-render without a polling interval.
- useEffect(() => {
- onRegisterTick(() => tick((n) => n + 1));
- }, [onRegisterTick]);
-
- useInput((input, key) => {
- // Reconstruct the escape sequences that the legacy key handlers expect.
- let data = input;
- if (key.escape) data = "\x1b";
- else if (key.return) data = "\r";
- else if (key.backspace || key.delete) data = "\x7f";
- else if (key.tab) data = "\t";
- else if (key.upArrow) data = "\x1b[A";
- else if (key.downArrow) data = "\x1b[B";
- else if (key.leftArrow) data = "\x1b[D";
- else if (key.rightArrow) data = "\x1b[C";
- onInput(data);
- tick((n) => n + 1);
- });
-
- return ;
-}
-
-/**
- * Ink-backed TUI runtime.
- *
- * Purpose: drop-in replacement for the legacy TUI render engine. Mounting
- * this drives the entire Ink React tree and forwards terminal input to
- * the root Component's handleInput chain. invalidate() triggers an
- * immediate React re-render via an event-driven tick signal — no polling.
- *
- * Consumer: TUI class; standalone callers can use this to render any
- * Component tree under Ink.
- *
- * @param root - The root Component whose render() output fills the screen.
- * @param onInput - Called with each decoded key string for legacy handlers.
- * @returns Handle with stop() to unmount and invalidate() to request repaint.
- */
-export function startInkRenderer(
- root: Component,
- onInput: (data: string) => void,
-): { stop: () => void; invalidate: () => void } {
- // Mutable signal populated by InkApp via onRegisterTick once the React
- // tree has mounted. invalidate() fires this to trigger a synchronous tick.
- let _tick: (() => void) | null = null;
- const onRegisterTick = (tick: () => void) => {
- _tick = tick;
- };
-
- const { unmount } = render(
- ,
- { exitOnCtrlC: false },
- );
- return {
- stop: () => {
- _tick = null;
- unmount();
- },
- invalidate: () => _tick?.(),
- };
-}
diff --git a/packages/tui/src/tui.ts b/packages/tui/src/tui.ts
index c1d9a569a..1e1511262 100644
--- a/packages/tui/src/tui.ts
+++ b/packages/tui/src/tui.ts
@@ -11,7 +11,6 @@ import {
AutonomousStatusBar,
} from "./components/autonomous-status-bar.js";
import { syncHardwareCursorForIME } from "./hardware-cursor.js";
-import { startInkRenderer } from "./ink-bridge.js";
import { isKeyRelease, matchesKey } from "./keys.js";
import {
applyLineResets,
@@ -205,8 +204,6 @@ export class TUI extends Container {
private fullRedrawCount = 0;
private stopped = false;
private _lastRenderedComponents: string[] | null = null;
- private _useInk = false;
- private _inkHandle: { stop(): void; invalidate(): void } | null = null;
// === Sticky bottom scrolling ===
private isScrolledToBottom = true; // Track if user is scrolled to bottom
@@ -232,19 +229,6 @@ export class TUI extends Container {
}
}
- /**
- * Opt into the Ink-backed render loop.
- *
- * Purpose: switch the TUI from the hand-rolled differential renderer to Ink's
- * React render loop so components can be migrated to native Ink nodes
- * incrementally. Must be called before start().
- *
- * Consumer: interactive-mode and any callers that want Ink layout.
- */
- useInk(): void {
- this._useInk = true;
- }
-
get fullRedraws(): number {
return this.fullRedrawCount;
}
@@ -420,27 +404,6 @@ export class TUI extends Container {
if (!this.terminal.isTTY) {
return;
}
- // Ink-backed render path: Ink manages raw mode, input, and screen output.
- // The legacy differential renderer (doRender) is bypassed entirely on TTY.
- // process.stdout.isTTY guards this path — Ink requires a real interactive
- // TTY to mount. useInk() is kept as an explicit opt-in for callers that
- // want Ink on non-standard terminal configurations. Use PI_LEGACY_TUI=1
- // to force the legacy renderer for debugging.
- if (
- (this._useInk || process.stdout.isTTY) &&
- process.env.PI_LEGACY_TUI !== "1"
- ) {
- // Wrap `this` in a plain Component so the private handleInput doesn't
- // conflict with the public Component.handleInput? signature.
- const root: Component = {
- render: (w) => this.render(w),
- invalidate: () => this.invalidate(),
- };
- this._inkHandle = startInkRenderer(root, (data) =>
- this.handleInput(data),
- );
- return;
- }
this.terminal.start(
(data) => this.handleInput(data),
() => this.requestRender(),
@@ -486,14 +449,6 @@ export class TUI extends Container {
}
this.overlayStack = [];
- // Ink-backed path: unmount the Ink renderer and return; Ink restores the
- // terminal to cooked mode and shows the cursor itself.
- if (this._inkHandle) {
- this._inkHandle.stop();
- this._inkHandle = null;
- return;
- }
-
// Move cursor to the end of the content to prevent overwriting/artifacts on exit
if (this.previousLines.length > 0) {
const targetRow = this.previousLines.length; // Line after the last content
@@ -513,12 +468,6 @@ export class TUI extends Container {
requestRender(force = false): void {
// Skip rendering on non-TTY stdout to prevent CPU burn (issue #3095)
if (!this.terminal.isTTY) return;
- // Ink-backed path: Ink owns the terminal — delegate to the Ink handle and
- // do NOT call doRender(), which would write conflicting ANSI escapes.
- if (this._inkHandle) {
- this._inkHandle.invalidate();
- return;
- }
if (force) {
this.previousLines = [];
this.previousWidth = -1; // -1 triggers widthChanged, forcing a full clear
diff --git a/packages/tui/tsconfig.json b/packages/tui/tsconfig.json
index 434a219b5..2af393ca5 100644
--- a/packages/tui/tsconfig.json
+++ b/packages/tui/tsconfig.json
@@ -19,11 +19,8 @@
"resolveJsonModule": true,
"allowImportingTsExtensions": false,
"useDefineForClassFields": false,
- "jsx": "react-jsx",
- "jsxImportSource": "react",
"types": [
- "node",
- "react"
+ "node"
],
"outDir": "./dist",
"rootDir": "./src"
diff --git a/tsconfig.extensions.json b/tsconfig.extensions.json
index d84893e5b..4236ca0e9 100644
--- a/tsconfig.extensions.json
+++ b/tsconfig.extensions.json
@@ -8,8 +8,6 @@
"ignoreDeprecations": "6.0",
"target": "ES2024",
"lib": ["ES2024", "DOM", "DOM.Iterable"],
- "jsx": "react-jsx",
- "jsxImportSource": "react",
"rootDir": ".",
"paths": {
"@singularity-forge/coding-agent": [