Skip to content

feat: TUI overhaul — package extraction, open-source prep, demos#8

Open
gavin-jeong wants to merge 50 commits intomasterfrom
feat/tui-overhaul
Open

feat: TUI overhaul — package extraction, open-source prep, demos#8
gavin-jeong wants to merge 50 commits intomasterfrom
feat/tui-overhaul

Conversation

@gavin-jeong
Copy link
Collaborator

Summary

  • Package extraction: internal/tmux/ (pane management, live detection), internal/extract/ (URL/file extraction)
  • Global command mode (:) with context-aware suggestions per view
  • URL/file extraction actions (xu/f) with search, multi-select, scoped to block/message/session
  • Side-question support (/btw aside agents) — correct timestamp placement, context filtering, ?:btw badge
  • Open-source prep: module rename sendbirdkeyolk, LICENSE copyright, debug gating, README demos
  • Performance: batch tmux detection (N+2 → 3 subprocesses), cached vars
  • Demo GIFs: 6 automated recordings via docs/record-demos.sh

Test plan

  • go build ./... clean
  • go test ./... — 240 tests pass
  • go vet ./... clean
  • No sendbird references in source
  • Manual: command mode from all views
  • Manual: URL/file extraction
  • Manual: aside agent display

… pref persistence

- Fix preview not auto-scrolling to newest content during live tail
  by removing sp.Focus guard from scrollConvPreviewToTail and calling
  RefreshFoldCursor after changing BlockCursor
- Fix resize resetting preview to first line by preserving CacheKey
  (entry identity) on dimension changes and only clearing render cache
- Add persistent TypeFoldPrefs/TypeFmtPrefs on SplitPane that survive
  across entry changes, populated via SyncTypePrefs on user interaction
- Fix format prefs being cleared by left-key navigation: SyncTypePrefs
  now takes syncFmt param, false for left key (navigation, not intent)
- Fix stale conv.split.List pointer after newConvList() calls
- Fix live tail selecting agent/task sub-items instead of last message
- Skip updateConvPreview during refreshConversation in live tail mode
  to prevent cache poisoning
- Add scenario-based UX tests for preview updates, live tail, resize,
  and fold state (18 tests)
- Refactor session/scanner and tui/stats into smaller focused files
…CLI flags

- Command mode (`:` key): vim-style command palette with fuzzy suggestions
  for group/preview/view switching, set:ratio, refresh, keymap:edit
- Config explorer (v→c): browse and edit Claude config files with split preview
- Hooks view (v→h): inspect Claude Code hook configurations
- Live pane proxy: unified tmux pane capture for live preview and shell-in-preview
- Input modal: send text to live Claude sessions via tmux
- CLI flags: -group, -preview for initial state; -search for startup filter
- Keymap: configurable Command key, bootstrap config via keymap:edit command
- scrollToSearchMatch now re-renders content with updated highlights
  so viewport actually moves to each match on repeated n/N presses
- Current match highlighted in cyan, other matches in yellow
- buildMsgFullSearchMatches uses rendered content (with folds/cursor)
  so line numbers match the actual viewport
refreshRespondingState() re-checks IsResponding via os.Stat on every
3s tick, even when liveUpdate is disabled. Previously IsResponding was
only set at initial scan time and never cleared, causing sessions to
show [BUSY] indefinitely after they stopped responding.
switchToTmuxPane now calls `tmux switch-client -t <session>` when the
target pane is in a different tmux session than the current one.
Previously select-window/select-pane alone couldn't cross session
boundaries.
The liveCaptureMsg handler always called GotoBottom(), which snapped the
preview back to the bottom after every capture — even when the user had
scrolled up via pgup/ctrl+b. Added paneProxy.scrolled flag that is set
on scroll keys and prevents GotoBottom(). Reset when exiting copy mode
(ctrl+q) or unfocusing the preview (left arrow).
The previous approach entered tmux copy mode on the remote pane for
pgup/pgdown, which had two problems:
1. GotoBottom() on every capture snapped back to bottom immediately
2. Copy mode intercepted all keystrokes, breaking key forwarding

