feat(debugger-tui): B.2 — stack/locals pane (Phase B milestone 2 of #691)#739
Conversation
Implements EXECUTION_PLAN.md Milestone B.2 (stack/locals pane).
What ships in B.2:
- tui_state_t gains frame stack fields (frame_count, frame_scripts[200][256],
frame_lines[200], selected_frame) and locals fields (local_names, local_values,
local_count); TUI_MAX_FRAMES=200, TUI_MAX_LOCALS=500 defined in
debugger_tui_internal.h
- tui_store_stack() parses debug/getStack result.frames[] (outermost-first);
tui_store_locals() parses debug/getLocals result.locals[] ({name,value,type});
tui_free_locals() frees the heap arrays on teardown
- tui_write_line() extended to route by result key: "lines"->getSource (B.1),
"frames"->getStack (B.2), "locals"->getLocals (B.2)
- draw_stack_pane() replaces the B.0 placeholder: top section shows frame list
(format " L1 outer.script:10") with BOXEN_ATTR_REVERSE on selected frame;
"--- Locals ---" separator; bottom section shows "varname = value" per local
- on_stack_input(): separate input callback for the stack pane; UP/DOWN arrows
move selected_frame and call tui_select_frame() which updates script_path and
current_line cross-pane (same load_source side-effect per plan B.2 Sentinels)
- debugger_tui_state_teardown() now frees locals alongside script lines
- log_warn calls removed from all three tui_store_* truncation paths: log_write
resolves to NULL via -Wl,-undefined,dynamic_lookup in the test build, causing
EXC_BAD_ACCESS when the over-limit path is hit in test_*_capped tests
Proactively applied B.1 round-1 reviewer lessons:
- draw_stack_pane: char linebuf[512]; avail = min(w, sizeof(linebuf)-1) BEFORE
snprintf/memcpy -- same cap pattern as draw_script_pane post-fix
- Both store helpers: count capped (TUI_MAX_FRAMES, TUI_MAX_LOCALS) before any
array access; early-return on count<=0 before any mutation
- calloc (not malloc) for local_names/local_values; strdup failure truncates
local_count to successfully populated slots
- All cJSON field accesses guarded with cJSON_IsNumber/cJSON_IsString before use
What is NOT in B.2 (per plan):
- Step/continue keybinds (B.3)
- Breakpoint UI (B.4)
- Cmd-double-click identifier resolution (B.5)
- Full op_dispatch-based source reload on frame selection (wired in B.6)
8 new unit tests (test_stack_pane_draws_frames, test_frame_selection_updates_script_pane,
test_locals_render, test_getstack_response_parses, test_getlocals_response_parses,
test_stack_frame_count_capped, test_locals_count_capped,
test_suspended_fires_getstack_getlocals).
Verification:
- make build: clean
- run_headless_tests.sh: 687/687 (679 baseline + 8 new B.2 tests)
- test-integration: 2186 pass / 21 baseline failures (identical names)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ps + frame desync Round-1 /gate fix loop on PR #739. One P1 (bar-raiser) and 2 actionable P2s. Concurrency APPROVED with no concerns. Security APPROVED. 1. P1 (bar-raiser): tui_store_stack and tui_store_locals leaked stale state on legitimately empty arrays. count <= 0 early-return after cJSON_GetArraySize conflated non-array (invalid) with empty array (legitimately empty). Empty arrays carry definite info ("the stack is now empty") -- distinct from B.1 P1-E's "field absent = preserve" case. Move destructive clear before count check. 2. P2 (security): unbounded per-local-value strdup in tui_store_locals could drive heap to ~500 * len(value). Cap name at TUI_LOCAL_NAME_MAX = 256 bytes; cap value at TUI_LOCAL_VALUE_MAX = 4096 bytes. UserTalk types coerced to display form can be multi-MB; this prevents memory-exhaustion DoS from a malformed/hostile debug/getLocals. 3. P2 (bar-raiser): tui_select_frame overwrote script_path + current_line while script_lines still held the previous frame's source, displaying wrong-source-with-frame-line. When the selected frame's script differs from the currently loaded source, blank the script pane until B.6 wires the live op_dispatch reload. The script pane already renders empty content gracefully. P2 items deferred to follow-up issue per /auto Phase 7: - log_warn NULL-safe in test build (test-infra fix) - "... <N more frames truncated>" visible indicator in renderer - level recompute vs wire value - content_rows separator on empty locals - frame-select bounds tests at UP-from-0 and DOWN-from-last Verified: - make build: clean - ./tools/run_headless_tests.sh: 691/691 pass (+4 regression tests; was 687) - cd tests && make test-integration: 2186 pass / 21 baseline failures (failure names character-for-character identical) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
/gate Round 2 — ConvergenceVerdict: PASS — all reviewers APPROVE, 0 P0/P1 remaining.
Round-1 P1 + P2s addressed in commit
|
/auto SummaryWhat went well: Two-round convergence with proactive lesson-application paying off. The sub-agent applied B.1's hard-won patterns (cap-before-allocation, calloc, cJSON_IsX guards, linebuf bounds) up front, so this round's reviewers found state-machine bugs (empty-array clobber) and per-value memory bounds rather than the buffer-overflow class B.1 had. P1 count dropped milestone-over-milestone (B.0: 4, B.1: 5, B.2: 1) — that's the right direction. B.2's sub-agent also retroactively fixed a latent crash in B.1's What could improve: The retroactive Recommendations: For the upcoming B.3 (keybinds), still defer the shell integration test from #736 until B.4 or B.6 — the keybind paths are still mostly synthesizable. But B.6 (lazy-attach wiring) MUST have a real-binary smoke test because that's where the transport's 🤖 Generated with Claude Code /auto |
Summary
Implements Milestone B.2 of
planning/phase_b/EXECUTION_PLAN.md— stack/locals pane. Builds on B.1 (#737, merged atb1a085a52).What this PR proves: the TUI consumes the full
debug/suspendednotification surface end-to-end.debug/suspendednow fans out todebug/getSource(B.1),debug/getStack, anddebug/getLocalsrequests; their responses populate the right pane with the call stack at the top and innermost-frame locals at the bottom. Frame selection via arrow keys updates the script pane's path/line indicators.What ships
frontier-cli/debugger_tui.c—tui_store_stack,tui_store_locals, extendedtui_write_lineto dispatchdebug/getStackanddebug/getLocalsresponses, newdraw_stack_panecallback rendering stack + locals, frame-selection arrow-key handler, cross-pane source path update on frame selectfrontier-cli/debugger_tui_internal.h—TUI_MAX_FRAMES = 200,TUI_MAX_LOCALS = 500, 5 new state fields (frame_scripts,frame_lines,frame_count,selected_frame,locals_*)tests/debugger_tui_tests.c— 8 new behavioral tests covering store-stack, store-locals, cap-truncation, render assertions, frame selection + cross-pane updateDiff: 3 files, +709/-16.
Deviations from the plan (all minor, all documented in commit)
debug/getLocalsreturn shape. Plan said "flat JSON object." Actual atdebug_handler.c:1900isresult.locals= array of{name, value, type}. Code-verified by the sub-agent and parsed correctly.log_warnremoved fromtui_store_*truncation paths — including a retroactive fix to B.1'stui_store_source. Reason:-Wl,-undefined,dynamic_lookupin the test build resolveslog_writeto NULL; the cap-exceeded path would crash the test binary on segfault at address 0x0. This is a real latent bug in B.1 (theTUI_MAX_SOURCE_LINEScap added in PR feat(debugger-tui): B.1 — script pane + debug/getSource wiring (Phase B milestone 1 of #691) #737 round-1's P1-B fix) that never fired because no test sent 10000+ lines. Truncation now silent with an explanatory comment.Frame ordering: outermost-first in
debug/getStackwire format. UP arrow decreasesselected_frame(toward outermost), DOWN increases toward innermost.Cross-pane source reload on frame select is stub-only in B.2 — direct field update from frame data arrays, no
op_dispatchcall. Full livedebug/getSourcereload on frame select wires in B.6 oncedebug_set_attach_transportis connected. Matches the plan's "implement it by having the stack pane's input callback call the sameload_sourcefunction" — in B.2 that function is the direct field update.TDD red/green
RED (compile error, correct fail-on-undefined-state — first TDD attempt before implementing B.2 state struct):
Intermediate failure (uncovered the B.1 log_warn latent bug):
GREEN (after removing log_warn + completing implementation):
Test plan
make build— clean./tools/run_headless_tests.sh— 687/687 pass (was 679; +8 new B.2 tests)cd tests && make test-integration— 2186 pass / 21 baseline failures (failure names character-for-character identical to baseline)What's NOT in this PR
debug_set_attach_transportwiring (B.6)Proactive bounds-checking applied (lesson from B.1 round 1)
Following the security review pattern from PR #737:
sizeof(buf) - 1before any memcpy/memsettui_store_stackcaps atTUI_MAX_FRAMES = 200;tui_store_localscaps atTUI_MAX_LOCALS = 500calloc, notmalloc; truncate count to populated slots on strdup failurecJSON_IsNumber/cJSON_IsString; absent fields preserve previous state (don't reset to sentinel)tui_store_*helpers early-return oncount <= 0before mutating other state fieldsSentinel verification
GIL hot path indirectly touched (transport callback in B.6+). Per
/autoPhase 5 criterion 8, main-session integration verification run from this worktree at HEADf82a5b72a. Integration failure-name set matches baseline character-for-character.🤖 Generated with Claude Code