Skip to content

Commit 1b82e6e

Browse files
authored
Added API Inspector Panel, Several TUI improvements (#231)
1 parent e8ebbee commit 1b82e6e

35 files changed

+1914
-192
lines changed

.claude/skills/reflection/SKILL.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
---
2+
name: reflection
3+
description: This skill should be used when the user says "reflection", "reflect on the changes", "reflect on this session", or asks to capture lessons learned from the current conversation.
4+
disable-model-invocation: true
5+
---
6+
7+
# Reflection
8+
9+
The goal is to identify lessons from this session that should be permanently captured in CLAUDE.md — so future sessions benefit without repeating the same mistakes, clarifications, or decisions.
10+
11+
Review the **entire conversation history** — every message, correction, and preference — as the primary source. Code diffs are supplementary.
12+
13+
## Current Git State (supplementary context)
14+
15+
- Branch: !`git branch --show-current`
16+
- Changes since last commit: !`git diff --stat HEAD`
17+
- Staged changes: !`git diff --stat --cached`
18+
19+
## Process
20+
21+
1. **Read the full conversation from top to bottom.** Pay attention to:
22+
- Questions the user had to answer that should have been obvious from CLAUDE.md
23+
- Corrections the user made to your approach or output
24+
- Preferences or constraints the user stated (even casually)
25+
- Things you got wrong on the first attempt and had to revise
26+
- Decisions made about architecture, naming, tooling, or workflow
27+
- Anything the user explicitly said to always/never do
28+
29+
2. **Review the diffs** (supplementary) — Read modified files for context on what was built and why, but do not let this overshadow lessons from the conversation itself.
30+
31+
3. **Identify lessons** in these categories:
32+
- **Patterns & conventions** — Things that worked well and should be encoded as rules
33+
- **Gotchas & pitfalls** — Things that caused confusion, required retries, or were non-obvious
34+
- **Architecture decisions** — Choices made that future sessions should know about
35+
- **Workflow & communication preferences** — How the user prefers to work, communicate, or receive output
36+
- **Outdated/wrong memory** — Anything in CLAUDE.md or MEMORY.md that turned out to be incorrect or missing
37+
38+
4. **Read the current CLAUDE.md** to avoid duplicating what's already there and to find gaps.
39+
40+
5. **Propose edits to CLAUDE.md** — For each lesson worth keeping, suggest the specific text to add, change, or remove, and where it belongs.
41+
42+
6. **If no lessons are found**, explicitly state: "No CLAUDE.md updates needed from this session." This confirms the session was considered.
43+
44+
7. **Do not apply edits automatically.** Present proposals to the user and wait for approval.
45+
46+
8. **After applying approved edits**, print a brief summary in chat of what changed.
47+
48+
## Output Format
49+
50+
**When lessons are found:**
51+
```
52+
## Reflection
53+
54+
### [Category]
55+
**Lesson**: <what was learned>
56+
**Proposed CLAUDE.md change**: <exact text, with target section>
57+
58+
---
59+
(one block per lesson)
60+
```
61+
62+
**When no lessons are found:**
63+
```
64+
## Reflection
65+
66+
No CLAUDE.md updates needed from this session. The following were considered but already covered or not worth persisting:
67+
- <item> — already in CLAUDE.md / too session-specific / etc.
68+
```
69+
70+
**After applying approved changes:**
71+
```
72+
## CLAUDE.md updated
73+
74+
- Added: "<description>" under ## Section
75+
- Modified: "<what changed>" in ## Section
76+
- Removed: "<what was removed>"
77+
```

.claude/tui-plan.md

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -916,6 +916,175 @@ Used by both the JSON Viewer and File Content Viewer. Same approach as existing
916916
917917
---
918918
919+
### Phase 8 — API Inspector Panel
920+
921+
Goal: a collapsible right-hand panel (toggled with `l`) that shows live TFE API calls — method, path, status, duration, full request body, and full response body — as the TUI makes them in the background. Scrollable, filterable, and non-blocking with respect to main-view navigation.
922+
923+
```
924+
┌────────────────────────┬─┬────────────────────────────┐
925+
Header (full width) │
926+
Breadcrumb (full width) │
927+
├────────────────────────┤│├────────────────────────────┤
928+
│ ││ API Inspector [l] close
929+
│ Main view ││ GET /workspaces 200 45ms│ ← cursor (▶)
930+
│ (narrowed when panel ││ POST /runs 201 89ms│
931+
│ is open) ││ GET /projects 200 12ms│
932+
│ ││────────────────────────────-│
933+
│ ││ ─── REQUEST ─────────────── │
934+
│ ││ GET /api/v2/workspaces │
935+
│ ││ ─── RESPONSE ────────────── │
936+
│ ││ 200 OK • 45ms │
937+
│ ││ { "data": [...] } │
938+
├────────────────────────┤│├────────────────────────────┤
939+
│ Status bar (full width) │
940+
│ CLI hint (full width) │
941+
└────────────────────────┴─┴────────────────────────────┘
942+
```
943+
944+
#### 8a. Event Bus & Transport Integration
945+
946+
**New file: `client/eventbus.go`**
947+
```go
948+
type APIEvent struct {
949+
Timestamp time.Time
950+
Method string
951+
URL string // full URL (for filter matching)
952+
Path string // path only, scheme+host stripped (for display)
953+
StatusCode int
954+
Duration time.Duration
955+
ReqBody string // request body; empty for GET/DELETE
956+
RespBody string // pretty-printed JSON response body
957+
Err string // non-empty if RoundTrip errored
958+
}
959+
960+
type APIEventBus struct { ch chan APIEvent } // buffered, size 256
961+
962+
func NewAPIEventBus() *APIEventBus
963+
func (b *APIEventBus) Send(e APIEvent) // non-blocking (drops when full)
964+
func (b *APIEventBus) Receive() <-chan APIEvent
965+
```
966+
967+
**`client/http_logger.go` changes:**
968+
- Add `EventBus *APIEventBus` field to `LoggingTransport`
969+
- Record `start := time.Now()` at top of `RoundTrip`
970+
- Consolidate to **one** `DumpResponse(resp, true)` call (shared by file log, trace log, event bus)
971+
- Extract request body from `DumpRequestOut`, split at `\r\n\r\n`
972+
- Pretty-print response JSON with `json.Indent` before publishing
973+
- Publish `APIEvent` when `t.EventBus != nil` (independent of `TFX_LOG`)
974+
975+
**`client/client.go` changes:**
976+
- Add `EventBus *APIEventBus` field to `TfxClient`
977+
- New `NewFromViperForTUI(bus *APIEventBus) (*TfxClient, error)` — always installs a `LoggingTransport` with the bus set (additionally handles `TFX_LOG` if set)
978+
979+
**`tui/run.go` changes:**
980+
```go
981+
bus := client.NewAPIEventBus()
982+
c, err := client.NewFromViperForTUI(bus)
983+
m := newModel(c) // c.EventBus is set; Init() starts the listener
984+
```
985+
986+
#### 8b. Model State & Width Parameterization
987+
988+
**New Model fields:**
989+
```go
990+
showDebug bool
991+
debugFocused bool // Tab toggles keyboard focus to panel
992+
apiEvents []client.APIEvent // ring buffer, max 100, newest at index 0
993+
debugCursor int // selected call index
994+
debugBodyScroll int // scroll offset in request/response viewer
995+
debugFilter string
996+
debugFiltering bool
997+
```
998+
999+
**New helpers:**
1000+
```go
1001+
func (m Model) debugPanelWidth() int // clamped min 52 / max 90 / ~35% of total
1002+
func (m Model) mainWidth() int // m.width when closed; m.width-panelW-1 when open
1003+
func (m Model) padContent(rendered string, style lipgloss.Style) string // pads to mainWidth()
1004+
```
1005+
1006+
**Width refactor:** Replace `m.width``m.mainWidth()` and `m.pad()``m.padContent()` in all content-area renderers (`renderContent()` and all sub-renderers in model.go, cvfiles.go, organizations.go, runs.go, variables.go, configversions.go, stateversions.go, detail view files). Full-width zones (header, breadcrumb, statusbar, clihint) keep `m.width`/`pad()`.
1007+
1008+
**Bubble Tea event listener** — always active (events buffer even when panel is closed):
1009+
```go
1010+
// Init(): tea.Batch(..., waitForAPIEvent(m.c.EventBus))
1011+
1012+
func waitForAPIEvent(bus *client.APIEventBus) tea.Cmd {
1013+
return func() tea.Msg { return <-bus.Receive() }
1014+
}
1015+
1016+
// Update():
1017+
case client.APIEvent:
1018+
m.apiEvents = append([]client.APIEvent{msg}, m.apiEvents...)
1019+
if len(m.apiEvents) > 100 { m.apiEvents = m.apiEvents[:100] }
1020+
if m.debugCursor > 0 { m.debugCursor++ } // track same call on new arrivals
1021+
return m, waitForAPIEvent(m.c.EventBus)
1022+
```
1023+
1024+
**Key routing:**
1025+
- `l` (global) — toggle `showDebug`; clear `debugFocused` on close
1026+
- `Tab` (when `showDebug`) — toggle `debugFocused`
1027+
- When `m.debugFocused && m.showDebug`, route all keys to `handleDebugPanelKey()` first
1028+
1029+
#### 8c. Debug Panel Renderer
1030+
1031+
**New file: `tui/debugpanel.go`**
1032+
1033+
`renderDebugPanel() string` — renders to exactly `contentHeight()` lines at `debugPanelWidth()` columns:
1034+
- Title bar (1 line): ` API Inspector` + `[Tab] focus [l] close` right-aligned
1035+
- Call list (top 40% of height): newest first, with `` cursor marker
1036+
- `METHOD /path STATUS DUR` — method and status color-coded
1037+
- Method: GET=blue, POST=green, DELETE=red, PATCH/PUT=amber
1038+
- Status: 2xx=green, 4xx=amber, 5xx=red, 0=dim (error)
1039+
- Divider + optional filter bar
1040+
- Request/Response viewer (remainder): shows selected call's req body + response body, scrollable via `debugBodyScroll`, response JSON highlighted with `colorizeJSONLine()`
1041+
1042+
`filteredDebugEvents(m Model) []client.APIEvent` — case-insensitive method+path filter.
1043+
1044+
`handleDebugPanelKey()` keys:
1045+
- ``/`k` / ``/`j` — navigate call list
1046+
- `g`/`G` — top/bottom of list
1047+
- `ctrl+u` / `ctrl+d` — scroll response viewer half-page
1048+
- `/` — start filter input
1049+
- `esc` — clear filter → unfocus panel → close panel (successive presses)
1050+
- `Tab` — unfocus (return focus to main view)
1051+
1052+
#### 8d. View() Horizontal Join
1053+
1054+
**`View()` restructure:**
1055+
```go
1056+
if m.showDebug {
1057+
joined := joinPanels(m.renderContent(), m.renderDebugPanel())
1058+
content = lipgloss.JoinVertical(lipgloss.Left,
1059+
m.renderHeader(), m.renderBreadcrumb(), joined,
1060+
m.renderStatusBar(), m.renderCliHint())
1061+
}
1062+
```
1063+
1064+
**`joinPanels(left, right string) string`** — line-by-line zip with dim `` separator.
1065+
1066+
#### 8e. Polish
1067+
1068+
- Help overlay: add `[global] l`, `[inspector] Tab/↑↓/ctrl+u-d / / esc` entries
1069+
- Status bar: show `[inspector]` accent badge when `debugFocused == true`
1070+
- No CLI hint change needed
1071+
1072+
#### 8f. New Files Summary
1073+
1074+
| File | Purpose |
1075+
|---|---|
1076+
| `client/eventbus.go` | `APIEvent`, `APIEventBus` |
1077+
| `tui/debugpanel.go` | `renderDebugPanel()`, `joinPanels()`, `filteredDebugEvents()`, `handleDebugPanelKey()` |
1078+
1079+
#### 8g. Key Design Decisions
1080+
- Subscription always runs (events buffered before panel opens)
1081+
- `NewFromViperForTUI` always installs transport — no `TFX_LOG` required
1082+
- One `DumpResponse` per request (shared across logging channels)
1083+
- `padContent()`/`mainWidth()` is the invariant separating full-width vs. content-area rows
1084+
- `debugCursor` increments on new events to track the same call when cursor is not at 0
1085+
1086+
---
1087+
9191088
## 11. Open Questions
9201089
9211090
- [x] **Resolved:** `tfx tui` subcommand (not `--tui` flag). Cobra's required-flag check runs before `PersistentPreRun`; subcommand goes through `postInitCommands`/`presetRequiredFlags` normally.

.github/workflows/build.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: TFx Build
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches-ignore:
7+
- main # covered by latest.yml
8+
9+
jobs:
10+
build:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- uses: actions/setup-go@v5
16+
with:
17+
go-version-file: go.mod
18+
cache: true
19+
20+
- name: Build
21+
run: go build ./...
22+
23+
- name: Test
24+
run: go test ./...

.github/workflows/latest.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,17 @@ jobs:
2424
go-version-file: go.mod
2525
cache: true
2626

27+
- name: Compute next version
28+
id: version
29+
run: |
30+
LATEST_TAG=$(git describe --tags --abbrev=0 --match "v[0-9]*.[0-9]*.[0-9]*" 2>/dev/null || echo "v0.0.0")
31+
RAW="${LATEST_TAG#v}"
32+
IFS='.' read -r MAJOR MINOR PATCH <<< "$RAW"
33+
SHORT_SHA=$(git rev-parse --short HEAD)
34+
VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))-${SHORT_SHA}"
35+
echo "version=${VERSION}" >> $GITHUB_OUTPUT
36+
echo "Computed: stable=${LATEST_TAG}, next=${VERSION}"
37+
2738
- name: Delete existing latest release
2839
env:
2940
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -42,3 +53,4 @@ jobs:
4253
env:
4354
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4455
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
56+
TFX_VERSION: ${{ steps.version.outputs.version }}

