Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 96 additions & 35 deletions pi-coding-agent-menu.el
Original file line number Diff line number Diff line change
Expand Up @@ -204,25 +204,79 @@ from either chat or input buffer."
"Format OBJ's current thinking-display value for the transient menu."
(propertize (symbol-name (oref obj value)) 'face 'transient-value))

(defun pi-coding-agent--new-session-ready-p (chat-buf)
"Return non-nil when CHAT-BUF can safely start a fresh session.
Server-owned streaming may be reset deliberately; unresolved local ownership
and another transition may not be discarded."
(with-current-buffer chat-buf
(cond
((pi-coding-agent--session-transition-active-p)
(message "Pi: Cannot start a new session while session is switching")
nil)
((pi-coding-agent--prompt-start-wait-active-p)
(message "Pi: Cannot start a new session while prompt acceptance is pending")
nil)
((or pi-coding-agent--followup-queue
(pi-coding-agent--followup-drain-pending-p))
(message "Pi: Cannot start a new session with queued follow-ups")
nil)
(pi-coding-agent--local-user-message
(message "Pi: Wait for pi to echo your prompt before starting a new session")
nil)
(t t))))

;;;###autoload
(defun pi-coding-agent-new-session ()
"Start a new pi session (reset)."
(interactive)
(when-let* ((proc (pi-coding-agent--get-process))
(chat-buf (pi-coding-agent--get-chat-buffer)))
(pi-coding-agent--rpc-async proc '(:type "new_session")
(lambda (response)
(let* ((data (plist-get response :data))
(cancelled (plist-get data :cancelled)))
(if (and (eq (plist-get response :success) t)
(pi-coding-agent--json-false-p cancelled))
(when (buffer-live-p chat-buf)
(with-current-buffer chat-buf
(pi-coding-agent--clear-chat-buffer)
(pi-coding-agent--refresh-header))
(pi-coding-agent--refresh-session-state proc chat-buf)
(message "Pi: New session started"))
(message "Pi: New session cancelled")))))))
(chat-buf (pi-coding-agent--get-chat-buffer))
((pi-coding-agent--new-session-ready-p chat-buf)))
(let ((generation
(with-current-buffer chat-buf
(pi-coding-agent--begin-session-transition proc))))
(condition-case err
(pi-coding-agent--rpc-async
proc '(:type "new_session")
(lambda (response)
(when (pi-coding-agent--session-transition-current-p
chat-buf proc generation)
(condition-case callback-error
(let* ((success (eq (plist-get response :success) t))
(data (plist-get response :data))
(cancelled (plist-get data :cancelled)))
(cond
((and success
(pi-coding-agent--json-false-p cancelled))
(unwind-protect
(with-current-buffer chat-buf
(pi-coding-agent--clear-chat-buffer)
(pi-coding-agent--refresh-header))
(pi-coding-agent--refresh-session-state proc chat-buf))
(message "Pi: New session started"))
(t
(with-current-buffer chat-buf
(pi-coding-agent--finish-session-transition generation))
(if (and success cancelled
(not (pi-coding-agent--json-false-p cancelled)))
(message "Pi: New session cancelled")
(message "Pi: Failed to start new session: %s"
(or (plist-get response :error)
"unknown error"))))))
((error quit)
(when (pi-coding-agent--session-transition-current-p
chat-buf proc generation)
(with-current-buffer chat-buf
(pi-coding-agent--finish-session-transition generation)))
(if (eq (car callback-error) 'quit)
(signal (car callback-error) (cdr callback-error))
(message "Pi: Failed to start new session: %s"
(error-message-string callback-error))))))))
((error quit)
(when (buffer-live-p chat-buf)
(with-current-buffer chat-buf
(pi-coding-agent--finish-session-transition generation)))
(signal (car err) (cdr err)))))))

