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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ let g:llm_agent_max_tokens=2000
let g:llm_agent_session_mode=1
let g:llm_agent_temperature = 0.7
let g:llm_agent_lang = 'Chinese'
let g:llm_agent_split_direction = 'vertical'
let g:llm_agent_split_direction = 'vertical' " or 'horizontal' or 'popup'
let g:split_ratio=4
let g:llm_agent_enable_tools=1
let g:chat_persona='default'
Expand All @@ -183,7 +183,7 @@ let g:llm_agent_log_level=0 " 0=off, 1=basic, 2=verbose
- **g:llm_agent_session_mode**: Maintain persistent conversation history across sessions. Default: 1 (enabled). When enabled, conversations are saved to `.vim-llm-agent/history.txt`. Set to 0 to disable history persistence.
- **g:llm_agent_temperature**: Controls response randomness (0.0-1.0). Higher = more creative, lower = more focused. Default: 0.7
- **g:llm_agent_lang**: Request responses in a specific language (e.g., `'Chinese'`, `'Spanish'`). Default: none (English)
- **g:llm_agent_split_direction**: Window split direction: `'vertical'` or `'horizontal'`. Default: `'vertical'`
- **g:llm_agent_split_direction**: Window display mode: `'vertical'`, `'horizontal'`, or `'popup'`. Default: `'vertical'`. The `'popup'` option uses Vim 8+ floating window for a non-intrusive overlay display.
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

README says the 'popup' option uses “Vim 8+” floating windows, but the code warns that it requires Vim 8.2+ with +popupwin (has('popupwin')). Update the README to match the actual minimum requirement and mention the automatic fallback behavior.

Suggested change
- **g:llm_agent_split_direction**: Window display mode: `'vertical'`, `'horizontal'`, or `'popup'`. Default: `'vertical'`. The `'popup'` option uses Vim 8+ floating window for a non-intrusive overlay display.
- **g:llm_agent_split_direction**: Window display mode: `'vertical'`, `'horizontal'`, or `'popup'`. Default: `'vertical'`. The `'popup'` option requires Vim 8.2+ with `+popupwin` support (`has('popupwin')`) for a non-intrusive overlay display; if popup windows are unavailable, it automatically falls back to a regular split.