.goreleaser-latest.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ env:
99
builds:
1010
- binary: tfx-latest
1111
ldflags:
12-
- -s -w -X github.com/straubt1/tfx/version.Version={{ .Version }}
12+
- -s -w -X github.com/straubt1/tfx/version.Version={{ .Env.TFX_VERSION }}
1313
- -s -w -X github.com/straubt1/tfx/version.Build={{ .ShortCommit }}
1414
- -s -w -X github.com/straubt1/tfx/version.Date={{ .Date }}
1515
- -s -w -X github.com/straubt1/tfx/version.BuiltBy=goreleaser

CHANGELOG.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
**Added**
1111

12-
* `tfx workspace run policy` — show policy check and evaluation details for a run, supports both legacy Sentinel policy checks and newer policy evaluations (OPA/Sentinel via task stages)
12+
* `tfx tui` — new interactive full-screen TUI browser for HCP Terraform and TFE; navigate organizations → projects → workspaces → runs / variables / configuration versions / state versions using keyboard shortcuts
13+
* `tfx tui` — detail views for organizations, projects, workspaces, runs, variables, configuration versions, and state versions
14+
* `tfx tui` — state version JSON viewer with syntax highlighting and line numbers (`o` from state version detail)
15+
* `tfx tui` — configuration version archive browser: download and browse files inside a config version tarball, with a file content viewer (`x` from config version detail)
16+
* `tfx tui` — Live API Inspector panel (`l` key): collapsible right-side panel that captures every TFE API call in real-time, showing method, path, HTTP status, and duration; press Enter on a call to see the full request and pretty-printed, syntax-highlighted response body; supports `/` text filter and `Tab` focus switching
17+
* `tfx tui` — Instance Info modal (`i` key): centered popup overlaid on the current view showing application name, hostname, API version, TFE version, and live health check status (`/_health_check`) with color-coded UP/DOWN indicators
18+
* `tfx workspace run policy` — show policy check and evaluation details for a run; supports both legacy Sentinel policy checks and newer policy evaluations (OPA/Sentinel via task stages)
1319
* `--logs` flag for `tfx workspace run policy` — include raw policy output (Sentinel logs and OPA `output.print`)
1420

21+
**Fixed**
22+
23+
* `tfx tui` — CLI hint bar now shows correct command syntax for all detail views (configuration-version, state-version, variable flags)
24+
* `tfx tui` — multi-line variable values no longer break table layout; collapsed to `` in the list view and expanded row-per-line in the detail view
25+
* `tfx tui` — variable list column order changed to KEY | CATEGORY | SENSITIVE | VALUE
26+
27+
**Changed**
28+
29+
* Documentation site migrated to Starlight
30+
1531
## [v0.2.1] - 2026-03-01
1632

1733
**Added**

0 commit comments

Comments
 (0)