(defun pi-coding-agent--session-list-directory (&optional chat-buf)
"Return the directory containing CHAT-BUF's current JSONL session file.
Expand Down Expand Up @@ -310,6 +364,7 @@ Call this when starting a new session to ensure no stale state persists."
pi-coding-agent--tool-block-order-counter 0
pi-coding-agent--thinking-block-order-counter 0)
(pi-coding-agent--set-activity-phase "idle" 'reset t)
(pi-coding-agent--clear-local-user-message-region)
(pi-coding-agent--clear-unsupported-extension-ui-warnings)
(pi-coding-agent--invalidate-history-loads)
(pi-coding-agent--finish-session-transition
Expand Down Expand Up @@ -447,28 +502,34 @@ handled, even when the response failed."
(let* ((own-generation (null generation))
(generation (or generation
(pi-coding-agent--begin-session-transition))))
(pi-coding-agent--rpc-async proc '(:type "get_state")
(lambda (response)
(when (pi-coding-agent--session-transition-current-p
chat-buf proc generation)
(unwind-protect
(when (eq (plist-get response :success) t)
(pi-coding-agent--apply-state-response chat-buf response)
(when (buffer-live-p chat-buf)
(condition-case err
(pi-coding-agent--rpc-async proc '(:type "get_state")
(lambda (response)
(when (pi-coding-agent--session-transition-current-p
chat-buf proc generation)
(unwind-protect
(when (eq (plist-get response :success) t)
(pi-coding-agent--apply-state-response chat-buf response)
(when (buffer-live-p chat-buf)
(with-current-buffer chat-buf
(unless session-file
(when-let* ((current-session-file
(plist-get pi-coding-agent--state
:session-file)))
(pi-coding-agent--update-session-name-from-file
current-session-file)))
(force-mode-line-update t))))
(when completion-callback
(funcall completion-callback response))
(when (and own-generation (buffer-live-p chat-buf))
(with-current-buffer chat-buf
(unless session-file
(when-let* ((current-session-file
(plist-get pi-coding-agent--state
:session-file)))
(pi-coding-agent--update-session-name-from-file
current-session-file)))
(force-mode-line-update t))))
(when completion-callback
(funcall completion-callback response))
(when (and own-generation (buffer-live-p chat-buf))
(with-current-buffer chat-buf
(pi-coding-agent--finish-session-transition
generation)))))))))))
(pi-coding-agent--finish-session-transition
generation)))))))
((error quit)
(when (and own-generation (buffer-live-p chat-buf))
(with-current-buffer chat-buf
(pi-coding-agent--finish-session-transition generation)))
(signal (car err) (cdr err))))))))

