From e87b231f790a8fabb40734a4ba5ea11fa976462d Mon Sep 17 00:00:00 2001 From: DanBradbury Date: Wed, 5 Mar 2025 18:54:11 -0800 Subject: [PATCH 1/8] initital push of copilot skeleton --- copilot_chat.vim | 230 +++++++++++++++++++++++++++++++++++++++++++++ plugin/copilot.vim | 6 ++ 2 files changed, 236 insertions(+) create mode 100644 copilot_chat.vim diff --git a/copilot_chat.vim b/copilot_chat.vim new file mode 100644 index 00000000..a7845d82 --- /dev/null +++ b/copilot_chat.vim @@ -0,0 +1,230 @@ +function! CopilotChat() + " Open a new split window for the chat + split + enew + setlocal buftype=nofile + setlocal bufhidden=hide + setlocal noswapfile + setlocal nonumber + setlocal norelativenumber + setlocal wrap + + " Set the buffer name to indicate it's a chat window + file CopilotChat + + " Add initial message + call append(0, 'Welcome to Copilot Chat! Type your message below:') + + " Move cursor to the end of the buffer + normal! G +endfunction + +function! SubmitChatMessage() + " Get the last line of the buffer (user's message) + let l:message = getline('$') + + " Call the CoPilot API to get a response + let l:response = CopilotAPIRequest(l:message) + + " Append the parsed response to the buffer + call append(line('$'), 'Copilot: ' . l:response) + + " Move cursor to the end of the buffer + normal! G +endfunction + +function! GetBearerToken() + " Replace with actual token setup and device registry logic + let l:token_url = 'https://github.com/login/device/code' + " headers = { + " 'accept': 'application/json', + " 'editor-version': 'Neovim/0.6.1', + " 'editor-plugin-version': 'copilot.vim/1.16.0', + " 'content-type': 'application/json', + " 'user-agent': 'GithubCopilot/1.155.0', + " 'accept-encoding': 'gzip,deflate,br' + " } + + let l:token_headers = [ + \ 'Accept: application/json', + \ 'User-Agent: GithubCopilot/1.155.0', + \ 'Accept-Encoding: gzip,deflate,br', + \ 'Editor-Plugin-Version: copilot.vim/1.16.0', + \ 'Editor-Version: Neovim/0.6.1', + \ 'Content-Type: application/json', + \ ] + let l:token_data = json_encode({ + \ 'client_id': 'Iv1.b507a08c87ecfe98', + \ 'scope': 'read:user' + \ }) + + " Construct the curl command for token setup + let l:curl_cmd = 'curl -s -X POST --compressed ' + for header in l:token_headers + let l:curl_cmd .= '-H "' . header . '" ' + endfor + let l:curl_cmd .= "-d '" . l:token_data . "' " . l:token_url + echom l:curl_cmd + + " Execute the curl command + let l:response = system(l:curl_cmd) + + " Check for errors in the response + if v:shell_error != 0 + echom 'Error: ' . v:shell_error + return '' + endif + + " Parse the response to get the device code and user code + let l:json_response = json_decode(l:response) + let l:device_code = l:json_response.device_code + let l:user_code = l:json_response.user_code + let l:verification_uri = l:json_response.verification_uri + + " Display the user code and verification URI to the user + echom 'Please visit ' . l:verification_uri . ' and enter the code: ' . l:user_code + + " Wait for the user to complete the device verification + sleep 30 + + " Poll the token endpoint to get the Bearer token + let l:token_poll_url = 'https://github.com/login/oauth/access_token' + let l:token_poll_data = json_encode({ + \ 'client_id': 'Iv1.b507a08c87ecfe98', + \ 'device_code': l:device_code, + \ 'grant_type': 'urn:ietf:params:oauth:grant-type:device_code' + \ }) + + " Construct the curl command for token polling + let l:curl_cmd = 'curl -s -X POST --compressed ' + for header in l:token_headers + let l:curl_cmd .= '-H "' . header . '" ' + endfor + let l:curl_cmd .= "-d '" . l:token_poll_data . "' " . l:token_poll_url + + " Execute the curl command + let l:response = system(l:curl_cmd) + + " Check for errors in the response + if v:shell_error != 0 + echom 'Error: ' . v:shell_error + return '' + endif + + " Parse the response to get the Bearer token + let l:json_response = json_decode(l:response) + let l:bearer_token = l:json_response.access_token + let $COPILOT_BEARER_TOKEN = l:bearer_token + + " Return the Bearer token + return l:bearer_token +endfunction + +function! GetChatToken() + let l:token_url = 'https://api.github.com/copilot_internal/v2/token' + let l:token_headers = [ + \ 'Content-Type: application/json', + \ 'Editor-Version: vscode/1.80.1', + \ 'Authorization: token ' . $COPILOT_BEARER_TOKEN, + \ ] + let l:token_data = json_encode({ + \ 'client_id': 'Iv1.b507a08c87ecfe98', + \ 'scope': 'read:user' + \ }) + + " Construct the curl command for token setup + let l:curl_cmd = 'curl -s -X GET ' + for header in l:token_headers + let l:curl_cmd .= '-H "' . header . '" ' + endfor + let l:curl_cmd .= "-d '" . l:token_data . "' " . l:token_url + + " Execute the curl command + let l:response = system(l:curl_cmd) + echom l:response + + " Check for errors in the response + if v:shell_error != 0 + echom 'Error: ' . v:shell_error + return '' + endif + + " Parse the response to get the device code and user code + let l:json_response = json_decode(l:response) + return l:json_response.token +endfunction + +function! CopilotAPIRequest(message) + " Replace with actual API call logic + let l:url = 'https://api.githubcopilot.com/chat/completions' + if exists('$COPILOT_BEARER_TOKEN') + let l:bearer_token = $COPILOT_BEARER_TOKEN + else + let l:bearer_token = GetBearerToken() + endif + + let l:chat_token = GetChatToken() + let l:headers = [ + \ 'Content-Type: application/json', + \ 'Authorization: Bearer ' . l:chat_token, + \ 'Editor-Version: vscode/1.80.1' + \ ] + let l:messages = [{'content': a:message, 'role': 'user'}] + let l:data = json_encode({ + \ 'intent': v:false, + \ 'model': 'gpt-4o', + \ 'temperature': 0, + \ 'top_p': 1, + \ 'n': 1, + \ 'stream': v:true, + \ 'messages': l:messages + \ }) + + " Construct the curl command as a single string + let l:curl_cmd = 'curl -s -X POST ' + for header in l:headers + let l:curl_cmd .= '-H "' . header . '" ' + endfor + let l:curl_cmd .= "-d '" . l:data . "' " . l:url + + " Print the curl command for debugging + echom l:curl_cmd + + " Execute the curl command + let l:response = system(l:curl_cmd) + + " Print the raw response for debugging + echom 'Response: ' . l:response + + " Check for errors in the response + if v:shell_error != 0 + echom 'Error: ' . v:shell_error + return 'Error: ' . l:response + endif + + " Parse the response + let l:result = '' + let l:resp_text_lines = split(l:response, "\n") + for line in l:resp_text_lines + if line =~ '^data: {' + let l:json_completion = json_decode(line[6:]) + try + let l:result .= l:json_completion.choices[0].delta.content + catch + let l:result .= '\\n' + endtry + endif + endfor + + " Return the parsed response + return l:result +endfunction + +" Map the command to open the chat +command! CopilotChat call CopilotChat() + +" Map the command to submit a chat message +command! SubmitChatMessage call SubmitChatMessage() + +" Add key mapping to submit chat message +nnoremap cs :SubmitChatMessage \ No newline at end of file diff --git a/plugin/copilot.vim b/plugin/copilot.vim index 4d3d2162..2919e804 100644 --- a/plugin/copilot.vim +++ b/plugin/copilot.vim @@ -112,3 +112,9 @@ let s:dir = expand(':h:h') if getftime(s:dir . '/doc/copilot.txt') > getftime(s:dir . '/doc/tags') silent! execute 'helptags' fnameescape(s:dir . '/doc') endif + +" Source the copilot_chat.vim file +source /Users/bradbd/Documents/Github/copilot.vim/copilot_chat.vim + +" Add key mapping to open Copilot Chat +nnoremap cc :CopilotChat From e93fbccefe861698441928544781065b4b53bc28 Mon Sep 17 00:00:00 2001 From: Dan Bradbury Date: Fri, 7 Mar 2025 05:35:24 +0000 Subject: [PATCH 2/8] devcontainer setup to make working on windows tolerable --- .devcontainer/Dockerfile | 5 +++++ .devcontainer/devcontainer.json | 4 ++++ .devcontainer/vimrc | 10 ++++++++++ plugin/copilot.vim | 8 +------- copilot_chat.vim => plugin/copilot_chat.vim | 18 ++++++++++++++---- 5 files changed, 34 insertions(+), 11 deletions(-) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/vimrc rename copilot_chat.vim => plugin/copilot_chat.vim (94%) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..1b08b3cb --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,5 @@ +FROM alpine:latest +RUN apk update && apk add git vim curl +RUN mkdir ~/.vim +RUN mkdir ~/.vim/bundle +RUN git clone https://github.com/VundleVim/Vundle.vim.git ~/.vim/bundle/Vundle.vim \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..b3110303 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,4 @@ +{ + "dockerFile": "Dockerfile", + "postStartCommand": "cp .devcontainer/vimrc ~/.vimrc && vim +PluginInstall +qall && cp -r * ~/.vim/bundle/copilot.vim/" +} \ No newline at end of file diff --git a/.devcontainer/vimrc b/.devcontainer/vimrc new file mode 100644 index 00000000..92fbd22c --- /dev/null +++ b/.devcontainer/vimrc @@ -0,0 +1,10 @@ +set nocompatible " be iMproved, required +filetype off " required + +" set the runtime path to include Vundle and initialize +set rtp+=~/.vim/bundle/Vundle.vim +call vundle#begin() +Plugin 'VundleVim/Vundle.vim' +Plugin 'DanBradbury/copilot.vim' +call vundle#end() " required +filetype plugin indent on " required \ No newline at end of file diff --git a/plugin/copilot.vim b/plugin/copilot.vim index 2919e804..5f35aa6a 100644 --- a/plugin/copilot.vim +++ b/plugin/copilot.vim @@ -111,10 +111,4 @@ endif let s:dir = expand(':h:h') if getftime(s:dir . '/doc/copilot.txt') > getftime(s:dir . '/doc/tags') silent! execute 'helptags' fnameescape(s:dir . '/doc') -endif - -" Source the copilot_chat.vim file -source /Users/bradbd/Documents/Github/copilot.vim/copilot_chat.vim - -" Add key mapping to open Copilot Chat -nnoremap cc :CopilotChat +endif \ No newline at end of file diff --git a/copilot_chat.vim b/plugin/copilot_chat.vim similarity index 94% rename from copilot_chat.vim rename to plugin/copilot_chat.vim index a7845d82..013c5066 100644 --- a/copilot_chat.vim +++ b/plugin/copilot_chat.vim @@ -1,3 +1,6 @@ +let s:plugin_dir = expand(':p:h') +let s:token_file = s:plugin_dir . '/copilot_token' + function! CopilotChat() " Open a new split window for the chat split @@ -64,10 +67,11 @@ function! GetBearerToken() let l:curl_cmd .= '-H "' . header . '" ' endfor let l:curl_cmd .= "-d '" . l:token_data . "' " . l:token_url - echom l:curl_cmd " Execute the curl command let l:response = system(l:curl_cmd) + echom 'Response: ' . l:response + exit(1) " Check for errors in the response if v:shell_error != 0 @@ -141,7 +145,7 @@ function! GetChatToken() " Execute the curl command let l:response = system(l:curl_cmd) - echom l:response + echo l:response " Check for errors in the response if v:shell_error != 0 @@ -154,8 +158,11 @@ function! GetChatToken() return l:json_response.token endfunction +function! CheckDeviceToken() +endfunction + function! CopilotAPIRequest(message) - " Replace with actual API call logic + "CheckDeviceToken() let l:url = 'https://api.githubcopilot.com/chat/completions' if exists('$COPILOT_BEARER_TOKEN') let l:bearer_token = $COPILOT_BEARER_TOKEN @@ -227,4 +234,7 @@ command! CopilotChat call CopilotChat() command! SubmitChatMessage call SubmitChatMessage() " Add key mapping to submit chat message -nnoremap cs :SubmitChatMessage \ No newline at end of file +nnoremap cs :SubmitChatMessage + +" Add key mapping to open Copilot Chat +nnoremap cc :CopilotChat \ No newline at end of file From 36b0e0f736a3b3060a65e3fe3fa558e43969eda5 Mon Sep 17 00:00:00 2001 From: Dan Bradbury Date: Fri, 7 Mar 2025 07:29:56 +0000 Subject: [PATCH 3/8] Use token files in plugin directory to only require single login + fix multi-line update to buffer on chat response + adds move.sh + run-chat.sh --- .devcontainer/Dockerfile | 5 ++--- move.sh | 2 ++ plugin/copilot_chat.vim | 43 ++++++++++++++++++---------------------- 3 files changed, 23 insertions(+), 27 deletions(-) create mode 100644 move.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 1b08b3cb..29b6ace8 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,5 +1,4 @@ FROM alpine:latest -RUN apk update && apk add git vim curl -RUN mkdir ~/.vim -RUN mkdir ~/.vim/bundle +RUN apk update && apk add git vim curl wslview +RUN mkdir -p ~/.vim/bundle RUN git clone https://github.com/VundleVim/Vundle.vim.git ~/.vim/bundle/Vundle.vim \ No newline at end of file diff --git a/move.sh b/move.sh new file mode 100644 index 00000000..ff5dba3f --- /dev/null +++ b/move.sh @@ -0,0 +1,2 @@ +#!/bin/sh +cp -r * ~/.vim/bundle/copilot.vim/ \ No newline at end of file diff --git a/plugin/copilot_chat.vim b/plugin/copilot_chat.vim index 013c5066..76726549 100644 --- a/plugin/copilot_chat.vim +++ b/plugin/copilot_chat.vim @@ -1,5 +1,9 @@ -let s:plugin_dir = expand(':p:h') -let s:token_file = s:plugin_dir . '/copilot_token' +scriptencoding utf-8 + +let s:plugin_dir = expand(':p:h:h') +let s:device_token_file = s:plugin_dir . "/.device_token" +let s:chat_token_file = s:plugin_dir . "/.chat_token" + function! CopilotChat() " Open a new split window for the chat @@ -30,7 +34,7 @@ function! SubmitChatMessage() let l:response = CopilotAPIRequest(l:message) " Append the parsed response to the buffer - call append(line('$'), 'Copilot: ' . l:response) + call append(line('$'), split(l:response, "\n")) " Move cursor to the end of the buffer normal! G @@ -39,14 +43,6 @@ endfunction function! GetBearerToken() " Replace with actual token setup and device registry logic let l:token_url = 'https://github.com/login/device/code' - " headers = { - " 'accept': 'application/json', - " 'editor-version': 'Neovim/0.6.1', - " 'editor-plugin-version': 'copilot.vim/1.16.0', - " 'content-type': 'application/json', - " 'user-agent': 'GithubCopilot/1.155.0', - " 'accept-encoding': 'gzip,deflate,br' - " } let l:token_headers = [ \ 'Accept: application/json', @@ -70,8 +66,6 @@ function! GetBearerToken() " Execute the curl command let l:response = system(l:curl_cmd) - echom 'Response: ' . l:response - exit(1) " Check for errors in the response if v:shell_error != 0 @@ -86,10 +80,8 @@ function! GetBearerToken() let l:verification_uri = l:json_response.verification_uri " Display the user code and verification URI to the user - echom 'Please visit ' . l:verification_uri . ' and enter the code: ' . l:user_code - - " Wait for the user to complete the device verification - sleep 30 + echo 'Please visit ' . l:verification_uri . ' and enter the code: ' . l:user_code + call input("Press Enter to continue...\n") " Poll the token endpoint to get the Bearer token let l:token_poll_url = 'https://github.com/login/oauth/access_token' @@ -118,18 +110,18 @@ function! GetBearerToken() " Parse the response to get the Bearer token let l:json_response = json_decode(l:response) let l:bearer_token = l:json_response.access_token - let $COPILOT_BEARER_TOKEN = l:bearer_token + call writefile([l:bearer_token], s:device_token_file) " Return the Bearer token return l:bearer_token endfunction -function! GetChatToken() +function! GetChatToken(bearer_token) let l:token_url = 'https://api.github.com/copilot_internal/v2/token' let l:token_headers = [ \ 'Content-Type: application/json', \ 'Editor-Version: vscode/1.80.1', - \ 'Authorization: token ' . $COPILOT_BEARER_TOKEN, + \ 'Authorization: token ' . a:bearer_token, \ ] let l:token_data = json_encode({ \ 'client_id': 'Iv1.b507a08c87ecfe98', @@ -164,13 +156,16 @@ endfunction function! CopilotAPIRequest(message) "CheckDeviceToken() let l:url = 'https://api.githubcopilot.com/chat/completions' - if exists('$COPILOT_BEARER_TOKEN') - let l:bearer_token = $COPILOT_BEARER_TOKEN + + if filereadable(s:device_token_file) + echo "READING DEVICE TOKEN" + let l:bearer_token = join(readfile(s:device_token_file), "\n") + echo l:bearer_token else let l:bearer_token = GetBearerToken() endif - let l:chat_token = GetChatToken() + let l:chat_token = GetChatToken(l:bearer_token) let l:headers = [ \ 'Content-Type: application/json', \ 'Authorization: Bearer ' . l:chat_token, @@ -218,7 +213,7 @@ function! CopilotAPIRequest(message) try let l:result .= l:json_completion.choices[0].delta.content catch - let l:result .= '\\n' + let l:result .= "\n" endtry endif endfor From 4f165ce5ee43615805bf8bd9674ac385d8b8ebbc Mon Sep 17 00:00:00 2001 From: Dan Bradbury Date: Fri, 7 Mar 2025 07:30:04 +0000 Subject: [PATCH 4/8] missed --- run-chat.sh | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 run-chat.sh diff --git a/run-chat.sh b/run-chat.sh new file mode 100644 index 00000000..f2c2a7ff --- /dev/null +++ b/run-chat.sh @@ -0,0 +1,2 @@ +#!/bin/sh +vim -c "CopilotChat" -c "SubmitChatMessage" \ No newline at end of file From d689c7d662da094af7ba1d7e228f261e9be32c6f Mon Sep 17 00:00:00 2001 From: Dan Bradbury Date: Sun, 9 Mar 2025 10:10:21 +0000 Subject: [PATCH 5/8] Use job_start to run chat completion in background + markdown formatting in chat window --- .devcontainer/vimrc | 1 + plugin/copilot_chat.vim | 147 +++++++++++++++++----------------------- 2 files changed, 65 insertions(+), 83 deletions(-) diff --git a/.devcontainer/vimrc b/.devcontainer/vimrc index 92fbd22c..dece5afa 100644 --- a/.devcontainer/vimrc +++ b/.devcontainer/vimrc @@ -6,5 +6,6 @@ set rtp+=~/.vim/bundle/Vundle.vim call vundle#begin() Plugin 'VundleVim/Vundle.vim' Plugin 'DanBradbury/copilot.vim' +Plugin 'preservim/vim-markdown' call vundle#end() " required filetype plugin indent on " required \ No newline at end of file diff --git a/plugin/copilot_chat.vim b/plugin/copilot_chat.vim index 76726549..efa09128 100644 --- a/plugin/copilot_chat.vim +++ b/plugin/copilot_chat.vim @@ -4,7 +4,6 @@ let s:plugin_dir = expand(':p:h:h') let s:device_token_file = s:plugin_dir . "/.device_token" let s:chat_token_file = s:plugin_dir . "/.chat_token" - function! CopilotChat() " Open a new split window for the chat split @@ -15,33 +14,24 @@ function! CopilotChat() setlocal nonumber setlocal norelativenumber setlocal wrap + set filetype=markdown " Set the buffer name to indicate it's a chat window file CopilotChat - " Add initial message call append(0, 'Welcome to Copilot Chat! Type your message below:') - " Move cursor to the end of the buffer normal! G endfunction function! SubmitChatMessage() - " Get the last line of the buffer (user's message) + " TODO: this should be grabbing the full prompt between the separators let l:message = getline('$') - " Call the CoPilot API to get a response - let l:response = CopilotAPIRequest(l:message) - - " Append the parsed response to the buffer - call append(line('$'), split(l:response, "\n")) - - " Move cursor to the end of the buffer - normal! G + call AsyncRequest(l:message) endfunction function! GetBearerToken() - " Replace with actual token setup and device registry logic let l:token_url = 'https://github.com/login/device/code' let l:token_headers = [ @@ -137,7 +127,7 @@ function! GetChatToken(bearer_token) " Execute the curl command let l:response = system(l:curl_cmd) - echo l:response + "echo l:response " Check for errors in the response if v:shell_error != 0 @@ -151,85 +141,76 @@ function! GetChatToken(bearer_token) endfunction function! CheckDeviceToken() + " fetch models + " if the call fails we should get a new chat token and update the file endfunction -function! CopilotAPIRequest(message) - "CheckDeviceToken() - let l:url = 'https://api.githubcopilot.com/chat/completions' +function! AsyncRequest(message) + let s:curl_output = [] + let l:url = 'https://api.githubcopilot.com/chat/completions' - if filereadable(s:device_token_file) - echo "READING DEVICE TOKEN" - let l:bearer_token = join(readfile(s:device_token_file), "\n") - echo l:bearer_token - else - let l:bearer_token = GetBearerToken() - endif - - let l:chat_token = GetChatToken(l:bearer_token) - let l:headers = [ - \ 'Content-Type: application/json', - \ 'Authorization: Bearer ' . l:chat_token, - \ 'Editor-Version: vscode/1.80.1' - \ ] - let l:messages = [{'content': a:message, 'role': 'user'}] - let l:data = json_encode({ - \ 'intent': v:false, - \ 'model': 'gpt-4o', - \ 'temperature': 0, - \ 'top_p': 1, - \ 'n': 1, - \ 'stream': v:true, - \ 'messages': l:messages - \ }) - - " Construct the curl command as a single string - let l:curl_cmd = 'curl -s -X POST ' - for header in l:headers - let l:curl_cmd .= '-H "' . header . '" ' - endfor - let l:curl_cmd .= "-d '" . l:data . "' " . l:url - - " Print the curl command for debugging - echom l:curl_cmd - - " Execute the curl command - let l:response = system(l:curl_cmd) - - " Print the raw response for debugging - echom 'Response: ' . l:response + if filereadable(s:device_token_file) + let l:bearer_token = join(readfile(s:device_token_file), "\n") + else + let l:bearer_token = GetBearerToken() + endif + + let l:chat_token = GetChatToken(l:bearer_token) + let l:messages = [{'content': a:message, 'role': 'user'}] + let l:data = json_encode({ + \ 'intent': v:false, + \ 'model': 'gpt-4o', + \ 'temperature': 0, + \ 'top_p': 1, + \ 'n': 1, + \ 'stream': v:true, + \ 'messages': l:messages + \ }) + + let l:curl_cmd = [ + \ "curl", + \ "-s", + \ "-X", + \ "POST", + \ "-H", + \ "Content-Type: application/json", + \ "-H", "Authorization: Bearer " . l:chat_token, + \ "-H", "Editor-Version: vscode/1.80.1", + \ "-d", + \ l:data, + \ l:url] + + let job = job_start(l:curl_cmd, {'out_cb': function('HandleCurlOutput'), 'exit_cb': function('HandleCurlClose'), 'err_cb': function('HandleCurlError')}) + return job +endfunction - " Check for errors in the response - if v:shell_error != 0 - echom 'Error: ' . v:shell_error - return 'Error: ' . l:response - endif +function! HandleCurlError(channel, msg) + echom "handling curl error" + echom a:msg +endfunction - " Parse the response - let l:result = '' - let l:resp_text_lines = split(l:response, "\n") - for line in l:resp_text_lines - if line =~ '^data: {' - let l:json_completion = json_decode(line[6:]) - try - let l:result .= l:json_completion.choices[0].delta.content - catch - let l:result .= "\n" - endtry - endif - endfor +function! HandleCurlClose(channel, msg) + let l:result = '' + for line in s:curl_output + if line =~ '^data: {' + let l:json_completion = json_decode(line[6:]) + try + let l:result .= l:json_completion.choices[0].delta.content + catch + let l:result .= "\n" + endtry + endif + endfor + call append(line('$'), split(l:result, "\n")) + normal! G +endfunction - " Return the parsed response - return l:result +function! HandleCurlOutput(channel, msg) + call add(s:curl_output, a:msg) endfunction -" Map the command to open the chat command! CopilotChat call CopilotChat() - -" Map the command to submit a chat message command! SubmitChatMessage call SubmitChatMessage() -" Add key mapping to submit chat message nnoremap cs :SubmitChatMessage - -" Add key mapping to open Copilot Chat nnoremap cc :CopilotChat \ No newline at end of file From b69acf49316c76295f2127068a98c3b399b9308f Mon Sep 17 00:00:00 2001 From: Dan Bradbury Date: Sun, 9 Mar 2025 18:31:55 -0700 Subject: [PATCH 6/8] Support windows with Invoke-WebRequest + HttpIt helper function --- plugin/copilot_chat.vim | 163 +++++++++++++++++++++------------------- 1 file changed, 84 insertions(+), 79 deletions(-) diff --git a/plugin/copilot_chat.vim b/plugin/copilot_chat.vim index efa09128..0ad865fc 100644 --- a/plugin/copilot_chat.vim +++ b/plugin/copilot_chat.vim @@ -3,6 +3,14 @@ scriptencoding utf-8 let s:plugin_dir = expand(':p:h:h') let s:device_token_file = s:plugin_dir . "/.device_token" let s:chat_token_file = s:plugin_dir . "/.chat_token" +let s:token_headers = [ + \ 'Accept: application/json', + \ 'User-Agent: GithubCopilot/1.155.0', + \ 'Accept-Encoding: gzip,deflate,br', + \ 'Editor-Plugin-Version: copilot.vim/1.16.0', + \ 'Editor-Version: Neovim/0.6.1', + \ 'Content-Type: application/json', + \ ] function! CopilotChat() " Open a new split window for the chat @@ -31,78 +39,93 @@ function! SubmitChatMessage() call AsyncRequest(l:message) endfunction -function! GetBearerToken() - let l:token_url = 'https://github.com/login/device/code' - - let l:token_headers = [ - \ 'Accept: application/json', - \ 'User-Agent: GithubCopilot/1.155.0', - \ 'Accept-Encoding: gzip,deflate,br', - \ 'Editor-Plugin-Version: copilot.vim/1.16.0', - \ 'Editor-Version: Neovim/0.6.1', - \ 'Content-Type: application/json', - \ ] - let l:token_data = json_encode({ - \ 'client_id': 'Iv1.b507a08c87ecfe98', - \ 'scope': 'read:user' - \ }) - - " Construct the curl command for token setup - let l:curl_cmd = 'curl -s -X POST --compressed ' - for header in l:token_headers - let l:curl_cmd .= '-H "' . header . '" ' - endfor - let l:curl_cmd .= "-d '" . l:token_data . "' " . l:token_url - - " Execute the curl command - let l:response = system(l:curl_cmd) - - " Check for errors in the response - if v:shell_error != 0 - echom 'Error: ' . v:shell_error - return '' +function HttpIt(method, url, headers, body) + if has("win32") + let l:ps_cmd = 'powershell -Command "' + let l:ps_cmd .= '$headers = @{' + for header in a:headers + let [key, value] = split(header, ": ") + let l:ps_cmd .= "'" . key . "'='" . value . "';" + endfor + let l:ps_cmd .= "};" + if a:method != "GET" + let l:ps_cmd .= '$body = ConvertTo-Json @{' + for obj in keys(a:body) + let l:ps_cmd .= obj . "='" . a:body[obj] . "';" + endfor + let l:ps_cmd .= "};" + endif + let l:ps_cmd .= "Invoke-WebRequest -Uri '" . a:url . "' -Method " .a:method . " -Headers $headers -Body $body -ContentType 'application/json' | Select-Object -ExpandProperty Content" + let l:ps_cmd .= '"' + let l:response = system(l:ps_cmd) + else + let l:token_headers = [ + \ 'Accept: application/json', + \ 'User-Agent: GithubCopilot/1.155.0', + \ 'Accept-Encoding: gzip,deflate,br', + \ 'Editor-Plugin-Version: copilot.vim/1.16.0', + \ 'Editor-Version: Neovim/0.6.1', + \ 'Content-Type: application/json', + \ ] + let l:token_data = json_encode(a:body) + + " Construct the curl command for token setup + let l:curl_cmd = 'curl -s -X POST --compressed ' + for header in a:headers + let l:curl_cmd .= '-H "' . header . '" ' + endfor + let l:curl_cmd .= "-d '" . l:token_data . "' " . l:token_url + + let l:response = system(l:curl_cmd) + + " Check for errors in the response + if v:shell_error != 0 + echom 'Error: ' . v:shell_error + return '' + endif endif + return l:response +endfunction + +function! GetDeviceToken() + let l:token_url = 'https://github.com/login/device/code' + let l:headers = [ + \ 'Accept: application/json', + \ 'User-Agent: GithubCopilot/1.155.0', + \ 'Accept-Encoding: gzip,deflate,br', + \ 'Editor-Plugin-Version: copilot.vim/1.16.0', + \ 'Editor-Version: Neovim/0.6.1', + \ 'Content-Type: application/json', + \ ] + let l:data = { + \ 'client_id': 'Iv1.b507a08c87ecfe98', + \ 'scope': 'read:user' + \ } + + return HttpIt("POST", l:token_url, l:headers, l:data) +endfunction - " Parse the response to get the device code and user code +function! GetBearerToken() + let l:response = GetDeviceToken() let l:json_response = json_decode(l:response) let l:device_code = l:json_response.device_code let l:user_code = l:json_response.user_code let l:verification_uri = l:json_response.verification_uri - " Display the user code and verification URI to the user echo 'Please visit ' . l:verification_uri . ' and enter the code: ' . l:user_code call input("Press Enter to continue...\n") - " Poll the token endpoint to get the Bearer token let l:token_poll_url = 'https://github.com/login/oauth/access_token' - let l:token_poll_data = json_encode({ - \ 'client_id': 'Iv1.b507a08c87ecfe98', - \ 'device_code': l:device_code, - \ 'grant_type': 'urn:ietf:params:oauth:grant-type:device_code' - \ }) - - " Construct the curl command for token polling - let l:curl_cmd = 'curl -s -X POST --compressed ' - for header in l:token_headers - let l:curl_cmd .= '-H "' . header . '" ' - endfor - let l:curl_cmd .= "-d '" . l:token_poll_data . "' " . l:token_poll_url - - " Execute the curl command - let l:response = system(l:curl_cmd) - - " Check for errors in the response - if v:shell_error != 0 - echom 'Error: ' . v:shell_error - return '' - endif - - " Parse the response to get the Bearer token - let l:json_response = json_decode(l:response) + let l:token_poll_data = { + \ 'client_id': 'Iv1.b507a08c87ecfe98', + \ 'device_code': l:device_code, + \ 'grant_type': 'urn:ietf:params:oauth:grant-type:device_code' + \ } + let l:access_token_response = HttpIt("POST", l:token_poll_url, s:token_headers, l:token_poll_data) + let l:json_response = json_decode(l:access_token_response) let l:bearer_token = l:json_response.access_token call writefile([l:bearer_token], s:device_token_file) - " Return the Bearer token return l:bearer_token endfunction @@ -113,29 +136,11 @@ function! GetChatToken(bearer_token) \ 'Editor-Version: vscode/1.80.1', \ 'Authorization: token ' . a:bearer_token, \ ] - let l:token_data = json_encode({ + let l:token_data = { \ 'client_id': 'Iv1.b507a08c87ecfe98', \ 'scope': 'read:user' - \ }) - - " Construct the curl command for token setup - let l:curl_cmd = 'curl -s -X GET ' - for header in l:token_headers - let l:curl_cmd .= '-H "' . header . '" ' - endfor - let l:curl_cmd .= "-d '" . l:token_data . "' " . l:token_url - - " Execute the curl command - let l:response = system(l:curl_cmd) - "echo l:response - - " Check for errors in the response - if v:shell_error != 0 - echom 'Error: ' . v:shell_error - return '' - endif - - " Parse the response to get the device code and user code + \ } + let l:response = HttpIt("GET", l:token_url, l:token_headers, l:token_data) let l:json_response = json_decode(l:response) return l:json_response.token endfunction From 7d95b58e679a44af8fc75c37972282f4db025ba5 Mon Sep 17 00:00:00 2001 From: Dan Bradbury Date: Mon, 10 Mar 2025 05:47:52 +0000 Subject: [PATCH 7/8] Cleanup for non-windows using HttpIt + updates to docker setup --- .devcontainer/Dockerfile | 2 +- .devcontainer/devcontainer.json | 9 ++++++++- plugin/copilot_chat.vim | 15 ++------------- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 29b6ace8..aaf3e122 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ FROM alpine:latest -RUN apk update && apk add git vim curl wslview +RUN apk update && apk add git vim curl RUN mkdir -p ~/.vim/bundle RUN git clone https://github.com/VundleVim/Vundle.vim.git ~/.vim/bundle/Vundle.vim \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b3110303..25d7ba15 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,4 +1,11 @@ { "dockerFile": "Dockerfile", - "postStartCommand": "cp .devcontainer/vimrc ~/.vimrc && vim +PluginInstall +qall && cp -r * ~/.vim/bundle/copilot.vim/" + "postStartCommand": "cp .devcontainer/vimrc ~/.vimrc && vim +PluginInstall +qall && cp -r * ~/.vim/bundle/copilot.vim/", + "customizations": { + "vscode": { + "extensions": [ + "XadillaX.viml" + ] + } + } } \ No newline at end of file diff --git a/plugin/copilot_chat.vim b/plugin/copilot_chat.vim index 0ad865fc..ce27fb0e 100644 --- a/plugin/copilot_chat.vim +++ b/plugin/copilot_chat.vim @@ -59,26 +59,15 @@ function HttpIt(method, url, headers, body) let l:ps_cmd .= '"' let l:response = system(l:ps_cmd) else - let l:token_headers = [ - \ 'Accept: application/json', - \ 'User-Agent: GithubCopilot/1.155.0', - \ 'Accept-Encoding: gzip,deflate,br', - \ 'Editor-Plugin-Version: copilot.vim/1.16.0', - \ 'Editor-Version: Neovim/0.6.1', - \ 'Content-Type: application/json', - \ ] let l:token_data = json_encode(a:body) - " Construct the curl command for token setup - let l:curl_cmd = 'curl -s -X POST --compressed ' + let l:curl_cmd = 'curl -s -X ' . a:method . ' --compressed ' for header in a:headers let l:curl_cmd .= '-H "' . header . '" ' endfor - let l:curl_cmd .= "-d '" . l:token_data . "' " . l:token_url + let l:curl_cmd .= "-d '" . l:token_data . "' " . a:url let l:response = system(l:curl_cmd) - - " Check for errors in the response if v:shell_error != 0 echom 'Error: ' . v:shell_error return '' From 92821df4b7f8cd0069a63098bb0092adcea56506 Mon Sep 17 00:00:00 2001 From: Dan Bradbury Date: Mon, 10 Mar 2025 00:38:15 -0700 Subject: [PATCH 8/8] Make the interface prettier and try to match basic Nvim interface --- plugin/copilot_chat.vim | 71 +++++++++++++++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 14 deletions(-) diff --git a/plugin/copilot_chat.vim b/plugin/copilot_chat.vim index ce27fb0e..c547187b 100644 --- a/plugin/copilot_chat.vim +++ b/plugin/copilot_chat.vim @@ -12,9 +12,17 @@ let s:token_headers = [ \ 'Content-Type: application/json', \ ] +function! UserInputSeparator() + let l:width = winwidth(0)-2 + let l:separator = " " + let l:separator .= repeat('━', l:width) + call append(line('$'), l:separator) + call append(line('$'), '') +endfunction + function! CopilotChat() " Open a new split window for the chat - split + vsplit enew setlocal buftype=nofile setlocal bufhidden=hide @@ -27,14 +35,26 @@ function! CopilotChat() " Set the buffer name to indicate it's a chat window file CopilotChat + syntax match CopilotWelcome /^Welcome to Copilot Chat!.*$/ + syntax match CopilotSeparatorIcon /^/ containedin=CopilotSeparatorLine + syntax match CopilotSeparatorIcon /^/ containedin=CopilotSeparatorLine + syntax match CopilotSeparatorLine / ━\+$/ + + highlight CopilotWelcome ctermfg=205 guifg=#ff69b4 + highlight CopilotSeparatorIcon ctermfg=45 guifg=#00d7ff + highlight CopilotSeparatorLine ctermfg=205 guifg=#ff69b4 + call append(0, 'Welcome to Copilot Chat! Type your message below:') + call UserInputSeparator() normal! G endfunction function! SubmitChatMessage() - " TODO: this should be grabbing the full prompt between the separators - let l:message = getline('$') + let l:separator_line = search(' ━\+$', 'nw') + let l:start_line = l:separator_line + 1 + let l:end_line = line('$') + let l:message = join(getline(l:start_line, l:end_line), "\n") call AsyncRequest(l:message) endfunction @@ -139,6 +159,17 @@ function! CheckDeviceToken() " if the call fails we should get a new chat token and update the file endfunction +function! UpdateWaitingDots() + let l:line = line('$') + let l:current_text = getline(l:line) + if l:current_text =~ '^Waiting for response' + let l:dots = len(matchstr(l:current_text, '\..*$')) + let l:new_dots = (l:dots % 3) + 1 + call setline(l:line, 'Waiting for response' . repeat('.', l:new_dots)) + endif + return 1 +endfunction + function! AsyncRequest(message) let s:curl_output = [] let l:url = 'https://api.githubcopilot.com/chat/completions' @@ -150,6 +181,9 @@ function! AsyncRequest(message) endif let l:chat_token = GetChatToken(l:bearer_token) + call append(line('$'), "Waiting for response") + let s:waiting_timer = timer_start(500, {-> UpdateWaitingDots()}, {'repeat': -1}) + let l:messages = [{'content': a:message, 'role': 'user'}] let l:data = json_encode({ \ 'intent': v:false, @@ -186,17 +220,26 @@ endfunction function! HandleCurlClose(channel, msg) let l:result = '' for line in s:curl_output - if line =~ '^data: {' - let l:json_completion = json_decode(line[6:]) - try - let l:result .= l:json_completion.choices[0].delta.content - catch - let l:result .= "\n" - endtry - endif - endfor - call append(line('$'), split(l:result, "\n")) - normal! G + if line =~ '^data: {' + let l:json_completion = json_decode(line[6:]) + try + let l:content = l:json_completion.choices[0].delta.content + if type(l:content) != type(v:null) + let l:result .= l:content + endif + catch + let l:result .= "\n" + endtry + endif + endfor + + let l:width = winwidth(0)-2 + let l:separator = " " + let l:separator .= repeat('━', l:width) + call append(line('$'), l:separator) + call append(line('$'), split(l:result, "\n")) + call UserInputSeparator() + normal! G endfunction function! HandleCurlOutput(channel, msg)