11import assert from "node:assert/strict" ;
22import { test } from "vitest" ;
3- import { Input , SelectList } from "@earendil-works/pi-tui" ;
3+ import {
4+ Editor ,
5+ Input ,
6+ SelectList ,
7+ TuiMainScreen ,
8+ type EditorTheme ,
9+ type Terminal ,
10+ } from "@earendil-works/pi-tui" ;
11+ import { dirname , join } from "node:path" ;
12+ import { fileURLToPath , pathToFileURL } from "node:url" ;
413
514import {
615 installAutocompletePositionPatch ,
@@ -16,6 +25,8 @@ import {
1625 setBashExecPromptSpacing ,
1726 type BashExecSpacingEditor ,
1827} from "../src/bash-exec-spacing.ts" ;
28+ import { installNeutralBorderColorPatch , setNeutralBorderColor } from "../src/border-color.ts" ;
29+ import { getUiTweaksPatchState } from "../src/patch-state.ts" ;
1930import {
2031 getInputPromptPrefix ,
2132 installInputPromptPrefixPatch ,
@@ -186,6 +197,28 @@ class TestEditor implements BashExecSpacingEditor {
186197 }
187198}
188199
200+ class FakeTerminal implements Terminal {
201+ columns = 80 ;
202+ rows = 24 ;
203+
204+ get kittyProtocolActive ( ) : boolean {
205+ return false ;
206+ }
207+
208+ start ( ) : void { }
209+ stop ( ) : void { }
210+ async drainInput ( ) : Promise < void > { }
211+ write ( _data : string ) : void { }
212+ moveBy ( ) : void { }
213+ hideCursor ( ) : void { }
214+ showCursor ( ) : void { }
215+ clearLine ( ) : void { }
216+ clearFromCursor ( ) : void { }
217+ clearScreen ( ) : void { }
218+ setTitle ( ) : void { }
219+ setProgress ( ) : void { }
220+ }
221+
189222function createTestModelSelector ( ) : TestModelSelector {
190223 return {
191224 addedComponents : [ ] ,
@@ -627,3 +660,113 @@ test("slash command source patch is idempotent", () => {
627660
628661 assert . equal ( Reflect . get ( interactiveMode , "prefixAutocompleteDescription" ) , patchedPrefix ) ;
629662} ) ;
663+
664+ test ( "neutral border patch renders real editor borders with the text color" , async ( ) => {
665+ const codingAgentEntry = fileURLToPath ( import . meta. resolve ( "@earendil-works/pi-coding-agent" ) ) ;
666+ const themeUrl = pathToFileURL (
667+ join ( dirname ( codingAgentEntry ) , "modes/interactive/theme/theme.js" ) ,
668+ ) . href ;
669+ // Pi's internal theme module is intentionally resolved from the installed runtime package.
670+ const themeModule : unknown = ( await import ( themeUrl ) ) as unknown ;
671+ if ( typeof themeModule !== "object" || themeModule === null ) {
672+ assert . fail ( "missing Pi theme module" ) ;
673+ }
674+ const initTheme : unknown = Reflect . get ( themeModule , "initTheme" ) ;
675+ if ( typeof initTheme !== "function" ) {
676+ assert . fail ( "invalid Pi theme module" ) ;
677+ }
678+
679+ const Theme : unknown = Reflect . get ( themeModule , "Theme" ) ;
680+ if ( ( typeof Theme !== "object" && typeof Theme !== "function" ) || Theme === null ) {
681+ assert . fail ( "invalid Pi Theme constructor" ) ;
682+ }
683+ const themePrototype : unknown = Reflect . get ( Theme , "prototype" ) ;
684+ if (
685+ ( typeof themePrototype !== "object" && typeof themePrototype !== "function" ) ||
686+ themePrototype === null
687+ ) {
688+ assert . fail ( "invalid Pi Theme prototype" ) ;
689+ }
690+ const originalFgDescriptor = Reflect . getOwnPropertyDescriptor ( themePrototype , "fg" ) ;
691+ if (
692+ originalFgDescriptor === undefined ||
693+ typeof Reflect . get ( themePrototype , "fg" ) !== "function"
694+ ) {
695+ assert . fail ( "invalid Pi Theme.fg descriptor" ) ;
696+ }
697+ const neutralBorderPatchKey = Symbol . for ( "zigai.pi-ui-tweaks.neutral-border-color-patched" ) ;
698+ const originalPatchMarkerDescriptor = Reflect . getOwnPropertyDescriptor (
699+ themePrototype ,
700+ neutralBorderPatchKey ,
701+ ) ;
702+
703+ const themeKey = Symbol . for ( "@earendil-works/pi-coding-agent:theme" ) ;
704+ const oldThemeKey = Symbol . for ( "@mariozechner/pi-coding-agent:theme" ) ;
705+ const hadTheme = Reflect . has ( globalThis , themeKey ) ;
706+ const hadOldTheme = Reflect . has ( globalThis , oldThemeKey ) ;
707+ const originalTheme : unknown = Reflect . get ( globalThis , themeKey ) ;
708+ const originalOldTheme : unknown = Reflect . get ( globalThis , oldThemeKey ) ;
709+ const originalNeutralBorderColor = getUiTweaksPatchState ( ) . neutralBorderColor ;
710+
711+ try {
712+ Reflect . apply ( initTheme , themeModule , [ undefined , false ] ) ;
713+
714+ const getEditorTheme : unknown = Reflect . get ( themeModule , "getEditorTheme" ) ;
715+ const theme : unknown = Reflect . get ( themeModule , "theme" ) ;
716+ const themeFg : unknown = Reflect . get ( theme ?? { } , "fg" ) ;
717+ if ( typeof getEditorTheme !== "function" || typeof themeFg !== "function" ) {
718+ assert . fail ( "invalid Pi theme module" ) ;
719+ }
720+
721+ const editorThemeValue : unknown = Reflect . apply ( getEditorTheme , themeModule , [ ] ) ;
722+ const textBorder : unknown = Reflect . apply ( themeFg , theme , [ "text" , "─" ] ) ;
723+ const mutedBorder : unknown = Reflect . apply ( themeFg , theme , [ "borderMuted" , "─" ] ) ;
724+ if (
725+ typeof editorThemeValue !== "object" ||
726+ editorThemeValue === null ||
727+ typeof textBorder !== "string" ||
728+ typeof mutedBorder !== "string"
729+ ) {
730+ assert . fail ( "invalid Pi editor theme" ) ;
731+ }
732+
733+ await installNeutralBorderColorPatch ( ) ;
734+ setNeutralBorderColor ( true ) ;
735+ // SAFETY: The runtime adapter above verifies Pi returned the editor-theme object;
736+ // the real Editor constructor exercises its complete contract below.
737+ const editor = new Editor (
738+ new TuiMainScreen ( new FakeTerminal ( ) ) ,
739+ editorThemeValue as EditorTheme ,
740+ ) ;
741+ const neutralTopBorder = editor . render ( 12 ) [ 0 ] ?? "" ;
742+
743+ setNeutralBorderColor ( false ) ;
744+ const originalTopBorder = editor . render ( 12 ) [ 0 ] ?? "" ;
745+
746+ assert . equal ( neutralTopBorder , textBorder . repeat ( 12 ) ) ;
747+ assert . equal ( originalTopBorder , mutedBorder . repeat ( 12 ) ) ;
748+ assert . notEqual ( neutralTopBorder , originalTopBorder ) ;
749+ } finally {
750+ setNeutralBorderColor ( originalNeutralBorderColor ) ;
751+ Reflect . defineProperty ( themePrototype , "fg" , originalFgDescriptor ) ;
752+ if ( originalPatchMarkerDescriptor === undefined ) {
753+ Reflect . deleteProperty ( themePrototype , neutralBorderPatchKey ) ;
754+ } else {
755+ Reflect . defineProperty (
756+ themePrototype ,
757+ neutralBorderPatchKey ,
758+ originalPatchMarkerDescriptor ,
759+ ) ;
760+ }
761+ if ( hadTheme ) {
762+ Reflect . set ( globalThis , themeKey , originalTheme ) ;
763+ } else {
764+ Reflect . deleteProperty ( globalThis , themeKey ) ;
765+ }
766+ if ( hadOldTheme ) {
767+ Reflect . set ( globalThis , oldThemeKey , originalOldTheme ) ;
768+ } else {
769+ Reflect . deleteProperty ( globalThis , oldThemeKey ) ;
770+ }
771+ }
772+ } ) ;
0 commit comments