;;;###autoload
(defun pi-coding-agent-reload ()
Expand Down
47 changes: 38 additions & 9 deletions pi-coding-agent-render.el
Original file line number Diff line number Diff line change
Expand Up @@ -103,15 +103,39 @@ call ID in `pi-coding-agent--live-tool-blocks'.")
(unless (pi-coding-agent--history-postprocessing-deferred-p)
(pi-coding-agent--decorate-tables-in-region start end)))

(defun pi-coding-agent--display-user-message (text &optional timestamp)
(defun pi-coding-agent--display-user-message (text &optional timestamp track-region)
"Display user message TEXT in the chat buffer.
If TIMESTAMP (Emacs time value) is provided, display it in the header."
(let ((start (with-current-buffer (pi-coding-agent--get-chat-buffer) (point-max))))
If TIMESTAMP (Emacs time value) is provided, display it in the header. When
TRACK-REGION is non-nil, return a marker pair bounding the inserted turn."
(let* ((chat-buffer (pi-coding-agent--get-chat-buffer))
(start (with-current-buffer chat-buffer (point-max))))
(pi-coding-agent--append-to-chat
(concat "\n" (pi-coding-agent--make-separator "You" timestamp) "\n"
text "\n"))
(with-current-buffer (pi-coding-agent--get-chat-buffer)
(pi-coding-agent--decorate-tables-unless-deferred start (point-max)))))
(with-current-buffer chat-buffer
(pi-coding-agent--decorate-tables-unless-deferred start (point-max))
(when track-region
(cons (copy-marker start nil) (copy-marker (point-max) nil))))))

(defun pi-coding-agent--discard-local-user-message ()
"Retract the speculative local user turn when pi handled it invisibly."
(unwind-protect
(when-let* ((region pi-coding-agent--local-user-message-region)
(start (marker-position (car region)))
(end (marker-position (cdr region)))
((<= start end)))
(let ((inhibit-read-only t))
(save-restriction
(widen)
(delete-region start end))))
(setq pi-coding-agent--local-user-message nil)
(pi-coding-agent--clear-local-user-message-region)))

(defun pi-coding-agent--handle-no-turn-local-prompt ()
"Release speculative local echo state, then process queued follow-ups."
(unwind-protect
(pi-coding-agent--discard-local-user-message)
(pi-coding-agent--schedule-followup-queue-processing)))

(defun pi-coding-agent--display-agent-start ()
"Display separator for new agent turn.
Expand Down Expand Up @@ -655,6 +679,7 @@ follow-up as a fresh prompt.")
"Finalize agent turn: normalize whitespace, handle abort, schedule queue."
;; Reset per-turn state for clean next turn.
(setq pi-coding-agent--local-user-message nil)
(pi-coding-agent--clear-local-user-message-region)
(setq pi-coding-agent--in-thinking-block nil)
(pi-coding-agent--reset-thinking-state)
(let ((was-aborted pi-coding-agent--aborted))
Expand Down Expand Up @@ -749,20 +774,22 @@ transitions; prompt submission marks the local pre-event window as busy."
text
(lambda ()
(when (pi-coding-agent--drop-followup text)
(pi-coding-agent--display-user-message text (current-time))
(setq pi-coding-agent--local-user-message-region
(pi-coding-agent--display-user-message text (current-time) t))
(setq pi-coding-agent--local-user-message text)
(setq pi-coding-agent--assistant-header-shown nil)))
#'pi-coding-agent--restore-followup-queue-to-input
#'pi-coding-agent--schedule-followup-queue-processing))
#'pi-coding-agent--handle-no-turn-local-prompt))
(t
(pi-coding-agent--send-prompt
text
(lambda ()
(pi-coding-agent--display-user-message text (current-time))
(setq pi-coding-agent--local-user-message-region
(pi-coding-agent--display-user-message text (current-time) t))
(setq pi-coding-agent--local-user-message text)
(setq pi-coding-agent--assistant-header-shown nil))
(lambda () (pi-coding-agent--restore-input-text text))
#'pi-coding-agent--schedule-followup-queue-processing))))
#'pi-coding-agent--handle-no-turn-local-prompt))))

(defun pi-coding-agent--process-followup-queue ()
"Send the oldest follow-up only when it is safe to become the next prompt.
Expand Down Expand Up @@ -1183,6 +1210,7 @@ which asks upfront before any buffers are touched."
(pi-coding-agent--set-process nil)
(pi-coding-agent--set-activity-phase "idle")
(setq pi-coding-agent--local-user-message nil)
(pi-coding-agent--clear-local-user-message-region)
(setq pi-coding-agent--pre-compaction-status nil)
(pi-coding-agent--cancel-followup-drain-timer)
(pi-coding-agent--invalidate-prompt-start-wait)
Expand Down Expand Up @@ -1228,6 +1256,7 @@ Updates buffer-local state and renders display updates."
(local-msg pi-coding-agent--local-user-message))
;; Clear local tracking
(setq pi-coding-agent--local-user-message nil)
(pi-coding-agent--clear-local-user-message-region)
;; Display if: no local message, OR pi's message differs (expanded template)
(when (and text
(or (null local-msg)
Expand Down
78 changes: 68 additions & 10 deletions pi-coding-agent-ui.el
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,7 @@ This is a read-only buffer showing the conversation history."
(setq-local pi-coding-agent--history-load-generation 0)
(setq-local pi-coding-agent--session-transition-generation 0)
(setq-local pi-coding-agent--session-transition-active nil)
(setq-local pi-coding-agent--local-user-message-region nil)
;; Disable hl-line-mode: its post-command-hook overlay update causes
;; scroll oscillation in buffers with invisible text + variable heights.
(setq-local global-hl-line-mode nil)
Expand Down Expand Up @@ -1417,6 +1418,16 @@ When nil and we receive message_start role=user, we display it.
When set but different from pi's message, we display pi's version
\(e.g., expanded template).")

(defvar-local pi-coding-agent--local-user-message-region nil
"Marker pair bounding the locally displayed user turn awaiting pi's echo.")

(defun pi-coding-agent--clear-local-user-message-region ()
"Detach and clear markers for the locally displayed pending user turn."
(when (consp pi-coding-agent--local-user-message-region)
(set-marker (car pi-coding-agent--local-user-message-region) nil)
(set-marker (cdr pi-coding-agent--local-user-message-region) nil))
(setq pi-coding-agent--local-user-message-region nil))

(defvar-local pi-coding-agent--prompt-start-wait-active nil
"Non-nil while a prompt is waiting for response, agent_start, or fallback.")

Expand Down Expand Up @@ -2445,22 +2456,68 @@ returns the frontend to idle for that no-turn success path.")
(pi-coding-agent--prompt-start-wait-active-p)
(= generation pi-coding-agent--prompt-start-generation)))

(defun pi-coding-agent--finish-prompt-without-agent-start
(chat-buf generation on-no-agent-start)
"Finish CHAT-BUF prompt GENERATION after Pi confirms no agent turn.
Call ON-NO-AGENT-START after releasing local ownership."
(when (and (buffer-live-p chat-buf)
(with-current-buffer chat-buf
(pi-coding-agent--prompt-start-current-p generation)))
(with-current-buffer chat-buf
(setq pi-coding-agent--prompt-start-wait-active nil)
(setq pi-coding-agent--prompt-start-generation
(1+ pi-coding-agent--prompt-start-generation))
(when (eq pi-coding-agent--status 'sending)
(setq pi-coding-agent--status 'idle)
(pi-coding-agent--set-activity-phase "idle"))
(when on-no-agent-start
(funcall on-no-agent-start)))))

(defun pi-coding-agent--probe-prompt-start-state
(chat-buf generation on-no-agent-start)
"Ask Pi whether CHAT-BUF prompt GENERATION started an agent turn.
Call ON-NO-AGENT-START only after Pi authoritatively reports idle."
(let ((proc (and (buffer-live-p chat-buf)
(with-current-buffer chat-buf
pi-coding-agent--process))))
(when (and proc (process-live-p proc))
(condition-case nil
(pi-coding-agent--rpc-async
proc '(:type "get_state")
(lambda (response)
(when (and (buffer-live-p chat-buf)
(with-current-buffer chat-buf
(pi-coding-agent--prompt-start-current-p generation)))
(let* ((data (plist-get response :data))
(active
(and (eq (plist-get response :success) t)
(or (pi-coding-agent--normalize-boolean
(plist-get data :isStreaming))
(pi-coding-agent--normalize-boolean
(plist-get data :isCompacting))))))
(if (or active (not (eq (plist-get response :success) t)))
(pi-coding-agent--schedule-prompt-start-fallback
chat-buf generation on-no-agent-start)
(pi-coding-agent--finish-prompt-without-agent-start
chat-buf generation on-no-agent-start))))))
(error
(pi-coding-agent--schedule-prompt-start-fallback
chat-buf generation on-no-agent-start))))))

(defun pi-coding-agent--clear-sending-if-no-agent-start
(chat-buf generation &optional on-no-agent-start)
"Return CHAT-BUF to idle if GENERATION produced no agent_start.
When ON-NO-AGENT-START is non-nil, call it after the session returns to idle."
"Check whether CHAT-BUF prompt GENERATION produced no agent_start.
Elapsed time alone is not authoritative: query Pi before releasing local prompt
ownership or invoking ON-NO-AGENT-START."
(when (buffer-live-p chat-buf)
(with-current-buffer chat-buf
(when (pi-coding-agent--prompt-start-current-p generation)
(setq pi-coding-agent--prompt-start-timer nil)
(setq pi-coding-agent--prompt-start-wait-active nil)
(setq pi-coding-agent--prompt-start-generation
(1+ pi-coding-agent--prompt-start-generation))
(when (eq pi-coding-agent--status 'sending)
(setq pi-coding-agent--status 'idle)
(pi-coding-agent--set-activity-phase "idle")
(when on-no-agent-start
(funcall on-no-agent-start)))))))
(if (memq pi-coding-agent--status '(streaming compacting))
(pi-coding-agent--schedule-prompt-start-fallback
chat-buf generation on-no-agent-start)
(pi-coding-agent--probe-prompt-start-state
chat-buf generation on-no-agent-start))))))

(defun pi-coding-agent--schedule-prompt-start-fallback
(chat-buf generation &optional on-no-agent-start)
Expand Down Expand Up @@ -2540,6 +2597,7 @@ Resets activity phase and status to idle."
(with-current-buffer chat-buf
(pi-coding-agent--invalidate-prompt-start-wait)
(setq pi-coding-agent--local-user-message nil)
(pi-coding-agent--clear-local-user-message-region)
(setq pi-coding-agent--pre-compaction-status nil)
(setq pi-coding-agent--status 'idle)
(pi-coding-agent--set-activity-phase "idle"))))
Expand Down
Loading
Loading