Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions lua/claudecode/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,18 @@ function M.validate(config)
end
end

-- Validate terminal keymaps if present
if config.terminal.keymaps then
assert(type(config.terminal.keymaps) == "table", "terminal.keymaps must be a table")
if config.terminal.keymaps.exit_terminal ~= nil then
local exit_type = type(config.terminal.keymaps.exit_terminal)
assert(
exit_type == "string" or (exit_type == "boolean" and config.terminal.keymaps.exit_terminal == false),
"terminal.keymaps.exit_terminal must be a string or false"
)
end
end

local valid_log_levels = { "trace", "debug", "info", "warn", "error" }
local is_valid_log_level = false
for _, level in ipairs(valid_log_levels) do
Expand Down
221 changes: 221 additions & 0 deletions lua/claudecode/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,68 @@ function M._create_commands()
end, {
desc = "Close the Claude Code terminal window",
})

-- Multi-session commands
vim.api.nvim_create_user_command("ClaudeCodeNew", function(opts)
local cmd_args = opts.args and opts.args ~= "" and opts.args or nil
local session_id = terminal.open_new_session({}, cmd_args)
logger.info("command", "Created new Claude Code session: " .. session_id)
end, {
nargs = "*",
desc = "Create a new Claude Code terminal session",
})

vim.api.nvim_create_user_command("ClaudeCodeSessions", function()
M.show_session_picker()
end, {
desc = "Show Claude Code session picker",
})

