summaryrefslogtreecommitdiff
path: root/lisp/agent-shell-kimi.el
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-07-11 08:50:14 +0200
committercashmere <cashmere@autistici.org>2026-07-11 08:50:14 +0200
commitc74c3526fe719ff8127d17f44b67a33772adc334 (patch)
tree7b05d6cad835f8704430ac38c4f22358601fe846 /lisp/agent-shell-kimi.el
parentc6579ddf761aaa1c922213310dc1f46347ea06ca (diff)
downloademacs-c74c3526fe719ff8127d17f44b67a33772adc334.tar.gz
2026-07-11T08:50:16+02:00
Diffstat (limited to 'lisp/agent-shell-kimi.el')
-rw-r--r--lisp/agent-shell-kimi.el612
1 files changed, 612 insertions, 0 deletions
diff --git a/lisp/agent-shell-kimi.el b/lisp/agent-shell-kimi.el
new file mode 100644
index 0000000..3a9d948
--- /dev/null
+++ b/lisp/agent-shell-kimi.el
@@ -0,0 +1,612 @@
+;;; agent-shell-kimi.el --- Kimi Code agent configurations -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2026 Nicolai Singh
+
+;; This package 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 package 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 <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; This file includes Kimi Code configuration using `kimi acp'.
+;;
+;; It exposes the Kimi-specific flags that are available when starting an
+;; ACP server (`--yolo', `--auto', `--plan', `--model', `--add-dir',
+;; `--skills-dir') and integrates with Kimi's session index so recent
+;; sessions can be resumed from Emacs.
+
+;;; Code:
+
+(eval-when-compile
+ (require 'cl-lib))
+(require 'json)
+(require 'shell-maker)
+(require 'acp)
+
+(declare-function agent-shell--indent-string "agent-shell")
+(declare-function agent-shell--make-acp-client "agent-shell")
+(declare-function agent-shell-make-agent-config "agent-shell")
+(autoload 'agent-shell-make-agent-config "agent-shell")
+(declare-function agent-shell--dwim "agent-shell")
+(declare-function agent-shell-start "agent-shell")
+(declare-function shell-maker-welcome-message "shell-maker")
+(defvar shell-maker-prompt)
+(defvar shell-maker-prompt-regexp)
+
+(defcustom agent-shell-kimi-acp-command
+ '("kimi" "acp")
+ "Command and parameters for the Kimi Code CLI client.
+
+The first element is the command name, and the rest are command parameters.
+This is the base command; flags controlled by the other custom variables
+in this file are prepended before the `acp' subcommand."
+ :type '(repeat string)
+ :group 'agent-shell)
+
+(defcustom agent-shell-kimi-default-model-id
+ nil
+ "Default Kimi model ID.
+
+Must be one of the model ID's displayed under \"Available models\"
+when starting a new shell.
+
+Can be set to either a string or a function that returns a string."
+ :type '(choice (const nil) string function)
+ :group 'agent-shell)
+
+(defcustom agent-shell-kimi-default-session-mode-id
+ nil
+ "Default Kimi Code session mode ID.
+
+Must be one of the mode ID's displayed under \"Available modes\"
+when starting a new shell.
+
+Can be set to either a string or a function that returns a string."
+ :type '(choice (const nil) string function)
+ :group 'agent-shell)
+
+(defcustom agent-shell-kimi-environment
+ nil
+ "Environment variables for the Kimi Code CLI client.
+
+This should be a list of environment variables to be used when
+starting the Kimi client process."
+ :type '(repeat string)
+ :group 'agent-shell)
+
+(defcustom agent-shell-kimi-yolo
+ nil
+ "When non-nil, pass `--yolo' to `kimi' so all actions are auto-approved.
+
+See `kimi --help' for the exact semantics."
+ :type 'boolean
+ :group 'agent-shell)
+
+(defcustom agent-shell-kimi-auto
+ nil
+ "When non-nil, pass `--auto' to `kimi' to start in auto permission mode."
+ :type 'boolean
+ :group 'agent-shell)
+
+(defcustom agent-shell-kimi-plan
+ nil
+ "When non-nil, pass `--plan' to `kimi' to start in plan mode."
+ :type 'boolean
+ :group 'agent-shell)
+
+(defcustom agent-shell-kimi-model
+ nil
+ "Kimi model alias passed to `--model'.
+
+Can be either a string or a function that returns a string. When nil,
+no `--model' flag is added and Kimi uses its configured default."
+ :type '(choice (const nil) string function)
+ :group 'agent-shell)
+
+(defcustom agent-shell-kimi-additional-dirs
+ nil
+ "List of additional workspace directories passed via `--add-dir'.
+
+Each entry should be an expanded directory path."
+ :type '(repeat string)
+ :group 'agent-shell)
+
+(defcustom agent-shell-kimi-skills-dirs
+ nil
+ "List of skills directories passed via `--skills-dir'.
+
+Each entry should be an expanded directory path."
+ :type '(repeat string)
+ :group 'agent-shell)
+
+(defcustom agent-shell-kimi-extra-command-params
+ nil
+ "Extra command-line parameters passed to `kimi' before the `acp' subcommand.
+
+Use this for flags that do not yet have a dedicated custom variable."
+ :type '(repeat string)
+ :group 'agent-shell)
+
+(defcustom agent-shell-kimi-session-index
+ (expand-file-name "~/.kimi-code/session_index.jsonl")
+ "Path to Kimi's session index file.
+
+Used by `agent-shell-kimi-resume-session' to list recent sessions."
+ :type 'file
+ :group 'agent-shell)
+
+(cl-defun agent-shell-kimi-make-authentication (&key api-key login none)
+ "Create a Kimi authentication configuration.
+
+API-KEY is a Kimi API key string or a function that returns one.
+LOGIN when non-nil indicates login-based (device-code) authentication.
+NONE when non-nil indicates no authentication method is requested.
+
+Only one of API-KEY, LOGIN, or NONE should be provided."
+ (when (> (seq-count #'identity (list api-key login none)) 1)
+ (error "Cannot specify multiple authentication methods - choose one"))
+ (unless (> (seq-count #'identity (list api-key login none)) 0)
+ (error "Must specify one of :api-key, :login, or :none"))
+ (cond
+ (api-key `((:api-key . ,api-key)))
+ (login `((:login . t)))
+ (none `((:none . t)))))
+
+(defcustom agent-shell-kimi-authentication
+ (agent-shell-kimi-make-authentication :none t)
+ "Configuration for Kimi authentication.
+
+For no explicit authentication (default):
+
+ (setq agent-shell-kimi-authentication
+ (agent-shell-kimi-make-authentication :none t))
+
+For login-based authentication:
+
+ (setq agent-shell-kimi-authentication
+ (agent-shell-kimi-make-authentication :login t))
+
+For API key (string):
+
+ (setq agent-shell-kimi-authentication
+ (agent-shell-kimi-make-authentication :api-key \"your-key\"))
+
+For API key (function):
+
+ (setq agent-shell-kimi-authentication
+ (agent-shell-kimi-make-authentication :api-key (lambda () ...)))"
+ :type 'alist
+ :group 'agent-shell)
+
+(defcustom agent-shell-kimi-login-command
+ '("kimi" "login")
+ "Command used by `agent-shell-kimi-login' to run the device-code flow."
+ :type '(repeat string)
+ :group 'agent-shell)
+
+(defcustom agent-shell-kimi-show-model-in-prompt
+ t
+ "When non-nil, include the active model/mode in the Kimi shell prompt."
+ :type 'boolean
+ :group 'agent-shell)
+
+;;;; Faces
+
+(defface agent-shell-kimi-prompt-face
+ '((t (:foreground "#81c8be" :weight bold :inherit fixed-pitch)))
+ "Face for the Kimi shell prompt."
+ :group 'agent-shell)
+
+(defface agent-shell-kimi-user-face
+ '((t (:inherit default)))
+ "Face for user messages in the Kimi shell."
+ :group 'agent-shell)
+
+(defface agent-shell-kimi-assistant-face
+ '((t (:foreground "#8caaee")))
+ "Face for assistant messages in the Kimi shell."
+ :group 'agent-shell)
+
+(defface agent-shell-kimi-tool-face
+ '((t (:foreground "#ca9ee6")))
+ "Face for tool-use sections in the Kimi shell."
+ :group 'agent-shell)
+
+(defface agent-shell-kimi-thought-face
+ '((t (:foreground "#a5adce" :slant italic)))
+ "Face for thought-process sections in the Kimi shell."
+ :group 'agent-shell)
+
+(defface agent-shell-kimi-error-face
+ '((t (:foreground "#e78284" :weight bold)))
+ "Face for errors in the Kimi shell."
+ :group 'agent-shell)
+
+;;;; Authentication & helpers
+
+(defun agent-shell-kimi-key ()
+ "Get the Kimi API key from `agent-shell-kimi-authentication'."
+ (cond ((stringp (map-elt agent-shell-kimi-authentication :api-key))
+ (map-elt agent-shell-kimi-authentication :api-key))
+ ((functionp (map-elt agent-shell-kimi-authentication :api-key))
+ (condition-case _err
+ (funcall (map-elt agent-shell-kimi-authentication :api-key))
+ (error
+ "API key not found. Check out `agent-shell-kimi-authentication'")))
+ (t
+ nil)))
+
+(defun agent-shell-kimi-login ()
+ "Run `kimi login' to authenticate via the device-code flow."
+ (interactive)
+ (unless agent-shell-kimi-login-command
+ (user-error "`agent-shell-kimi-login-command' is not set"))
+ (let ((command (mapconcat #'shell-quote-argument agent-shell-kimi-login-command " ")))
+ (async-shell-command command "*Kimi login*")))
+
+(defun agent-shell-kimi--resolve-string-or-function (value)
+ "Resolve VALUE if it is a function, otherwise return it as-is."
+ (if (functionp value)
+ (funcall value)
+ value))
+
+(defun agent-shell-kimi--command-params ()
+ "Return the complete list of parameters passed to `kimi' before `acp'."
+ (let ((params nil))
+ (when agent-shell-kimi-yolo
+ (push "--yolo" params))
+ (when agent-shell-kimi-auto
+ (push "--auto" params))
+ (when agent-shell-kimi-plan
+ (push "--plan" params))
+ (when-let* ((model (agent-shell-kimi--resolve-string-or-function agent-shell-kimi-model))
+ ((not (string-empty-p model))))
+ (push "--model" params)
+ (push model params))
+ (dolist (dir (reverse agent-shell-kimi-additional-dirs))
+ (push "--add-dir" params)
+ (push dir params))
+ (dolist (dir (reverse agent-shell-kimi-skills-dirs))
+ (push "--skills-dir" params)
+ (push dir params))
+ (dolist (arg (reverse agent-shell-kimi-extra-command-params))
+ (push arg params))
+ (nreverse params)))
+
+(defun agent-shell-kimi--active-model-string ()
+ "Return the currently configured Kimi model alias, or nil if none."
+ (when-let* ((model (agent-shell-kimi--resolve-string-or-function
+ agent-shell-kimi-model))
+ ((not (string-empty-p model))))
+ model))
+
+(defun agent-shell-kimi--active-mode-string ()
+ "Return the currently configured Kimi session mode ID, or nil if none."
+ (when-let* ((mode (agent-shell-kimi--resolve-string-or-function
+ agent-shell-kimi-default-session-mode-id))
+ ((not (string-empty-p mode))))
+ mode))
+
+(defun agent-shell-kimi--prompt-string ()
+ "Return the propertized Kimi shell prompt string."
+ (let ((extra nil))
+ (when agent-shell-kimi-show-model-in-prompt
+ (let ((model (agent-shell-kimi--active-model-string))
+ (mode (agent-shell-kimi--active-mode-string)))
+ (when (or model mode)
+ (setq extra (concat "("
+ (or model "default")
+ (when mode (concat "|" mode))
+ ")")))))
+ (propertize (concat "Kimi" extra "> ")
+ 'font-lock-face 'agent-shell-kimi-prompt-face)))
+
+(defun agent-shell-kimi-update-prompt ()
+ "Update the prompt in the current Kimi shell buffer."
+ (interactive)
+ (when (eq major-mode 'agent-shell-mode)
+ (setq-local shell-maker-prompt (agent-shell-kimi--prompt-string))
+ (setq-local shell-maker-prompt-regexp
+ (concat "^" (regexp-quote (substring-no-properties shell-maker-prompt))))))
+
+(defun agent-shell-kimi-make-config ()
+ "Create a Kimi Code agent configuration.
+
+Returns an agent configuration alist using `agent-shell-make-agent-config'."
+ (agent-shell-make-agent-config
+ :identifier 'kimi
+ :mode-line-name "Kimi"
+ :buffer-name "Kimi"
+ :shell-prompt (agent-shell-kimi--prompt-string)
+ :shell-prompt-regexp "Kimi[^>]*> "
+ :welcome-function #'agent-shell-kimi--welcome-message
+ :client-maker (lambda (buffer)
+ (agent-shell-kimi-make-client :buffer buffer))
+ :default-model-id (lambda () (agent-shell-kimi--resolve-string-or-function
+ agent-shell-kimi-default-model-id))
+ :default-session-mode-id (lambda () (agent-shell-kimi--resolve-string-or-function
+ agent-shell-kimi-default-session-mode-id))
+ :install-instructions "See https://www.kimi.com/code for installation."))
+
+(defun agent-shell-kimi-start-agent ()
+ "Start an interactive Kimi Code agent shell."
+ (interactive)
+ (agent-shell--dwim :config (agent-shell-kimi-make-config)
+ :new-shell t))
+
+(defun agent-shell-kimi-start-coding ()
+ "Start a Kimi shell for normal coding.
+
+This is equivalent to `agent-shell-kimi-start-agent' but explicitly
+resets `--plan' and `--yolo' so the shell starts in plain coding mode."
+ (interactive)
+ (let ((agent-shell-kimi-plan nil)
+ (agent-shell-kimi-yolo nil))
+ (agent-shell-kimi-start-agent)))
+
+(defun agent-shell-kimi-start-plan ()
+ "Start a Kimi shell in plan mode (`--plan')."
+ (interactive)
+ (let ((agent-shell-kimi-plan t)
+ (agent-shell-kimi-yolo nil))
+ (agent-shell-kimi-start-agent)))
+
+(defun agent-shell-kimi-start-yolo ()
+ "Start a Kimi shell in yolo/auto-approve mode (`--yolo')."
+ (interactive)
+ (let ((agent-shell-kimi-yolo t))
+ (agent-shell-kimi-start-agent)))
+
+(defun agent-shell-kimi-start-review ()
+ "Start a Kimi shell tuned for code review.
+
+Enables `--plan' and, if no model is set, falls back to
+`kimi-for-coding'. Customize `agent-shell-kimi-model' to change the
+review model."
+ (interactive)
+ (let ((agent-shell-kimi-plan t)
+ (agent-shell-kimi-yolo nil)
+ (agent-shell-kimi-model (or agent-shell-kimi-model "kimi-for-coding")))
+ (agent-shell-kimi-start-agent)))
+
+(cl-defun agent-shell-kimi-make-client (&key buffer)
+ "Create a Kimi Code ACP client with BUFFER as context."
+ (unless buffer
+ (error "Missing required argument: :buffer"))
+ (let* ((command (car agent-shell-kimi-acp-command))
+ (base-params (cdr agent-shell-kimi-acp-command))
+ (kimi-params (agent-shell-kimi--command-params))
+ ;; Global `kimi' flags go before the `acp' subcommand.
+ (command-params (if (string= (car base-params) "acp")
+ (append kimi-params base-params)
+ (append base-params kimi-params)))
+ (env-vars (append agent-shell-kimi-environment
+ (when-let* ((api-key (agent-shell-kimi-key))
+ ((not (map-elt agent-shell-kimi-authentication :none))))
+ (list (format "KIMI_API_KEY=%s" api-key))))))
+ (agent-shell--make-acp-client :command command
+ :command-params command-params
+ :environment-variables env-vars
+ :context-buffer buffer)))
+
+(defun agent-shell-kimi--read-jsonl (path)
+ "Return a list of parsed JSON objects from PATH (one per line)."
+ (when (file-exists-p path)
+ (with-temp-buffer
+ (insert-file-contents path)
+ (let ((result nil))
+ (goto-char (point-min))
+ (while (not (eobp))
+ (let ((line (string-trim (thing-at-point 'line t))))
+ (unless (string-empty-p line)
+ (push (json-parse-string line :object-type 'alist) result)))
+ (forward-line 1))
+ (nreverse result)))))
+
+(defun agent-shell-kimi--session-mtime (session)
+ "Return the modification time of SESSION's directory, or 0 if missing."
+ (let ((dir (cdr (assq 'sessionDir session))))
+ (if (file-directory-p dir)
+ (time-to-seconds (file-attribute-modification-time (file-attributes dir)))
+ 0)))
+
+(defun agent-shell-kimi--relative-time (seconds)
+ "Return a human-readable string for SECONDS since the epoch."
+ (let ((delta (max 0 (- (float-time) seconds))))
+ (cond ((< delta 60) "just now")
+ ((< delta 3600) (format "%d min ago" (floor delta 60)))
+ ((< delta 86400) (format "%d hr ago" (floor delta 3600)))
+ ((< delta 604800) (format "%d days ago" (floor delta 86400)))
+ (t (format "%d wks ago" (floor delta 604800))))))
+
+(defun agent-shell-kimi--session-project-name (session)
+ "Return a project-like name for SESSION's working directory."
+ (let ((dir (abbreviate-file-name (or (cdr (assq 'workDir session)) ""))))
+ (if (string-empty-p dir)
+ "?"
+ (file-name-nondirectory (directory-file-name dir)))))
+
+(defun agent-shell-kimi--session-label (session)
+ "Return a display label for SESSION suitable for `completing-read'."
+ (let ((id (cdr (assq 'sessionId session)))
+ (dir (abbreviate-file-name (or (cdr (assq 'workDir session)) "")))
+ (project (agent-shell-kimi--session-project-name session))
+ (mtime (agent-shell-kimi--session-mtime session)))
+ (format "%s %s %s %s"
+ project
+ (agent-shell-kimi--relative-time mtime)
+ dir
+ (or id ""))))
+
+(defun agent-shell-kimi--session-current-p (session)
+ "Return non-nil if SESSION's workDir is under `default-directory'."
+ (let ((dir (cdr (assq 'workDir session)))
+ (cwd (expand-file-name default-directory)))
+ (and dir
+ (string-prefix-p cwd (expand-file-name dir)))))
+
+(defun agent-shell-kimi-clean-session-index ()
+ "Remove session index entries whose working directory no longer exists.
+
+Returns the number of removed entries. The original index file is
+renamed with a timestamp backup before rewriting."
+ (interactive)
+ (unless (file-exists-p agent-shell-kimi-session-index)
+ (user-error "Session index not found: %s" agent-shell-kimi-session-index))
+ (let* ((sessions (agent-shell-kimi--read-jsonl agent-shell-kimi-session-index))
+ (valid (seq-filter (lambda (s)
+ (let ((dir (cdr (assq 'workDir s))))
+ (and dir (file-directory-p dir))))
+ sessions))
+ (removed (- (length sessions) (length valid))))
+ (when (> removed 0)
+ (rename-file agent-shell-kimi-session-index
+ (format "%s.bak-%s" agent-shell-kimi-session-index
+ (format-time-string "%Y%m%d-%H%M%S"))
+ t)
+ (with-temp-file agent-shell-kimi-session-index
+ (dolist (s valid)
+ (insert (json-encode s) "\n"))))
+ (message "Removed %d stale Kimi session(s)" removed)
+ removed))
+
+(defun agent-shell-kimi-resume-session (cleanup)
+ "Resume a recent Kimi session by selecting from the session index.
+
+With prefix arg CLEANUP, first remove index entries whose working
+directory no longer exists."
+ (interactive "P")
+ (when cleanup
+ (agent-shell-kimi-clean-session-index))
+ (let* ((sessions (agent-shell-kimi--read-jsonl agent-shell-kimi-session-index))
+ (sessions (seq-filter (lambda (s)
+ (let ((dir (cdr (assq 'workDir s))))
+ (and dir (file-directory-p dir))))
+ sessions))
+ (sessions (seq-sort-by #'agent-shell-kimi--session-mtime #'> sessions))
+ ;; Surface sessions from the current project first.
+ (sessions (append (seq-filter #'agent-shell-kimi--session-current-p sessions)
+ (seq-remove #'agent-shell-kimi--session-current-p sessions)))
+ (choices (mapcar (lambda (s)
+ (cons (agent-shell-kimi--session-label s) s))
+ sessions))
+ (choice (completing-read "Resume Kimi session: " choices nil t))
+ (session (cdr (assoc choice choices)))
+ (dir (cdr (assq 'workDir session)))
+ (id (cdr (assq 'sessionId session))))
+ (unless session
+ (user-error "No session selected"))
+ (unless (and dir (file-directory-p dir))
+ (user-error "Session working directory no longer exists: %s" dir))
+ (let ((default-directory dir))
+ (agent-shell-start :config (agent-shell-kimi-make-config)
+ :session-id id))))
+
+(defun agent-shell-kimi--welcome-status-line ()
+ "Return a short status line for the Kimi welcome message."
+ (let ((model (or (agent-shell-kimi--active-model-string) "default"))
+ (mode (or (agent-shell-kimi--active-mode-string) "default")))
+ (concat
+ (propertize "model" 'font-lock-face 'agent-shell-kimi-tool-face)
+ ": " model " "
+ (propertize "mode" 'font-lock-face 'agent-shell-kimi-tool-face)
+ ": " mode " "
+ (propertize "send" 'font-lock-face 'agent-shell-kimi-thought-face) " M-RET "
+ (propertize "interrupt" 'font-lock-face 'agent-shell-kimi-thought-face) " C-c C-c "
+ (propertize "menu" 'font-lock-face 'agent-shell-kimi-thought-face) " SPC a k")))
+
+(defun agent-shell-kimi--welcome-message (config)
+ "Return Kimi ASCII art using `shell-maker' CONFIG."
+ (let ((art (agent-shell--indent-string 4 (agent-shell-kimi--ascii-art)))
+ (message (string-trim-left (shell-maker-welcome-message config) "\n")))
+ (concat "\n\n"
+ art
+ "\n\n"
+ " " (agent-shell-kimi--welcome-status-line) "\n\n"
+ message)))
+
+(defun agent-shell-kimi--ascii-art ()
+ "Kimi ASCII art."
+ (let* ((is-dark (eq (frame-parameter nil 'background-mode) 'dark))
+ (text (string-trim "
+ ██╗ ██╗ ██╗ ███╗ ███╗ ██╗
+ ██║ ██╔╝ ██║ ████╗ ████║ ██║
+ █████╔╝ ██║ ██╔████╔██║ ██║
+ ██╔═██╗ ██║ ██║╚██╔╝██║ ██║
+ ██║ ██╗ ██║ ██║ ╚═╝ ██║ ██║
+ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
+" "\n")))
+ (propertize text 'font-lock-face (if is-dark
+ '(:foreground "#dddddd" :inherit fixed-pitch)
+ '(:foreground "#000000" :inherit fixed-pitch)))))
+
+(defcustom agent-shell-kimi-model-choices
+ '("kimi-for-coding" "kimi-k2" "kimi-k2-5")
+ "List of model aliases offered by `agent-shell-kimi-set-model'.
+
+Set this to the model aliases your Kimi CLI installation supports."
+ :type '(repeat string)
+ :group 'agent-shell)
+
+(defun agent-shell-kimi-set-model (model)
+ "Interactively set `agent-shell-kimi-model' to MODEL.
+
+Use `agent-shell-kimi-model-choices' for completion. An empty choice
+clears the model and uses Kimi's configured default."
+ (interactive
+ (list (completing-read "Kimi model (empty for default): "
+ agent-shell-kimi-model-choices
+ nil nil
+ (or (agent-shell-kimi--active-model-string) ""))))
+ (setq agent-shell-kimi-model (if (string-empty-p model) nil model))
+ (message "Kimi model set to %s" (or agent-shell-kimi-model "default")))
+
+(defun agent-shell-kimi-set-directory (dir)
+ "Add DIR to `agent-shell-kimi-additional-dirs'.
+
+Existing entries are preserved; duplicates are removed."
+ (interactive "DAdditional workspace directory: ")
+ (setq agent-shell-kimi-additional-dirs
+ (seq-uniq (append agent-shell-kimi-additional-dirs
+ (list (expand-file-name dir)))))
+ (message "Kimi additional dirs: %s" agent-shell-kimi-additional-dirs))
+
+(defcustom agent-shell-kimi-pulse-on-done
+ t
+ "When non-nil, pulse the mode-line when Kimi finishes responding.
+
+Only applies when the Kimi buffer is not the selected window; the
+pulse is meant as a subtle TUI notification."
+ :type 'boolean
+ :group 'agent-shell)
+
+(declare-function shell-maker--announce-response "shell-maker")
+
+(defun agent-shell-kimi-response-done-pulse (buffer)
+ "Pulse the mode-line to notify that BUFFER has finished responding.
+
+Meant to be added as :after advice to `shell-maker--announce-response'."
+ (when (and agent-shell-kimi-pulse-on-done
+ (buffer-live-p buffer)
+ (not (eq buffer (window-buffer (selected-window)))))
+ (let ((face 'agent-shell-kimi-prompt-face)
+ (original-face (face-attribute 'mode-line :inherit)))
+ (set-face-attribute 'mode-line nil :inherit face)
+ (run-with-timer 0.3 nil
+ (lambda ()
+ (set-face-attribute 'mode-line nil :inherit original-face))))))
+
+(advice-add 'shell-maker--announce-response :after #'agent-shell-kimi-response-done-pulse)
+
+(provide 'agent-shell-kimi)
+
+;;; agent-shell-kimi.el ends here