feat(tui): QA fixes + approval Ctrl keys, workflow focus, session UUID

- gate session-list nav on idle so in-session arrows/jk don't move the list (5a)
- approval dismiss on esc + in-session 'a' reopens a pending approval (5b)
- auto-focus a newly started workflow session (was running invisibly)
- approval actions on Ctrl chords (^a/^r/^x) matching the modal; bare/Alt letters no longer approve; diff closes on ^x/esc
- steer hint in the approval modal; session UUID in the input bar
- env-gated debug logging (CORREX_TUI_LOG) with k.Alt in the key trace
This commit is contained in:
2026-06-01 23:23:29 +04:00
parent 94f7ad0ee9
commit da3f6c84a3
7 changed files with 114 additions and 21 deletions
+35 -14
View File
@@ -93,6 +93,9 @@ func (m *Model) applyServerPhased(msg protocol.ServerMessage) {
// --- key handling ---
func (m Model) handleKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
debugLog("KEY type=%v runes=%q alt=%v | ds=%s input=%s edit=%s overlay=%d steering=%v steerBuf=%q dismissed=%v entered=%v sel=%s",
k.Type, string(k.Runes), k.Alt, m.displayState(), m.inputMode, m.editMode, m.overlay,
m.steering, m.steerBuffer, m.approvalDismissed, m.sessionEntered, m.selectedID)
// Ctrl+C is a universal hard-quit safety; everything else is bare-key modal.
if k.Type == tea.KeyCtrlC {
m.quitting = true
@@ -120,8 +123,28 @@ func (m Model) handleNormalKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
case tea.KeyEnter:
return m.normalEnter()
case tea.KeyEsc:
if ds == StateApproval {
m.approvalDismissed = true
return m, nil
}
m.filter = ""
return m, nil
case tea.KeyCtrlA:
if ds == StateApproval {
return m.decide("APPROVE")
}
return m, nil
case tea.KeyCtrlR:
if ds == StateApproval {
return m.decide("REJECT")
}
return m, nil
case tea.KeyCtrlX:
if m.currentDiff() != "" {
m.overlay = OverlayDiff
m.diffScrollOffset = 0
}
return m, nil
}
if k.Type != tea.KeyRunes {
return m, nil
@@ -167,27 +190,20 @@ func (m Model) handleNormalKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
} else {
m.cycleChatMode()
}
case "x":
if m.currentDiff() != "" {
m.overlay = OverlayDiff
m.diffScrollOffset = 0
}
case "c":
if m.selectedID != "" {
m.client.Send(protocol.CancelSession(m.selectedID))
}
case "a":
if ds == StateApproval {
return m.decide("APPROVE")
if ds == StateInSession {
if s := m.session(m.selectedID); s != nil && s.Pending != nil {
m.approvalDismissed = false
}
}
case "A":
if ds == StateApproval {
return m.autoApprove()
}
case "r":
if ds == StateApproval {
return m.decide("REJECT")
}
case "q":
m.quitting = true
return m, tea.Quit
@@ -277,6 +293,7 @@ func (m Model) normalEnter() (tea.Model, tea.Cmd) {
m.client.Send(protocol.StartSession(wf.ID))
m.wfVisible = false
m.wfIndex = -1
m.pendingWorkflowFocus = true
return m, nil
}
if m.selectedID != "" {
@@ -316,7 +333,7 @@ func (m Model) handleOverlayKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
}
case k.Type == tea.KeyDown || runeIs(k, "j"):
m.diffScrollOffset++
case runeIs(k, "x"):
case k.Type == tea.KeyCtrlX:
m.overlay = OverlayNone
}
case OverlayEventInspector:
@@ -595,7 +612,9 @@ func (m *Model) navUp() {
m.wfNav(-1)
return
}
m.listNav(-1)
if m.displayState() == StateIdle {
m.listNav(-1)
}
}
func (m *Model) navDown() {
@@ -607,7 +626,9 @@ func (m *Model) navDown() {
m.wfNav(1)
return
}
m.listNav(1)
if m.displayState() == StateIdle {
m.listNav(1)
}
}
func (m *Model) wfNav(dir int) {