Skip to content

Commit af1f238

Browse files
authored
Merge pull request #9 from codejunkie99/codex-gateflow-tui-curses-fix
Harden GateFlow TUI terminal support
2 parents 62c33f1 + 1bd07c4 commit af1f238

8 files changed

Lines changed: 104 additions & 19 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
{
1111
"name": "gateflow",
1212
"description": "AI-powered hardware development platform \u2014 design, verify, synthesize, release, and deploy working RTL with natural language. 20 agents, 27 skills, 8 IP blocks.",
13-
"version": "2.5.0",
13+
"version": "2.5.1",
1414
"author": {
1515
"name": "codejunkie99",
1616
"github": "https://github.com/codejunkie99"

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,7 @@ For detailed release notes, see [`releases.md`](releases.md).
590590

591591
| Version | Date | What Changed |
592592
|---------|------|-------------|
593+
| **2.5.1** | 2026-05-21 | TUI terminal compatibility fixes for cursor and colorless PTYs |
593594
| **2.5.0** | 2026-05-21 | OpenClaw-style CLI/TUI, release readiness workflow, deterministic validators, synced marketplace/docs/index/mirrors |
594595
| **2.4.0** | 2026-04-11 | Deep skill enrichment across verification, synthesis, orchestration, architecture, learning, IP, and planning |
595596
| **2.3.0** | 2026-03-27 | Quality pass, expanded IP docs, new commands, and component reference fixes |

plugins/gateflow/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "gateflow",
3-
"version": "2.5.0",
3+
"version": "2.5.1",
44
"description": "AI-powered hardware development platform \u2014 design, verify, synthesize, release, and deploy working RTL with natural language. 20 agents, 27 skills, 8 IP blocks.",
55
"author": {
66
"name": "codejunkie99",

releases.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Releases
22

3+
## 2.5.1 (2026-05-21) — TUI Terminal Compatibility
4+
5+
Patch release for the GateFlow terminal console.
6+
7+
### Fixes
8+
- Made the interactive `/gf-tui` curses path tolerate terminals that reject
9+
`curs_set(0)`.
10+
- Added color fallback handling for PTYs with no usable curses color pairs.
11+
- Added regression coverage for cursor and color capability failures.
12+
- Updated the release validator default and TUI release check to follow the
13+
plugin manifest version.
14+
315
## 2.5.0 (2026-05-21) — CLI/TUI + Release Readiness
416

517
GateFlow now ships with a deterministic release-prep path so versioned plugin

tests/test_gateflow_tui.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def test_json_mode_returns_machine_readable_inventory(self):
3636
payload = tui.build_payload(ROOT)
3737

3838
self.assertEqual("gateflow", payload["plugin"]["name"])
39-
self.assertEqual("2.5.0", payload["plugin"]["version"])
39+
self.assertEqual("2.5.1", payload["plugin"]["version"])
4040
self.assertEqual(20, payload["inventory"]["agents"])
4141
self.assertEqual(27, payload["inventory"]["skills"])
4242
self.assertEqual(21, payload["inventory"]["commands"])
@@ -55,6 +55,46 @@ def test_cli_snapshot_mode(self):
5555
self.assertEqual(0, completed.returncode)
5656
self.assertIn("GateFlow Terminal", completed.stdout)
5757

58+
def test_hide_cursor_ignores_unsupported_terminals(self):
59+
tui = load_tui()
60+
61+
class FakeCurses:
62+
error = RuntimeError
63+
64+
@staticmethod
65+
def curs_set(_visibility):
66+
raise RuntimeError("curs_set() returned ERR")
67+
68+
self.assertFalse(tui._hide_cursor(FakeCurses))
69+
70+
def test_terminal_styles_fall_back_without_color_pairs(self):
71+
tui = load_tui()
72+
73+
class FakeCurses:
74+
error = RuntimeError
75+
COLOR_RED = 1
76+
COLOR_GREEN = 2
77+
COLOR_YELLOW = 3
78+
COLOR_CYAN = 4
79+
A_BOLD = 10
80+
A_DIM = 20
81+
82+
@staticmethod
83+
def init_pair(_pair, _foreground, _background):
84+
raise ValueError("Color pair is greater than COLOR_PAIRS-1")
85+
86+
@staticmethod
87+
def color_pair(_pair):
88+
raise AssertionError("color_pair should not be used when init_pair fails")
89+
90+
styles = tui._terminal_styles(FakeCurses)
91+
92+
self.assertEqual(FakeCurses.A_BOLD, styles["accent"])
93+
self.assertEqual(0, styles["ok"])
94+
self.assertEqual(0, styles["warn"])
95+
self.assertEqual(FakeCurses.A_DIM, styles["muted"])
96+
self.assertEqual(0, styles["footer"])
97+
5898

5999
if __name__ == "__main__":
60100
unittest.main()

tests/test_validate_gateflow.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,13 @@ def test_inventory_matches_release_target(self):
3535
def test_repository_passes_release_checks(self):
3636
validator = load_validator()
3737

38-
result = validator.run_checks(ROOT, expected_version="2.5.0")
38+
result = validator.run_checks(ROOT, expected_version="2.5.1")
3939

4040
self.assertEqual([], result.errors)
4141

4242
def test_cli_reports_success(self):
4343
completed = subprocess.run(
44-
[sys.executable, str(VALIDATOR), "--version", "2.5.0"],
44+
[sys.executable, str(VALIDATOR), "--version", "2.5.1"],
4545
cwd=ROOT,
4646
text=True,
4747
capture_output=True,

tools/gateflow_tui.py

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,9 @@ def build_payload(root: Path) -> dict:
5151
root = root.resolve()
5252
validator = _load_validator()
5353
inventory = validator.discover_inventory(root)
54-
release = validator.run_checks(root, expected_version="2.5.0")
5554
plugin = _read_json(root / "plugins" / "gateflow" / ".claude-plugin" / "plugin.json")
55+
plugin_version = plugin.get("version", "unknown")
56+
release = validator.run_checks(root, expected_version=plugin_version)
5657

5758
health = {
5859
"doctor": "ready" if (root / "plugins/gateflow/commands/gf-doctor.md").exists() else "missing",
@@ -76,7 +77,7 @@ def build_payload(root: Path) -> dict:
7677
return {
7778
"mode": "OpenClaw-style local mode",
7879
"workspace": str(root),
79-
"plugin": {"name": plugin.get("name", "gateflow"), "version": plugin.get("version", "unknown")},
80+
"plugin": {"name": plugin.get("name", "gateflow"), "version": plugin_version},
8081
"inventory": {
8182
"agents": len(inventory.agents),
8283
"skills": len(inventory.skills),
@@ -103,6 +104,40 @@ def _status(value: str | dict, plain: bool) -> str:
103104
return value
104105

105106

107+
def _hide_cursor(curses_module=curses) -> bool:
108+
try:
109+
curses_module.curs_set(0)
110+
except curses_module.error:
111+
return False
112+
return True
113+
114+
115+
def _terminal_styles(curses_module=curses) -> dict[str, int]:
116+
styles = {
117+
"accent": getattr(curses_module, "A_BOLD", 0),
118+
"ok": 0,
119+
"warn": 0,
120+
"muted": getattr(curses_module, "A_DIM", 0),
121+
"footer": 0,
122+
}
123+
try:
124+
curses_module.init_pair(1, curses_module.COLOR_RED, -1)
125+
curses_module.init_pair(2, curses_module.COLOR_GREEN, -1)
126+
curses_module.init_pair(3, curses_module.COLOR_YELLOW, -1)
127+
curses_module.init_pair(4, curses_module.COLOR_CYAN, -1)
128+
styles.update(
129+
{
130+
"accent": curses_module.color_pair(1) | getattr(curses_module, "A_BOLD", 0),
131+
"ok": curses_module.color_pair(2),
132+
"warn": curses_module.color_pair(3),
133+
"footer": curses_module.color_pair(4),
134+
}
135+
)
136+
except (curses_module.error, ValueError):
137+
pass
138+
return styles
139+
140+
106141
def render_snapshot(root: Path, plain: bool = False) -> str:
107142
payload = build_payload(root)
108143
inv = payload["inventory"]
@@ -153,14 +188,11 @@ def add(y: int, x: int, text: str, attr=0) -> None:
153188
if 0 <= y < height:
154189
stdscr.addnstr(y, x, text, max(0, width - x - 1), attr)
155190

156-
curses.init_pair(1, curses.COLOR_RED, -1)
157-
curses.init_pair(2, curses.COLOR_GREEN, -1)
158-
curses.init_pair(3, curses.COLOR_YELLOW, -1)
159-
curses.init_pair(4, curses.COLOR_CYAN, -1)
160-
accent = curses.color_pair(1) | curses.A_BOLD
161-
ok = curses.color_pair(2)
162-
warn = curses.color_pair(3)
163-
muted = curses.A_DIM
191+
styles = _terminal_styles()
192+
accent = styles["accent"]
193+
ok = styles["ok"]
194+
warn = styles["warn"]
195+
muted = styles["muted"]
164196

165197
add(0, 0, " GateFlow Terminal ", accent)
166198
add(0, 20, payload["mode"], muted)
@@ -196,15 +228,15 @@ def add(y: int, x: int, text: str, attr=0) -> None:
196228

197229
footer = message or "↑/↓ select Enter show command r refresh q quit"
198230
add(height - 2, 0, "─" * (width - 1), muted)
199-
add(height - 1, 1, footer, curses.color_pair(4))
231+
add(height - 1, 1, footer, styles["footer"])
200232
stdscr.refresh()
201233

202234

203235
def run_interactive(root: Path) -> int:
204236
payload = build_payload(root)
205237

206238
def wrapped(stdscr) -> None:
207-
curses.curs_set(0)
239+
_hide_cursor()
208240
stdscr.keypad(True)
209241
selected = 0
210242
message = ""

tools/validate_gateflow.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ def _check_symlink(path: Path, expected_target: str, errors: list[str]) -> None:
132132
errors.append(f"{path} points to {target}, expected {expected_target}")
133133

134134

135-
def run_checks(root: Path, expected_version: str = "2.5.0") -> ValidationResult:
135+
def run_checks(root: Path, expected_version: str = "2.5.1") -> ValidationResult:
136136
root = root.resolve()
137137
inventory = discover_inventory(root)
138138
errors: list[str] = []
@@ -148,7 +148,7 @@ def run_checks(root: Path, expected_version: str = "2.5.0") -> ValidationResult:
148148
def main(argv: list[str] | None = None) -> int:
149149
parser = argparse.ArgumentParser(description=__doc__)
150150
parser.add_argument("--root", type=Path, default=Path.cwd(), help="Repository root")
151-
parser.add_argument("--version", default="2.5.0", help="Expected release version")
151+
parser.add_argument("--version", default="2.5.1", help="Expected release version")
152152
args = parser.parse_args(argv)
153153

154154
result = run_checks(args.root, expected_version=args.version)

0 commit comments

Comments
 (0)