;;; ai-code-backends-infra.el --- Infrastructure for AI Code Terminals  -*- lexical-binding: t; -*-

;; Author: Yoav Orot, Kang Tu, Silex, Steve Molitor, AI Agent
;; SPDX-License-Identifier: Apache-2.0

;; Keywords: ai, terminal, vterm, eat

;;; Commentary:
;; This library provides common infrastructure for AI-powered terminal interfaces,
;; including terminal backend abstraction (vterm/eat), window management,
;; and performance optimizations like anti-flicker and reflow glitch prevention.
;; Code was generated by AI Agent, using
;; https://github.com/manzaltu/claude-code-ide.el as reference
;; multi-session support came from https://github.com/stevemolitor/claude-code.el, Thanks Steve Molitor

;;; Code:

(require 'cl-lib)
(require 'subr-x)
(require 'ai-code-session)
(require 'ai-code-session-link)
;; Terminal-specific implementations live in dedicated modules so this
;; file can stay focused on shared session orchestration.
(require 'ai-code-backends-infra-vterm)
(require 'ai-code-backends-infra-eat)
(require 'ai-code-backends-infra-ghostel)

(declare-function ai-code--session-handle-at-input "ai-code-input" ())
(declare-function ai-code--session-project-root "ai-code-utils" ())

;; DONE: In previous PR: https://github.com/tninja/ai-code-interface.el/pull/349, it set default new AI session instance names from prompt buffer filenames
;; But now we want to set the default new AI session instance names as current branch name. If this name has been used for an existing session, then we can fallback to using the prompt buffer filename as the default new AI session instance name.

;;; Customization

(defgroup ai-code-backends-infra nil
  "Infrastructure for AI Code terminals."
  :group 'tools)

(defcustom ai-code-backends-infra-terminal-backend 'vterm
  "Terminal backend to use for sessions.
Can be either `vterm', `eat', or `ghostel'."
  :type '(choice (const :tag "vterm" vterm)
                 (const :tag "eat" eat)
                 (const :tag "ghostel" ghostel))
  :group 'ai-code-backends-infra)

(defcustom ai-code-backends-infra-window-side 'right
  "Side of the frame where the window should appear."
  :type '(choice (const :tag "Left" left)
                 (const :tag "Right" right)
                 (const :tag "Top" top)
                 (const :tag "Bottom" bottom))
  :group 'ai-code-backends-infra)

(defcustom ai-code-backends-infra-window-width 90
  "Body width of the side window when opened on left or right."
  :type 'integer
  :group 'ai-code-backends-infra)

(defcustom ai-code-backends-infra-window-height 20
  "Height of the side window when opened on top or bottom."
  :type 'integer
  :group 'ai-code-backends-infra)

(defcustom ai-code-backends-infra-use-side-window t
  "Whether to display the terminal in a side window."
  :type 'boolean
  :group 'ai-code-backends-infra)

(defcustom ai-code-backends-infra-focus-on-open t
  "Whether to focus the terminal window when it opens."
  :type 'boolean
  :group 'ai-code-backends-infra)

(defcustom ai-code-backends-infra-terminal-initialization-delay 0.1
  "Initialization delay for terminal stability."
  :type 'number
  :group 'ai-code-backends-infra)

(defcustom ai-code-backends-infra-prevent-reflow-glitch t
  "Workaround for terminal scrolling bug #1422."
  :type 'boolean
  :group 'ai-code-backends-infra)

(defcustom ai-code-backends-infra-strip-alternate-screen t
  "Strip alternate screen buffer sequences from terminal output.
When non-nil, remove the escape sequences that switch to and from
the alternate screen buffer (\\e[?1049h and \\e[?1049l).  TUI
applications like GitHub Copilot CLI hardcode these sequences,
which disables terminal scrollback.  Stripping them forces output
onto the normal screen buffer where scrollback is preserved."
  :type 'boolean
  :group 'ai-code-backends-infra)

(defcustom ai-code-backends-infra-scrollback-inject-interval 1.0
  "Minimum seconds between scrollback-preservation injections.
When a TUI application redraws via synchronized output, we inject
newlines to push visible content into the scrollback ring.  This
interval prevents flooding the scrollback with duplicate frames."
  :type 'number
  :group 'ai-code-backends-infra)

;;; Variables

(defvar ai-code-backends-infra--processes (make-hash-table :test 'equal)
  "Hash table mapping session keys to their processes.")

(defvar ai-code-backends-infra--last-accessed-buffer nil
  "The most recently accessed AI Code buffer.")

(defvar ai-code-backends-infra--directory-buffer-map (make-hash-table :test 'equal)
  "Hash table mapping (prefix . directory) to last selected session buffer.")

(defvar ai-code-backends-infra--file-session-map (make-hash-table :test 'equal)
  "Hash table mapping (prefix . file) to attached AI session buffer.")

(defvar ai-code-backends-infra--preferred-session-buffer nil
  "Preferred session buffer to place first when prompting for session selection.")

(defvar ai-code-backends-infra--reflow-advised-handlers nil
  "Resize handlers currently advised with reflow filter.")

(defvar-local ai-code-backends-infra--idle-timer nil
  "Timer for detecting idle state (response completion).")

(defvar-local ai-code-backends-infra--response-seen nil
  "Non-nil when the current response has been observed.
Observation happens either by the buffer being visible or by a notification
being sent for the response completion.")

(defvar-local ai-code-backends-infra--last-meaningful-output-time nil
  "Float timestamp of the most recent meaningful output.")

(defvar-local ai-code-backends-infra--session-directory nil
  "Normalized working directory associated with the current session buffer.")

(defvar-local ai-code-backends-infra--session-terminal-backend nil
  "Terminal backend used by the current session buffer.")

(defvar-local ai-code-backends-infra--session-prefix nil
  "Backend prefix associated with the current session buffer.")

(defvar-local ai-code-backends-infra--session-task-file nil
  "Task file associated with the current session buffer, when available.")

(defvar-local ai-code-backends-infra--multiline-input-sequence nil
  "Terminal sequence sent for multiline input in the current session buffer.")

(defvar-local ai-code-backends-infra--terminal-active-cursor-type nil
  "Cursor type to restore when returning to terminal interaction mode.")

(defvar-local ai-code-backends-infra--navigation-cursor-active nil
  "Non-nil when Emacs temporarily owns the cursor for output navigation.")

(defvar ai-code-cli-args-history nil
  "History list for CLI args prompts.")

(defconst ai-code-backends-infra--uuid-regexp
  "[[:xdigit:]]\\{8\\}-[[:xdigit:]]\\{4\\}-[[:xdigit:]]\\{4\\}-[[:xdigit:]]\\{4\\}-[[:xdigit:]]\\{12\\}"
  "Regexp matching the general UUID 8-4-4-4-12 text structure.
This validates the textual shape, but not UUID version or variant bits.")

(defun ai-code-backends-infra--selected-session-id ()
  "Return active region text if it has a UUID session id."
  (when (use-region-p)
    (let ((candidate
           (string-trim
            (buffer-substring-no-properties (region-beginning) (region-end)))))
      (when (string-match-p
             (concat "\\`" ai-code-backends-infra--uuid-regexp "\\'")
             candidate)
        candidate))))

(defcustom ai-code-backends-infra-idle-delay 5.0
  "Delay in seconds of inactivity before considering response complete.
After this period of terminal inactivity, a notification may be sent
if the AI session buffer is not currently visible."
  :type 'number
  :group 'ai-code-backends-infra)

;;; Terminal Output Preservation

(defconst ai-code-backends-infra--alternate-screen-regexp
  "\033\\[\\?1049[hl]"
  "Regexp matching alternate screen buffer enter/exit sequences.
Matches \\e[?1049h (enter) and \\e[?1049l (exit).")

(defconst ai-code-backends-infra--screen-clear-regexp
  "\033\\[2J"
  "Regexp matching the Erase Display (ED 2) screen clear sequence.")

(defconst ai-code-backends-infra--scrollback-clear-regexp
  "\033\\[3J"
  "Regexp matching the Erase Display (ED 3) scrollback clear sequence.")

(defconst ai-code-backends-infra--sync-redraw-regexp
  "\033\\[\\?2026h\033\\[1;1H"
  "Regexp matching synchronized-update frame start with cursor home.
Copilot CLI uses \\e[?2026h (begin synchronized update) followed
immediately by \\e[1;1H (cursor to row 1, col 1) to redraw the
entire screen in place.")

(defvar ai-code-backends-infra-strip-alternate-screen-debug nil
  "When non-nil, log alternate-screen filter matches to *Messages*.
Set to t to debug scrollback-preservation transformations.")

(defvar-local ai-code-backends-infra--last-scrollback-inject-time 0
  "Timestamp of the last scrollback-preservation injection.
Used to throttle injections per
`ai-code-backends-infra-scrollback-inject-interval'.")

(defvar-local ai-code-backends-infra--sync-redraw-scrollback nil
  "Non-nil means inject scrollback-preserving newlines before redraws.
This applies to synchronized-update frame redraws (\\e[?2026h\\e[1;1H).
Backends that hardcode alternate screen buffer should set this
to t via their post-start-fn.")

(defun ai-code-backends-infra--scroll-to-scrollback-sequence ()
  "Return a sequence that pushes visible content into scrollback.
Move cursor to the last row, emit enough newlines to scroll all
visible lines into the scrollback buffer, then cursor home."
  (let ((height (or (when-let ((win (get-buffer-window (current-buffer) t)))
                      (window-body-height win))
                    50)))
    (concat "\033[" (number-to-string height) ";1H"
            (make-string height ?\n)
            "\033[H")))

(defun ai-code-backends-infra--strip-alternate-screen-sequences (str)
  "Normalize terminal sequences in STR to preserve scrollback.
When `ai-code-backends-infra-strip-alternate-screen' is non-nil and
the current buffer is an AI session buffer, apply these transformations:
1. Strip alternate screen enter/exit (\\e[?1049h, \\e[?1049l).
2. Convert screen clear (\\e[2J) to a newline-scroll sequence so
   the current content is pushed into scrollback.  Throttled via
   `ai-code-backends-infra-scrollback-inject-interval'.
3. Strip scrollback clear (\\e[3J).
4. Convert erase-to-end (\\e[J / \\e[0J) preceded by cursor-home
   into a scrollback-preserving sequence.  Throttled to avoid
   flooding the scrollback ring with repeated TUI frames.
5. Detect synchronized-update frame redraws (\\e[?2026h\\e[1;1H)
   and inject scrollback preservation before the frame, throttled
   to avoid flooding."
  (if (and ai-code-backends-infra-strip-alternate-screen
           (ai-code-backends-infra--session-buffer-p (current-buffer)))
      (let* ((result str)
             (scroll-seq (ai-code-backends-infra--scroll-to-scrollback-sequence))
             (now (float-time))
             (inject-ok (>= (- now
                               ai-code-backends-infra--last-scrollback-inject-time)
                            ai-code-backends-infra-scrollback-inject-interval)))
        (when (and ai-code-backends-infra-strip-alternate-screen-debug
                   (string-match-p "\033\\[" str))
          (let ((visible (replace-regexp-in-string
                          "\033" "<ESC>" (substring str 0 (min 200 (length str))))))
            (message "alt-screen-filter: len=%d alt=%s 2J=%s 3J=%s H+J=%s sync=%s seq=[%s]"
                     (length str)
                     (if (string-match-p ai-code-backends-infra--alternate-screen-regexp str) "Y" "N")
                     (if (string-match-p ai-code-backends-infra--screen-clear-regexp str) "Y" "N")
                     (if (string-match-p ai-code-backends-infra--scrollback-clear-regexp str) "Y" "N")
                     (if (string-match-p "\033\\[H.*\033\\[J\\|\033\\[H.*\033\\[0J" str) "Y" "N")
                     (if (string-match-p ai-code-backends-infra--sync-redraw-regexp str) "Y" "N")
                     visible)))
        ;; 1. Strip alternate screen transitions.
        (setq result (replace-regexp-in-string
                      ai-code-backends-infra--alternate-screen-regexp "" result))
        ;; 2. Convert ED 2 (clear screen) to newline-scroll, throttled
        ;;    to avoid flooding the scrollback ring with repeated TUI
        ;;    frames.  When throttled the plain \e[2J clears the screen
        ;;    without preserving content, which is acceptable since a
        ;;    recent snapshot was already pushed.
        (when (and inject-ok
                   (string-match-p ai-code-backends-infra--screen-clear-regexp result))
          (setq ai-code-backends-infra--last-scrollback-inject-time now)
          (setq result (replace-regexp-in-string
                        ai-code-backends-infra--screen-clear-regexp
                        scroll-seq result t t)))
        ;; 3. Strip ED 3 (clear scrollback).
        (setq result (replace-regexp-in-string
                      ai-code-backends-infra--scrollback-clear-regexp "" result))
        ;; 4. Convert cursor-home + erase-to-end (\e[H\e[J) into a
        ;;    scrollback-preserving sequence, throttled to avoid flooding
        ;;    the scrollback ring with repeated TUI frames (e.g. the
        ;;    Claude Code badge appearing on every redraw).
        (when inject-ok
          (let ((home-erase-re "\033\\[H\033\\[J\\|\033\\[H\033\\[0J"))
            (when (string-match-p home-erase-re result)
              (setq ai-code-backends-infra--last-scrollback-inject-time now)
              (setq result (replace-regexp-in-string
                            home-erase-re
                            (concat scroll-seq "\033[H") result t t)))))
        ;; 5. Detect synchronized-update frames (\e[?2026h followed by
        ;;    cursor to row 1) and inject scrollback preservation before
        ;;    the frame redraw, throttled to avoid flooding the scrollback
        ;;    ring.  Only trigger on chunks > 500 bytes (meaningful
        ;;    redraws, not small cursor movements).  Guarded by the
        ;;    buffer-local `--sync-redraw-scrollback' flag so that only
        ;;    backends that explicitly opt in (e.g. Copilot CLI) get
        ;;    the injection.
        (when (and ai-code-backends-infra--sync-redraw-scrollback
                   (> (length result) 500)
                   (string-match ai-code-backends-infra--sync-redraw-regexp result)
                   (>= (- (float-time)
                          ai-code-backends-infra--last-scrollback-inject-time)
                       ai-code-backends-infra-scrollback-inject-interval))
          (setq ai-code-backends-infra--last-scrollback-inject-time (float-time))
          (let ((match-start (match-beginning 0)))
            (setq result (concat (substring result 0 match-start)
                                 scroll-seq
                                 (substring result match-start)))))
        result)
    str))

(declare-function ai-code-notifications-response-ready "ai-code-notifications" (&optional backend-name))

(defun ai-code-backends-infra--output-meaningful-p (output)
  "Return non-nil if OUTPUT has meaningful printable content."
  (let* ((str (or output ""))
         ;; Strip OSC sequences (ESC ] ... BEL or ESC ] ... ESC \).
         (str (replace-regexp-in-string "\x1b\\][^\x07\x1b]*\\(?:\x07\\|\x1b\\\\\\)" "" str))
         ;; Strip ANSI escape sequences.
         (str (replace-regexp-in-string "\x1b\\[[0-9;?]*[ -/]*[@-~]" "" str))
         ;; Strip other control characters.
         (str (replace-regexp-in-string "[\x00-\x1f\x7f]" "" str)))
    (string-match-p "[^ \t\n\r]" str)))

(defun ai-code-backends-infra--buffer-user-visible-p (buffer)
  "Return non-nil when BUFFER is visible in any live window."
  (and (get-buffer-window-list buffer nil t) t))

(defun ai-code-backends-infra--check-response-complete (buffer)
  "Check if AI response is complete in BUFFER and notify if enabled."
  (when (buffer-live-p buffer)
    (with-current-buffer buffer
      (if (ai-code-backends-infra--idle-delay-elapsed-p)
          (let ((visible (ai-code-backends-infra--buffer-user-visible-p buffer)))
            (if visible
                (setq ai-code-backends-infra--response-seen t)
              (when (not ai-code-backends-infra--response-seen)
                (setq ai-code-backends-infra--response-seen t)
                (when (require 'ai-code-notifications nil t)
                  (when (fboundp 'ai-code-notifications-response-ready)
                    (let ((buffer-name (buffer-name buffer)))
                      ;; Extract backend name from buffer name format: *<backend>[<dir>]*
                      ;; Example: "*codex[my-project]*" extracts "codex"
                      ;; Regex breakdown:
                      ;;   \\*       - matches literal asterisk
                      ;;   \\(       - start capture group 1
                      ;;   [^[]+     - one or more chars that are not '['
                      ;;   \\)       - end capture group 1 (this is the backend name)
                      ;;   \\[       - matches literal '['
                      (when (string-match "\\*\\([^[]+\\)\\[" buffer-name)
                        (let ((backend-name (match-string 1 buffer-name)))
                          (ai-code-notifications-response-ready backend-name)))))))))
        (ai-code-backends-infra--schedule-idle-check)))))

(defun ai-code-backends-infra--schedule-idle-check ()
  "Schedule a check for response completion after idle period.
The timer is reset only after meaningful output is observed."
  (when ai-code-backends-infra--idle-timer
    (cancel-timer ai-code-backends-infra--idle-timer))
  (let ((buffer (current-buffer)))
    (setq ai-code-backends-infra--idle-timer
          (run-at-time ai-code-backends-infra-idle-delay nil
                       #'ai-code-backends-infra--check-response-complete
                       buffer))))

(defun ai-code-backends-infra--idle-delay-elapsed-p ()
  "Return non-nil when idle delay has elapsed since last output."
  (let ((last ai-code-backends-infra--last-meaningful-output-time))
    (or (null last)
        (>= (- (float-time) last) ai-code-backends-infra-idle-delay))))

(defun ai-code-backends-infra--note-meaningful-output ()
  "Record meaningful output and schedule idle tracking."
  (setq ai-code-backends-infra--response-seen nil
        ai-code-backends-infra--last-meaningful-output-time (float-time))
  (ai-code-backends-infra--schedule-idle-check))

(defun ai-code-backends-infra--configure-session-input-shortcuts ()
  "Install session-local shortcuts for prompt-oriented terminal input."
  (when (fboundp 'ai-code--session-handle-at-input)
    (local-set-key (kbd "@") #'ai-code--session-handle-at-input))
  (when (fboundp 'ai-code--session-handle-hash-input)
    (local-set-key (kbd "#") #'ai-code--session-handle-hash-input)))

;;; Terminal Backend Abstraction

(defun ai-code-backends-infra--terminal-ensure-backend ()
  "Ensure the selected terminal backend is available."
  (pcase ai-code-backends-infra-terminal-backend
    ('vterm (ai-code-backends-infra-vterm-ensure-backend))
    ('eat (ai-code-backends-infra-eat-ensure-backend))
    ('ghostel (ai-code-backends-infra-ghostel-ensure-backend))
    (_ (user-error "Invalid terminal backend: %s" ai-code-backends-infra-terminal-backend)))
  ;; Keep reflow advice synchronized with current backend/settings.
  (ai-code-backends-infra--sync-reflow-filter-advice))

(defun ai-code-backends-infra--terminal-dispatch (operation &rest args)
  "Dispatch OPERATION to the current terminal backend, passing ARGS.
Constructs function name as `ai-code-backends-infra-<backend>-<operation>'."
  (let* ((backend (ai-code-backends-infra--current-terminal-backend))
         (fn (intern (format "ai-code-backends-infra-%s-%s" backend operation))))
    (if (fboundp fn)
        (apply fn args)
      (error "Terminal backend %s does not support %s (looked up: %s)"
             backend operation fn))))

(defun ai-code-backends-infra--current-terminal-backend ()
  "Return terminal backend for current buffer operations."
  (or ai-code-backends-infra--session-terminal-backend
      ai-code-backends-infra-terminal-backend))

(defun ai-code-backends-infra--terminal-navigation-mode-p ()
  "Return non-nil when the current terminal buffer is in navigation mode."
  (pcase (ai-code-backends-infra--current-terminal-backend)
    ('vterm (ai-code-backends-infra-vterm-navigation-mode-p))
    ('eat (ai-code-backends-infra-eat-navigation-mode-p))
    ('ghostel (ai-code-backends-infra-ghostel-navigation-mode-p))
    (_ nil)))

(defun ai-code-backends-infra--sync-terminal-cursor ()
  "Hand cursor ownership between the terminal and Emacs navigation modes."
  (if (ai-code-backends-infra--terminal-navigation-mode-p)
      (unless ai-code-backends-infra--navigation-cursor-active
        (setq ai-code-backends-infra--terminal-active-cursor-type cursor-type
              ai-code-backends-infra--navigation-cursor-active t
              cursor-type t))
    (when ai-code-backends-infra--navigation-cursor-active
      (setq cursor-type ai-code-backends-infra--terminal-active-cursor-type
            ai-code-backends-infra--navigation-cursor-active nil))))

(defun ai-code-backends-infra--install-navigation-cursor-sync ()
  "Install buffer-local hooks for cursor handoff in terminal navigation modes."
  (ai-code-backends-infra--terminal-dispatch "install-navigation-cursor-sync"))

(defun ai-code-backends-infra--terminal-send-string (string)
  "Send STRING to the terminal in the current buffer."
  (ai-code-backends-infra--terminal-dispatch "send-string" string))

(defun ai-code-backends-infra--terminal-send-escape ()
  "Send escape key to the terminal in the current buffer."
  (ai-code-backends-infra--terminal-dispatch "send-escape"))

(defun ai-code-backends-infra--terminal-send-return ()
  "Send return key to the terminal in the current buffer."
  (ai-code-backends-infra--terminal-dispatch "send-return"))

(defun ai-code-backends-infra--terminal-send-backspace ()
  "Send backspace key to the terminal in the current buffer."
  (ai-code-backends-infra--terminal-dispatch "send-backspace"))

(defun ai-code-backends-infra--terminal-send-multiline-input ()
  "Send the configured multiline-input sequence for the current session buffer."
  (interactive)
  (unless ai-code-backends-infra--multiline-input-sequence
    (user-error "No multiline input sequence configured for this session"))
  (ai-code-backends-infra--terminal-send-string
   ai-code-backends-infra--multiline-input-sequence))

(defun ai-code-backends-infra--configure-multiline-input (sequence)
  "Configure multiline input keybindings in the current session buffer.
SEQUENCE is the terminal sequence sent for `S-<return>' and `C-<return>'."
  (when sequence
    (setq-local ai-code-backends-infra--multiline-input-sequence sequence)
    (local-set-key (kbd "S-<return>")
                   #'ai-code-backends-infra--terminal-send-multiline-input)
    (local-set-key (kbd "C-<return>")
                   #'ai-code-backends-infra--terminal-send-multiline-input)))

(defun ai-code-backends-infra--configure-session-buffer (buffer
                                                         &optional escape-fn
                                                         multiline-input-sequence)
  "Configure BUFFER with shared session keybindings.
ESCAPE-FN is bound to `C-<escape>' when non-nil.
MULTILINE-INPUT-SEQUENCE configures `S-<return>' and `C-<return>' when non-nil."
  (with-current-buffer buffer
    (when escape-fn
      (local-set-key (kbd "C-<escape>") escape-fn))
    (ai-code-backends-infra--configure-multiline-input
     multiline-input-sequence)
    (ai-code-session-link--linkify-session-region (point-min) (point-max))))

;;; Reflow and Window Management

(defun ai-code-backends-infra--terminal-resize-handler (&optional backend)
  "Retrieve the resize handling function for BACKEND.
When BACKEND is nil, use `ai-code-backends-infra-terminal-backend'."
  (pcase (or backend ai-code-backends-infra-terminal-backend)
    ('vterm (ai-code-backends-infra-vterm-resize-handler))
    ('eat (ai-code-backends-infra-eat-resize-handler))
    ('ghostel (ai-code-backends-infra-ghostel-resize-handler))
    (_ (error "Unsupported terminal backend"))))

(defun ai-code-backends-infra--session-buffer-p (buffer)
  "Check if BUFFER belongs to an AI session."
  (when-let ((name (if (stringp buffer) buffer (buffer-name buffer))))
    (string-match-p "\\`\\*.*\\[.*\\].*\\*\\'" name)))

(defun ai-code-backends-infra--terminal-reflow-filter (original-fn &rest args)
  "Filter terminal reflows to prevent height-only resize triggers.
ORIGINAL-FN is the terminal resize function and ARGS are its arguments.
Suppress reflow when terminal width is unchanged or when the session
buffer is in scroll/copy mode, working around bug #1422."
  (let* ((base-result (apply original-fn args))
         (dimensions-stable t))
    (dolist (win (window-list))
      (when-let* ((buf (window-buffer win))
                  ((ai-code-backends-infra--session-buffer-p buf)))
        (let* ((new-width (window-width win))
               (cached-width (window-parameter win 'ai-code-backends-infra-cached-width)))
          (unless (eql new-width cached-width)
            (setq dimensions-stable nil)
            (set-window-parameter win 'ai-code-backends-infra-cached-width new-width)))))
    (cond
     ;; Not in a session buffer - pass through
     ((not (ai-code-backends-infra--session-buffer-p (current-buffer)))
      base-result)
     ;; In scroll/copy mode - suppress reflow to avoid disrupting navigation
     ((ai-code-backends-infra--terminal-navigation-mode-p)
      nil)
     ;; Width changed - allow reflow
     ((not dimensions-stable)
      base-result)
     ;; Height-only change with reflow glitch prevention - suppress
     (ai-code-backends-infra-prevent-reflow-glitch
      nil)
     ;; Default - pass through
     (t base-result))))

(defun ai-code-backends-infra--sync-reflow-filter-advice ()
  "Add or remove terminal reflow advice according to current settings."
  (let* ((resize-handler (ai-code-backends-infra--terminal-resize-handler))
         (enabled (and ai-code-backends-infra-prevent-reflow-glitch
                       (eq ai-code-backends-infra-terminal-backend 'eat))))
    (dolist (handler (cl-copy-list ai-code-backends-infra--reflow-advised-handlers))
      (unless (and enabled (eq handler resize-handler))
        (when (advice-member-p #'ai-code-backends-infra--terminal-reflow-filter
                               handler)
          (advice-remove handler
                         #'ai-code-backends-infra--terminal-reflow-filter))
        (setq ai-code-backends-infra--reflow-advised-handlers
              (delq handler ai-code-backends-infra--reflow-advised-handlers))))
    (unless enabled
      (when (advice-member-p #'ai-code-backends-infra--terminal-reflow-filter
                             resize-handler)
        (advice-remove resize-handler
                       #'ai-code-backends-infra--terminal-reflow-filter))
      (setq ai-code-backends-infra--reflow-advised-handlers
            (delq resize-handler ai-code-backends-infra--reflow-advised-handlers)))
    (when (and enabled resize-handler)
      (unless (advice-member-p #'ai-code-backends-infra--terminal-reflow-filter
                               resize-handler)
        (advice-add resize-handler
                    :around
                    #'ai-code-backends-infra--terminal-reflow-filter))
      (cl-pushnew resize-handler ai-code-backends-infra--reflow-advised-handlers
                  :test #'eq))))

(defun ai-code-backends-infra--display-buffer-in-side-window (buffer)
  "Display BUFFER in a side window."
  (let ((window
         (if ai-code-backends-infra-use-side-window
             (let* ((side ai-code-backends-infra-window-side)
                    (display-buffer-alist
                     `((,(regexp-quote (buffer-name buffer))
                        (display-buffer-in-side-window)
                        (side . ,side)
                        (slot . 0)
                        ,@(when (memq side '(left right))
                            `((window-width
                               . ,#'ai-code-backends-infra--fit-side-window-body-width)))
                        ,@(when (memq side '(top bottom))
                            `((window-height . ,ai-code-backends-infra-window-height)))
                        (window-parameters . ((no-delete-other-windows . t)))))))
               (display-buffer buffer))
           (display-buffer buffer))))
    (setq ai-code-backends-infra--last-accessed-buffer buffer)
    (when (and window ai-code-backends-infra-focus-on-open)
      (select-window window))
    ;; Sync terminal dimensions with the actual window size.
    ;; The buffer may have been created with different dimensions before
    ;; being displayed in this window.
    (when (and window (buffer-live-p buffer))
      (ai-code-backends-infra--sync-terminal-dimensions buffer window))
    window))

(defun ai-code-backends-infra--fit-side-window-body-width (window)
  "Resize WINDOW so its body width matches `ai-code-backends-infra-window-width'."
  (let ((delta (- ai-code-backends-infra-window-width
                  (window-body-width window))))
    (unless (zerop delta)
      (window-resize window delta t))))

(defun ai-code-backends-infra--sync-terminal-dimensions (buffer window)
  "Sync terminal dimensions in BUFFER to match WINDOW size.
This ensures the terminal process has the correct dimensions after
the buffer has been displayed in its final window, which may differ
from the window where it was initially created."
  (when (and buffer window (buffer-live-p buffer) (window-live-p window))
    (with-current-buffer buffer
      (when-let ((proc (get-buffer-process buffer)))
        (let ((backend (ai-code-backends-infra--current-terminal-backend))
              (windows (or (get-buffer-window-list buffer nil t)
                           (list window))))
          (if (eq backend 'ghostel)
              (when-let ((size (funcall (ai-code-backends-infra-ghostel-resize-handler)
                                        proc
                                        windows)))
                (set-process-window-size proc (cdr size) (car size)))
            (pcase backend
              ('vterm
               (let ((result
                      (funcall (ai-code-backends-infra--terminal-resize-handler
                                'vterm)
                               proc windows)))
                 (when result
                   (ai-code-backends-infra-vterm-flush-render-queue buffer))
                 result))
              (_
               (set-process-window-size proc
                                        (window-body-height window)
                                        (window-body-width window))))))))))

;;; Session Helpers

(defun ai-code-backends-infra--session-working-directory ()
  "Return the working directory, preferring the current project root."
  (ai-code--session-project-root))

(defun ai-code-backends-infra--normalize-session-directory (directory)
  "Return DIRECTORY normalized for robust session matching."
  (file-name-as-directory (expand-file-name directory)))

(defun ai-code-backends-infra--normalize-file-path (file)
  "Return normalized absolute path for FILE."
  (expand-file-name file))

(declare-function magit-get-current-branch "magit-git" ())

(defun ai-code-backends-infra--default-instance-name (&optional existing-instance-names)
  "Return the default instance name for the current buffer, or nil.
When the current git branch is available and not in EXISTING-INSTANCE-NAMES,
use the branch name.  Otherwise fall back to the prompt buffer filename."
  (when (and (eq major-mode 'ai-code-prompt-mode)
             (stringp buffer-file-name)
             (> (length buffer-file-name) 0))
    (let ((branch (ignore-errors (magit-get-current-branch))))
      (if (and branch (not (member branch existing-instance-names)))
          branch
        (file-name-nondirectory buffer-file-name)))))

(defun ai-code-backends-infra--file-session-map-key (prefix source-buffer)
  "Return file-session map key for PREFIX and SOURCE-BUFFER."
  (when (and prefix (buffer-live-p source-buffer))
    (with-current-buffer source-buffer
      (when (and (stringp buffer-file-name)
                 (> (length buffer-file-name) 0))
        (cons prefix
              (ai-code-backends-infra--normalize-file-path buffer-file-name))))))

(defun ai-code-backends-infra--remember-file-session-buffer (prefix source-buffer session-buffer)
  "Remember SESSION-BUFFER as attached session for SOURCE-BUFFER and PREFIX."
  (when-let ((key (ai-code-backends-infra--file-session-map-key prefix source-buffer)))
    (if (and session-buffer (buffer-live-p session-buffer))
        (puthash key session-buffer ai-code-backends-infra--file-session-map)
      (remhash key ai-code-backends-infra--file-session-map))))

(defun ai-code-backends-infra--attached-file-session (prefix source-buffer working-dir)
  "Return attached session state for PREFIX and SOURCE-BUFFER.
WORKING-DIR is the directory used to validate compatible sessions.
Return a cons of (BUFFER . MISSING-P)."
  (let ((key (ai-code-backends-infra--file-session-map-key prefix source-buffer)))
    (if (null key)
        (cons nil nil)
      (let* ((attached (gethash key ai-code-backends-infra--file-session-map))
             (valid (and (buffer-live-p attached)
                         (or (ai-code-backends-infra--parse-session-buffer-name
                             (buffer-name attached)
                              prefix)
                             (ai-code-backends-infra--session-buffer-matches-directory-p
                              attached
                              working-dir)))))
        (cond
         (valid
          (cons attached nil))
         (attached
          (remhash key ai-code-backends-infra--file-session-map)
          (cons nil t))
         (t
          (cons nil nil)))))))

(defun ai-code-backends-infra--resolve-session-buffer (buffer-name missing-message prefix working-dir
                                                                  force-prompt source-buffer)
  "Resolve session buffer using BUFFER-NAME or selection rules.
MISSING-MESSAGE is used when no target session exists.
When PREFIX and WORKING-DIR are present, prefer the attached session for
SOURCE-BUFFER unless FORCE-PROMPT is non-nil."
  (let* ((file-session-key (and prefix
                                source-buffer
                                (ai-code-backends-infra--file-session-map-key
                                 prefix
                                 source-buffer)))
         (attached-state (and prefix working-dir
                              (ai-code-backends-infra--attached-file-session
                               prefix
                               source-buffer
                               working-dir)))
         (attached-buffer (car-safe attached-state))
         (attached-missing (cdr-safe attached-state))
         (needs-initial-file-selection (and (null buffer-name)
                                            file-session-key
                                            (null attached-buffer)
                                            (not attached-missing)))
         (effective-force-prompt
          (or force-prompt
              attached-missing
              needs-initial-file-selection))
         (buffer (or (and buffer-name (get-buffer buffer-name))
                     (and attached-buffer (not force-prompt) attached-buffer)
                     (and prefix working-dir
                          (let ((ai-code-backends-infra--preferred-session-buffer
                                 attached-buffer))
                            (ai-code-backends-infra--select-session-buffer
                             prefix
                             working-dir
                             effective-force-prompt))))))
    (when (and attached-missing (null buffer-name))
      (message "Attached AI session for this file no longer exists. Please select a target session again."))
    (if buffer
        (progn
          (ai-code-backends-infra--remember-session-buffer prefix working-dir buffer)
          (ai-code-backends-infra--remember-file-session-buffer prefix source-buffer buffer)
          buffer)
      (user-error "%s" missing-message))))

(defun ai-code-backends-infra--set-session-directory (buffer directory)
  "Store DIRECTORY on BUFFER for exact session matching."
  (when (and (buffer-live-p buffer) (stringp directory))
    (with-current-buffer buffer
      (setq-local ai-code-backends-infra--session-directory
                  (ai-code-backends-infra--normalize-session-directory directory)))))

(defun ai-code-backends-infra--buffer-session-directory (buffer)
  "Return BUFFER session directory, using legacy `default-directory' as fallback."
  (when (buffer-live-p buffer)
    (with-current-buffer buffer
      (cond
       ((and (stringp ai-code-backends-infra--session-directory)
             (> (length ai-code-backends-infra--session-directory) 0))
        (ai-code-backends-infra--normalize-session-directory
         ai-code-backends-infra--session-directory))
        ((and (stringp default-directory)
              (> (length default-directory) 0))
         (ai-code-backends-infra--normalize-session-directory
          default-directory))
        (t nil)))))

(defun ai-code-backends-infra--source-task-file (source-buffer)
  "Return the AI task file associated with SOURCE-BUFFER, or nil."
  (when (buffer-live-p source-buffer)
    (with-current-buffer source-buffer
      (when (and (stringp buffer-file-name)
                 (string-suffix-p ".org" buffer-file-name)
                 (or (eq major-mode 'ai-code-prompt-mode)
                     (string-match-p "/\\.ai\\.code\\.files/" buffer-file-name)))
        (expand-file-name buffer-file-name)))))

(defun ai-code-backends-infra--sync-session-registry (buffer working-dir prefix
                                                             &optional task-file)
  "Register or update BUFFER in the AI session registry.
WORKING-DIR is the session root.  PREFIX identifies the backend.
TASK-FILE records the originating AI task file when available."
  (when (buffer-live-p buffer)
    (let ((resolved-task-file
           (or task-file
               (with-current-buffer buffer
                 ai-code-backends-infra--session-task-file))))
      (with-current-buffer buffer
        (setq-local ai-code-backends-infra--session-prefix prefix)
        (setq-local ai-code-backends-infra--session-task-file resolved-task-file))
      (ai-code-session-register
       :buffer buffer
       :backend prefix
       :repo-root working-dir
       :task-file resolved-task-file))))

(defun ai-code-backends-infra--session-buffer-name (prefix directory &optional instance-name)
  "Return a session buffer name for PREFIX in DIRECTORY.
When INSTANCE-NAME is non-nil and not \"default\", include it in the name."
  (let* ((base (file-name-nondirectory (directory-file-name directory)))
         (instance (and instance-name
                        (not (string= instance-name ""))
                        (not (string= instance-name "default"))
                        instance-name)))
    (format "*%s[%s%s]*"
            prefix
            base
            (if instance (format ":%s" instance) ""))))

(defun ai-code-backends-infra--normalize-instance-name (instance-name)
  "Return a normalized INSTANCE-NAME, defaulting to \"default\"."
  (if (and instance-name (not (string= instance-name "")))
      instance-name
    "default"))

(defun ai-code-backends-infra--session-key (directory instance-name)
  "Return a session key for DIRECTORY and INSTANCE-NAME."
  (cons directory (ai-code-backends-infra--normalize-instance-name instance-name)))

(defun ai-code-backends-infra--session-map-key (prefix directory)
  "Return a map key for PREFIX and DIRECTORY."
  (cons prefix (expand-file-name directory)))

(defun ai-code-backends-infra--parse-session-buffer-name (buffer-name prefix)
  "Parse BUFFER-NAME for PREFIX.
Return a cons of (base-name . instance-name) or nil."
  (when (string-match
         (format "\\`\\*%s\\[\\([^]:]+\\)\\(?::\\([^]]+\\)\\)?\\]\\*\\'"
                 (regexp-quote prefix))
         buffer-name)
    (cons (match-string 1 buffer-name)
          (match-string 2 buffer-name))))

(defun ai-code-backends-infra--session-instance-name (buffer-name prefix)
  "Return instance name for BUFFER-NAME with PREFIX."
  (when-let ((parsed (ai-code-backends-infra--parse-session-buffer-name buffer-name prefix)))
    (ai-code-backends-infra--normalize-instance-name (cdr parsed))))

(defun ai-code-backends-infra--find-session-buffers (prefix directory)
  "Return session buffers for PREFIX in DIRECTORY."
  (let ((base (file-name-nondirectory (directory-file-name directory)))
        (target-directory (ai-code-backends-infra--normalize-session-directory directory)))
    (cl-remove-if-not
     (lambda (buf)
       (when-let ((parsed (ai-code-backends-infra--parse-session-buffer-name
                           (buffer-name buf)
                           prefix)))
         (if-let ((buffer-directory (ai-code-backends-infra--buffer-session-directory buf)))
             (string= (ai-code-backends-infra--normalize-session-directory buffer-directory)
                      target-directory)
           (string= (car parsed) base))))
     (buffer-list))))

(defun ai-code-backends-infra--remember-session-buffer (prefix directory buffer)
  "Remember BUFFER as the last selected session for PREFIX and DIRECTORY."
  (when (and prefix directory buffer)
    (puthash (ai-code-backends-infra--session-map-key prefix directory)
             buffer
             ai-code-backends-infra--directory-buffer-map)))

(defun ai-code-backends-infra--forget-session-buffer (prefix directory buffer)
  "Forget BUFFER if it's the remembered session for PREFIX and DIRECTORY."
  (when (and prefix directory buffer)
    (let* ((key (ai-code-backends-infra--session-map-key prefix directory))
           (existing (gethash key ai-code-backends-infra--directory-buffer-map)))
      (when (eq existing buffer)
        (remhash key ai-code-backends-infra--directory-buffer-map)))))

(defun ai-code-backends-infra--session-buffer-matches-directory-p (buffer directory)
  "Return non-nil when BUFFER is live and still belongs to DIRECTORY."
  (and (buffer-live-p buffer)
       (when-let ((buffer-directory
                   (ai-code-backends-infra--buffer-session-directory buffer)))
         (string=
          (ai-code-backends-infra--normalize-session-directory buffer-directory)
          (ai-code-backends-infra--normalize-session-directory directory)))))

(defun ai-code-backends-infra--select-session-buffer (prefix directory &optional force-prompt)
  "Select a session buffer for PREFIX in DIRECTORY.
FORCE-PROMPT means always prompt even if a session was remembered.
Returns the selected buffer or nil if none exist."
  (let* ((remembered (gethash (ai-code-backends-infra--session-map-key prefix directory)
                              ai-code-backends-infra--directory-buffer-map))
         (buffers (ai-code-backends-infra--find-session-buffers prefix directory)))
    (when (and remembered
               (ai-code-backends-infra--session-buffer-matches-directory-p
                remembered
                directory)
               (not (memq remembered buffers)))
      (push remembered buffers))
    (cond
     ((null buffers) nil)
     ((= (length buffers) 1)
      (ai-code-backends-infra--remember-session-buffer prefix directory (car buffers))
      (car buffers))
     (t
      (let* ((preferred (if (memq ai-code-backends-infra--preferred-session-buffer buffers)
                            ai-code-backends-infra--preferred-session-buffer
                          (and (memq remembered buffers) remembered)))
             (ordered-buffers (if preferred
                                  (cons preferred (delq preferred (copy-sequence buffers)))
                                buffers))
             (choices (delq nil
                            (mapcar (lambda (buf)
                                      (when-let ((instance
                                                  (ai-code-backends-infra--session-instance-name
                                                   (buffer-name buf)
                                                   prefix)))
                                        (cons instance buf)))
                                    ordered-buffers)))
             (candidates (mapcar #'car choices))
             (default-candidate (car candidates)))
        (if (and (not force-prompt) remembered (memq remembered buffers))
            remembered
          (let ((selection (completing-read
                            (format "Select %s session: " prefix)
                            candidates
                            nil t nil nil default-candidate)))
            (let ((buffer (cdr (assoc selection choices))))
              (ai-code-backends-infra--remember-session-buffer prefix directory buffer)
              buffer))))))))

(defun ai-code-backends-infra--prompt-for-instance-name (existing-instance-names
                                                         &optional force-prompt
                                                         default-instance-name)
  "Prompt for a new instance name.
EXISTING-INSTANCE-NAMES is a list of existing instance names.
If FORCE-PROMPT is nil and there are no existing instances, return \"default\".
DEFAULT-INSTANCE-NAME seeds the minibuffer when prompting."
  (if (or existing-instance-names force-prompt)
      (let ((proposed-name ""))
        (while (or (string= proposed-name "")
                   (member proposed-name existing-instance-names))
          (setq proposed-name
                (read-string (if existing-instance-names
                                 (format "Instance name (existing: %s): "
                                         (mapconcat #'identity existing-instance-names ", "))
                                "Instance name: ")
                             nil nil (or (and (> (length proposed-name) 0) proposed-name)
                                         default-instance-name)))
          (cond
           ((string= proposed-name "")
            (message "Instance name cannot be empty. Please enter a name.")
            (sit-for 1))
           ((member proposed-name existing-instance-names)
            (message "Instance name '%s' already exists. Please choose a different name." proposed-name)
            (sit-for 1))))
        proposed-name)
    "default"))

(defun ai-code-backends-infra--resolve-start-command (program switches arg &optional prompt-label)
  "Build command string for PROGRAM.
SWITCHES is the default command-line argument list.
Use it as default input when ARG is non-nil and CLI args are prompted.
PROMPT-LABEL is used in the minibuffer prompt.
When resuming and the active region contains a UUID, prompt as though ARG
were non-nil and append that UUID to the default CLI args."
  (let* ((found-resume-switch
          (cl-some (lambda (switch)
                     (member switch '("resume" "--resume")))
                   switches))
         (selected-session-id
          (and (null arg)
               found-resume-switch
               (ai-code-backends-infra--selected-session-id)))
         (prompt-p (or arg selected-session-id))
         (default-args (mapconcat #'identity
                                   (append switches
                                           (and selected-session-id
                                                (list selected-session-id)))
                                   " "))
         (prompt (format "%s args: " (or prompt-label "CLI")))
         (prompt-args (when prompt-p
                           (read-string prompt default-args 'ai-code-cli-args-history)))
         (resolved-args (if prompt-p
                            (split-string-shell-command prompt-args)
                          switches))
         (command (mapconcat #'identity
                             (cons program resolved-args)
                             " ")))
    (list :command command :args resolved-args)))

(defun ai-code-backends-infra--cleanup-session (directory buffer-name process-table
                                                          &optional instance-name prefix event)
  "Clean up a session for DIRECTORY using BUFFER-NAME and PROCESS-TABLE.
INSTANCE-NAME and PREFIX identify the session map entry to remove.
EVENT is the process sentinel event string.  When EVENT is non-nil and does
not start with \"finished\", the buffer is preserved so the user can inspect
any error output left behind by the CLI."
  (let* ((resolved-instance (or instance-name
                                (and prefix
                                     (ai-code-backends-infra--session-instance-name
                                      buffer-name
                                      prefix))
                                 "default"))
         (key (ai-code-backends-infra--session-key directory resolved-instance)))
    (remhash key process-table))
  (when-let ((buffer (get-buffer buffer-name)))
    (ai-code-backends-infra--forget-session-buffer prefix directory buffer)
    (ai-code-session-unregister buffer)
    (when (buffer-live-p buffer)
      (when (or (null event)
                (string-prefix-p "finished" event))
        (kill-buffer buffer)))))

(defun ai-code-backends-infra--resolve-session-target (working-dir buffer-name
                                                                   prefix instance-name
                                                                   force-prompt)
  "Return resolved session target information.
WORKING-DIR is the session directory.
BUFFER-NAME is the explicit terminal buffer name, when provided.
PREFIX enables instance-based naming.
INSTANCE-NAME overrides interactive instance selection when non-nil.
FORCE-PROMPT forces instance prompting when PREFIX is non-nil.
Return a plist with :instance-name, :buffer-name, and :session-key."
  (let* ((existing-buffers (and prefix
                                (ai-code-backends-infra--find-session-buffers
                                 prefix
                                 working-dir)))
         (existing-instance-names (mapcar (lambda (buf)
                                            (ai-code-backends-infra--session-instance-name
                                             (buffer-name buf)
                                             prefix))
                                          existing-buffers))
         (resolved-instance (cond
                             (instance-name
                              (ai-code-backends-infra--normalize-instance-name instance-name))
                             (prefix
                              (ai-code-backends-infra--prompt-for-instance-name
                               existing-instance-names
                               force-prompt
                               (ai-code-backends-infra--default-instance-name
                                existing-instance-names)))
                             (t "default")))
         (resolved-buffer-name (or buffer-name
                                    (and prefix
                                         (ai-code-backends-infra--session-buffer-name
                                          prefix
                                         working-dir
                                         resolved-instance)))))
    (list :instance-name resolved-instance
          :buffer-name resolved-buffer-name
          :session-key (ai-code-backends-infra--session-key
                        working-dir
                        resolved-instance))))

(defun ai-code-backends-infra--resolve-session-context (working-dir buffer-name
                                                                    process-table
                                                                    prefix instance-name
                                                                    force-prompt)
  "Return resolved session context for session lifecycle operations.
WORKING-DIR is the session directory.
BUFFER-NAME is the explicit terminal buffer name, when provided.
PROCESS-TABLE maps session keys to processes.
PREFIX enables instance-based naming.
INSTANCE-NAME overrides interactive instance selection when non-nil.
FORCE-PROMPT forces instance prompting when PREFIX is non-nil.
Return a plist with target information plus the current buffer and process."
  (let* ((session-target (ai-code-backends-infra--resolve-session-target
                          working-dir
                          buffer-name
                          prefix
                          instance-name
                          force-prompt))
         (session-key (plist-get session-target :session-key))
         (resolved-buffer-name (plist-get session-target :buffer-name)))
    (append session-target
            (list :buffer (get-buffer resolved-buffer-name)
                  :existing-process (gethash session-key process-table)))))

(defun ai-code-backends-infra--reuse-session-window (buffer working-dir
                                                            prefix multiline-input-sequence
                                                            &optional task-file)
  "Toggle visibility for an existing session BUFFER.
WORKING-DIR, PREFIX, MULTILINE-INPUT-SEQUENCE, and TASK-FILE refresh session
state.
When BUFFER is already visible, close its window.
Otherwise refresh session-local state and display it."
  (if (get-buffer-window buffer)
      (delete-window (get-buffer-window buffer))
    (ai-code-backends-infra--set-session-directory buffer working-dir)
    (ai-code-backends-infra--configure-session-buffer
     buffer nil multiline-input-sequence)
    (ai-code-backends-infra--remember-session-buffer prefix working-dir buffer)
    (ai-code-backends-infra--sync-session-registry
     buffer working-dir prefix task-file)
    (ai-code-backends-infra--display-buffer-in-side-window buffer)))

(defun ai-code-backends-infra--finalize-started-session (buffer process
                                                                 working-dir buffer-name
                                                                 process-table resolved-instance
                                                                 prefix escape-fn cleanup-fn
                                                                 multiline-input-sequence
                                                                 post-start-fn
                                                                 &optional task-file)
  "Finalize a successfully started session BUFFER and PROCESS.
WORKING-DIR, BUFFER-NAME, PROCESS-TABLE, RESOLVED-INSTANCE, and PREFIX
identify the session for cleanup and reuse.  ESCAPE-FN, CLEANUP-FN,
MULTILINE-INPUT-SEQUENCE, POST-START-FN, and TASK-FILE install session
behavior."
  (let ((previous-sentinel
         (ignore-errors
           (process-get process 'ai-code-backends-infra--ghostel-sentinel))))
    (set-process-sentinel
     process
     (lambda (proc event)
       (when previous-sentinel
         (ignore-errors
           (funcall previous-sentinel proc event)))
       (ai-code-backends-infra--cleanup-session
        working-dir
        buffer-name
        process-table
        resolved-instance
        prefix
        event)
       (when cleanup-fn
         (funcall cleanup-fn)))))
  (ai-code-backends-infra--configure-session-buffer
   buffer escape-fn multiline-input-sequence)
  (when post-start-fn
    (funcall post-start-fn buffer process resolved-instance))
  (with-current-buffer buffer
    (add-hook 'kill-buffer-hook
              (lambda ()
                (ai-code-session-unregister (current-buffer))
                (ai-code-backends-infra--forget-session-buffer
                 prefix
                 working-dir
                 (current-buffer)))
              nil t))
  (ai-code-backends-infra--remember-session-buffer prefix working-dir buffer)
  (ai-code-backends-infra--sync-session-registry
   buffer working-dir prefix task-file)
  (ai-code-backends-infra--display-buffer-in-side-window buffer))

(defun ai-code-backends-infra--handle-session-start-failure (buffer session-key process-table)
  "Handle startup failure for BUFFER and SESSION-KEY in PROCESS-TABLE."
  (remhash session-key process-table)
  (if (buffer-live-p buffer)
      (progn
        (pop-to-buffer buffer)
        (message "CLI failed to start - see buffer for error details"))
    (message "CLI failed to start - process exited immediately")))

(defun ai-code-backends-infra--toggle-or-create-session (working-dir buffer-name process-table command
                                                                     &optional escape-fn cleanup-fn
                                                                     instance-name prefix force-prompt
                                                                     env-vars multiline-input-sequence
                                                                     post-start-fn)
  "Toggle or create a terminal session.
WORKING-DIR is the directory for the session.
BUFFER-NAME is the terminal buffer name.
PROCESS-TABLE maps session keys to processes.
COMMAND is the shell command to run.
ESCAPE-FN is bound to `C-<escape>' inside the session buffer when non-nil.
CLEANUP-FN is called with no arguments when the process exits.
INSTANCE-NAME overrides instance selection when non-nil.
PREFIX enables instance selection when BUFFER-NAME is nil.
When FORCE-PROMPT is non-nil, always prompt for a new instance name.
ENV-VARS is a list of additional environment variable strings, for example
\"VAR=value\", passed to the terminal session on creation.
MULTILINE-INPUT-SEQUENCE configures `S-<return>' and `C-<return>' to send
that sequence inside the session buffer.
POST-START-FN is called with (BUFFER PROCESS INSTANCE-NAME) after a new
session starts successfully."
  (setq process-table (or process-table ai-code-backends-infra--processes))
  (ai-code-backends-infra--cleanup-dead-processes process-table)
  (let* ((source-buffer (current-buffer))
         (task-file (ai-code-backends-infra--source-task-file source-buffer))
         (session-context (ai-code-backends-infra--resolve-session-context
                            working-dir
                            buffer-name
                            process-table
                            prefix
                           instance-name
                           force-prompt))
         (resolved-instance (plist-get session-context :instance-name))
         (resolved-buffer-name (plist-get session-context :buffer-name))
         (session-key (plist-get session-context :session-key))
         (existing-process (plist-get session-context :existing-process))
         (buffer (plist-get session-context :buffer)))
    (if (and existing-process (process-live-p existing-process) buffer)
       (progn
           (ai-code-backends-infra--reuse-session-window
            buffer
            working-dir
            prefix
            multiline-input-sequence
            task-file)
           (ai-code-backends-infra--remember-file-session-buffer
            prefix source-buffer buffer))
      (let* ((buffer-and-process
              (ai-code-backends-infra--create-terminal-session
               resolved-buffer-name working-dir command env-vars))
             (new-buffer (car buffer-and-process))
             (process (cdr buffer-and-process)))
        (puthash session-key process process-table)
        ;; Wait for initialization before checking process status
        (sleep-for ai-code-backends-infra-terminal-initialization-delay)
         ;; Check if process is still alive after initialization delay
         (if (and process (process-live-p process))
             (progn
               (ai-code-backends-infra--finalize-started-session
                new-buffer
                process
                working-dir
                resolved-buffer-name
                process-table
                resolved-instance
                prefix
                escape-fn
                cleanup-fn
                multiline-input-sequence
                post-start-fn
                task-file)
                (ai-code-backends-infra--remember-file-session-buffer
                 prefix source-buffer new-buffer))
           (ai-code-backends-infra--handle-session-start-failure
            new-buffer
            session-key
           process-table))))))

(defun ai-code-backends-infra--switch-to-session-buffer (buffer-name missing-message
                                                                    &optional prefix working-dir force-prompt)
  "Switch to BUFFER-NAME or signal MISSING-MESSAGE.
FORCE-PROMPT means always prompt when PREFIX and WORKING-DIR are provided.
When PREFIX and WORKING-DIR are provided, select from multiple sessions."
  (let* ((source-buffer (current-buffer))
         (buffer (ai-code-backends-infra--resolve-session-buffer
                  buffer-name
                  missing-message
                  prefix
                  working-dir
                  force-prompt
                  source-buffer)))
    (if-let ((window (get-buffer-window buffer)))
        (select-window window)
      (ai-code-backends-infra--display-buffer-in-side-window buffer))))

(defun ai-code-backends-infra--send-line-to-session (buffer-name missing-message line
                                                                &optional prefix working-dir force-prompt)
  "Send LINE to BUFFER-NAME or signal MISSING-MESSAGE.
When PREFIX and WORKING-DIR are provided, select from multiple sessions."
  (let* ((source-buffer (current-buffer))
         (buffer (ai-code-backends-infra--resolve-session-buffer
                  buffer-name
                  missing-message
                  prefix
                  working-dir
                  force-prompt
                  source-buffer)))
    (with-current-buffer buffer
      (ai-code-backends-infra--remember-session-buffer prefix working-dir buffer)
      (ai-code-backends-infra--terminal-send-string line)
      (sit-for 0.5) ;; 0.1 might be too low for some cli backends such as github copilot cli
      (ai-code-backends-infra--terminal-send-return))))

;;; Generic Session Creation

(defun ai-code-backends-infra--create-terminal-session (buffer-name working-dir command env-vars)
  "Generic function to create a terminal session.
BUFFER-NAME is the name for the buffer.
WORKING-DIR is the directory.
COMMAND is the shell command to run.
ENV-VARS is a list of environment variables."
  (ai-code-backends-infra--terminal-ensure-backend)
  (pcase ai-code-backends-infra-terminal-backend
    ('vterm
     (ai-code-backends-infra-vterm-create-session
      buffer-name working-dir command env-vars))
    ('eat
     (ai-code-backends-infra-eat-create-session
      buffer-name working-dir command env-vars))
    ('ghostel
     (ai-code-backends-infra-ghostel-create-session
      buffer-name working-dir command env-vars))
    (_ (error "Unknown backend"))))

(defun ai-code-backends-infra--cleanup-dead-processes (table)
  "Clean up dead processes from TABLE."
  (maphash (lambda (key proc)
             (unless (process-live-p proc)
               (remhash key table)))
           table))

;;; Evil Mode Integration

(defvar ai-code-backends-infra--evil-original-spc-command nil
  "Original command for SPC in `evil-normal-state-map'.")

(declare-function ai-code-send-command "ai-code" (&optional arg))

(defun ai-code-backends-infra--evil-spc-command ()
  "In AI session buffers, run `ai-code-send-command'.
Otherwise, run the original command for SPC."
  (interactive)
  (if (ai-code-backends-infra--session-buffer-p (current-buffer))
      (call-interactively #'ai-code-send-command)
    (when ai-code-backends-infra--evil-original-spc-command
      (call-interactively ai-code-backends-infra--evil-original-spc-command))))

;;;###autoload
(defun ai-code-backends-infra-evil-setup ()
  "Setup AI Code integration with Evil mode.
This function configures SPC key binding in Evil normal state for
AI session buffers.  Call this function after Evil is loaded,
typically in your Emacs configuration with:
  (with-eval-after-load \\='evil (ai-code-backends-infra-evil-setup))"
  (interactive)
  (when (and (featurep 'evil) (boundp 'evil-normal-state-map))
    (unless ai-code-backends-infra--evil-original-spc-command
      (setq ai-code-backends-infra--evil-original-spc-command
            (lookup-key evil-normal-state-map (kbd "SPC"))))
    (define-key evil-normal-state-map (kbd "SPC")
                #'ai-code-backends-infra--evil-spc-command)))

(require 'ai-code-backends-infra-etc)
(ai-code-backends-infra-etc-activate)

(provide 'ai-code-backends-infra)
;;; ai-code-backends-infra.el ends here