Copilot uses AI. Check for mistakes.
- **g:split_ratio**: Split window size ratio. If set to 4, the window will be 1/4 of the screen. Default: 3
- **g:chat_persona**: Default AI persona to load on startup. Must match a key in `g:gpt_personas` or `g:llm_agent_custom_persona`. Default: `'default'`. See [Custom Personas](#custom-personas) section.
- **g:llm_agent_enable_tools**: Enable AI tool/function calling capabilities (allows AI to search files, read files, etc.). Default: 1 (enabled). Supported by OpenAI and Anthropic providers.
Expand Down
134 changes: 115 additions & 19 deletions autoload/chatgpt.vim
Original file line number Diff line number Diff line change
Expand Up @@ -55,34 +55,130 @@ EOF
endif
endfunction

" Check if popup window support is available
function! chatgpt#has_popup_support() abort
return has('popupwin') && exists('*popup_create')
endfunction

" Create popup window for LLM output
function! chatgpt#create_popup_window(chat_gpt_session_id) abort
" Calculate popup dimensions
let width = min([float2nr(&columns * 0.8), &columns - 4])
let height = min([float2nr(&lines * 0.6), &lines - 4])
" Ensure position values are at least 2 to avoid border overlap
let row = max([2, float2nr((&lines - height) / 2)])
let col = max([2, float2nr((&columns - width) / 2)])

" Get or create buffer
let bufnr = bufnr(a:chat_gpt_session_id)
if bufnr == -1
let bufnr = bufadd(a:chat_gpt_session_id)
call bufload(bufnr)
call setbufvar(bufnr, '&buftype', 'nofile')
call setbufvar(bufnr, '&bufhidden', 'hide')
call setbufvar(bufnr, '&swapfile', 0)
call setbufvar(bufnr, '&ft', 'markdown')
call setbufvar(bufnr, '&syntax', 'markdown')
call setbufvar(bufnr, '&wrap', 1)
call setbufvar(bufnr, '&linebreak', 1)
Comment on lines +80 to +83
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrap and linebreak are window-local options in Vim; setting them via setbufvar(bufnr, '&wrap', ...) / setbufvar(bufnr, '&linebreak', ...) won’t reliably apply to the popup window (and may be ignored). Prefer setting these with the popup options (where supported) and/or via win_execute(winid, 'setlocal wrap linebreak') after creating the popup window.

Copilot uses AI. Check for mistakes.
endif
Comment on lines +72 to +84
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Buffer initialization (bufadd/bufload + setbufvar calls) is duplicated in chatgpt#create_popup_window() and again in the popup branch of chatgpt#display_response(). This makes it easy for the two paths to drift (e.g., option differences) and increases maintenance cost. Consider extracting a small helper to “ensure session buffer exists and is configured” and reuse it from both places.

Copilot uses AI. Check for mistakes.

" Create popup window
let opts = {
\ 'line': row,
\ 'col': col,
\ 'minwidth': width,
\ 'maxwidth': width,
\ 'minheight': height,
\ 'maxheight': height,
\ 'border': [1, 1, 1, 1],
\ 'borderchars': ['─', '│', '─', '│', '┌', '┐', '┘', '└'],
\ 'title': ' LLM Agent ',
\ 'padding': [0, 1, 0, 1],
\ 'close': 'button',
\ 'resize': 1,
\ 'wrap': 1,
\ 'mapping': 0,
\ }

let winid = popup_create(bufnr, opts)

" Store popup window ID for later reference
let g:llm_agent_popup_winid = winid

return winid
endfunction

" Display ChatGPT responses in a buffer
function! chatgpt#display_response(response, finish_reason, chat_gpt_session_id)
let response = a:response
let finish_reason = a:finish_reason
let chat_gpt_session_id = a:chat_gpt_session_id
if !bufexists(chat_gpt_session_id)
let split_dir = exists('g:llm_agent_split_direction') ? g:llm_agent_split_direction : (exists('g:chat_gpt_split_direction') ? g:chat_gpt_split_direction : 'vertical')
if split_dir ==# 'vertical'
silent execute winwidth(0)/g:split_ratio.'vnew '. chat_gpt_session_id
let split_dir = exists('g:llm_agent_split_direction') ? g:llm_agent_split_direction : (exists('g:chat_gpt_split_direction') ? g:chat_gpt_split_direction : 'vertical')

" Handle popup window mode
if split_dir ==# 'popup'
if !chatgpt#has_popup_support()
echohl WarningMsg
echo "Popup windows require Vim 8.2+ with +popupwin feature. Falling back to vertical split."
echohl None
let split_dir = 'vertical'
else
silent execute winheight(0)/g:split_ratio.'new '. chat_gpt_session_id
" Check if popup already exists and is valid
let popup_exists = exists('g:llm_agent_popup_winid') && g:llm_agent_popup_winid > 0 && popup_getpos(g:llm_agent_popup_winid) != {}

if !popup_exists
call chatgpt#create_popup_window(chat_gpt_session_id)
endif
Comment on lines 126 to +132
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The popup window ID is stored in a single global (g:llm_agent_popup_winid). chatgpt#display_response() can be called with different chat_gpt_session_id values (e.g., persistent session vs other commands), and this implementation will reuse the same popup even when the session buffer changes, causing the popup to show the wrong session or stop updating. Consider tracking popup winids per session id, or explicitly retarget the existing popup to the current session buffer when chat_gpt_session_id changes.

Copilot uses AI. Check for mistakes.

" Get buffer number from popup or create new one
let bufnr = bufnr(chat_gpt_session_id)
if bufnr == -1
let bufnr = bufadd(chat_gpt_session_id)
call bufload(bufnr)
call setbufvar(bufnr, '&buftype', 'nofile')
call setbufvar(bufnr, '&bufhidden', 'hide')
call setbufvar(bufnr, '&swapfile', 0)
call setbufvar(bufnr, '&ft', 'markdown')
call setbufvar(bufnr, '&syntax', 'markdown')
call setbufvar(bufnr, '&wrap', 1)
call setbufvar(bufnr, '&linebreak', 1)
endif

" Update popup content
let winid = exists('g:llm_agent_popup_winid') ? g:llm_agent_popup_winid : 0
if winid > 0
Comment on lines +144 to +150
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue here: wrap/linebreak are window-local, so setting them with setbufvar() on the buffer won’t ensure the popup window actually uses those settings. Use win_execute() on the popup winid (or popup options) to apply window-local settings.

Suggested change
call setbufvar(bufnr, '&wrap', 1)
call setbufvar(bufnr, '&linebreak', 1)
endif
" Update popup content
let winid = exists('g:llm_agent_popup_winid') ? g:llm_agent_popup_winid : 0
if winid > 0
endif
" Update popup content
let winid = exists('g:llm_agent_popup_winid') ? g:llm_agent_popup_winid : 0
if winid > 0
call win_execute(winid, 'setlocal wrap')
call win_execute(winid, 'setlocal linebreak')

Copilot uses AI. Check for mistakes.
call popup_settext(winid, getbufline(bufnr, 1, '$'))
" Scroll to bottom of popup
call win_execute(winid, 'normal! G')
endif
Comment on lines +148 to +154
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In popup mode the popup content is updated (popup_settext + scroll) before the new response is appended to the session buffer (setbufline happens later). This means the popup can display stale content (often one response behind), and it also won’t be refreshed after the buffer changes. Consider updating the popup after setbufline(), or avoid popup_settext entirely and rely on the buffer-backed popup created by popup_create(bufnr, ...) and just scroll it after writing.

Suggested change
" Update popup content
let winid = exists('g:llm_agent_popup_winid') ? g:llm_agent_popup_winid : 0
if winid > 0
call popup_settext(winid, getbufline(bufnr, 1, '$'))
" Scroll to bottom of popup
call win_execute(winid, 'normal! G')
endif
" Do not manually copy buffer text into the popup before the response
" is written. The popup is buffer-backed, so it should reflect the
" buffer contents after the later setbufline() update.

Copilot uses AI. Check for mistakes.
endif
call setbufvar(chat_gpt_session_id, '&buftype', 'nofile')
call setbufvar(chat_gpt_session_id, '&bufhidden', 'hide')
call setbufvar(chat_gpt_session_id, '&swapfile', 0)
setlocal modifiable
setlocal wrap
setlocal linebreak
call setbufvar(chat_gpt_session_id, '&ft', 'markdown')
call setbufvar(chat_gpt_session_id, '&syntax', 'markdown')
endif

if bufwinnr(chat_gpt_session_id) == -1
let split_dir = exists('g:llm_agent_split_direction') ? g:llm_agent_split_direction : (exists('g:chat_gpt_split_direction') ? g:chat_gpt_split_direction : 'vertical')
if split_dir ==# 'vertical'
execute winwidth(0)/g:split_ratio.'vsplit ' . chat_gpt_session_id
else
execute winheight(0)/g:split_ratio.'split ' . chat_gpt_session_id
" Handle traditional split modes
if split_dir !=# 'popup'
if !bufexists(chat_gpt_session_id)
if split_dir ==# 'vertical'
silent execute winwidth(0)/g:split_ratio.'vnew '. chat_gpt_session_id
else
silent execute winheight(0)/g:split_ratio.'new '. chat_gpt_session_id
endif
call setbufvar(chat_gpt_session_id, '&buftype', 'nofile')
call setbufvar(chat_gpt_session_id, '&bufhidden', 'hide')
call setbufvar(chat_gpt_session_id, '&swapfile', 0)
setlocal modifiable
setlocal wrap
setlocal linebreak
call setbufvar(chat_gpt_session_id, '&ft', 'markdown')
call setbufvar(chat_gpt_session_id, '&syntax', 'markdown')
endif

if bufwinnr(chat_gpt_session_id) == -1
if split_dir ==# 'vertical'
execute winwidth(0)/g:split_ratio.'vsplit ' . chat_gpt_session_id
else
execute winheight(0)/g:split_ratio.'split ' . chat_gpt_session_id
endif
endif
endif

Expand Down