vim.api.nvim_create_user_command("ClaudeCodeSwitch", function(opts)
local session_index = opts.args and tonumber(opts.args)
if not session_index then
logger.error("command", "ClaudeCodeSwitch requires a session number")
return
end

local sessions = terminal.list_sessions()
if session_index < 1 or session_index > #sessions then
logger.error("command", "Invalid session number: " .. session_index .. " (have " .. #sessions .. " sessions)")
return
end

terminal.switch_to_session(sessions[session_index].id)
logger.info("command", "Switched to session " .. session_index)
end, {
nargs = 1,
desc = "Switch to Claude Code session by number",
})

vim.api.nvim_create_user_command("ClaudeCodeCloseSession", function(opts)
local session_index = opts.args and opts.args ~= "" and tonumber(opts.args)

if session_index then
local sessions = terminal.list_sessions()
if session_index < 1 or session_index > #sessions then
logger.error("command", "Invalid session number: " .. session_index .. " (have " .. #sessions .. " sessions)")
return
end
terminal.close_session(sessions[session_index].id)
logger.info("command", "Closed session " .. session_index)
else
-- Close active session
local active_id = terminal.get_active_session_id()
if active_id then
terminal.close_session(active_id)
logger.info("command", "Closed active session")
else
logger.warn("command", "No active session to close")
end
end
end, {
nargs = "?",
desc = "Close a Claude Code session by number (or active session if no number)",
})
else
logger.error(
"init",
Expand Down Expand Up @@ -1080,6 +1142,165 @@ M.open_with_model = function(additional_args)
end)
end

---Show session picker UI for selecting between active sessions
function M.show_session_picker()
local terminal = require("claudecode.terminal")
local sessions = terminal.list_sessions()

if #sessions == 0 then
logger.warn("command", "No active Claude Code sessions")
return
end

local active_session_id = terminal.get_active_session_id()

-- Format session items for display
local items = {}
for i, session in ipairs(sessions) do
local age = math.floor((vim.loop.now() - session.created_at) / 1000 / 60)
local age_str
if age < 1 then
age_str = "just now"
elseif age == 1 then
age_str = "1 min ago"
else
age_str = age .. " mins ago"
end

local active_marker = session.id == active_session_id and " (active)" or ""
table.insert(items, {
index = i,
session = session,
display = string.format("[%d] %s - %s%s", i, session.name, age_str, active_marker),
})
end

-- Try to use available picker (Snacks, fzf-lua, or vim.ui.select)
local pick_ok = M._try_picker(items, function(item)
if item and item.session then
terminal.switch_to_session(item.session.id)
end
end)

if not pick_ok then
-- Fallback to vim.ui.select
vim.ui.select(items, {
prompt = "Select Claude Code session:",
format_item = function(item)
return item.display
end,
}, function(choice)
if choice and choice.session then
terminal.switch_to_session(choice.session.id)
end
end)
end
end

---Try to use an enhanced picker (Snacks or fzf-lua)
---@param items table[] Items to pick from
---@param on_select function Callback when item is selected
---@return boolean success Whether an enhanced picker was used
function M._try_picker(items, on_select)
-- Try Snacks picker first
local snacks_ok, Snacks = pcall(require, "snacks")
if snacks_ok and Snacks and Snacks.picker then
local picker_items = {}
for _, item in ipairs(items) do
table.insert(picker_items, {
text = item.display,
item = item,
})
end

Snacks.picker.pick({
source = "claude_sessions",
items = picker_items,
format = function(item)
return { { item.text } }
end,
layout = {
preview = false,
},
confirm = function(picker, item)
picker:close()
if item and item.item then
on_select(item.item)
end
end,
actions = {
close_session = function(picker, item)
if item and item.item and item.item.session then
local terminal_mod = require("claudecode.terminal")
terminal_mod.close_session(item.item.session.id)
vim.notify("Closed session: " .. item.item.session.name, vim.log.levels.INFO)
picker:close()
end
end,
},
win = {
input = {
keys = {
["<C-x>"] = { "close_session", mode = { "i", "n" }, desc = "Close session" },
},
},
list = {
keys = {
["<C-x>"] = { "close_session", mode = { "n" }, desc = "Close session" },
},
},
},
title = "Claude Sessions (Ctrl-X: close)",
})
return true
end

-- Try fzf-lua
local fzf_ok, fzf = pcall(require, "fzf-lua")
if fzf_ok and fzf then
local display_items = {}
local item_map = {}
for _, item in ipairs(items) do
table.insert(display_items, item.display)
item_map[item.display] = item
end

fzf.fzf_exec(display_items, {
prompt = "Claude Sessions> ",
actions = {
["default"] = function(selected)
if selected and selected[1] then
local item = item_map[selected[1]]
if item then
on_select(item)
end
end
end,
["ctrl-x"] = {
fn = function(selected)
if selected and selected[1] then
local item = item_map[selected[1]]
if item and item.session then
local terminal_mod = require("claudecode.terminal")
terminal_mod.close_session(item.session.id)
vim.notify("Closed session: " .. item.session.name, vim.log.levels.INFO)
end
end
end,
-- Close picker after action since session list changed
exec_silent = true,
},
},
fzf_opts = {
["--header"] = "Enter: switch | Ctrl-X: close session",
},
})
return true
end

return false
end

---Get version information
---@return { version: string, major: integer, minor: integer, patch: integer, prerelease: string|nil }
function M.get_version()
Expand Down
41 changes: 41 additions & 0 deletions lua/claudecode/selection.lua
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
---Manages selection tracking and communication with the Claude server.
---Supports session-aware selection tracking for multi-session environments.
---@module 'claudecode.selection'
local M = {}

local logger = require("claudecode.logger")
local session_manager = require("claudecode.session")
local terminal = require("claudecode.terminal")

M.state = {
Expand Down Expand Up @@ -236,6 +238,13 @@ function M.update_selection()

if changed then
M.state.latest_selection = current_selection

-- Also update the active session's selection state
local active_session_id = session_manager.get_active_session_id()
if active_session_id then
session_manager.update_selection(active_session_id, current_selection)
end

if M.server then
M.send_selection_update(current_selection)
end
Expand Down Expand Up @@ -538,8 +547,18 @@ function M.has_selection_changed(new_selection)
end

---Sends the selection update to the Claude server.
---Uses session-aware sending if available, otherwise broadcasts to all.
---@param selection table The selection object to send.
function M.send_selection_update(selection)
-- Try to send to active session first
if M.server.send_to_active_session then
local sent = M.server.send_to_active_session("selection_changed", selection)
if sent then
return
end
end

-- Fallback to broadcast
M.server.broadcast("selection_changed", selection)
end

Expand All @@ -549,6 +568,28 @@ function M.get_latest_selection()
return M.state.latest_selection
end

---Gets the selection for a specific session.
---@param session_id string The session ID
---@return table|nil The selection object for the session, or nil if none recorded.
function M.get_session_selection(session_id)
return session_manager.get_selection(session_id)
end

---Gets the selection for the active session.
---Falls back to global latest_selection if no session-specific selection.
---@return table|nil The selection object, or nil if none recorded.
function M.get_active_session_selection()
local active_session_id = session_manager.get_active_session_id()
if active_session_id then
local session_selection = session_manager.get_selection(active_session_id)
if session_selection then
return session_selection
end
end
-- Fallback to global selection
return M.state.latest_selection
end

---Sends the current selection to Claude.
---This function is typically invoked by a user command. It forces an immediate
---update and sends the latest selection.
Expand Down
Loading