diff --git a/README.md b/README.md index 518e757..3a7532c 100644 --- a/README.md +++ b/README.md @@ -217,6 +217,7 @@ Additional useful commands are implemented through hooks (see below). | `PrtProvider ` | Switch the provider (empty arg triggers fzf) | | `PrtModel ` | Switch the model (empty arg triggers fzf) | | `PrtStatus` | Prints current provider and model selection | +| `PrtThinking` | Toggle or configure thinking mode for supported providers | | __Interactive__ | | | `PrtRewrite` | Rewrites the visual selection based on a provided prompt | | `PrtEdit` | Like `PrtRewrite` but you can change the last prompt | @@ -549,6 +550,18 @@ sources = cmp.config.sources({ ... ``` +## Thinking Mode + +The `PrtThinking` command allows you to toggle or configure the "thinking" feature for providers that support it: + +- `PrtThinking` - Toggle thinking on/off (default budget: 1024 tokens) +- `PrtThinking 2048` - Enable thinking with a specific budget of 2048 tokens +- `PrtThinking status` - Show current thinking settings + +The thinking feature allows models to show their intermediate reasoning steps in a popup window +before providing a final response. Currently, this is fully implemented for the Anthropic provider, +but the framework supports adding this capability to other providers that can stream intermediate thinking steps. + ## Statusline Support Knowing the current chat or command model can be shown using your favorite statusline plugin. @@ -640,4 +653,4 @@ ls -l | command nvim - -c "normal ggVGy" -c ":PrtChatNew" -c "normal p" ## Star History -[![Star History Chart](https://api.star-history.com/svg?repos=frankroeder/parrot.nvim&type=Date)](https://star-history.com/#frankroeder/parrot.nvim&Date) +[![Star History Chart](https://api.star-history.com/svg?repos=frankroeder/parrot.nvim&type=Date)](https://star-history.com/#frankroeder/parrot.nvim&Date) \ No newline at end of file diff --git a/lua/parrot/chat_handler.lua b/lua/parrot/chat_handler.lua index 101aae0..101cfa7 100644 --- a/lua/parrot/chat_handler.lua +++ b/lua/parrot/chat_handler.lua @@ -69,6 +69,17 @@ function ChatHandler:set_provider(selected_prov, is_chat) local _prov = init_provider(selected_prov, endpoint, api_key, style, models) self.current_provider[is_chat and "chat" or "command"] = _prov self.state:set_provider(_prov.name, is_chat) + + -- Apply saved thinking config from state (if any) + local mode = is_chat and "chat" or "command" + local thinking_config = self.state:get_thinking(selected_prov, mode) + if thinking_config then + if not self.providers[selected_prov].params[mode].thinking then + self.providers[selected_prov].params[mode].thinking = {} + end + self.providers[selected_prov].params[mode].thinking = thinking_config + end + self.state:refresh(self.available_providers, self.available_models) self:prepare_commands() end @@ -83,6 +94,16 @@ function ChatHandler:get_provider(is_chat) end self:set_provider(prov, is_chat) current_prov = self.current_provider[is_chat and "chat" or "command"] + + -- Apply saved thinking config from state (if any) + local mode = is_chat and "chat" or "command" + local thinking_config = self.state:get_thinking(prov, mode) + if thinking_config then + if not self.providers[prov].params[mode].thinking then + self.providers[prov].params[mode].thinking = {} + end + self.providers[prov].params[mode].thinking = thinking_config + end end return current_prov end @@ -1571,6 +1592,24 @@ end ---@param payload table Payload for the API. ---@param handler function Response handler function. ---@param on_exit function | nil Optional on_exit handler. +--- Toggle or configure thinking functionality for providers that support it +---@param params table Parameters for thinking configuration +function ChatHandler:thinking(params) + local buf = vim.api.nvim_get_current_buf() + local file_name = vim.api.nvim_buf_get_name(buf) + local is_chat = utils.is_chat(buf, file_name, self.options.chat_dir) + local provider = self:get_provider(is_chat) + + -- Delegate to provider-specific thinking configuration if available + if provider.configure_thinking then + provider:configure_thinking(params, is_chat, self.providers, self.state) + return + end + + -- If the provider doesn't have a configure_thinking method, warn and return + logger.warning("Thinking is not supported by the " .. provider.name .. " provider") +end + function ChatHandler:query(buf, provider, payload, handler, on_exit) -- make sure handler is a function if type(handler) ~= "function" then diff --git a/lua/parrot/config.lua b/lua/parrot/config.lua index 15c3584..0ba748c 100644 --- a/lua/parrot/config.lua +++ b/lua/parrot/config.lua @@ -422,6 +422,7 @@ function M.setup(opts) Provider = "provider", Retry = "retry", Edit = "edit", + Thinking = "thinking", } M.chat_handler = ChatHandler:new(M.options, M.providers, M.available_providers, M.available_models, M.cmd) diff --git a/lua/parrot/provider/anthropic.lua b/lua/parrot/provider/anthropic.lua index 233b276..9586f71 100644 --- a/lua/parrot/provider/anthropic.lua +++ b/lua/parrot/provider/anthropic.lua @@ -210,4 +210,96 @@ function Anthropic:get_available_models(online) return ids end +--- Toggle or configure thinking functionality for Claude models +---@param params table Parameters for thinking configuration +---@param is_chat boolean Whether this is for chat or command context +---@param providers table Provider configuration table +---@param state table|nil Optional state object for persistence +---@return nil +function Anthropic:configure_thinking(params, is_chat, providers, state) + local logger = require("parrot.logger") + local args = params.args or "" + local mode = is_chat and "chat" or "command" + + -- Check current thinking settings with "status" command + if args == "status" then + local current = providers[self.name].params[mode].thinking + if current then + logger.info(string.format("Thinking is enabled with budget of %d tokens for %s", + current.budget_tokens or 0, mode)) + else + logger.info("Thinking is disabled for " .. mode) + end + return + elseif args ~= "" then + -- Parse budget_tokens from args + local budget_tokens = tonumber(args) + if budget_tokens and budget_tokens > 0 then + -- Set thinking parameters in the provider config + if not providers[self.name].params[mode].thinking then + providers[self.name].params[mode].thinking = {} + end + + local thinking_config = { + type = "enabled", + budget_tokens = budget_tokens + } + + providers[self.name].params[mode].thinking = thinking_config + + -- Save to state if provided + if state then + state:set_thinking(self.name, mode, thinking_config) + end + + logger.info(string.format("Set thinking budget to %d tokens for %s", + budget_tokens, mode)) + else + logger.warning("Invalid thinking budget. Please provide a positive number.") + end + else + -- Toggle thinking on/off + local current = providers[self.name].params[mode].thinking + + if current then + -- Thinking is enabled, disable it + -- Store the current config before disabling for future restoration + if state then + -- Save the current state temporarily (it's already in state storage) + providers[self.name].params[mode]._stored_thinking = vim.deepcopy(current) + state:set_thinking(self.name, mode, nil) + end + + providers[self.name].params[mode].thinking = nil + logger.info("Disabled thinking for " .. mode) + else + -- Thinking is disabled, enable it with previous budget if available + local stored_config = providers[self.name].params[mode]._stored_thinking + local thinking_config + + if stored_config then + -- Restore previous configuration + thinking_config = vim.deepcopy(stored_config) + providers[self.name].params[mode]._stored_thinking = nil + else + -- Use default if no previous configuration exists + thinking_config = { + type = "enabled", + budget_tokens = 1024 + } + end + + providers[self.name].params[mode].thinking = thinking_config + + -- Save to state if provided + if state then + state:set_thinking(self.name, mode, thinking_config) + end + + logger.info(string.format("Enabled thinking with budget of %d tokens for %s", + thinking_config.budget_tokens, mode)) + end + end +end + return Anthropic diff --git a/lua/parrot/state.lua b/lua/parrot/state.lua index 097abf8..78b7609 100644 --- a/lua/parrot/state.lua +++ b/lua/parrot/state.lua @@ -18,7 +18,11 @@ end function State:init_file_state(available_providers) if next(self.file_state) == nil then for _, prov in ipairs(available_providers) do - self.file_state[prov] = { chat_model = nil, command_model = nil } + self.file_state[prov] = { + chat_model = nil, + command_model = nil, + thinking = nil + } end end self.file_state.current_provider = self.file_state.current_provider or { chat = nil, command = nil } @@ -30,9 +34,14 @@ end function State:init_state(available_providers, available_models) self._state.current_provider = self._state.current_provider or { chat = nil, command = nil } for _, provider in ipairs(available_providers) do - self._state[provider] = self._state[provider] or { chat_model = nil, command_model = nil } + self._state[provider] = self._state[provider] or { + chat_model = nil, + command_model = nil, + thinking = nil + } self:load_models(provider, "chat_model", available_models) self:load_models(provider, "command_model", available_models) + self:load_thinking(provider) end end @@ -53,6 +62,14 @@ function State:load_models(provider, model_type, available_models) end end +--- Loads thinking configuration for the specified provider. +--- @param provider string # Name of the provider. +function State:load_thinking(provider) + if self.file_state and self.file_state[provider] and self.file_state[provider].thinking then + self._state[provider].thinking = vim.deepcopy(self.file_state[provider].thinking) + end +end + --- Refreshes the state with available providers and their models. --- @param available_providers table --- @param available_models table @@ -135,4 +152,35 @@ function State:get_last_chat() return self._state.last_chat end +--- Sets thinking configuration for a specific provider and mode. +--- @param provider string # Provider name. +--- @param mode string # Mode type ('chat' or 'command'). +--- @param thinking_config table|nil # Thinking configuration or nil to disable +function State:set_thinking(provider, mode, thinking_config) + if not self._state[provider] then + return + end + + if not self._state[provider].thinking then + self._state[provider].thinking = {} + end + + self._state[provider].thinking[mode] = thinking_config + self:save() +end + +--- Gets thinking configuration for a specific provider and mode. +--- @param provider string # Provider name. +--- @param mode string # Mode type ('chat' or 'command'). +--- @return table|nil # Thinking configuration or nil if not set +function State:get_thinking(provider, mode) + if not self._state[provider] or + not self._state[provider].thinking or + not self._state[provider].thinking[mode] then + return nil + end + + return self._state[provider].thinking[mode] +end + return State