Skip to content

Commit 17c75fb

Browse files
authored
Make GateFlow TUI responsive (#10)
Fix the OpenClaw-style terminal dashboard so narrow local terminals render as a stacked status view instead of overlapping columns. - add responsive layout helpers and text/path truncation - update release metadata to v2.5.2 - cover narrow dashboard rendering with unit tests
1 parent af1f238 commit 17c75fb

8 files changed

Lines changed: 168 additions & 23 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.1",
13+
"version": "2.5.2",
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.2** | 2026-05-21 | Responsive TUI layout for narrow terminal windows |
593594
| **2.5.1** | 2026-05-21 | TUI terminal compatibility fixes for cursor and colorless PTYs |
594595
| **2.5.0** | 2026-05-21 | OpenClaw-style CLI/TUI, release readiness workflow, deterministic validators, synced marketplace/docs/index/mirrors |
595596
| **2.4.0** | 2026-04-11 | Deep skill enrichment across verification, synthesis, orchestration, architecture, learning, IP, and planning |

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.1",
3+
"version": "2.5.2",
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.2 (2026-05-21) — Responsive TUI Layout
4+
5+
Patch release for narrow terminal windows.
6+
7+
### Fixes
8+
- Added a stacked interactive layout for terminals below 100 columns so the
9+
actions list no longer overlaps workspace, inventory, or health panels.
10+
- Clamped action descriptions and other terminal text to their available
11+
column width.
12+
- Added regression coverage for narrow terminal layout selection and text
13+
ellipsizing.
14+
315
## 2.5.1 (2026-05-21) — TUI Terminal Compatibility
416

517
Patch release for the GateFlow terminal console.

tests/test_gateflow_tui.py

Lines changed: 47 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.1", payload["plugin"]["version"])
39+
self.assertEqual("2.5.2", payload["plugin"]["version"])
4040
self.assertEqual(20, payload["inventory"]["agents"])
4141
self.assertEqual(27, payload["inventory"]["skills"])
4242
self.assertEqual(21, payload["inventory"]["commands"])
@@ -95,6 +95,52 @@ def color_pair(_pair):
9595
self.assertEqual(FakeCurses.A_DIM, styles["muted"])
9696
self.assertEqual(0, styles["footer"])
9797

98+
def test_narrow_terminals_use_stacked_layout(self):
99+
tui = load_tui()
100+
101+
self.assertEqual("stacked", tui._layout_mode(80))
102+
self.assertEqual("columns", tui._layout_mode(120))
103+
104+
def test_text_is_ellipsized_to_fit_column(self):
105+
tui = load_tui()
106+
107+
value = tui._fit_text("Validate plugin release readiness", 18)
108+
109+
self.assertEqual("Validate plugin...", value)
110+
self.assertLessEqual(len(value), 18)
111+
112+
def test_narrow_draw_stacks_panels_below_actions(self):
113+
tui = load_tui()
114+
payload = tui.build_payload(ROOT)
115+
116+
class FakeScreen:
117+
def __init__(self):
118+
self.rows = [" " * 80 for _ in range(32)]
119+
120+
def erase(self):
121+
pass
122+
123+
def getmaxyx(self):
124+
return (32, 80)
125+
126+
def addnstr(self, y, x, text, max_width, _attr=0):
127+
line = self.rows[y]
128+
clipped = text[:max_width]
129+
self.rows[y] = line[:x] + clipped + line[x + len(clipped) :]
130+
131+
def refresh(self):
132+
pass
133+
134+
screen = FakeScreen()
135+
136+
tui._draw(screen, payload, selected=6, message="")
137+
138+
action_row = next(row for row in screen.rows if "/gf-release" in row)
139+
workspace_row = next(index for index, row in enumerate(screen.rows) if "Workspace" in row)
140+
141+
self.assertNotIn("Workspace", action_row)
142+
self.assertGreater(workspace_row, 11)
143+
98144

99145
if __name__ == "__main__":
100146
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.1")
38+
result = validator.run_checks(ROOT, expected_version="2.5.2")
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.1"],
44+
[sys.executable, str(VALIDATOR), "--version", "2.5.2"],
4545
cwd=ROOT,
4646
text=True,
4747
capture_output=True,

tools/gateflow_tui.py

Lines changed: 102 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,66 @@ def _terminal_styles(curses_module=curses) -> dict[str, int]:
138138
return styles
139139

140140

141+
def _layout_mode(width: int) -> str:
142+
return "stacked" if width < 100 else "columns"
143+
144+
145+
def _fit_text(text: str, max_width: int) -> str:
146+
if max_width <= 0:
147+
return ""
148+
if len(text) <= max_width:
149+
return text
150+
if max_width <= 3:
151+
return "." * max_width
152+
return text[: max_width - 3].rstrip() + "..."
153+
154+
155+
def _short_path(path: str, max_width: int) -> str:
156+
if len(path) <= max_width:
157+
return path
158+
name = Path(path).name
159+
parent = Path(path).parent.name
160+
compact = f".../{parent}/{name}" if parent else f".../{name}"
161+
return _fit_text(compact, max_width)
162+
163+
164+
def _dashboard_rows(payload: dict, width: int, selected: int, message: str) -> list[tuple[str, str]]:
165+
max_width = max(20, width - 1)
166+
inv = payload["inventory"]
167+
health = payload["health"]
168+
rows: list[tuple[str, str]] = []
169+
170+
def row(text: str = "", style: str = "normal") -> None:
171+
rows.append((_fit_text(text, max_width), style))
172+
173+
row(f"GateFlow Terminal {payload['plugin']['name']} {payload['plugin']['version']}", "accent")
174+
row(payload["mode"], "muted")
175+
row("─" * max_width, "muted")
176+
row()
177+
row("Actions", "heading")
178+
for index, action in enumerate(payload["actions"], start=1):
179+
marker = ">" if index - 1 == selected else " "
180+
style = "selected" if index - 1 == selected else "normal"
181+
row(f"{marker} {index}. {action['command']:<11} {action['description']}", style)
182+
row()
183+
row("Workspace", "heading")
184+
row(f" path {_short_path(payload['workspace'], max_width - 10)}")
185+
row(f" plugin {payload['plugin']['name']} {payload['plugin']['version']}", "accent")
186+
row()
187+
row("Inventory", "heading")
188+
row(f" {inv['agents']} agents {inv['skills']} skills {inv['commands']} commands")
189+
row(f" {inv['ip_blocks']} IP blocks {inv['boards']} boards")
190+
row()
191+
row("Health", "heading")
192+
row(f" doctor {health['doctor']:<10} release {health['release']}")
193+
row(f" map {health['map']['status']:<10} verilator {health['verilator']}")
194+
row(f" yosys {health['yosys']:<10} sby {health['sby']}")
195+
row()
196+
row("─" * max_width, "muted")
197+
row(message or "↑/↓ select Enter show command r refresh q quit", "footer")
198+
return rows
199+
200+
141201
def render_snapshot(root: Path, plain: bool = False) -> str:
142202
payload = build_payload(root)
143203
inv = payload["inventory"]
@@ -182,11 +242,12 @@ def _draw(stdscr, payload: dict, selected: int, message: str) -> None:
182242
actions = payload["actions"]
183243
inv = payload["inventory"]
184244
health = payload["health"]
185-
left_width = min(34, max(24, width // 3))
245+
mode = _layout_mode(width)
186246

187247
def add(y: int, x: int, text: str, attr=0) -> None:
188248
if 0 <= y < height:
189-
stdscr.addnstr(y, x, text, max(0, width - x - 1), attr)
249+
max_width = max(0, width - x - 1)
250+
stdscr.addnstr(y, x, _fit_text(text, max_width), max_width, attr)
190251

191252
styles = _terminal_styles()
192253
accent = styles["accent"]
@@ -198,22 +259,47 @@ def add(y: int, x: int, text: str, attr=0) -> None:
198259
add(0, 20, payload["mode"], muted)
199260
add(1, 0, "─" * (width - 1), muted)
200261

262+
if mode == "stacked":
263+
style_attrs = {
264+
"accent": accent,
265+
"footer": styles["footer"],
266+
"heading": curses.A_BOLD,
267+
"muted": muted,
268+
"normal": 0,
269+
"selected": curses.A_REVERSE,
270+
}
271+
for y, (text, style) in enumerate(_dashboard_rows(payload, width, selected, message)):
272+
add(y, 0, text, style_attrs.get(style, 0))
273+
stdscr.refresh()
274+
return
275+
201276
add(3, 2, "Actions", curses.A_BOLD)
277+
right_x = 52 if mode == "columns" else 2
278+
action_desc_width = (right_x - 18) if mode == "columns" else (width - 18)
202279
for idx, action in enumerate(actions):
203280
attr = curses.A_REVERSE if idx == selected else 0
204-
add(5 + idx, 2, f"{idx + 1}. {action['command']}", attr)
205-
add(5 + idx, 16, action["description"], attr)
206-
207-
x = left_width + 2
208-
add(3, x, "Workspace", curses.A_BOLD)
209-
add(5, x, payload["workspace"])
210-
add(6, x, f"{payload['plugin']['name']} {payload['plugin']['version']}", accent)
211-
212-
add(8, x, "Inventory", curses.A_BOLD)
213-
add(10, x, f"{inv['agents']} agents {inv['skills']} skills {inv['commands']} commands")
214-
add(11, x, f"{inv['ip_blocks']} IP blocks {inv['boards']} boards")
215-
216-
add(13, x, "Health", curses.A_BOLD)
281+
y = 5 + idx
282+
add(y, 2, f"{idx + 1}. {action['command']}", attr)
283+
add(y, 16, _fit_text(action["description"], action_desc_width), attr)
284+
285+
if mode == "columns":
286+
workspace_y = 3
287+
inventory_y = 8
288+
health_y = 13
289+
else:
290+
workspace_y = 6 + len(actions)
291+
inventory_y = workspace_y + 5
292+
health_y = inventory_y + 5
293+
294+
add(workspace_y, right_x, "Workspace", curses.A_BOLD)
295+
add(workspace_y + 2, right_x, payload["workspace"])
296+
add(workspace_y + 3, right_x, f"{payload['plugin']['name']} {payload['plugin']['version']}", accent)
297+
298+
add(inventory_y, right_x, "Inventory", curses.A_BOLD)
299+
add(inventory_y + 2, right_x, f"{inv['agents']} agents {inv['skills']} skills {inv['commands']} commands")
300+
add(inventory_y + 3, right_x, f"{inv['ip_blocks']} IP blocks {inv['boards']} boards")
301+
302+
add(health_y, right_x, "Health", curses.A_BOLD)
217303
rows = [
218304
("doctor", health["doctor"]),
219305
("release", health["release"]),
@@ -224,7 +310,7 @@ def add(y: int, x: int, text: str, attr=0) -> None:
224310
]
225311
for offset, (name, value) in enumerate(rows):
226312
attr = ok if value in {"ready", "installed"} else warn
227-
add(15 + offset, x, f"{name:<10} {value}", attr)
313+
add(health_y + 2 + offset, right_x, f"{name:<10} {value}", attr)
228314

229315
footer = message or "↑/↓ select Enter show command r refresh q quit"
230316
add(height - 2, 0, "─" * (width - 1), muted)

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.1") -> ValidationResult:
135+
def run_checks(root: Path, expected_version: str = "2.5.2") -> 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.1") -> 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.1", help="Expected release version")
151+
parser.add_argument("--version", default="2.5.2", 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)