TUI: Ctrl-O cycles tabs (Alt-digits die in tmux/vterm)

ober

5da02b692071c181c70418c05bcf7df02b004468

diff --git a/src/jcode/ui/tui.ss b/src/jcode/ui/tui.ss
index b03cf46..d4c54ac 100644
--- a/src/jcode/ui/tui.ss
+++ b/src/jcode/ui/tui.ss
@@ -410,6 +410,14 @@
        (toast-add! 'success "Theme" "Theme cycled")
        (app-state-dirty?-set! state #t))
 
+      ;; Global: Ctrl-O cycle to the next tab. The Alt-1..9 direct jumps
+      ;; don't survive tmux/emacs-vterm (Alt is eaten or remapped), and
+      ;; Ctrl-Tab has no distinct byte sequence in a raw terminal (Tab IS
+      ;; Ctrl-I) -- a plain Ctrl-letter is what reliably gets through.
+      ((= key TB_KEY_CTRL_O)
+       (tui-log "  -> cycle-tab (ctrl-o)")
+       (cycle-tab! state))
+
       ;; Global: Ctrl-R toggle Reasoning (<think>) on latest assistant block
       ((= key TB_KEY_CTRL_R)
        (tui-log "  -> toggle-reasoning")
@@ -655,7 +663,7 @@
                "  /grok [prompt]      Open/focus a Grok CLI tab (sessioned)"
                "  /resume <id>        Resume a stored session in this tab (/sessions lists)"
                "  /side [prompt]      Open a parallel jcode conversation (own tab)"
-               "  /tab N              Switch to tab N (also Alt-1..Alt-9)"
+               "  /tab N              Switch to tab N (Ctrl-O cycles; Alt-1..9 where it works)"
                "  /tabs               List open tabs"
                "  /activity           Live harness activity screen (Esc returns)"
                "  /jcode              Switch back to the main jcode tab"
@@ -2326,6 +2334,16 @@
          (app-state-dirty?-set! state #t)
          #t)))))
 
+(def (cycle-tab! state)
+  ;; Ctrl-O: hop to the next tab, wrapping. The reliable way to switch
+  ;; under tmux/vterm where Alt-digit jumps never arrive.
+  (ensure-tabs-seeded! state)
+  (let ((n (length (app-state-tabs state))))
+    (if (< n 2)
+      (toast-add! 'info "Tabs" "Only one tab -- /side opens another")
+      (switch-tab! state
+        (modulo (+ (app-state-active-tab state) 1) n)))))
+
 (def (find-external-tab-idx state provider)
   (let loop ((tabs (app-state-tabs state)) (i 0))
     (cond
@@ -2404,7 +2422,7 @@
     (app-state-active-tab-set! state new-idx)
     (add-message! state
       (msg-block-system
-        (format "Side conversation (tab ~a). Alt-1..Alt-9 or /tab N to switch; /tabs to list; /close-tab to close."
+        (format "Side conversation (tab ~a). Ctrl-O or /tab N to switch; /tabs to list; /close-tab to close."
                 new-idx)))
     (when (and (string? initial-prompt)
                (not (string=? (string-trim initial-prompt) "")))