New approach: on first scroll, fetch scrollback history (capture-pane
-S -500) into the local viewport and scroll locally. Background tick
captures continue updating content but skip GotoBottom() while scrolled.
Scrolling back to bottom auto-exits scroll mode. Keystrokes are always
forwarded directly to the tmux pane, never intercepted by copy mode.
Local viewport scrolling didn't work reliably for live preview.
Replaced scroll keys with a jump shortcut (J or enter) that switches
to the actual tmux pane where native scrollback works properly.
- ctrl+up/ctrl+down: scroll local viewport with scrollback history
  (fetches 500 lines on first scroll, auto-exits at bottom)
- shift+enter: sends backslash + enter to tmux pane for multi-line
  Claude input
- Background captures skip GotoBottom() while scrolled
…lict)

ctrl+up/down is intercepted by macOS Mission Control. Changed to
ctrl+b (page up) and ctrl+f (page down) which work everywhere.
- Skip content updates from background tick while user is scrolled,
  preventing scrollback from being overwritten with visible-only capture
- Changed jump key from ctrl+J to ctrl+g (ctrl+j = enter in terminals,
  ctrl+shift+j not distinguishable)
- Show scroll position indicator while scrolled
ctrl+b/f now enters copy mode on the remote tmux pane and scrolls
there. Captures reflect what the pane displays (scrolled view).
GotoBottom is skipped while scrolled so the view stays put.
ctrl+q exits copy mode first if scrolled, then unfocuses.
…scrolled

capture-pane doesn't reflect tmux copy mode view, so scroll locally:
- ctrl+b/f fetches 500 lines of scrollback on first press, scrolls
  local viewport
- Background tick captures are fully skipped while scrolled (not just
  GotoBottom — SetContent was also resetting the viewport)
- ctrl+q exits scroll mode, ctrl+f at bottom auto-exits
…I fixes

- Add vim-style navigation defaults (hjkl, g/G, ctrl+b/ctrl+f) across all views
- Redesign tab/shift+tab: cycle group when list focused, cycle preview when preview focused
- Add floating edit hint box (like views/actions menus)
- Fix title bar: use subtle dark slate background instead of garish purple
- Fix ANSI style bleed from overlay hint boxes into session list items
- Clean up live preview shortcuts: ctrl+g (jump), ctrl+n (newline), ctrl+q (unfocus)
- Remove dead tmux scroll code (alternate screen buffer prevents history capture)
…nt boxes

overlayLine now tracks the last active SGR sequence from replaced bg cells
and re-emits it after the reset, so right-side text keeps its dim/color styling.
Scan live sessions synchronously (~44ms) before starting the TUI,
then load all remaining sessions asynchronously in the background.
Shows a subtle spinner in the title bar during the full scan (~620ms).

- Add ScanSessionsForPaths() to scan only the most recent file per project
- Add DetectLiveProjectPaths() to find running Claude processes quickly
- Move blocking ScanSessions out of main into async Init() Cmd
- Show live sessions immediately, full list populates seamlessly
…ions

Cache session metadata keyed by file path + modTime. On subsequent
scans, only re-parse JSONL files that have changed. Reduces full scan
from ~620ms to ~60ms (10x faster) on warm cache.
Load all session metadata from gob cache on startup instead of
scanning live processes. Falls back to live-only detection if no
cache exists. Full scan still runs async to refresh stale data.
Resolve conflicts:
- internal/tui/app.go: keep HEAD's Config fields (Keymap, GroupMode, PreviewMode) + ClaudeDir
- main.go: adopt master's CLAUDE_CONFIG_DIR env var support + keep HEAD's cached session loading
New view for browsing installed Claude Code plugins:
- List installed plugins grouped by marketplace
- Show version, component counts, blocked status
- Detail pane with manifest info, components, install path
- Search with / key, navigate with n/N
- Mouse support for scroll and click
- Scan skills, scripts, settings, memory dirs (not just agents/hooks/commands/mcps)
- Recurse into subdirectories for nested component layouts
- Parse marketplace.json sub-plugin definitions with component paths
- Discover available (not-installed) plugins from marketplace directories
- Group list into INSTALLED / AVAILABLE sections with marketplace sub-headers
- Show sub-plugins with component badges in detail preview
- Show installed/available status badge in list and detail
Extract reusable isolatedEnv component in tmux.go that creates a fake HOME
directory for running Claude in full isolation (no memories, CLAUDE.md, MCP
servers, or marketplace discovery). Auth via CLAUDE_CODE_OAUTH_TOKEN.

