diff --git a/ellie.el b/ellie.el
index 181b3b2..9b5f76a 100644
--- a/ellie.el
+++ b/ellie.el
@@ -562,52 +562,6 @@ Attaches to the last session if it still exists; otherwise creates a new one."
(defvar ellie--complete-timer nil
"Timer for polling completion result.")
-(defun ellie--complete-session-id ()
- "Return the deterministic copilot session ID for `default-directory'."
- (let* ((cwd (expand-file-name default-directory))
- ;; Emulate POSIX cksum: use the same CRC but just take the numeric hash.
- ;; We shell out once to stay consistent with the acme script.
- (hash (string-trim
- (shell-command-to-string
- (format "printf '%%s' %s | cksum | awk '{print $1}'"
- (shell-quote-argument cwd))))))
- (concat hash "-copilot")))
-
-(defun ellie--complete-ensure-session ()
- "Find or create the copilot session for `default-directory'.
-Returns the session directory path."
- (when (string-empty-p ellie-complete-backend)
- (user-error "Set `ellie-complete-backend' or $OLLIE_COMPLETE_BACKEND"))
- (when (string-empty-p ellie-complete-model)
- (user-error "Set `ellie-complete-model' or $OLLIE_COMPLETE_MODEL"))
- (let* ((id (ellie--complete-session-id))
- (sdir (expand-file-name id (ellie--sessions-dir))))
- (if (file-directory-p sdir)
- ;; Check for failed state; kill and recreate if needed.
- (let ((state (ellie--complete-state sdir)))
- (when (or (string-prefix-p "failed" state)
- (string-prefix-p "error" state))
- (ignore-errors (ellie--fwrite (expand-file-name "ctl" sdir) "kill\n"))
- (sleep-for 0.1)
- (ellie--complete-create-session id))
- sdir)
- (ellie--complete-create-session id)
- sdir)))
-
-(defun ellie--complete-create-session (id)
- "Create a copilot session with ID."
- (let ((spec (format "name=%s\ncwd=%s\nagent=copilot\nbackend=%s\nmodel=%s\n"
- id (expand-file-name default-directory)
- ellie-complete-backend ellie-complete-model)))
- (ellie--fwrite (ellie--sessions-new-path) spec)
- (let ((sdir (expand-file-name id (ellie--sessions-dir)))
- (deadline (+ (float-time) 5.0)))
- (while (and (not (file-directory-p sdir))
- (< (float-time) deadline))
- (sleep-for 0.05))
- (unless (file-directory-p sdir)
- (user-error "Timeout waiting for copilot session %s" id)))))
-
(defun ellie--complete-strip (text)
"Strip markdown fences and info lines from TEXT."
(let ((lines (split-string text "\n")))
@@ -617,25 +571,6 @@ Returns the session directory path."
lines))
(string-join lines "\n")))
-(defun ellie--complete-state (sdir)
- "Read the session state from cfg in SDIR."
- (let ((cfg (ellie--fread (expand-file-name "cfg" sdir))))
- (if (and cfg (string-match "^state=\\(.*\\)$" cfg))
- (string-trim (match-string 1 cfg))
- "unknown")))
-
-(defun ellie--read-from-byte-offset (path offset)
- "Read PATH and return content starting at byte OFFSET as a string."
- (if (or (<= offset 0) (not (file-exists-p path)))
- (or (ellie--fread path) "")
- (with-temp-buffer
- (set-buffer-multibyte nil)
- (insert-file-contents-literally path)
- (let ((raw (buffer-substring-no-properties
- (min (1+ offset) (point-max))
- (point-max))))
- (decode-coding-string raw 'utf-8)))))
-
;; Ghost text state.
(defvar ellie--ghost-overlay nil "Overlay showing the ghost completion.")
(defvar ellie--ghost-text nil "The pending ghost text string.")
@@ -675,15 +610,12 @@ If REGION is a cons (BEG . END), the ghost replaces that region on accept."
ellie--ghost-overlay ov)
(overlay-put ov 'display (propertize text 'face 'ellie-ghost))
(overlay-put ov 'ellie-ghost t))
- ;; No region: show ghost on next line.
- (save-excursion
- (goto-char pos)
- (end-of-line)
- (setq ellie--ghost-pos (point))
- (setq ellie--ghost-overlay (make-overlay (point) (point) buf))
- (overlay-put ellie--ghost-overlay 'after-string
- (propertize (concat "\n" text) 'face 'ellie-ghost))
- (overlay-put ellie--ghost-overlay 'ellie-ghost t)))
+ ;; No region: show ghost inline at point.
+ (setq ellie--ghost-pos pos)
+ (setq ellie--ghost-overlay (make-overlay pos pos buf))
+ (overlay-put ellie--ghost-overlay 'after-string
+ (propertize text 'face 'ellie-ghost))
+ (overlay-put ellie--ghost-overlay 'ellie-ghost t))
(ellie-ghost-mode 1)
(add-hook 'pre-command-hook #'ellie--ghost-pre-command nil t))))
@@ -710,10 +642,8 @@ If REGION is a cons (BEG . END), the ghost replaces that region on accept."
(delete-region (car region) (cdr region))
(goto-char (car region))
(insert text))
- (save-excursion
- (goto-char pos)
- (insert "\n" text))
- (goto-char (+ pos 1 (length text)))))))
+ (goto-char pos)
+ (insert text)))))
(defun ellie--ghost-accept-cmd ()
"Interactive command to accept ghost text."
@@ -727,14 +657,17 @@ If REGION is a cons (BEG . END), the ghost replaces that region on accept."
;;;###autoload
(defun ellie-complete ()
- "Request AI code completion and show it as ghost text on the next line.
+ "Request AI code completion and show it as ghost text.
\\`TAB' accepts the suggestion; any other key dismisses it.
Calling again while a suggestion is showing requests a new one."
(interactive)
+ (when (string-empty-p ellie-complete-backend)
+ (user-error "Set `ellie-complete-backend' or $OLLIE_COMPLETE_BACKEND"))
+ (when (string-empty-p ellie-complete-model)
+ (user-error "Set `ellie-complete-model' or $OLLIE_COMPLETE_MODEL"))
(let* ((prev-region ellie--ghost-region))
(ellie--ghost-dismiss)
- (let* ((sdir (ellie--complete-ensure-session))
- (filename (or (buffer-file-name) (buffer-name)))
+ (let* ((filename (or (buffer-file-name) (buffer-name)))
(has-region (or (use-region-p) prev-region))
(reg-beg (cond ((use-region-p) (region-beginning))
(prev-region (car prev-region))))
@@ -744,56 +677,43 @@ Calling again while a suggestion is showing requests a new one."
(prefix (buffer-substring-no-properties
(max (point-min) (- ctx-point ellie-complete-prefix-max))
ctx-point))
- (selected (if has-region
- (buffer-substring-no-properties reg-beg reg-end)
- ""))
(suffix (buffer-substring-no-properties
(if has-region reg-end (point))
(min (point-max) (+ (if has-region reg-end (point))
ellie-complete-suffix-max))))
- (prompt (if has-region
- (format "Replace the selected code in %s. Output ONLY the replacement text — no markdown fences, no explanation.\n\n\n%s\n\n\n%s\n\n\n%s\n"
- (file-name-nondirectory filename) prefix selected suffix)
- (format "Complete the code at the cursor in %s. Output ONLY the raw text to insert at the cursor position. Do NOT wrap the output in markdown code fences or backticks. No explanation, no preamble.\n\n\n%s\n\n\n%s\n"
- (file-name-nondirectory filename) prefix suffix)))
- (cur-point (point))
- (region-cons (and has-region (cons reg-beg reg-end)))
- (buf (current-buffer)))
- (when has-region (deactivate-mark))
- (message "ellie: completing...")
- (ellie--fwrite (expand-file-name "prompt" sdir) prompt)
- (when ellie--complete-timer (cancel-timer ellie--complete-timer))
- (let ((deadline (+ (float-time) 30.0)))
- (setq ellie--complete-timer
- (run-with-timer 0.3 0.3
- (lambda ()
- (condition-case err
- (let ((state (ellie--complete-state sdir)))
- (cond
- ((string= state "idle")
- (cancel-timer ellie--complete-timer)
- (setq ellie--complete-timer nil)
- (let* ((offset (string-to-number
- (or (ellie--fread
- (expand-file-name "offset" sdir))
- "0")))
- (chat-path (expand-file-name "chat" sdir))
- (result (string-trim-right
- (ellie--complete-strip
- (ellie--read-from-byte-offset chat-path offset)))))
- (ellie--fwrite (expand-file-name "ctl" sdir) "clear\n")
- (if (string-empty-p (string-trim result))
- (message "ellie: empty completion")
- (ellie--ghost-show result cur-point buf region-cons)
- (message "ellie: TAB/RET accept · M-/ retry · any key dismiss"))))
- ((> (float-time) deadline)
- (cancel-timer ellie--complete-timer)
- (setq ellie--complete-timer nil)
- (message "ellie: completion timed out"))))
- (error
- (cancel-timer ellie--complete-timer)
- (setq ellie--complete-timer nil)
- (message "ellie: completion error: %s" err))))))))))
+ (cur-point (point))
+ (region-cons (and has-region (cons reg-beg reg-end)))
+ (buf (current-buffer))
+ (ucomplete (expand-file-name "u/complete" ellie-mount-directory)))
+ (when has-region (deactivate-mark))
+ (message "ellie: completing...")
+ (let* ((process-environment
+ (append (list (concat "OLLIE=" ellie-mount-directory))
+ process-environment))
+ (cmd (format "%s -file %s -suffix %s -model %s -backend %s"
+ (shell-quote-argument ucomplete)
+ (shell-quote-argument (file-name-nondirectory filename))
+ (shell-quote-argument suffix)
+ (shell-quote-argument ellie-complete-model)
+ (shell-quote-argument ellie-complete-backend)))
+ (proc-buf (generate-new-buffer " *ellie-complete*")))
+ (when ellie--complete-timer (cancel-timer ellie--complete-timer))
+ (set-process-sentinel
+ (start-process-shell-command "ellie-complete" proc-buf
+ (format "printf '%%s' %s | %s"
+ (shell-quote-argument prefix) cmd))
+ (lambda (proc _event)
+ (when (eq (process-status proc) 'exit)
+ (let ((result (string-trim-right
+ (ellie--complete-strip
+ (with-current-buffer (process-buffer proc)
+ (buffer-string))))))
+ (kill-buffer (process-buffer proc))
+ (if (or (not (zerop (process-exit-status proc)))
+ (string-empty-p (string-trim result)))
+ (message "ellie: empty completion")
+ (ellie--ghost-show result cur-point buf region-cons)
+ (message "ellie: TAB/RET accept · M-/ retry · any key dismiss"))))))))))
;;;###autoload
(define-key global-map (kbd "M-/") #'ellie-complete)