refactor: reorganize skill numbering, remove obsolete skills, rename shared libs

- Rename: 00→80 claude-settings-optimizer, 88→79 dintel-skill-update,
  92→81 mac-optimizer, 93→82 tui-design-template
- Rename: dintel-shared → _dintel-shared (consistent with _ourdigital-shared)
- Remove: 61-gtm-manager, 62-gtm-guardian (obsolete), 99_archive
- Update all dintel-* skill refs (114 occurrences across 31 files)
- Sync README.md, CLAUDE.md, AGENTS.md with new structure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-23 19:32:44 +09:00
parent ee355479b7
commit 877db1aa0f
115 changed files with 200 additions and 10979 deletions

View File

@@ -0,0 +1,28 @@
# 93-tui-designer
Build Norton Commander / Gopher style TUI wizard interfaces for any Python CLI tool using Rich.
## Purpose
Reusable patterns and battle-tested gotchas for building retro-style terminal wizards with:
- Dual-panel layout (status + content)
- Stack-based Gopher navigation (numbered menus, Back, Home)
- Arrow-key list selector for long option lists
- Bilingual i18n with runtime toggle
- Keyboard-driven input (F-keys with alphanumeric fallbacks)
- Three-tier responsive layout (wide/narrow/single panel)
## Structure
```
93-tui-designer/
code/SKILL.md # Main skill (architecture, patterns, gotchas)
shared/references/
dtm-wizard-reference.md # DTM Agent implementation reference
docs/ # Future: logs, lessons learned
```
## First Implementation
DTM Agent TUI Wizard — 20 files, 21 screens, 29 tests.
See `shared/references/dtm-wizard-reference.md` for details.

View File