- newIsolatedEnv() seeds onboarding state from both ~/.claude/.claude.json
  and ~/.claude.json, writes empty MCP config
- isolatedEnv.Script() builds shell command with HOME/cd/oauth/mcp isolation
- isolatedEnv.RunPopup() launches in tmux display-popup
- Refactor plugin test (buildPluginTestEnv) and config test (buildConfigTestEnv)
  to use shared isolatedEnv
- Plugin actions menu with enable/disable/update/uninstall (double-press confirm)
- R key to refresh plugin list
…scroll

- e key opens plugin component files in $EDITOR
- i key installs available (not-installed) plugins via claude plugin install
- Reference docs (references/ dir + README.md) shown in plugin detail
- R key refreshes both config and plugin views
- Nested tmux session in display-popup for scroll support
- Reusable isolatedEnv component for test environments
Parse the `source` field from marketplace manifest sub-plugins and
scan the directory for all components (settings, references, etc.)
instead of relying only on explicitly listed paths.
- Space toggles selection on components (✓ marker)
- x opens actions menu (e:edit)
- e opens all selected files in $EDITOR (or single cursor item)
- esc clears selection before closing detail view
- c: copy plugin install path to clipboard
- o: open shell at plugin install path
- Available in both plugin list (x actions menu) and detail view
Memory files referenced via @~/.claude/... paths break in test env
because HOME points to a temp dir. Instead of symlinking individual
memory files, embed their content directly into a generated CLAUDE.md.
Remove --system-prompt and --setting-sources "" flags that prevented
Claude from reading the generated CLAUDE.md. Now Claude discovers
config files naturally from the fake HOME directory:
- CLAUDE.md with embedded memory content
- Symlinked memory files for @reference resolution
- Symlinked hooks, skills, settings as before
Strip @reference lines from CLAUDE.md to avoid broken paths in
test env. Only embed the actual content of selected files, no
boilerplate header or unresolvable references.
HOME=tmpDir means @~/.claude/... references resolve correctly to
the fake HOME. No need to generate or embed — just symlink
selected files (including CLAUDE.md and memory) into place.
Create an editor wrapper script that restores real HOME before
launching vim/nvim, so editor config is found. Also exports
REAL_HOME for manual use inside the test env.
Files outside ~/.claude/ (project CLAUDE.md, local configs) were
silently skipped because extractRelConfigPath returned empty.
Now places them at cwd level: project/.claude/* → tmpDir/.claude/*,
project/CLAUDE.md → tmpDir/CLAUDE.md.
- Fix test env memory: when root CLAUDE.md selected, symlink all its
  @referenced files; when only refs selected, generate minimal CLAUDE.md
- Export ExtractFileReferences for cross-package use
- Add TmuxWindowName to Session, searchable via win:<name> filter
- Capture #{window_name} from tmux panes for all sessions
- Extract OAuth token from keychain when CLAUDE_CODE_OAUTH_TOKEN unset,
  enabling connector MCPs (Slack, Atlassian) in test env
- Add -view flag to launch directly into config/plugins/stats view
- Send initViewMsg after first WindowSizeMsg to safely initialize views
- Comprehensive README rewrite covering all views, keybindings, search
  filters, command mode, config/plugin explorers, and CLI flags
When deleting a hook from the config explorer, also remove its command
entry from settings.json (matched by script path + event type). Cleans
up empty matcher blocks and event types. Undo restores both the script
file and the original settings.json content.

- Export ExtractScriptPath for cross-package use
- Store settings path in ConfigItem.RefBy for hooks
- Backup settings.json before modification for undo support
- Command mode (:) available from all views, not just sessions
- Multi-command support (e.g. "view:config page:hooks")
- View+page jump commands (view:stats:tools, page:memory, etc.)
- URL extraction action (x→u) with search, multi-select, browser open
- File path extraction action (x→f) from tool_use blocks (Read/Write/Edit/Glob/Grep)
- Context-aware scoping: block → message → session fallback
- Unified actions menu (x) for conversation and message-full views
- Vim hjkl navigation in copy mode
- Batch tmux process detection (N+2 → 3 subprocess calls)
- Fix pgrep self-match by using -x for exact binary name
- Fix macOS code signing cache issue in Makefile install
Move tmux interaction code (740 lines) into internal/tmux/ with three
files: pane.go (commands, key mapping), live.go (session detection),
isolated.go (test environment).

Move URL/file extraction logic into internal/extract/ with two files:
extract.go (URL parsing, categorization, browser open) and files.go
(file path extraction from tool_use blocks).

The tui package now imports these as dependencies instead of containing
them, reducing its non-test code from ~19K to ~12K lines.
Command suggestions now filter by current view:
- Sessions: group, preview, set:ratio, refresh
- Config: page filters (memory, hooks, mcp, ...)
- Stats: page filters (tools, errors, overview)
- All views: view navigation, keymap:edit

Also adds set:ratio to the registry for tab completion and
compacts page: command entries with view bitmask annotations.
…ype, skip compact files

- Use last message timestamp instead of first for subagent ordering
- Read agent-*.meta.json for reliable agentType (e.g., "Explore", "general-purpose")
- Skip agent-acompact-*.jsonl auto-compaction artifacts from subagent list
…, README

- Split XML system tags (<system-reminder>, <task-notification>, etc.) into
  foldable system_tag content blocks, folded by default
- Collapse side-question (aside_question) background context into a single
  summary entry showing message count
- Parse imagePasteIds from JSONL, extract base64 images to temp files
- Show 🖼 badge on messages with images in conversation list
- `i` key opens first image from current message (macOS open)
- `Enter` on image block opens it directly
- `e` edit menu includes `i:image #N` choices for cached/extracted images
- Add golden snapshot tests for rendering (testdata/*.golden)
- Add tests for splitSystemTags, filterSideQuestionContext, defaultFolds
- Comprehensive README update with all features documented
…, README

- Fold system tags (<system-reminder>, <context>) in message preview
- Filter aside_question agent context: show only the btw Q&A, not parent context
- Place agents at correct chronological position via timestamp matching
- Aside agents display with ?:btw purple badge
- Context-aware edit menu: s:agent when inside subagent, p:parent for parent
- Image block rendering support in message preview
- Add markdown table rendering test with golden file
- Export ExtractFileReferences/ExtractScriptPath in session/config.go
- Rename module github.com/sendbird/ccx → github.com/keyolk/ccx
- Add 6 animated demo GIFs with automated recording script
- README: add demo section, context-aware command table, debug docs
- LICENSE: fill copyright (2025-2026 Gavin Jeong), fix MIT→Apache mismatch
- Gate debug logging behind CCX_DEBUG env var
- Fix old project name csb → ccx in temp file prefix
- Expand .gitignore (vendor, *.log, .DS_Store, dist, *.prof)
# Conflicts:
#	internal/session/config.go
#	internal/session/models.go
#	internal/session/scanner_subagent.go
#	internal/tui/app.go
#	internal/tui/blockfilter.go
#	internal/tui/blockfilter_test.go
#	internal/tui/cmdmode.go
#	internal/tui/cmdmode_test.go
#	internal/tui/config.go
#	internal/tui/config_test.go
#	internal/tui/conversation.go
#	internal/tui/conversation_models.go
#	internal/tui/conversation_render.go
#	internal/tui/conversation_ux_test.go
#	internal/tui/keymap.go
#	internal/tui/live_preview_test.go
#	internal/tui/messages.go
#	internal/tui/msgfull.go
#	internal/tui/plugins.go
#	internal/tui/plugins_test.go
#	internal/tui/stats.go
#	internal/tui/stats_detail.go
#	internal/tui/tmux.go
#	main.go
# Conflicts:
#	README.md
#	internal/extract/extract.go
#	internal/extract/files.go
#	internal/tmux/live.go
#	internal/tui/app.go
#	internal/tui/cmdmode.go
#	internal/tui/config.go
#	internal/tui/live_preview_test.go
#	internal/tui/merge_test.go
#	internal/tui/plugins.go
#	internal/tui/render_test.go
#	internal/tui/urls.go
#	main.go
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants