;;; ellama-transient.el --- Transient menus for ellama -*- lexical-binding: t; package-lint-main-file: "ellama.el"; -*-

;; Copyright (C) 2023-2026  Free Software Foundation, Inc.

;; Author: Sergey Kostyaev <sskostyaev@gmail.com>
;; SPDX-License-Identifier: GPL-3.0-or-later

;; This file is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.

;; This file is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:
;;
;; Ellama is a tool for interacting with large language models from Emacs.
;; It allows you to ask questions and receive responses from the
;; LLMs.  Ellama can perform various tasks such as translation, code
;; review, summarization, enhancing grammar/spelling or wording and
;; more through the Emacs interface.  Ellama natively supports streaming
;; output, making it effortless to use with your preferred text editor.
;;

;;; Code:
(require 'ellama)
(require 'transient)
(require 'ellama-context)
(eval-when-compile
  (require 'llm-ollama)
  (require 'llm-openai))

(defcustom ellama-transient-system-show-limit 45
  "Maximum length of system message to show."
  :type 'integer
  :group 'ellama)

(defvaralias 'ellama-transient-ollama-model-name
  'ellama-transient-model-name)
(defvar ellama-transient-model-name "")
(defvar ellama-transient-temperature 0.7)
(defvar ellama-transient-context-length 4096)
(defvar ellama-transient-host "localhost")
(defvar ellama-transient-port 11434)
(defvar ellama-transient-url nil)
(defvar ellama-transient-provider nil)
(defvar ellama-transient-provider-type-selected-p nil)
(defvar ellama--current-session-uid)
(defvar ellama-max-tokens)
(defvar ellama-audio-recording-duration)

(declare-function ellama-ask-image "ellama"
                  (image prompt &optional create-session &rest args))
(declare-function ellama-ask-audio "ellama"
                  (audio prompt &optional create-session &rest args))
(declare-function ellama-ask-audio-recording "ellama"
                  (duration prompt &optional create-session &rest args))
(declare-function ellama-chat-with-image "ellama"
                  (image prompt &optional create-session &rest args))
(declare-function ellama-chat-with-audio "ellama"
                  (audio prompt &optional create-session &rest args))
(declare-function ellama-chat-with-audio-recording "ellama"
                  (duration prompt &optional create-session &rest args))
(declare-function ellama--provider-slot-offset "ellama" (provider slot))
(declare-function ellama--provider-slot-value "ellama" (provider slot))
(declare-function ellama--provider-with-slot-value "ellama"
                  (provider slot value))

(defun ellama-transient-system-show ()
  "Show transient system message."
  (format "System message (%s)"
          (string-limit (car (string-lines ellama-global-system))
                        ellama-transient-system-show-limit)))

(transient-define-suffix ellama-transient-set-system ()
  "Set system message.
If a region is active, use the text within the region as the system message.
Otherwise, prompt the user to enter a system message."
  (interactive)
  (if (region-active-p)
      (setq ellama-global-system (buffer-substring-no-properties
                                  (region-beginning) (region-end)))
    (let* ((msg-string (read-string "Set system mesage: "))
           (msg (when (not (string-empty-p msg-string)) msg-string)))
      (setq ellama-global-system msg))))

(defun ellama-transient-set-system-from-buffer ()
  "Set system message from current buffer."
  (interactive)
  (setq ellama-global-system (buffer-substring-no-properties
                              (point-min) (point-max))))

(transient-define-suffix ellama-transient-set-model ()
  "Set model name."
  (interactive)
  (setq ellama-transient-model-name
        (ellama-transient-read-model-name ellama-transient-provider)))

(defalias 'ellama-transient-set-ollama-model
  'ellama-transient-set-model)

(transient-define-suffix ellama-transient-set-temperature ()
  "Set temperature value."
  (interactive)
  (setq ellama-transient-temperature (read-number "Enter temperature: ")))

(transient-define-suffix ellama-transient-set-context-length ()
  "Set context length."
  (interactive)
  (setq ellama-transient-context-length (read-number "Enter context length: ")))

(transient-define-suffix ellama-transient-set-max-tokens ()
  "Set maximum output tokens."
  (interactive)
  (let* ((input (read-string
                 "Enter max tokens (empty for default): "
                 (when ellama-max-tokens
                   (number-to-string ellama-max-tokens))))
         (value (unless (string-empty-p input)
                  (unless (string-match-p "\\`[[:digit:]]+\\'" input)
                    (error "Max tokens must be a positive integer"))
                  (let ((number (string-to-number input)))
                    (unless (> number 0)
                      (error "Max tokens must be a positive integer"))
                    number))))
    (setq ellama-max-tokens value)))

(transient-define-suffix ellama-transient-set-host ()
  "Set host address."
  (interactive)
  (setq ellama-transient-host (read-string "Enter host: ")))

(transient-define-suffix ellama-transient-set-port ()
  "Set port number."
  (interactive)
  (setq ellama-transient-port (read-number "Enter port: ")))

(transient-define-suffix ellama-transient-set-url ()
  "Set API URL."
  (interactive)
  (setq ellama-transient-url (read-string "Enter URL: " ellama-transient-url)))

(transient-define-suffix ellama-transient-reset-model-fields ()
  "Reset model fields to provider defaults."
  (interactive)
  (setq ellama-transient-model-name nil
        ellama-transient-temperature nil
        ellama-transient-context-length nil
        ellama-max-tokens nil))

(defvar ellama-provider-list '(ellama-provider
                               ellama-coding-provider
                               ellama-translation-provider
                               ellama-extraction-provider
                               ellama-summarization-provider
                               ellama-naming-provider)
  "List of providers.")

(defcustom ellama-transient-provider-types
  '((llm-ollama "Ollama" llm-ollama make-llm-ollama)
    (llm-openai-compatible "OpenAI-compatible"
                           llm-openai make-llm-openai-compatible)
    (llm-openai "OpenAI" llm-openai make-llm-openai)
    (llm-claude "Claude" llm-claude make-llm-claude)
    (llm-gemini "Gemini" llm-gemini make-llm-gemini)
    (llm-deepseek "DeepSeek" llm-deepseek make-llm-deepseek)
    (llm-openrouter "OpenRouter" llm-openai make-llm-openrouter)
    (llm-azure "Azure OpenAI" llm-azure make-llm-azure)
    (llm-github "GitHub Models" llm-github make-llm-github)
    (llm-vertex "Vertex AI" llm-vertex make-llm-vertex)
    (llm-gpt4all "GPT4All" llm-gpt4all make-llm-gpt4all)
    (llm-llamacpp "Llama.cpp" llm-llamacpp make-llm-llamacpp))
  "Provider types available in the model transient.
Each entry has the form (TYPE LABEL LIBRARY CONSTRUCTOR)."
  :type '(repeat
          (list (symbol :tag "Provider type")
                (string :tag "Label")
                (symbol :tag "Library")
                (symbol :tag "Constructor")))
  :group 'ellama)

(defun ellama-transient--provider-symbol (provider)
  "Return provider variable symbol from PROVIDER."
  (cond
   ((symbolp provider) provider)
   ((and (consp provider) (symbolp (cdr provider))) (cdr provider))
   ((and (consp provider) (symbolp (car provider))) (car provider))))

(defun ellama-transient--provider-label (provider)
  "Return safe completion label for PROVIDER variable symbol."
  (let* ((name (symbol-name provider))
         (role (cond
                ((string= name "ellama-provider") "default")
                ((and (string-prefix-p "ellama-" name)
                      (string-suffix-p "-provider" name))
                 (substring name 7 (- (length name) 9)))
                (t "custom"))))
    (format "%s [%s]" role name)))

(defun ellama-transient--provider-candidates ()
  "Return safe provider completion candidates.
Provider values are intentionally not included in the display
strings, because they may contain API keys."
  (mapcan (lambda (provider)
            (when-let ((symbol (ellama-transient--provider-symbol provider)))
              (list (cons (ellama-transient--provider-label symbol) symbol))))
          ellama-provider-list))

(defun ellama-transient--read-provider-symbol ()
  "Read and return an Ellama provider variable symbol."
  (let* ((candidates (ellama-transient--provider-candidates))
         (choice (completing-read "Select provider: " candidates nil t)))
    (or (cdr (assoc choice candidates))
        (error "Unknown provider: %s" choice))))

(defun ellama-transient--provider-type-entry (type)
  "Return provider type registry entry for TYPE."
  (assq type ellama-transient-provider-types))

(defun ellama-transient--provider-type-available-p (entry)
  "Return non-nil when provider type ENTRY is available."
  (let ((library (nth 2 entry)))
    (or (featurep library)
        (locate-library (symbol-name library)))))

(defun ellama-transient--provider-type-candidates ()
  "Return available provider type completion candidates."
  (mapcan
   (lambda (entry)
     (when (ellama-transient--provider-type-available-p entry)
       (list (cons (nth 1 entry) (car entry)))))
   ellama-transient-provider-types))

(defun ellama-transient--read-provider-type ()
  "Read and return an available provider type."
  (let* ((candidates (ellama-transient--provider-type-candidates))
         (choice (completing-read "Select provider type: "
                                  candidates nil t)))
    (or (cdr (assoc choice candidates))
        (error "Unknown provider type: %s" choice))))

(defun ellama-transient--provider-type-name (&optional provider)
  "Return display name for PROVIDER type."
  (when-let* ((provider (or provider ellama-transient-provider))
              (type (type-of provider)))
    (if-let ((entry (ellama-transient--provider-type-entry type)))
        (nth 1 entry)
      (symbol-name type))))

(defun ellama-transient-provider-type-description ()
  "Return transient provider type description."
  (format "Provider Type (%s)"
          (or (ellama-transient--provider-type-name) "none")))

(defun ellama-transient--configured-providers ()
  "Return configured provider objects."
  (append
   (when ellama-transient-provider
     (list ellama-transient-provider))
   (mapcan
    (lambda (provider)
      (when-let ((symbol (ellama-transient--provider-symbol provider)))
        (when (and (boundp symbol) (symbol-value symbol))
          (list (symbol-value symbol)))))
    ellama-provider-list)
   (mapcar #'cdr ellama-providers)))

(defun ellama-transient--make-provider-of-type (type)
  "Return a configured or new provider of TYPE."
  (or (seq-find (lambda (provider)
                  (eq (type-of provider) type))
                (ellama-transient--configured-providers))
      (let* ((entry (or (ellama-transient--provider-type-entry type)
                        (error "Unknown provider type: %s" type)))
             (library (nth 2 entry))
             (constructor (nth 3 entry)))
        (condition-case err
            (progn
              (require library)
              (unless (fboundp constructor)
                (error "Provider constructor is unavailable: %s" constructor))
              (funcall constructor))
          (error
           (error "Could not create %s provider: %s"
                  (nth 1 entry) (error-message-string err)))))))

(transient-define-suffix ellama-transient-set-provider-type ()
  "Select provider type for the transient model."
  (interactive)
  (let ((provider
         (ellama-transient--make-provider-of-type
          (ellama-transient--read-provider-type))))
    (ellama-fill-transient-model provider)
    (setq ellama-transient-provider-type-selected-p t)))

(transient-define-suffix ellama-transient-model-get-from-provider ()
  "Fill transient model from provider."
  (interactive)
  (ellama-fill-transient-model
   (symbol-value (ellama-transient--read-provider-symbol))))

(transient-define-suffix ellama-transient-model-get-from-current-session ()
  "Fill transient model from current session."
  (interactive)
  (when-let ((session (ellama-get-current-session)))
    (ellama-fill-transient-model
     (ellama-session-provider session))))

(transient-define-suffix ellama-transient-set-provider ()
  "Set transient model to provider."
  (interactive)
  (let* ((provider (ellama-transient--read-provider-symbol))
         (base-provider
          (if ellama-transient-provider-type-selected-p
              ellama-transient-provider
            (symbol-value provider)))
         (new-provider
          (let ((ellama-transient-provider base-provider))
            (ellama-construct-provider-from-transient))))
    (set provider new-provider)
    (setq ellama-transient-provider new-provider)
    ;; if you change `ellama-provider' you probably want new chat session
    (when (equal provider 'ellama-provider)
      (setq ellama--current-session-id nil
            ellama--current-session-uid nil))))

;;;###autoload (autoload 'ellama-select-model "ellama-transient" nil t)
(transient-define-prefix ellama-select-model ()
  "Select model."
  [["Model"
    ("f" "Load from provider" ellama-transient-model-get-from-provider
     :transient t)
    ("F" "Load from current session"
     ellama-transient-model-get-from-current-session
     :description (lambda ()
                    (format "Load from current session (%s)"
                            ellama--current-session-id))
     :transient t)
    ("m" "Set Model" ellama-transient-set-model
     :transient t
     :description ellama-transient-model-description)
    ("t" "Set Temperature" ellama-transient-set-temperature
     :transient t
     :description ellama-transient-temperature-description)
    ("c" "Set Context Length" ellama-transient-set-context-length
     :if (lambda () (ellama-transient--ollama-provider-p
                     ellama-transient-provider))
     :transient t
     :description ellama-transient-context-length-description)
    ("x" "Set Max Tokens" ellama-transient-set-max-tokens
     :transient t
     :description ellama-transient-max-tokens-description)
    ("r" "Reset model fields" ellama-transient-reset-model-fields
     :transient t)
    ("P" "Set Provider Type" ellama-transient-set-provider-type
     :transient t
     :description ellama-transient-provider-type-description)
    ("S" "Set provider" ellama-transient-set-provider
     :transient t)
    ("s" "Set provider and quit" ellama-transient-set-provider)]
   ["Connection"
    ("h" "Set Host" ellama-transient-set-host
     :if (lambda () (ellama-transient--ollama-provider-p
                     ellama-transient-provider))
     :transient t
     :description (lambda () (if ellama-transient-host
                                 (format "Host (%s)" ellama-transient-host)
                               "Host")))
    ("p" "Set Port" ellama-transient-set-port
     :if (lambda () (ellama-transient--ollama-provider-p
                     ellama-transient-provider))
     :transient t
     :description (lambda () (if ellama-transient-port
                                 (format "Port (%s)" ellama-transient-port)
                               "Port")))
    ("u" "Set URL" ellama-transient-set-url
     :if (lambda () (ellama-transient--provider-slot-present-p
                     ellama-transient-provider 'url))
     :transient t
     :description (lambda () (if ellama-transient-url
                                 (format "URL (%s)" ellama-transient-url)
                               "URL")))]
   ["Quit" ("q" "Quit" transient-quit-one)]])

(defalias 'ellama-select-ollama-model 'ellama-select-model)

(defun ellama-transient--default-value-p (value)
  "Return non-nil when VALUE means provider default."
  (or (null value)
      (and (stringp value) (string-empty-p value))))

(defun ellama-transient--field-description (name value &optional format)
  "Return description for NAME and VALUE.
FORMAT is used for non-default VALUE."
  (format "%s (%s)" name
          (if (ellama-transient--default-value-p value)
              "default"
            (if format
                (format format value)
              value))))

(defun ellama-transient-model-description ()
  "Return transient model description."
  (ellama-transient--field-description "Model" ellama-transient-model-name))

(defun ellama-transient-temperature-description ()
  "Return transient temperature description."
  (ellama-transient--field-description
   "Temperature" ellama-transient-temperature "%.2f"))

(defun ellama-transient-context-length-description ()
  "Return transient context length description."
  (ellama-transient--field-description
   "Context Length" ellama-transient-context-length "%d"))

(defun ellama-transient-max-tokens-description ()
  "Return transient max tokens description."
  (ellama-transient--field-description
   "Max Tokens" ellama-max-tokens "%d"))

(defun ellama-transient--ollama-provider-p (provider)
  "Return non-nil when PROVIDER is an Ollama provider."
  (declare-function llm-ollama-p "ext:llm-ollama")
  (and provider
       (fboundp 'llm-ollama-p)
       (llm-ollama-p provider)))

(defun ellama-transient--openai-compatible-provider-p (provider)
  "Return non-nil when PROVIDER is OpenAI-compatible."
  (declare-function llm-openai-compatible-p "ext:llm-openai")
  (and provider
       (fboundp 'llm-openai-compatible-p)
       (llm-openai-compatible-p provider)))

(defun ellama-transient--openai-provider-p (provider)
  "Return non-nil when PROVIDER is an OpenAI provider."
  (declare-function llm-openai-p "ext:llm-openai")
  (and provider
       (fboundp 'llm-openai-p)
       (llm-openai-p provider)))

(defun ellama-transient--provider-slot-present-p (provider slot)
  "Return non-nil when PROVIDER has SLOT."
  (and provider
       (ellama--provider-slot-offset provider slot)))

(defun ellama-transient--alist (value)
  "Return VALUE as an alist."
  (cond
   ((vectorp value) (append value nil))
   ((listp value) value)))

(defun ellama-transient--standard-temperature (provider)
  "Return default chat temperature for PROVIDER."
  (declare-function llm-standard-chat-provider-p "ext:llm-provider-utils")
  (declare-function llm-standard-chat-provider-default-chat-temperature
                    "ext:llm-provider-utils")
  (when (and provider
             (fboundp 'llm-standard-chat-provider-p)
             (llm-standard-chat-provider-p provider))
    (llm-standard-chat-provider-default-chat-temperature provider)))

(defun ellama-transient--set-standard-temperature (provider)
  "Set PROVIDER default chat temperature from transient."
  (declare-function llm-standard-chat-provider-p "ext:llm-provider-utils")
  (declare-function llm-standard-chat-provider-default-chat-temperature
                    "ext:llm-provider-utils")
  (declare-function (setf llm-standard-chat-provider-default-chat-temperature)
                    "ext:llm-provider-utils")
  (when (and provider
             (fboundp 'llm-standard-chat-provider-p)
             (llm-standard-chat-provider-p provider))
    (setf (llm-standard-chat-provider-default-chat-temperature provider)
          ellama-transient-temperature)))

(defun ellama-transient--provider-context-length (provider)
  "Return default context length from PROVIDER."
  (declare-function llm-ollama-default-chat-non-standard-params
                    "ext:llm-ollama")
  (when (ellama-transient--ollama-provider-p provider)
    (when-let ((params (llm-ollama-default-chat-non-standard-params provider)))
      (alist-get "num_ctx" (ellama-transient--alist params)
                 nil nil #'string=))))

(defun ellama-transient--provider-model (provider)
  "Return chat model from PROVIDER."
  (declare-function llm-ollama-chat-model "ext:llm-ollama")
  (declare-function llm-openai-chat-model "ext:llm-openai")
  (declare-function llm-openai-compatible-chat-model "ext:llm-openai")
  (cond
   ((ellama-transient--ollama-provider-p provider)
    (llm-ollama-chat-model provider))
   ((ellama-transient--openai-compatible-provider-p provider)
    (llm-openai-compatible-chat-model provider))
   ((ellama-transient--openai-provider-p provider)
    (llm-openai-chat-model provider))
   ((ellama-transient--provider-slot-present-p provider 'chat-model)
    (ellama--provider-slot-value provider 'chat-model))))

(defun ellama-transient--provider-url (provider)
  "Return API URL from PROVIDER."
  (declare-function llm-openai-compatible-url "ext:llm-openai")
  (cond
   ((ellama-transient--openai-compatible-provider-p provider)
    (llm-openai-compatible-url provider))
   ((ellama-transient--provider-slot-present-p provider 'url)
    (ellama--provider-slot-value provider 'url))))

(defun ellama-transient--provider-models (provider)
  "Return available chat models for PROVIDER."
  (when (and provider (member 'model-list (llm-capabilities provider)))
    (condition-case err
        (llm-models provider)
      (error
       (message "Ellama could not fetch model list: %s" err)
       nil))))

(defun ellama-transient-read-model-name (&optional provider)
  "Read model name for PROVIDER."
  (let* ((provider (or provider ellama-provider))
         (models (ellama-transient--provider-models provider))
         (default (if (ellama-transient--default-value-p
                       ellama-transient-model-name)
                      (ellama-transient--provider-model provider)
                    ellama-transient-model-name)))
    (if models
        (completing-read "Select model: " models nil nil nil nil default)
      (read-string "Enter model: " default))))

(defun ellama-transient--fill-ollama (provider)
  "Set transient Ollama fields from PROVIDER."
  (declare-function llm-ollama-host "ext:llm-ollama")
  (declare-function llm-ollama-port "ext:llm-ollama")
  (declare-function llm-ollama-chat-model "ext:llm-ollama")
  (declare-function llm-ollama-default-chat-temperature "ext:llm-ollama")
  (declare-function llm-ollama-default-chat-non-standard-params
                    "ext:llm-ollama")
  (when (ellama-transient--ollama-provider-p provider)
    (setq ellama-transient-model-name (llm-ollama-chat-model provider))
    (setq ellama-transient-temperature
          (llm-ollama-default-chat-temperature provider))
    (setq ellama-transient-host (llm-ollama-host provider))
    (setq ellama-transient-port (llm-ollama-port provider))
    (setq ellama-transient-context-length
          (ellama-transient--provider-context-length provider))))

(defun ellama-fill-transient-model (provider)
  "Set transient model fields from PROVIDER."
  (setq ellama-transient-provider provider
        ellama-transient-provider-type-selected-p nil)
  (when-let ((model (ellama-transient--provider-model provider)))
    (setq ellama-transient-model-name model))
  (setq ellama-transient-temperature
        (ellama-transient--standard-temperature provider))
  (setq ellama-transient-url (ellama-transient--provider-url provider))
  (ellama-transient--fill-ollama provider))

(defalias 'ellama-fill-transient-ollama-model
  'ellama-fill-transient-model)

(defun ellama-transient--replace-param (params key value)
  "Return PARAMS with KEY set to VALUE."
  (let ((params (copy-tree (ellama-transient--alist params))))
    (if-let ((cell (assoc key params)))
        (setcdr cell value)
      (push (cons key value) params))
    params))

(defun ellama-transient--remove-param (params key)
  "Return PARAMS with KEY removed."
  (cl-remove key (copy-tree (ellama-transient--alist params))
             :key #'car :test #'equal))

(defun ellama-transient--provider-default-model (provider)
  "Return default chat model for PROVIDER type."
  (when-let ((constructor (intern-soft (format "make-%s" (type-of provider)))))
    (when (fboundp constructor)
      (condition-case nil
          (ellama-transient--provider-model (funcall constructor))
        (error nil)))))

(defun ellama-transient--effective-model (provider)
  "Return transient model or PROVIDER default model."
  (if (ellama-transient--default-value-p ellama-transient-model-name)
      (ellama-transient--provider-default-model provider)
    ellama-transient-model-name))

(defun ellama-transient--set-ollama-fields (provider)
  "Set Ollama-specific PROVIDER fields from transient."
  (declare-function llm-ollama-chat-model "ext:llm-ollama")
  (declare-function llm-ollama-host "ext:llm-ollama")
  (declare-function llm-ollama-port "ext:llm-ollama")
  (declare-function llm-ollama-default-chat-non-standard-params
                    "ext:llm-ollama")
  (declare-function (setf llm-ollama-chat-model) "ext:llm-ollama")
  (declare-function (setf llm-ollama-host) "ext:llm-ollama")
  (declare-function (setf llm-ollama-port) "ext:llm-ollama")
  (declare-function (setf llm-ollama-default-chat-non-standard-params)
                    "ext:llm-ollama")
  (setf (llm-ollama-chat-model provider)
        (ellama-transient--effective-model provider)
        (llm-ollama-host provider) ellama-transient-host
        (llm-ollama-port provider) ellama-transient-port)
  (setf (llm-ollama-default-chat-non-standard-params provider)
        (if ellama-transient-context-length
            (ellama-transient--replace-param
             (llm-ollama-default-chat-non-standard-params provider)
             "num_ctx" ellama-transient-context-length)
          (ellama-transient--remove-param
           (llm-ollama-default-chat-non-standard-params provider)
           "num_ctx")))
  provider)

(defun ellama-transient--set-openai-fields (provider)
  "Set OpenAI-compatible PROVIDER fields from transient."
  (declare-function llm-openai-chat-model "ext:llm-openai")
  (declare-function llm-openai-compatible-chat-model "ext:llm-openai")
  (declare-function llm-openai-compatible-url "ext:llm-openai")
  (declare-function (setf llm-openai-chat-model) "ext:llm-openai")
  (declare-function (setf llm-openai-compatible-chat-model) "ext:llm-openai")
  (declare-function (setf llm-openai-compatible-url) "ext:llm-openai")
  (cond
   ((ellama-transient--openai-compatible-provider-p provider)
    (setf (llm-openai-compatible-chat-model provider)
          (ellama-transient--effective-model provider))
    (when ellama-transient-url
      (setf (llm-openai-compatible-url provider) ellama-transient-url)))
   ((ellama-transient--openai-provider-p provider)
    (setf (llm-openai-chat-model provider)
          (ellama-transient--effective-model provider))))
  provider)

(defun ellama-transient--set-generic-provider-fields (provider)
  "Set generic PROVIDER fields from transient."
  (when (ellama-transient--provider-slot-present-p provider 'chat-model)
    (setq provider
          (ellama--provider-with-slot-value
           provider 'chat-model
           (ellama-transient--effective-model provider))))
  (when (and ellama-transient-url
             (ellama-transient--provider-slot-present-p provider 'url))
    (setq provider
          (ellama--provider-with-slot-value
           provider 'url ellama-transient-url)))
  provider)

(defun ellama-construct-provider-from-transient (&optional base-provider)
  "Make provider from transient menu using BASE-PROVIDER."
  (declare-function make-llm-ollama "ext:llm-ollama")
  (declare-function copy-llm-ollama "ext:llm-ollama")
  (declare-function copy-llm-openai "ext:llm-openai")
  (declare-function copy-llm-openai-compatible "ext:llm-openai")
  (let* ((base-provider (or ellama-transient-provider base-provider))
         (provider
          (cond
           ((ellama-transient--ollama-provider-p base-provider)
            (copy-llm-ollama base-provider))
           ((ellama-transient--openai-compatible-provider-p base-provider)
            (copy-llm-openai-compatible base-provider))
           ((ellama-transient--openai-provider-p base-provider)
            (copy-llm-openai base-provider))
           ((not base-provider)
            (require 'llm-ollama)
            (make-llm-ollama))
           ((or (recordp base-provider)
                (vectorp base-provider))
            (copy-sequence base-provider))
           (t
            (error "Provider type does not support transient model changes")))))
    (ellama-transient--set-standard-temperature provider)
    (cond
     ((ellama-transient--ollama-provider-p provider)
      (ellama-transient--set-ollama-fields provider))
     ((or (ellama-transient--openai-compatible-provider-p provider)
          (ellama-transient--openai-provider-p provider))
      (ellama-transient--set-openai-fields provider))
     (t
      (setq provider
            (ellama-transient--set-generic-provider-fields provider))))
    provider))

(defun ellama-construct-ollama-provider-from-transient ()
  "Make Ollama provider from transient menu."
  (let ((ellama-transient-provider nil))
    (ellama-construct-provider-from-transient)))

(transient-define-suffix ellama-transient-code-review (&optional args)
  "Review the code.  ARGS used for transient arguments."
  (interactive (list (transient-args transient-current-command)))
  (ellama-code-review
   (transient-arg-value "--new-session" args)
   :ephemeral (transient-arg-value "--ephemeral" args)))

(defun ellama--context-summary ()
  "Return a summary of context elements as a string."
  (let ((context (append
                  ellama-context-global ellama-context-ephemeral))
        (total-chars 0)
        (summary))

    (cl-loop for element in context do
             (let* ((content
                     (ellama-context-element-extract element))
                    (chars (length content))
                    (name (ellama-context-element-display element)))
               (cl-incf total-chars chars)
               (push
                (if (ellama-context-element-quote-p element)
                    (format "%s (%d chars region)" name chars)
                  (format "%s" name))
                summary)))
    (concat "Context: " (string-join summary ", ") (format " (total %d chars)" total-chars))))


(defun ellama--transient-context ()
  "Summarise session and context for transient menus."
  (format "%s %s %s %s"
          (propertize "Session:" 'face 'ellama-key-face)
          (ellama-get-current-session-id)
          (propertize "Context: " 'face 'ellama-key-face)
          (ellama--context-summary)))

;;;###autoload (autoload 'ellama-transient-code-menu "ellama-transient" nil t)
(transient-define-prefix ellama-transient-code-menu ()
  "Code Commands."
  ["Session Options"
   :description (lambda () (ellama--transient-context))
   ("-n" "Create New Session" "--new-session")]
  ["Ephemeral sessions"
   :if (lambda () ellama-session-auto-save)
   ("-e" "Create Ephemeral Session" "--ephemeral")]
  [["Code Commands"
    ("c" "Complete" ellama-code-complete)
    ("a" "Add" ellama-code-add)
    ("e" "Edit" ellama-code-edit)
    ("i" "Improve" ellama-code-improve)
    ("r" "Review" ellama-transient-code-review)
    ("m" "Generate Commit Message" ellama-generate-commit-message)]
   ["Quit" ("q" "Quit" transient-quit-one)]])

;;;###autoload (autoload 'ellama-transient-summarize-menu "ellama-transient" nil t)
(transient-define-prefix ellama-transient-summarize-menu ()
  "Summarize Commands."
  [["Summarize Commands"
    ("s" "Summarize" ellama-summarize)
    ("w" "Summarize Webpage" ellama-summarize-webpage)
    ("k" "Summarize Killring" ellama-summarize-killring)]
   ["Quit" ("q" "Quit" transient-quit-one)]])

;;;###autoload (autoload 'ellama-transient-session-menu "ellama-transient" nil t)
(transient-define-prefix ellama-transient-session-menu ()
  "Session Commands."
  [["Session Commands"
    ("l" "Load Session" ellama-load-session)
    ("r" "Rename Session" ellama-session-rename)
    ("d" "Delete Session" ellama-session-delete)
    ("a" "Activate Session" ellama-session-switch)
    ("k" "Kill Session" ellama-session-kill)
    ("c" "Compact Current Session" ellama-session-compact-current)
    ("C" "Compact Session" ellama-session-compact)]
   ["Quit" ("q" "Quit" transient-quit-one)]])

;;;###autoload (autoload 'ellama-transient-improve-menu "ellama-transient" nil t)
(transient-define-prefix ellama-transient-improve-menu ()
  "Improve Commands."
  [["Improve Commands"
    ("w" "Improve Wording" ellama-improve-wording)
    ("g" "Improve Grammar" ellama-improve-grammar)
    ("c" "Improve Conciseness" ellama-improve-conciseness)]
   ["Quit" ("q" "Quit" transient-quit-one)]])

;;;###autoload (autoload 'ellama-transient-make-menu "ellama-transient" nil t)
(transient-define-prefix ellama-transient-make-menu ()
  "Make Commands."
  [["Make Commands"
    ("l" "Make List" ellama-make-list)
    ("t" "Make Table" ellama-make-table)
    ("f" "Make Format" ellama-make-format)]
   ["Quit" ("q" "Quit" transient-quit-one)]])

(transient-define-suffix ellama-transient-ask-line (&optional args)
  "Ask line.  ARGS used for transient arguments."
  (interactive (list (transient-args transient-current-command)))
  (ellama-ask-line
   (transient-arg-value "--new-session" args)
   :ephemeral (transient-arg-value "--ephemeral" args)))

(transient-define-suffix ellama-transient-ask-selection (&optional args)
  "Ask selection.  ARGS used for transient arguments."
  (interactive (list (transient-args transient-current-command)))
  (ellama-ask-selection
   (transient-arg-value "--new-session" args)
   :ephemeral (transient-arg-value "--ephemeral" args)))

(transient-define-suffix ellama-transient-ask-about (&optional args)
  "Ask about current buffer or region.  ARGS used for transient arguments."
  (interactive (list (transient-args transient-current-command)))
  (ellama-ask-about
   (transient-arg-value "--new-session" args)
   :ephemeral (transient-arg-value "--ephemeral" args)))

(transient-define-suffix ellama-transient-ask-image (&optional args)
  "Ask about an image.  ARGS used for transient arguments."
  (interactive (list (transient-args transient-current-command)))
  (ellama-ask-image
   (read-file-name "Image: " nil nil t)
   (read-string "Ask Ellama: ")
   (transient-arg-value "--new-session" args)
   :ephemeral (transient-arg-value "--ephemeral" args)))

(transient-define-suffix ellama-transient-ask-audio (&optional args)
  "Ask about an audio file.  ARGS used for transient arguments."
  (interactive (list (transient-args transient-current-command)))
  (ellama-ask-audio
   (read-file-name "Audio: " nil nil t)
   (read-string "Ask Ellama: ")
   (transient-arg-value "--new-session" args)
   :ephemeral (transient-arg-value "--ephemeral" args)))

(transient-define-suffix ellama-transient-ask-audio-recording (&optional args)
  "Record audio and ask about it.  ARGS used for transient arguments."
  (interactive (list (transient-args transient-current-command)))
  (ellama-ask-audio-recording
   (read-number "Record seconds: " ellama-audio-recording-duration)
   (read-string "Ask Ellama: ")
   (transient-arg-value "--new-session" args)
   :ephemeral (transient-arg-value "--ephemeral" args)))

;;;###autoload (autoload 'ellama-transient-ask-menu "ellama-transient" nil t)
(transient-define-prefix ellama-transient-ask-menu ()
  "Ask Commands."
  ["Session Options"
   :description (lambda () (ellama--transient-context))
   ("-n" "Create New Session" "--new-session")]
  ["Ephemeral sessions"
   :if (lambda () ellama-session-auto-save)
   ("-e" "Create Ephemeral Session" "--ephemeral")]
  [["Ask Commands"
    ("l" "Ask Line" ellama-transient-ask-line)
    ("s" "Ask Selection" ellama-transient-ask-selection)
    ("a" "Ask About" ellama-transient-ask-about)
    ("i" "Ask Image" ellama-transient-ask-image)
    ("A" "Ask Audio" ellama-transient-ask-audio)
    ("R" "Record Audio" ellama-transient-ask-audio-recording)]
   ["Quit" ("q" "Quit" transient-quit-one)]])

;;;###autoload (autoload 'ellama-transient-translate-menu "ellama-transient" nil t)
(transient-define-prefix ellama-transient-translate-menu ()
  "Translate Commands."
  [["Translate Commands"
    ("t" "Translate Text" ellama-translate)
    ("b" "Translate Buffer" ellama-translate-buffer)
    ("e" "Enable Translation" ellama-chat-translation-enable)
    ("d" "Disable Translation" ellama-chat-translation-disable)]
   ["Quit" ("q" "Quit" transient-quit-one)]])

(declare-function ellama-context-update-buffer "ellama-context")
(defvar ellama-context-buffer)

(transient-define-suffix ellama-transient-add-buffer (&optional args)
  "Add current buffer to context.
ARGS used for transient arguments."
  (interactive (list (transient-args transient-current-command)))
  (ellama-context-add-buffer
   (read-buffer "Buffer: ")
   (transient-arg-value "--ephemeral" args)))

(transient-define-suffix ellama-transient-add-directory (&optional args)
  "Add directory to context.
ARGS used for transient arguments."
  (interactive (list (transient-args transient-current-command)))
  (let ((directory (read-directory-name "Directory: ")))
    (ellama-context-add-directory
     directory
     (transient-arg-value "--ephemeral" args))))

(transient-define-suffix ellama-transient-add-file (&optional args)
  "Add file to context.
ARGS used for transient arguments."
  (interactive (list (transient-args transient-current-command)))
  (ellama-context-add-file (transient-arg-value "--ephemeral" args)))

(transient-define-suffix ellama-transient-add-image (&optional args)
  "Add image to context.
ARGS used for transient arguments."
  (interactive (list (transient-args transient-current-command)))
  (ellama-context-add-image
   (when (transient-arg-value "--ephemeral" args)
     'ephemeral)))

(transient-define-suffix ellama-transient-add-audio (&optional args)
  "Add audio to context.
ARGS used for transient arguments."
  (interactive (list (transient-args transient-current-command)))
  (ellama-context-add-audio
   (when (transient-arg-value "--ephemeral" args)
     'ephemeral)))

(transient-define-suffix ellama-transient-add-audio-recording (&optional args)
  "Record audio and add it to context.
ARGS used for transient arguments."
  (interactive (list (transient-args transient-current-command)))
  (ellama-context-add-audio-recording
   (when (transient-arg-value "--ephemeral" args)
     'ephemeral)))

(transient-define-suffix ellama-transient-start-audio-recording ()
  "Start microphone recording for context."
  (interactive)
  (ellama-context-start-audio-recording))

(transient-define-suffix ellama-transient-stop-audio-recording (&optional args)
  "Stop microphone recording and add it to context.
ARGS used for transient arguments."
  (interactive (list (transient-args transient-current-command)))
  (ellama-context-stop-audio-recording
   (when (transient-arg-value "--ephemeral" args)
     'ephemeral)))

(transient-define-suffix ellama-transient-add-selection (&optional args)
  "Add current selection to context.
ARGS used for transient arguments."
  (interactive (list (transient-args transient-current-command)))
  (when (region-active-p)
    (ellama-context-add-selection (transient-arg-value "--ephemeral" args))))

(transient-define-suffix ellama-transient-add-info-node (&optional args)
  "Add Info Node to context.
ARGS used for transient arguments."
  (interactive (list (transient-args transient-current-command)))
  (let ((info-node (Info-copy-current-node-name)))
    (ellama-context-add-info-node
     info-node
     (transient-arg-value "--ephemeral" args))))

;;;###autoload (autoload 'ellama-transient-context-menu "ellama-transient" nil t)
(transient-define-prefix ellama-transient-context-menu ()
  "Context Commands."
  ["Options"
   ("-e" "Use Ephemeral Context" "--ephemeral")]
  ["Context Commands"
   :description (lambda ()
                  (ellama-context-update-buffer)
                  (format "Current context:
%s" (with-current-buffer ellama-context-buffer
      (buffer-substring (point-min) (point-max)))))
   ["Add"
    ("b" "Add Buffer" ellama-transient-add-buffer)
    ("d" "Add Directory" ellama-transient-add-directory)
    ("f" "Add File" ellama-transient-add-file)
    ("I" "Add Image" ellama-transient-add-image)
    ("A" "Add Audio" ellama-transient-add-audio)
    ("R" "Record Audio" ellama-transient-add-audio-recording)
    ("s" "Add Selection" ellama-transient-add-selection)
    ("i" "Add Info Node" ellama-transient-add-info-node)]
   ["Manage"
    ("m" "Manage context" ellama-context-manage)
    ("D" "Delete element" ellama-context-element-remove-by-name)
    ("r" "Context reset" ellama-context-reset)]
   ["Recording"
    ("S" "Start Recording" ellama-transient-start-audio-recording
     :transient t)
    ("E" "Stop Recording" ellama-transient-stop-audio-recording
     :transient t)]
   ["Quit" ("q" "Quit" transient-quit-one)]])

;;;###autoload (autoload 'ellama-transient-blueprint-menu "ellama-transient" nil t)
(transient-define-prefix ellama-transient-blueprint-menu ()
  "Blueprint Menu."
  ["Blueprint Commands"
   ["Chat"
    ("b" "Blueprint" ellama-blueprint-select)
    ("U" "User defined blueprint" ellama-blueprint-select-user-defined-blueprint)
    ("C" "Community blueprint" ellama-community-prompts-select-blueprint)]
   ["Manage"
    ("c" "Create from buffer" ellama-blueprint-create)
    ("n" "New blueprint" ellama-blueprint-new)
    ("r" "Remove blueprint" ellama-blueprint-remove)]
   ["Quit" ("q" "Quit" transient-quit-one)]])

;;;###autoload (autoload 'ellama-transient-blueprint-mode-menu "ellama-transient" nil t)
(transient-define-prefix ellama-transient-blueprint-mode-menu ()
  ["Blueprint Commands"
   ["Chat"
    ("c" "Send to chat" ellama-send-buffer-to-new-chat-then-kill)
    ("a" "Plan and act" ellama-transient-plan-and-act-buffer-then-kill)]
   ["System Message"
    ("s" "Set system and chat" ellama-blueprint-chat-with-system-kill-buffer)
    ("S" "Set system and quit" ellama-blueprint-set-system-kill-buffer)]
   ["Create"
    ("C" "Create new blueprint from buffer" ellama-blueprint-create)]
   ["Variables"
    ("v" "Fill variables" ellama-blueprint-fill-variables)]
   ["Quit"
    ("k" "Kill" ellama-kill-current-buffer)
    ("q" "Quit" transient-quit-one)]])

(transient-define-suffix ellama-transient-plan-and-act-buffer-then-kill ()
  "Start a plan-and-act agent loop from current buffer and kill it."
  (interactive)
  (ellama-plan-and-act
   (buffer-substring-no-properties (point-min) (point-max))
   t)
  (ellama-kill-current-buffer))

;;;###autoload (autoload 'ellama-transient-tools-menu "ellama-transient" nil t)
(transient-define-prefix ellama-transient-tools-menu ()
  ["Tools Commands"
   :description (lambda ()
                  (format "Enabled tools:\n%s"
                          (string-join (mapcar (lambda (tool)
                                                 (llm-tool-name tool))
                                               ellama-tools-enabled)
                                       " ")))
   ["Tools"
    ("e" "Enable tool" ellama-tools-enable-by-name
     :transient t)
    ("E" "Enable all tools" ellama-tools-enable-all
     :transient t)
    ("d" "Disable tool" ellama-tools-disable-by-name
     :transient t)
    ("D" "Disable all tools" ellama-tools-disable-all
     :transient t)]
   ["Quit"
    ("k" "Kill" ellama-kill-current-buffer)
    ("q" "Quit" transient-quit-one)]])

(transient-define-suffix ellama-transient-chat (&optional args)
  "Chat with Ellama.  ARGS used for transient arguments."
  (interactive (list (transient-args transient-current-command)))
  (ellama-chat
   (read-string "Ask Ellama: ")
   (transient-arg-value "--new-session" args)
   :ephemeral (transient-arg-value "--ephemeral" args)))

(transient-define-suffix ellama-transient-plan-and-act (&optional args)
  "Start a plan-and-act agent loop.  ARGS used for transient arguments."
  (interactive (list (transient-args transient-current-command)))
  (ellama-plan-and-act
   (read-string "Ask ellama agent: ")
   (transient-arg-value "--new-session" args)
   :ephemeral (transient-arg-value "--ephemeral" args)))

(transient-define-suffix ellama-transient-chat-with-image (&optional args)
  "Chat with Ellama about an image.  ARGS used for transient arguments."
  (interactive (list (transient-args transient-current-command)))
  (ellama-chat-with-image
   (read-file-name "Image: " nil nil t)
   (read-string "Ask Ellama: ")
   (transient-arg-value "--new-session" args)
   :ephemeral (transient-arg-value "--ephemeral" args)))

(transient-define-suffix ellama-transient-chat-with-audio (&optional args)
  "Chat with Ellama about an audio file.  ARGS used for transient arguments."
  (interactive (list (transient-args transient-current-command)))
  (ellama-chat-with-audio
   (read-file-name "Audio: " nil nil t)
   (read-string "Ask Ellama: ")
   (transient-arg-value "--new-session" args)
   :ephemeral (transient-arg-value "--ephemeral" args)))

(transient-define-suffix ellama-transient-chat-with-audio-recording (&optional args)
  "Record audio and chat with Ellama about it.  ARGS used for transient arguments."
  (interactive (list (transient-args transient-current-command)))
  (ellama-chat-with-audio-recording
   (read-number "Record seconds: " ellama-audio-recording-duration)
   (read-string "Ask Ellama: ")
   (transient-arg-value "--new-session" args)
   :ephemeral (transient-arg-value "--ephemeral" args)))

;;;###autoload (autoload 'ellama-transient-main-menu "ellama-transient" nil t)
(transient-define-prefix ellama-transient-main-menu ()
  "Main Menu."
  ["Session Options"
   :description (lambda () (ellama--transient-context))
   ("-n" "Create New Session" "--new-session")]
  ["Ephemeral sessions"
   :if (lambda () ellama-session-auto-save)
   ("-e" "Create Ephemeral Session" "--ephemeral")]
  ["Main"
   [("c" "Chat" ellama-transient-chat)
    ("I" "Chat with image" ellama-transient-chat-with-image)
    ("E" "Chat with audio" ellama-transient-chat-with-audio)
    ("M" "Record audio chat" ellama-transient-chat-with-audio-recording)
    ("b" "Chat with blueprint" ellama-blueprint-select)
    ("B" "Blueprint Commands" ellama-transient-blueprint-menu)
    ("T" "Tools Commands" ellama-transient-tools-menu)]
   [("a" "Ask Commands" ellama-transient-ask-menu)
    ("C" "Code Commands" ellama-transient-code-menu)]]
  ["Text"
   [("w" "Write" ellama-write)
    ("P" "Proofread" ellama-proofread)
    ("k" "Text Complete" ellama-complete)
    ("g" "Text change" ellama-change)
    ("d" "Define word" ellama-define-word)]
   [("s" "Summarize Commands" ellama-transient-summarize-menu)
    ("i" "Improve Commands" ellama-transient-improve-menu)
    ("t" "Translate Commands" ellama-transient-translate-menu)
    ("m" "Make Commands" ellama-transient-make-menu)]]
  ["System"
   [("o" "Model" ellama-select-model)
    ("p" "Provider selection" ellama-provider-select)
    ("y" "Set system message" ellama-transient-set-system
     :transient t
     :description ellama-transient-system-show)
    ("Y" "Edit system message" ellama-blueprint-edit-system-message)]
   [("S" "Session Commands" ellama-transient-session-menu)
    ("x" "Context Commands" ellama-transient-context-menu)]]
  [["Problem solving"
    ("A" "Agent" ellama-transient-plan-and-act)]]
  [["Quit" ("q" "Quit" transient-quit-one)]]
  (interactive)
  (transient-setup 'ellama-transient-main-menu)
  (when (and (not ellama-transient-provider)
             (ellama-transient--default-value-p ellama-transient-model-name))
    (ellama-fill-transient-model ellama-provider)))

;;;###autoload (autoload 'ellama "ellama-transient" nil t)
(defalias 'ellama 'ellama-transient-main-menu)

(provide 'ellama-transient)
;;; ellama-transient.el ends here.