@@ -0,0 +1,303 @@
---
name: tui-design-template
description: Build Norton Commander / Gopher style TUI wizard interfaces for CLI tools using Python Rich. Covers architecture, components, keyboard input, bilingual i18n, and battle-tested gotchas.
version: 1.0.0
triggers:
- "build TUI", "TUI wizard", "terminal UI", "CLI wizard"
- "Norton Commander style", "Gopher style", "retro TUI"
- "Rich TUI", "interactive CLI", "keyboard navigation"
- "dual panel interface", "terminal wizard"
tools:
- Read
- Write
- Edit
- Bash
- Grep
- Glob
---
# TUI Designer
Build retro-style terminal wizard interfaces (Norton Commander + Gopher) for any Python CLI tool using the Rich library. No new dependencies — Rich + stdlib only.
## When to Use
- Building an interactive CLI wizard or setup flow
- Adding keyboard-driven navigation to an existing CLI tool
- Creating dual-panel terminal interfaces with status + menus
- Building bilingual (or multi-language) terminal interfaces
## Architecture Blueprint
### Component Structure
```
src/{project}/tui/
__init__.py # Public API: launch_tui()
i18n.py # String registry with runtime language toggle
input.py # Raw tty/termios keypress reader + escape parser
themes.py # Color scheme + NO_COLOR support
widgets.py # Shared primitives (status icons, mini-tables)
breadcrumb.py # Top navigation path bar
function_bar.py # Bottom F-key shortcut bar
status_panel.py # Left panel: system health / status
menu.py # Right panel: numbered menu items
dialog.py # Modal overlays (confirm, input, error)
core.py # Layout engine (assembles frame)
runner.py # Main event loop + screen stack
selector.py # Arrow-key list selector for long lists
renderers.py # Content renderers for leaf screens
screens/
__init__.py # Screen/MenuItem/Action dataclasses + registry
home.py # Root screen
{category}.py # One file per menu category
```
### Data Flow
```
User keypress
-> input.py (parse_key_bytes -> normalize_key)
-> runner.py (_handle_key dispatches)
-> Navigation: ScreenStack push/pop
-> F-keys: toggle language, refresh, help, quit
-> Digits: jump cursor or push screen
-> Arrows: ListSelector cursor movement
-> Enter: confirm selection via key_handler callback
-> core.py (render_frame assembles panels)
-> Console output
```
## Screen System
### Screen Dataclass
```python
@dataclass
class Screen:
id: str # "setup.credentials"
title: str # i18n key
breadcrumb: list[str] # i18n key path
menu_items: list[MenuItem] # For menu screens
content_renderer: Callable | None # For leaf screens: (console, status_data) -> str
key_handler: Callable | None # For interactive leaves: (app, key) -> bool
on_enter: Callable | None # Runs when screen is pushed
```
### Navigation Model (Gopher-style stack)
```
ScreenStack (LIFO):
[1-9] = push screen (menu) or jump cursor (leaf)
[B] = pop (back)
[H] = clear to root
[Q] = quit with confirm
Enter = confirm selection on leaf screens
Up/Down = navigate ListSelector items
```
### Screen Registration Pattern
```python
# screens/setup.py
from myapp.tui.screens import Screen, MenuItem, register_screen
from myapp.tui.renderers import render_credentials, handle_credential_key
register_screen(Screen(
id="setup.credentials",
title="setup.credentials",
breadcrumb=["nav.home", "home.setup", "setup.credentials"],
content_renderer=render_credentials,
key_handler=handle_credential_key, # For interactive leaves
))
```
## Keyboard Input
### Escape Sequence Parser
```python
# input.py — stdlib only, no dependencies
import tty, termios, select, sys, os
def read_key() -> str:
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
try:
tty.setraw(fd)
first = os.read(fd, 1)
if first == b"\x1b":
ready, _, _ = select.select([fd], [], [], 0.05) # 50ms timeout
if ready:
rest = os.read(fd, 5)
return parse_escape(first + rest)
return "escape"
return parse_single(first)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
```
### F-key Alphanumeric Fallbacks
Always provide fallbacks — terminals intercept F-keys unpredictably:
| F-key | Fallback | Action |
|-------|----------|--------|
| F1 | ? | Help |
| F3 | l | Language toggle |
| F5 | r | Refresh |
| F10 | q | Quit |
## ListSelector Component
For screens with 10+ selectable items, use `ListSelector` instead of number-only input:
```python
selector = ListSelector(
items=[SelectableItem(label="model-name", status="ready", data={...})],
on_select=my_callback,
header="Select model:", # str or Callable[[], str] for dynamic headers
footer="Hint text",
)
# In content_renderer: return selector.render()
# In key_handler: return selector.handle_key(app, key)
```
- Arrow Up/Down moves visible `>` cursor
- Enter/Space confirms selection
- 1-9 jumps cursor (does NOT auto-select — user must press Enter)
- Sections headers group items visually
- Callable headers re-evaluate on each render (for dynamic "Active:" display)
## Line Input Mode
For screens requiring text input (file paths, names, search), use `read_line()`:
```python
from myapp.tui.input import read_line
# In a key_handler:
def handle_my_key(app, key):
if key == "p":
app.console.print("Enter file path: ", end="")
path = read_line()
if path is None: # Escape pressed
return True
# Use the path...
return True
```
### Implementation pattern
```python
def read_line(prompt: str = "") -> str | None:
"""Read a full line in raw mode. Returns None on Escape/Ctrl-C."""
# Uses tty.setraw() same as read_key()
# Accumulates chars in buffer, prints each as typed
# Handles: Enter (return string), Escape (return None),
# Ctrl-C (return None), Backspace (delete last)
# Escape sequences (arrow keys pressed during input) are consumed and ignored
```
### Line Input Gotchas
| Gotcha | Problem | Fix |
|--------|---------|-----|
| Pasted paths with quotes | Users paste `'/path/to/file'` with quotes | Strip outer quotes: `path.strip("'\"")` |
| Tilde expansion | `~/.config/...` not expanded | Use `Path(path).expanduser()` |
| Raw mode echo | Characters not visible while typing | Manually `sys.stdout.write(ch)` each keystroke |
| Escape during input | Arrow keys produce garbage in buffer | Detect `0x1B`, consume rest of sequence, ignore |
| Module-level imports for testability | `import select` inside function can't be patched | Import `select`, `termios`, `tty` at module level (in `try/except`) |
## Bilingual i18n
Simple dict registry — no framework needed:
```python
STRINGS = {
"nav.back": {"en": "Back", "ko": "뒤로"},
...
}
_lang = "en"
def t(key: str) -> str:
return STRINGS.get(key, {}).get(_lang, key)
def toggle_language():
global _lang
_lang = "ko" if _lang == "en" else "en"
```
On toggle, clear ALL cached screen content so every screen re-renders.
## Three-Tier Responsive Layout
```python
if cols >= 100: # Full dual-panel
Columns([status_panel, content_panel], padding=(0, 1))
elif cols >= 80: # Narrow status
Columns([narrow_status, content_panel], padding=(0, 1))
else: # Single panel (status line + content stacked)
console.print(f"Status: {health}")
console.print(content)
```
Use `rich.columns.Columns` (NOT `rich.layout.Layout`) — Columns auto-fits content height.
---
## Gotchas (Battle-Tested)
### Rich Library
| Gotcha | Problem | Fix |
|--------|---------|-----|
| `Panel(box=None)` | `AttributeError: NoneType has no attribute substitute` | Use `box=SIMPLE` from `rich.box` |
| `Layout` stretches panels | Panels expand to fill available height with empty space | Use `Columns` with `expand=False` instead |
| `Status` spinner nesting | "Only one live display may be active at once" crash | Never nest `Status()` inside other Rich output. Use plain `console.print("Loading...")` |
| `[1]` in markup | Rich interprets `[1]` as a potential style tag | Escape: `\[1]` |
| Rich + raw stdin | `tty.setraw()` conflicts with Rich's terminal state | Always restore termios in `finally` block |
| `NO_COLOR` env var | Must respect `NO_COLOR` and `DTM_NO_COLOR` | Check env vars, return plain theme dict |
### Keyboard Input
| Gotcha | Problem | Fix |
|--------|---------|-----|
| F-key escape sequences | Vary between Terminal.app, iTerm2, VS Code | Map multiple sequences per key + provide alphanumeric fallbacks |
| Items 10+ unreachable | Pressing `1` then `0` selects item 1 immediately | Digits jump cursor only, Enter confirms. Items 10+ use arrows |
| `Ctrl+C` in raw mode | Crashes without terminal restoration | Wrap main loop in `try/except KeyboardInterrupt` |
| Non-interactive detection | Piped input hangs on `read_key()` | Check `sys.stdin.isatty()` before entering TUI mode |
### Content & Caching
| Gotcha | Problem | Fix |
|--------|---------|-----|
| API calls per render | `content_renderer` called on every keystroke | Cache rendered content in `_cached_content[screen.id]`; clear on F5 |
| Stale cached content | Model/account change doesn't update display | `invalidate_screen()` clears cache; use callable headers for dynamic data |
| Config has null names | `set_active_account(id)` saves id but not name | Resolve names from API as fallback; backfill to config on success |
| Korean char alignment | `{label:<12}` misaligns CJK double-width chars | Calculate display width: `sum(2 if ord(c) > 0x7F else 1 for c in s)` |
| Language toggle stale | Screens show old language after F3 | Clear ALL `_cached_content` on language toggle, not just current screen |
### UX Patterns
| Pattern | Why |
|---------|-----|
| Confirmation panels (0.8s delay) | Users need visual feedback that their action took effect |
| `<< active` markers | Users must see which item is currently selected at a glance |
| Section headers in lists | Group related items (Recommended / Installed / LM Studio) |
| Guidance hints at bottom | Tell users how to reach features that require other steps first |
| Breadcrumb path always visible | Users always know where they are in the hierarchy |
## Checklist for New TUI Projects
- [ ] `from __future__ import annotations` in all TUI files
- [ ] `_is_interactive()` guard before entering TUI mode
- [ ] `--no-tui` and `--legacy` CLI flags for fallback
- [ ] Non-TTY detection falls back to plain text
- [ ] `NO_COLOR` / `FORCE_COLOR` env var support
- [ ] F-key alphanumeric fallbacks defined
- [ ] `KeyboardInterrupt` handled in main loop
- [ ] Escape sequences for F1-F10, arrows mapped
- [ ] Content cached, invalidated on refresh/change
- [ ] All user-facing strings through `t()` i18n function
- [ ] Tests for: i18n, input parser, screen registry, navigation stack

