Repository navigation
feat: integrate useful upstream PRs (multi-session terminals, events, fixes) - #1
Merged
Merged
Conversation
Add ability to run multiple concurrent Claude Code terminal sessions with session management, smart ESC handling, and session-aware selection tracking. New commands: - ClaudeCodeNew: Create a new terminal session - ClaudeCodeSessions: Show session picker (supports fzf-lua) - ClaudeCodeSwitch: Switch to session by number - ClaudeCodeCloseSession: Close session by number or active session New features: - Smart ESC handling: double-tap ESC to exit terminal mode, single ESC sends to terminal (configurable via esc_timeout) - Session-aware selection tracking and message routing - OSC title handler for capturing terminal title changes - Configurable terminal keymaps (terminal.keymaps.exit_terminal) New modules: - lua/claudecode/session.lua: Session lifecycle management - lua/claudecode/terminal/osc_handler.lua: Terminal title detection
Add jobresize() calls to notify the terminal job of window dimensions when switching between sessions. This fixes the cursor appearing in the wrong position and line shifting after session switch. The terminal job needs to know its window dimensions to correctly calculate cursor position and line wrapping. Without this, the terminal renders based on stale window state from the previous session.
- Add clickable tabbar for session switching (floating and winbar modes) - Support left-click to switch sessions, middle-click to close - Add close button (✕) on each tab with same background as tab - Add new session button (+) for creating new sessions - Add scroll wheel support to cycle sessions in floating tabbar - Add mouse selection tracking (LeftRelease/LeftDrag) for better selection capture - Fix intentional close handling to suppress exit error on X click - Add config validation for terminal.tabs options
… available When a Claude terminal session exits (e.g., via Ctrl-C), the window now stays open and switches to display another available session instead of closing entirely. This provides a smoother multi-session experience. Changes: - Add session switching logic to TermClose handlers in both providers - Disconnect old terminal instance from window before buffer switch - Check session existence before destroying to prevent double-destruction - Add close_session_keep_window() for explicit session switching
Add dedicated window_manager module that owns the single terminal window. This separates window lifecycle from buffer lifecycle, fixing issues where: - Creating new sessions would reset window to default size - Switching between tabs could cause window duplication - Closing tabs could leave windows in wrong positions Changes: - Add window_manager.lua: singleton that manages THE terminal window - Refactor snacks.lua: create buffers only, delegate window to manager - Refactor native.lua: simplified buffer-only management - Update terminal.lua: initialize window_manager, improve tab navigation - New tab now selects the created session - Closing tab selects previous tab (or next if first)
- Add "Fork Features" section highlighting multi-session support and visual tab bar features unique to this fork - Add "Recommended Configuration" with practical floating window setup - Update all repo references from coder/claudecode.nvim to snirt/claudecode.nvim - Add multi-session keymaps to installation example - Update CHANGELOG with new features and bug fixes
Fixes #1 - Add defense-in-depth PID recovery from sessions and terminal buffers - Kill entire process tree (not just direct children) using process groups - Follow up with SIGKILL for any survivors after graceful SIGTERM - Add retry mechanism for PID tracking in snacks.lua (handles delayed job_id) - Track PIDs in external terminal provider - Add VimLeavePre autocmd to call cleanup_all() before server stops - Validate cleanup_strategy config option Tests: - Unit tests for defense-in-depth PID recovery - Integration tests with real processes verifying actual termination
Add WinEnter autocommand to selection tracking so keyboard navigation (ctrl-h, ctrl-l, :wincmd) properly updates the file reference in Claude Code. Previously only mouse clicks triggered updates. The fix cancels pending debounce timers and uses a 10ms delay to ensure window/buffer state is settled, matching the existing mouse handler behavior. Closes coder#2
When switching back to a tab containing the Claude Code terminal, Neovim re-equalizes window widths causing the terminal to appear narrow. Add restore_configured_width() helper that recalculates and applies the configured split_width_percentage on TabEnter and VimResized events. WinEnter is intentionally not modified to preserve manual user resizing within a tab.
Downgrade ECONNRESET/EOF/EPIPE client read errors to debug-level logs since they are expected when the terminal closes. Also make session.destroy_session idempotent by logging at debug level instead of warn when a session is already destroyed.
…der#165) Add snacks_picker_list filetype to the exclusion list in: - find_main_editor_window(): prevents picker from being selected as target - _create_diff_view_from_window(): creates split when picker is focused This fixes diff view behavior when using snacks.nvim picker. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix: ide diagnostics without URI schema * mock method
Cherry-picked from upstream coder/claudecode.nvim with conflict resolution. Preserves on_disconnect_cleanup for expected disconnection errors while adopting centralized _disconnect_client pattern.
Cherry-picked from upstream coder/claudecode.nvim.
Adds a dedicated describe block with 8 tests covering the planned 3-state ESC machine behavior (1x/2x ESC + timeout sends raw bytes, 3x ESC exits terminal mode). Tests currently produce 2 failures against the existing 2-state implementation.
Tests 3 and 4 now assert the key 3-state invariant: 2nd ESC must not exit terminal mode. Failing tests: double-ESC timeout sends 2x ESC, triple-ESC exits (not double), rapid triple-ESC exits, stale callback is a no-op.
- Assert timer:stop() called before timer:start() on 2nd ESC (libuv safety)
- Add mode assertion ("t") to Path 2 esc_timeout=0 keymap test
- Add clarifying comment on stale_callback capture in stale-timer test
…e Code Extends smart ESC handler from 2-state to 3-state machine. Single ESC (timeout) and double ESC (timeout) now forward raw ESC bytes to Claude Code, enabling cancel and rewind features. Triple ESC exits Neovim terminal mode. Closes coder#4
The previous implementation stopped the timer on 2nd ESC but never restarted it, permanently breaking the double-ESC timeout path that sends 2x raw ESC bytes to Claude Code for the rewind feature. Also update the test mock to track _stop_calls so the restart assertion remains accurate after stop+start leaves _stopped=false.
- Update state shape doc comment: count is 1|2 (never 0 stored in table) - Update setup_terminal_keymaps doc to not assume double-ESC fallback - Remove dead nil-timer guard in count=1 branch (new_timer() nil would have already crashed in the count=0 branch that created the state)
Sidebar toggles (file explorer, etc.) were triggering SIGWINCH via jobresize on every WinEnter, causing the TUI to redraw and reset its scroll position. Now jobresize is only sent immediately when the user enters the terminal window itself; otherwise the resize is marked pending and flushed on TermEnter.
With per-tab sessions, the TabClosed handler destroyed the session records of the closed tabpage but left their Claude jobs and hidden terminal buffers running, unreachable from any tab (and the OSC title watcher then warned about a non-existent session). A tab close now never kills or orphans Claude: tab_registry gains rehome_invalid_tabs(landing_tab), which moves the closed tab's sessions into the tabpage the user lands in. The landing tab keeps its own active session; if it had none, the closed tab's active session becomes its active one. With shared sessions (the default) nothing is bound to a tab, so the handler is a no-op.
When a Claude process exited, native on_exit and the Snacks TermClose handler took the replacement from get_active_session_id(), which resolves against the CURRENT tabpage (falling back to the global pointer) and could show another tab's session in this tab. The Snacks fallback also wrote a different session's instance into terminals[new_active_id], pairing a session with someone else's terminal. Both providers now capture the exited session's owner tabpage at exit time and choose among sessions with their own live terminal: - per_tab_sessions = true: only the owner tab's sessions; with none, the window is closed. If the owner tab is not the current one, the replacement only becomes that tab's active session. - per_tab_sessions = false: any session, the global active one first. A session is never mapped onto another session's terminal.
README, CHANGELOG and CLAUDE.md claimed each session had an isolated @ mention queue. It does not: mentions share one pending queue and are broadcast to every connected Claude session. Say so instead. Document terminal.per_tab_sessions (default false, sessions shared by all tabpages like upstream; true gives each tabpage its own sessions), the tab-close behavior (a closed tabpage's sessions move into the one you land in, never stopped), and that exit cleanup only targets Claude's own still-running terminal jobs.
…_navigation - window_manager.ensure_window/display_buffer take an optional per-call config, and the native provider passes its effective config, so split_side, split_width_percentage and auto_insert overrides (e.g. opts_override on open/toggle) apply again as they did upstream instead of only the setup-time values. - build_config forwards terminal.tabs, so the Snacks window title (the session tab strip) is set when tabs.enabled is true. - setup_terminal_keymaps installs split navigation only for an explicit split_navigation = true; nil no longer means on. - tests/mocks/vim.lua: drop the shadowed duplicate definitions of nvim_win_get_option and nvim_tabpage_list_wins (the later ones were already the effective ones; no behavior change).
…ing splits With shared sessions (per_tab_sessions = false) and a Snacks split, showing a session in a second tabpage opened a new split there and re-pointed term.win at it. get_session_window only reported term.win, so back in the first tabpage the toggle could not see the split already on screen and opened a second one; that original split could then never be closed by :ClaudeCode or :ClaudeCodeFocus. The provider now resolves the window that shows a session's buffer in the current tabpage (current_tab_win) and re-points term.win at it before hide/show (adopt_current_tab_win). get_session_window, hide_session, cc_show, the parking of other sessions, the patched term:toggle and the legacy toggles all go through it, so each tabpage holds at most one view of a session and every toggle hides or shows that view. Views in other tabpages are left alone, matching the native provider. Floats keep their park/unpark (hide=true) and cross-tab move behavior.
A client that hangs up as soon as it reads the 400, like a local port scanner probing for web servers, has its handle closed by the EOF teardown before the scheduled close runs, which then raises "handle … is already closing".
Bumps [handlebars](https://github.com/handlebars-lang/handlebars.js) from 4.7.9 to 4.7.10. - [Release notes](https://github.com/handlebars-lang/handlebars.js/releases) - [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/v4.7.10/release-notes.md) - [Commits](handlebars-lang/handlebars.js@v4.7.9...v4.7.10) --- updated-dependencies: - dependency-name: handlebars dependency-version: 4.7.10 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Michael Pacheco <git@michaelpacheco.org>
…e after a failed handshake
…7.9 to 4.7.10 in /scripts
…filter configurable
MichaelPachec0
force-pushed
the
integrate/upstream-prs
branch
from
October 28, 2026 07:14
17f8f77 to
1fb1db5
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Integrates the useful open PRs from upstream
coder/claudecode.nviminto this fork, plus fixes found while verifying them. Based on currentmain(c0deb41); merges cleanly.Upstream PRs merged as-is
coder#322 (no double close after a failed handshake), coder#323, coder#320, coder#318, coder#305 (dev dependency bumps in
scripts/), coder#317 (peer resets are disconnects), coder#312 (top/bottomsplit_side), coder#310 (empty-line selections), coder#308 (diff cleanup refocuses terminal), coder#304 (:ClaudeCodeAddpaths with spaces), coder#303 (unified new-tab diff reuses its tab), coder#299 (native terminal window flags), coder#279 (0.4.0 release), coder#250 (:ClaudeCodeAgents), coder#241 (user accept/reject cleans up), coder#203 (Shift+Enter newline), coder#202 (claudecodefiletype), coder#168 (unlisted terminal buffer).Upstream coder#164: multi-session terminals (merged, then reworked)
Multiple Claude sessions, tab bar, optional per-tabpage sessions (
terminal.per_tab_sessions, default off = upstream behavior). Reworked so nothing upstream regressed:parse_command,auto_insert, top/bottom splits, window pinning (winfixwidth/winfixheight/winfixbuf), Snacks park/unpark (coder#271),send_to_terminaltargets the active session, a user's own:terminalis never claimed, process cleanup is async and Unix-only (terminal.cleanup_strategy), coder#164's keymaps (smart ESC, split navigation, TUI scrolling) are opt-in and documented. A tab close re-homes its sessions instead of killing them.Upstream PRs ported (rewritten for the current code)
:ClaudeCodeDiffAccept/Denywork from any window in the tab (terminal, original-file side). Unlike the PR, two different Claude diffs in one tab produce a warning instead of an arbitrary pick.:ClaudeCode --resume(any new arguments, also:ClaudeCodeAgents) no longer gets dropped when a session is running. Instead of killing the running session as the PR did, it starts a separate session; repeating the same arguments still toggles.ide_nameoption for the/idepicker label. Opt-in (context = true) so the default staysNeovim; asks tmux about Neovim's own pane (-t $TMUX_PANE) in one call instead of three, and falls back to the cwd if tmux fails.UserautocmdsClaudeCodeConnected/DisconnectedandClaudeCodeTerminalOpened/Hidden(the diff and send events already existed). Terminal events come from one wrapper around the public terminal API instead of being scattered through both providers.:checktimeper burst, only with'autoread'on, skipped in the command-line window.b:claudecode_terminalflag, not their name, so aterminal_cmdwithout "claude" in it no longer sends the terminal's contents to Claude as your selection.terminal_filter_pattern(validated Lua pattern,falseto disable) covers Claude in another plugin's terminal.terminal.cwd/cwd_provider/git_repo_cwdsetups get resolvable paths.require("claudecode.terminal").reposition(opts)moves a running Snacks session between float and split without restarting Claude, rebuilt on the park/unpark logic so the new layout also survives hide/show.bind_address(default127.0.0.1) for container setups. IP literals only, the same address for the port probe and the real bind (the PR changed only one), and a startup warning whenever it is not loopback.Bugs found while testing
on_connectran at TCP accept, before the handshake, so any local process that opened a socket (port scanner, dev-server discovery daemon) could be bound to the newest Claude session ahead of the real CLI. It now runs only after the authenticated handshake.created_atcame from the cacheduv.now(), so sessions created in one tick tied and:ClaudeCodeSwitch N/tab bar numbering and client binding could swap. Ties now break on a creation sequence number./a/bthe file/a/bc/x.luawas sent as the broken path/x.lua. Now requires a path boundary.Tests
scripts/run_integration_tests_individually.sh(the CI integration job): 71 passing, 0 failing. New specs:terminal_multi_session_spec.lua(splits, pinning, resize, hide/show, session switch/close, send routing, cleanup, diff focus and accept/deny from any window, new-arguments sessions, visibility events),connection_events_spec.lua(real server, raw authenticated WebSocket client, binding regression,bind_addresshonored),buffer_refresh_spec.lua,selection_terminal_filter_spec.lua,snacks_reposition_spec.lua(pending when snacks.nvim is absent, as in CI).Not covered
provider = "none"/"external"), feat: add unified inline diff layout (ClaudeCodeBuffer) coder/claudecode.nvim#300/feat: streaming 'typing' reveal for the unified inline diff coder/claudecode.nvim#301 (compete with upstream's merged unified diff), feat: add and handle diff_opts.on_unsaved_changes option coder/claudecode.nvim#125 (changes requested), perf: speed up test and coverage runs coder/claudecode.nvim#184 (Makefile/nix tooling replaced by mise).