View File

@@ -0,0 +1,84 @@
# DTM Wizard — Reference Implementation
The DTM Agent TUI wizard is the first project built with this skill's patterns.
## Repository
- **Project**: D.intelligence Tag Manager Agent
- **Location**: `github.com/D-intelligence/dintel-gtm-agent`
- **TUI code**: `src/dtm/tui/` (20 files, ~2500 lines)
- **Tests**: `tools/tests/unit/test_tui_*.py` (29 tests)
## File Inventory
| File | Lines | Purpose |
|------|-------|---------|
| `__init__.py` | 13 | Public API |
| `i18n.py` | 93 | 60+ bilingual EN/KR strings |
| `input.py` | 93 | Escape sequence parser |
| `themes.py` | 48 | Norton Commander color scheme |
| `widgets.py` | 35 | Status icons, mini-tables |
| `breadcrumb.py` | 22 | Navigation path bar |
| `function_bar.py` | 33 | F-key shortcut bar |
| `status_panel.py` | 85 | Left panel health display |
| `menu.py` | 35 | Gopher-style numbered menu |
| `dialog.py` | 70 | Modal overlays |
| `core.py` | 100 | Three-tier layout engine |
| `runner.py` | 170 | Event loop + ScreenStack |
| `selector.py` | 156 | Arrow-key list selector |
| `renderers.py` | ~1100 | 15 leaf screen content renderers |
| `screens/*.py` | ~250 | 21 screen definitions |
## Screen Hierarchy (21 screens)
```
home
setup
setup.credentials
setup.account
setup.account.containers (dynamic sub-screen)
setup.ai
setup.connectivity
operations
ops.workflow
ops.container_analysis
ops.ai_analysis
ops.performance
configuration
config.review
config.change_account
config.change_container
config.export
diagnostics
diag.health
diag.test
diag.recommendations
```
## PRs and Evolution
| PR | Title | Key Changes |
|----|-------|-------------|
| #10 | Core TUI redesign | 19 modules, 28 tests |
| #11 | 7 usability fixes | Panel height, no-color, bilingual icons |
| #12 | Interactive selection | Account/container by number, help screen |
| #13 | Name resolution | Resolve IDs to names from API |
| #14 | Loading spinners | Visual feedback during API calls |
| #15 | AI model selector | Ollama + LM Studio detection |
| #16 | Batch UX fixes | ListSelector, export formats, crash fixes |
## External Service Detection Pattern
```python
def _detect_lm_studio() -> dict:
"""Detect LM Studio on localhost:1234."""
import requests
try:
resp = requests.get("http://localhost:1234/v1/models", timeout=3)
if resp.status_code == 200:
return {"available": True, "models": [m["id"] for m in resp.json().get("data", [])]}
except Exception:
pass
return {"available": False, "models": []}
```
This pattern works for any OpenAI-compatible local LLM server.