Skip to main content

pi/modes/rpc/
server.rs

1//! RPC server: JSONL stdin → command dispatch → JSONL stdout.
2//!
3//! Ports `.references/pi/packages/coding-agent/src/modes/rpc/rpc-mode.ts`
4//! (`runRpcMode`, `handleCommand`, `handleInputLine`, `shutdown`).
5//!
6//! # Dispatch contract
7//!
8//! - **prompt** — spawns the session run; writes `{success:true}` exactly once
9//!   via the preflight callback (before events), returns `None`; failure error
10//!   only if preflight never fired.
11//! - **All other commands** — await the session call and return the exact
12//!   [`RpcResponse`].
13//! - **Unknown command** — `error(id, type, "Unknown command: {type}")`; the
14//!   `id` is echoed.
15//! - **Malformed JSON** — `error(None, "parse", …)`; the `id` is NOT echoed.
16//! - **Backpressure** — awaited after every non-prompt response and after
17//!   parse errors.
18//!
19//! # Shutdown
20//!
21//! - **SIGTERM** → exit 143 (no flush).
22//! - **SIGHUP** (unix) → exit 129 (flush).
23//! - **stdin EOF** → exit 0 (flush).
24//! - **Extension shutdown handler** → exit 0 (flush), checked after each
25//!   command and after `agent_settled`.
26
27use std::io;
28use std::pin::Pin;
29use std::sync::atomic::{AtomicBool, Ordering};
30use std::sync::{Arc, Mutex};
31
32use futures::Future;
33use futures::future::BoxFuture;
34use pi_agent::{AgentMessage, QueueMode};
35use pi_ai::{ImageContent, Model, ModelThinkingLevel};
36use serde::Serialize;
37use serde_json::Value;
38use tokio::io::AsyncRead;
39use tokio::sync::{Notify, mpsc, oneshot};
40use tokio_util::sync::CancellationToken;
41
42use pi_ext::client::{HostUiRequest, HostUiResponse};
43use pi_ext::protocol::{ExtensionErrorEvent, NotifyLevel, SlotPlacement};
44
45use crate::core::agent_session::events::AgentSessionEvent;
46use crate::core::agent_session::extension::{ExtensionBindings, ExtensionMode};
47use crate::core::agent_session::prompt::PreflightCallback;
48use crate::core::agent_session_runtime::{ForkOutcome, ForkPosition};
49use crate::core::compaction::CompactionResult;
50use crate::core::extension_host::{ExtensionUiEvent, HostExtensionRunner};
51use crate::core::output_guard as output_guard_mod;
52use crate::core::sessions::SessionEntry;
53
54use super::extension_ui::ExtensionUiProxy;
55use super::jsonl::{JsonlLineReader, serialize_json_line};
56use super::types::{
57    BashResult, CycleModelData, CycleThinkingLevelData, ForkMessage, RpcCommand,
58    RpcExtensionUiRequest, RpcExtensionUiResponse, RpcResponse, RpcResponseData, RpcSessionState,
59    RpcSessionTreeNode, RpcSlashCommand, SessionStats, StreamingBehavior,
60};
61
62/// Serialize a value as a JSONL line, falling back to an empty string on
63/// serialization failure (our types are infallible serializers).
64fn to_jsonl<T: Serialize>(value: &T) -> String {
65    serialize_json_line(value).unwrap_or_default()
66}
67
68// ---------------------------------------------------------------------------
69// RpcSink — output abstraction
70// ---------------------------------------------------------------------------
71
72/// Future returned by [`RpcSink`] methods.
73type SinkFut = Pin<Box<dyn Future<Output = io::Result<()>> + Send>>;
74
75/// Protocol-stdout write/backpressure/flush abstraction.
76///
77/// All futures are `'static + Send` so they can be `tokio::spawn`'d from sync
78/// closures (preflight callbacks, event listeners).
79pub trait RpcSink: Send + Sync {
80    /// Write `text` (including trailing `\n`) to the ordered stdout sink.
81    fn write_stdout(&self, text: String) -> SinkFut;
82    /// Wait until every previously accepted write has finished draining.
83    fn backpressure(&self) -> SinkFut;
84    /// Wait for drain, then flush the underlying sink.
85    fn flush(&self) -> SinkFut;
86}
87
88/// [`OutputGuard`](crate::core::output_guard)-backed sink for production.
89#[derive(Clone, Copy, Debug)]
90pub struct OutputGuardSink;
91
92impl RpcSink for OutputGuardSink {
93    fn write_stdout(&self, text: String) -> SinkFut {
94        Box::pin(async move {
95            output_guard_mod::write_raw_stdout(text)
96                .await
97                .map_err(|e| io::Error::other(e.to_string()))
98        })
99    }
100    fn backpressure(&self) -> SinkFut {
101        Box::pin(async move {
102            output_guard_mod::wait_for_raw_stdout_backpressure()
103                .await
104                .map_err(|e| io::Error::other(e.to_string()))
105        })
106    }
107    fn flush(&self) -> SinkFut {
108        Box::pin(async move {
109            output_guard_mod::flush_raw_stdout()
110                .await
111                .map_err(|e| io::Error::other(e.to_string()))
112        })
113    }
114}
115
116/// In-memory sink for deterministic tests.
117#[derive(Clone, Default)]
118pub struct BufferSink {
119    stdout: Arc<Mutex<Vec<u8>>>,
120}
121
122impl BufferSink {
123    /// Create an empty buffer sink.
124    #[must_use]
125    pub fn new() -> Self {
126        Self::default()
127    }
128    /// Return the accumulated stdout bytes as a string.
129    #[must_use]
130    pub fn stdout_string(&self) -> String {
131        String::from_utf8_lossy(
132            &self
133                .stdout
134                .lock()
135                .unwrap_or_else(std::sync::PoisonError::into_inner),
136        )
137        .into_owned()
138    }
139    /// Return accumulated stdout split into trimmed lines.
140    #[must_use]
141    pub fn stdout_lines(&self) -> Vec<String> {
142        self.stdout_string()
143            .lines()
144            .filter(|l| !l.is_empty())
145            .map(str::to_owned)
146            .collect()
147    }
148}
149
150impl RpcSink for BufferSink {
151    fn write_stdout(&self, text: String) -> SinkFut {
152        let buf = Arc::clone(&self.stdout);
153        Box::pin(async move {
154            buf.lock()
155                .unwrap_or_else(std::sync::PoisonError::into_inner)
156                .extend_from_slice(text.as_bytes());
157            Ok(())
158        })
159    }
160    fn backpressure(&self) -> SinkFut {
161        Box::pin(async { Ok(()) })
162    }
163    fn flush(&self) -> SinkFut {
164        Box::pin(async { Ok(()) })
165    }
166}
167
168#[cfg(test)]
169#[derive(Clone, Default)]
170struct GatedSink {
171    stdout: Arc<Mutex<Vec<u8>>>,
172    writes_started: Arc<std::sync::atomic::AtomicUsize>,
173    started: Arc<Notify>,
174    release: Arc<Notify>,
175}
176
177#[cfg(test)]
178impl GatedSink {
179    async fn wait_for_write(&self, target: usize) {
180        loop {
181            let started = self.started.notified();
182            if self.writes_started.load(Ordering::SeqCst) >= target {
183                return;
184            }
185            started.await;
186        }
187    }
188}
189
190#[cfg(test)]
191impl RpcSink for GatedSink {
192    fn write_stdout(&self, text: String) -> SinkFut {
193        let sink = self.clone();
194        Box::pin(async move {
195            sink.writes_started.fetch_add(1, Ordering::SeqCst);
196            sink.started.notify_waiters();
197            sink.release.notified().await;
198            sink.stdout
199                .lock()
200                .unwrap_or_else(std::sync::PoisonError::into_inner)
201                .extend_from_slice(text.as_bytes());
202            Ok(())
203        })
204    }
205
206    fn backpressure(&self) -> SinkFut {
207        Box::pin(async { Ok(()) })
208    }
209
210    fn flush(&self) -> SinkFut {
211        Box::pin(async { Ok(()) })
212    }
213}
214
215// ---------------------------------------------------------------------------
216// RpcSessionHost — session + runtime abstraction
217// ---------------------------------------------------------------------------
218
219/// Callback the host invokes after a session replacement.
220pub type RebindCallback = Arc<dyn Fn() -> BoxFuture<'static, ()> + Send + Sync>;
221
222/// Result of model cycling.
223#[derive(Clone, Debug, PartialEq)]
224pub struct ModelCycleResult {
225    /// Model now active.
226    pub model: Model,
227    /// Effective thinking level after clamping.
228    pub thinking_level: ModelThinkingLevel,
229    /// Whether cycling was across scoped entries.
230    pub is_scoped: bool,
231}
232
233/// Abstracts the `AgentSessionRuntime` + `AgentSession` surface consumed by the
234/// 31 [`RpcCommand`] variants.
235///
236/// All async methods return [`BoxFuture<'static>`] so they can be `tokio::spawn`'d
237/// without borrowing the host; the preflight callback fires synchronously
238/// during the first poll of `prompt`, which is how the prompt-response
239/// frame is enqueued before any agent event in the central write FIFO.
240pub trait RpcSessionHost: Send + Sync {
241    // ---- Prompt lifecycle ----
242    /// Submit a user prompt. The `preflight` callback fires with `true` on
243    /// accept (before the run starts) or `false` on reject.
244    fn prompt(
245        &self,
246        message: String,
247        images: Vec<ImageContent>,
248        streaming_behavior: Option<StreamingBehavior>,
249        preflight: PreflightCallback,
250    ) -> BoxFuture<'static, Result<(), String>>;
251    /// Steer into the active turn.
252    fn steer(
253        &self,
254        message: String,
255        images: Vec<ImageContent>,
256    ) -> BoxFuture<'static, Result<(), String>>;
257    /// Queue a follow-up after the current turn finishes.
258    fn follow_up(
259        &self,
260        message: String,
261        images: Vec<ImageContent>,
262    ) -> BoxFuture<'static, Result<(), String>>;
263    /// Abort the active agent run + retry, wait for idle.
264    fn abort(&self) -> BoxFuture<'static, ()>;
265
266    // ---- State ----
267    /// Snapshot the current session for the `get_state` RPC.
268    fn get_state(&self) -> BoxFuture<'static, RpcSessionState>;
269
270    // ---- Model ----
271    /// List available models from the model runtime.
272    fn get_available_models(&self) -> BoxFuture<'static, Vec<Model>>;
273    /// Set the active model.
274    fn set_model(&self, model: Model) -> BoxFuture<'static, Result<(), String>>;
275    /// Cycle to the next model (scoped or all-available).
276    fn cycle_model(&self) -> BoxFuture<'static, Option<ModelCycleResult>>;
277    /// Set the thinking level. Resolves `true` only when the level change was
278    /// durably committed (or was already effective).
279    fn set_thinking_level(&self, level: ModelThinkingLevel) -> BoxFuture<'static, bool>;
280    /// Cycle to the next thinking level.
281    fn cycle_thinking_level(&self) -> BoxFuture<'static, Option<ModelThinkingLevel>>;
282
283    // ---- Queue modes ----
284    /// Set steering queue drain mode.
285    fn set_steering_mode(&self, mode: QueueMode) -> BoxFuture<'static, ()>;
286    /// Set follow-up queue drain mode.
287    fn set_follow_up_mode(&self, mode: QueueMode) -> BoxFuture<'static, ()>;
288
289    // ---- Compaction ----
290    /// Compact the session.
291    fn compact(
292        &self,
293        custom_instructions: Option<String>,
294    ) -> BoxFuture<'static, Result<CompactionResult, String>>;
295    /// Enable / disable auto-compaction.
296    fn set_auto_compaction(&self, enabled: bool) -> BoxFuture<'static, ()>;
297
298    // ---- Retry ----
299    /// Enable / disable auto-retry.
300    fn set_auto_retry(&self, enabled: bool) -> BoxFuture<'static, ()>;
301    /// Abort in-flight retry sleep.
302    fn abort_retry(&self) -> BoxFuture<'static, ()>;
303
304    // ---- Bash ----
305    /// Execute a bash command.
306    fn execute_bash(
307        &self,
308        command: String,
309        exclude_from_context: Option<bool>,
310    ) -> BoxFuture<'static, Result<BashResult, String>>;
311    /// Abort a running bash command.
312    fn abort_bash(&self) -> BoxFuture<'static, ()>;
313
314    // ---- Session data ----
315    /// Aggregate session statistics for `get_session_stats`.
316    fn get_session_stats(&self) -> BoxFuture<'static, SessionStats>;
317    /// Export the session to HTML. Returns the written file path.
318    fn export_to_html(
319        &self,
320        output_path: Option<String>,
321    ) -> BoxFuture<'static, Result<String, String>>;
322    /// Set the session display name.
323    fn set_session_name(&self, name: String) -> BoxFuture<'static, Result<(), String>>;
324
325    // ---- Session mutations ----
326    /// Start a new session. Returns `true` when cancelled by an extension hook.
327    fn new_session(
328        &self,
329        parent_session: Option<String>,
330    ) -> BoxFuture<'static, Result<bool, String>>;
331    /// Switch to another session file. Returns `true` when cancelled.
332    fn switch_session(&self, session_path: String) -> BoxFuture<'static, Result<bool, String>>;
333    /// Fork the session. Returns the fork outcome.
334    fn fork(
335        &self,
336        entry_id: String,
337        position: ForkPosition,
338    ) -> BoxFuture<'static, Result<ForkOutcome, String>>;
339
340    // ---- Session tree / entries ----
341    /// All session entries.
342    fn get_entries(&self) -> BoxFuture<'static, Vec<SessionEntry>>;
343    /// Current leaf entry id.
344    fn get_leaf_id(&self) -> BoxFuture<'static, Option<String>>;
345    /// Session tree (wire-facing nodes).
346    fn get_tree(&self) -> BoxFuture<'static, Vec<RpcSessionTreeNode>>;
347    /// Forkable user messages.
348    fn get_fork_messages(&self) -> BoxFuture<'static, Vec<ForkMessage>>;
349    /// Last assistant text content, if any.
350    fn get_last_assistant_text(&self) -> BoxFuture<'static, Option<String>>;
351    /// All agent messages.
352    fn get_messages(&self) -> BoxFuture<'static, Vec<AgentMessage>>;
353    /// Available slash commands (extension + prompt + skill).
354    fn get_commands(&self) -> BoxFuture<'static, Vec<RpcSlashCommand>>;
355
356    // ---- Subscribe / extensions ----
357    /// Subscribe to public session events. Returns an unsubscribe closure.
358    fn subscribe(
359        &self,
360        listener: Arc<dyn Fn(&AgentSessionEvent) + Send + Sync>,
361    ) -> Box<dyn Fn() + Send + Sync>;
362    /// Register a backpressure hook invoked when the agent has pending events.
363    fn register_backpressure_hook(
364        &self,
365        hook: Arc<dyn Fn() -> BoxFuture<'static, ()> + Send + Sync>,
366    ) -> Box<dyn Fn() + Send + Sync>;
367    /// Bind extensions for RPC mode.
368    fn bind_extensions_rpc(
369        &self,
370        bindings: ExtensionBindings,
371    ) -> BoxFuture<'static, Result<(), String>>;
372    /// Current concrete extension host, when this session uses one.
373    fn host_extension_runner(&self) -> Option<Arc<HostExtensionRunner>> {
374        None
375    }
376
377    // ---- Lifecycle ----
378    /// Dispose the current session and runtime.
379    fn dispose(&self) -> BoxFuture<'static, ()>;
380    /// Set the rebind callback invoked after session replacement.
381    fn set_rebind(&self, callback: Option<RebindCallback>);
382}
383
384// ---------------------------------------------------------------------------
385// ServerOutput types
386// ---------------------------------------------------------------------------
387
388/// Extension error notification (`type: "extension_error"`).
389#[derive(Clone, Debug, Serialize)]
390pub struct ExtensionErrorOutput {
391    #[serde(rename = "type")]
392    type_name: &'static str,
393    #[serde(rename = "extensionPath")]
394    extension_path: String,
395    event: String,
396    error: String,
397}
398
399impl ExtensionErrorOutput {
400    /// Create a new extension error output.
401    #[must_use]
402    pub fn new(
403        path: impl Into<String>,
404        event: impl Into<String>,
405        error: impl Into<String>,
406    ) -> Self {
407        Self {
408            type_name: "extension_error",
409            extension_path: path.into(),
410            event: event.into(),
411            error: error.into(),
412        }
413    }
414}
415
416/// Tagged stdout frame for test deserialization.
417#[derive(Debug, Serialize)]
418#[serde(untagged)]
419pub enum ServerOutput {
420    /// Command response.
421    Response(RpcResponse),
422    /// Raw agent event.
423    Event(Box<AgentSessionEvent>),
424    /// Extension UI request.
425    UiRequest(RpcExtensionUiRequest),
426    /// Extension error notification.
427    ExtensionError(ExtensionErrorOutput),
428}
429
430// ---------------------------------------------------------------------------
431// ServerState
432// ---------------------------------------------------------------------------
433
434type UnsubSlot = Mutex<Option<Box<dyn Fn() + Send + Sync>>>;
435
436fn lock_unsub(slot: &UnsubSlot) -> std::sync::MutexGuard<'_, Option<Box<dyn Fn() + Send + Sync>>> {
437    slot.lock()
438        .unwrap_or_else(std::sync::PoisonError::into_inner)
439}
440
441fn take_unsub(slot: &UnsubSlot) {
442    if let Some(unsub) = lock_unsub(slot).take() {
443        unsub();
444    }
445}
446
447/// Mutable server state shared between the event loop, event subscriber, and
448/// rebind callback.
449///
450/// All stdout writes go through `write_tx`. A dedicated writer actor consumes
451/// line and drain-barrier messages in FIFO order, preserving prompt-response
452/// ordering while allowing producers to await the actual server queue.
453enum WriteMessage {
454    Line(String),
455    Drain(oneshot::Sender<()>),
456}
457
458pub(crate) struct ServerState {
459    sink: Arc<dyn RpcSink>,
460    write_tx: mpsc::UnboundedSender<WriteMessage>,
461    proxy: ExtensionUiProxy,
462    shutdown_requested: Arc<AtomicBool>,
463    needs_rebind: Arc<AtomicBool>,
464    signal: Arc<Notify>,
465    unsubscribe_events: UnsubSlot,
466    unsubscribe_backpressure: UnsubSlot,
467    unsubscribe_extension_ui: UnsubSlot,
468}
469
470impl ServerState {
471    fn new(
472        sink: Arc<dyn RpcSink>,
473        write_tx: mpsc::UnboundedSender<WriteMessage>,
474        proxy: ExtensionUiProxy,
475    ) -> Self {
476        Self {
477            sink,
478            write_tx,
479            proxy,
480            shutdown_requested: Arc::new(AtomicBool::new(false)),
481            needs_rebind: Arc::new(AtomicBool::new(false)),
482            signal: Arc::new(Notify::new()),
483            unsubscribe_events: Mutex::new(None),
484            unsubscribe_backpressure: Mutex::new(None),
485            unsubscribe_extension_ui: Mutex::new(None),
486        }
487    }
488
489    /// Enqueue a JSONL line for ordered stdout emission (synchronous, FIFO).
490    fn enqueue(&self, line: String) {
491        let _ = self.write_tx.send(WriteMessage::Line(line));
492    }
493
494    async fn wait_for_output(&self) {
495        let (done_tx, done_rx) = oneshot::channel();
496        if self.write_tx.send(WriteMessage::Drain(done_tx)).is_ok() {
497            let _ = done_rx.await;
498        }
499    }
500
501    /// Rebind extensions + subscriptions on the current session.
502    ///
503    /// All stdout emission (events, extension errors) goes through `write_tx`
504    /// so the event loop drains them in FIFO order.
505    pub(crate) async fn rebind<H>(&self, host: &H)
506    where
507        H: RpcSessionHost + ?Sized,
508    {
509        take_unsub(&self.unsubscribe_events);
510        take_unsub(&self.unsubscribe_backpressure);
511        take_unsub(&self.unsubscribe_extension_ui);
512
513        let shutdown_flag = Arc::clone(&self.shutdown_requested);
514        let shutdown_signal = Arc::clone(&self.signal);
515        let error_tx = self.write_tx.clone();
516        let bindings = ExtensionBindings {
517            mode: Some(ExtensionMode::Rpc),
518            shutdown_handler: Some(Arc::new(move || {
519                shutdown_flag.store(true, Ordering::SeqCst);
520                shutdown_signal.notify_one();
521            })),
522            on_error: Some(Arc::new(move |path: &str, event: &str, error: &str| {
523                let output = ExtensionErrorOutput::new(path, event, error);
524                let _ = error_tx.send(WriteMessage::Line(to_jsonl(&output)));
525            })),
526            ..Default::default()
527        };
528        let _ = host.bind_extensions_rpc(bindings).await;
529
530        let event_tx = self.write_tx.clone();
531        let signal = Arc::clone(&self.signal);
532        let unsub = host.subscribe(Arc::new(move |event: &AgentSessionEvent| {
533            let _ = event_tx.send(WriteMessage::Line(to_jsonl(event)));
534            if matches!(event, AgentSessionEvent::AgentSettled) {
535                signal.notify_one();
536            }
537        }));
538        *lock_unsub(&self.unsubscribe_events) = Some(unsub);
539
540        let backpressure_tx = self.write_tx.clone();
541        let unsub_bp = host.register_backpressure_hook(Arc::new(move || {
542            let write_tx = backpressure_tx.clone();
543            Box::pin(async move {
544                let (done_tx, done_rx) = oneshot::channel();
545                if write_tx.send(WriteMessage::Drain(done_tx)).is_ok() {
546                    let _ = done_rx.await;
547                }
548            })
549        }));
550        *lock_unsub(&self.unsubscribe_backpressure) = Some(unsub_bp);
551
552        if let Some(runner) = host.host_extension_runner() {
553            let cancel = CancellationToken::new();
554            let cancel_on_unbind = cancel.clone();
555            *lock_unsub(&self.unsubscribe_extension_ui) = Some(Box::new(move || {
556                cancel_on_unbind.cancel();
557            }));
558
559            if let Some(requests) = runner.take_ui_requests() {
560                tokio::spawn(run_extension_dialog_bridge(
561                    Arc::clone(&runner),
562                    requests,
563                    self.proxy.clone(),
564                    self.write_tx.clone(),
565                    cancel.clone(),
566                ));
567            }
568            tokio::spawn(run_extension_event_bridge(
569                runner,
570                self.write_tx.clone(),
571                cancel,
572            ));
573        }
574    }
575
576    /// Cleanup before exit: cancel UI, unsubscribe, dispose, flush.
577    async fn cleanup<H>(&self, host: &H, exit_code: i32)
578    where
579        H: RpcSessionHost + ?Sized,
580    {
581        self.proxy.cancel_all();
582        take_unsub(&self.unsubscribe_events);
583        take_unsub(&self.unsubscribe_backpressure);
584        take_unsub(&self.unsubscribe_extension_ui);
585        host.dispose().await;
586        self.wait_for_output().await;
587        if exit_code != 143 {
588            let _ = self.sink.flush().await;
589        }
590    }
591}
592
593async fn run_extension_dialog_bridge(
594    runner: Arc<HostExtensionRunner>,
595    mut requests: mpsc::Receiver<HostUiRequest>,
596    proxy: ExtensionUiProxy,
597    write_tx: mpsc::UnboundedSender<WriteMessage>,
598    cancel: CancellationToken,
599) {
600    loop {
601        let request = tokio::select! {
602            () = cancel.cancelled() => break,
603            request = requests.recv() => match request {
604                Some(request) => request,
605                None => break,
606            },
607        };
608        let runner = Arc::clone(&runner);
609        let proxy = proxy.clone();
610        let write_tx = write_tx.clone();
611        let request_cancel = cancel.child_token();
612        tokio::spawn(async move {
613            bridge_extension_dialog(runner, request, proxy, write_tx, request_cancel).await;
614        });
615    }
616}
617
618async fn bridge_extension_dialog(
619    runner: Arc<HostExtensionRunner>,
620    request: HostUiRequest,
621    proxy: ExtensionUiProxy,
622    write_tx: mpsc::UnboundedSender<WriteMessage>,
623    cancel: CancellationToken,
624) {
625    let timeout_ms = match &request {
626        HostUiRequest::Select { request, .. } => request.options_meta.timeout_ms,
627        HostUiRequest::Confirm { request, .. } => request.options_meta.timeout_ms,
628        HostUiRequest::Input { request, .. } => request.options_meta.timeout_ms,
629        HostUiRequest::Editor { .. } => None,
630    };
631    let (rpc_request, response_rx) = proxy.create_dialog(|id| match &request {
632        HostUiRequest::Select { request, .. } => RpcExtensionUiRequest::Select {
633            id: id.to_owned(),
634            title: request.title.clone(),
635            options: request.options.clone(),
636            timeout: request.options_meta.timeout_ms,
637        },
638        HostUiRequest::Confirm { request, .. } => RpcExtensionUiRequest::Confirm {
639            id: id.to_owned(),
640            title: request.title.clone(),
641            message: request.message.clone(),
642            timeout: request.options_meta.timeout_ms,
643        },
644        HostUiRequest::Input { request, .. } => RpcExtensionUiRequest::Input {
645            id: id.to_owned(),
646            title: request.title.clone(),
647            placeholder: request.placeholder.clone(),
648            timeout: request.options_meta.timeout_ms,
649        },
650        HostUiRequest::Editor { request, .. } => RpcExtensionUiRequest::Editor {
651            id: id.to_owned(),
652            title: request.title.clone(),
653            prefill: request.prefill.clone(),
654        },
655    });
656    let rpc_id = rpc_request.id().to_owned();
657    let _ = write_tx.send(WriteMessage::Line(to_jsonl(&ServerOutput::UiRequest(
658        rpc_request,
659    ))));
660
661    let response = if let Some(timeout_ms) = timeout_ms {
662        tokio::select! {
663            () = cancel.cancelled() => None,
664            result = tokio::time::timeout(
665                std::time::Duration::from_millis(timeout_ms),
666                response_rx,
667            ) => result.ok().and_then(Result::ok),
668        }
669    } else {
670        tokio::select! {
671            () = cancel.cancelled() => None,
672            result = response_rx => result.ok(),
673        }
674    };
675    if response.is_none() {
676        let _ = proxy.route_response(RpcExtensionUiResponse::Cancelled { id: rpc_id });
677    }
678    let host_response = map_rpc_ui_response(&request, response);
679    let _ = runner.respond_ui(host_response).await;
680}
681
682fn map_rpc_ui_response(
683    request: &HostUiRequest,
684    response: Option<RpcExtensionUiResponse>,
685) -> HostUiResponse {
686    match request {
687        HostUiRequest::Select { id, .. } => HostUiResponse::Select {
688            id: *id,
689            value: match response {
690                Some(RpcExtensionUiResponse::Value { value, .. }) => Some(value),
691                _ => None,
692            },
693        },
694        HostUiRequest::Confirm { id, .. } => HostUiResponse::Confirm {
695            id: *id,
696            confirmed: match response {
697                Some(RpcExtensionUiResponse::Confirmed { confirmed, .. }) => confirmed,
698                _ => false,
699            },
700        },
701        HostUiRequest::Input { id, .. } => HostUiResponse::Input {
702            id: *id,
703            value: match response {
704                Some(RpcExtensionUiResponse::Value { value, .. }) => Some(value),
705                _ => None,
706            },
707        },
708        HostUiRequest::Editor { id, .. } => HostUiResponse::Editor {
709            id: *id,
710            value: match response {
711                Some(RpcExtensionUiResponse::Value { value, .. }) => Some(value),
712                _ => None,
713            },
714        },
715    }
716}
717
718async fn run_extension_event_bridge(
719    runner: Arc<HostExtensionRunner>,
720    write_tx: mpsc::UnboundedSender<WriteMessage>,
721    cancel: CancellationToken,
722) {
723    let mut events = runner.subscribe_ui();
724    let mut errors = runner.subscribe_errors();
725    for slot in runner.current_slots() {
726        let request = map_extension_ui_event(ExtensionUiEvent::Slot(slot));
727        let _ = write_tx.send(WriteMessage::Line(to_jsonl(&ServerOutput::UiRequest(
728            request,
729        ))));
730    }
731    loop {
732        tokio::select! {
733            () = cancel.cancelled() => break,
734            event = events.recv() => {
735                let event = match event {
736                    Ok(event) => event,
737                    Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
738                    Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
739                };
740                let request = map_extension_ui_event(event);
741                let _ = write_tx.send(WriteMessage::Line(to_jsonl(
742                    &ServerOutput::UiRequest(request),
743                )));
744            }
745            error = errors.recv() => {
746                let error = match error {
747                    Ok(error) => error,
748                    Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
749                    Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
750                };
751                let output = map_extension_error_event(&error);
752                let _ = write_tx.send(WriteMessage::Line(to_jsonl(
753                    &ServerOutput::ExtensionError(output),
754                )));
755            }
756        }
757    }
758}
759
760fn map_extension_error_event(error: &ExtensionErrorEvent) -> ExtensionErrorOutput {
761    let mut extension_path = "<runtime>".to_owned();
762    let mut event = error.code.clone();
763    let mut message = error.message.clone();
764    let mut structured = false;
765
766    if let Some(data) = error.data.as_ref() {
767        if let Some(value) = data
768            .get("extensionPath")
769            .or_else(|| data.get("path"))
770            .and_then(Value::as_str)
771        {
772            value.clone_into(&mut extension_path);
773            structured = true;
774        }
775        if let Some(value) = data.get("event").and_then(Value::as_str) {
776            value.clone_into(&mut event);
777            structured = true;
778        }
779        if let Some(value) = data.get("error").and_then(Value::as_str) {
780            value.clone_into(&mut message);
781            structured = true;
782        }
783    }
784
785    if !structured
786        && let Some(rest) = error.message.strip_prefix('[')
787        && let Some((path, rest)) = rest.split_once("] ")
788        && let Some((event_name, error_message)) = rest.split_once(": ")
789        && !path.is_empty()
790        && !event_name.is_empty()
791    {
792        path.clone_into(&mut extension_path);
793        event_name.clone_into(&mut event);
794        error_message.clone_into(&mut message);
795    }
796
797    ExtensionErrorOutput::new(extension_path, event, message)
798}
799
800fn map_extension_ui_event(event: ExtensionUiEvent) -> RpcExtensionUiRequest {
801    match event {
802        ExtensionUiEvent::Notify(notification) => ExtensionUiProxy::notify(
803            &notification.message,
804            Some(match notification.level {
805                NotifyLevel::Info => super::types::NotifyType::Info,
806                NotifyLevel::Warning => super::types::NotifyType::Warning,
807                NotifyLevel::Error => super::types::NotifyType::Error,
808            }),
809        ),
810        ExtensionUiEvent::Slot(slot) => {
811            let lines = slot
812                .lines
813                .iter()
814                .map(|line| line.iter().map(|run| run.text.as_str()).collect::<String>())
815                .collect::<Vec<_>>();
816            let placement = match slot.placement {
817                SlotPlacement::Footer | SlotPlacement::BelowEditor => {
818                    Some(super::types::WidgetPlacement::BelowEditor)
819                }
820                _ => Some(super::types::WidgetPlacement::AboveEditor),
821            };
822            ExtensionUiProxy::set_widget(&slot.key, Some(&lines), placement)
823        }
824        ExtensionUiEvent::Dispose { key } => ExtensionUiProxy::set_widget(&key, None, None),
825    }
826}
827
828// ---------------------------------------------------------------------------
829// handle_command — dispatch one RpcCommand
830// ---------------------------------------------------------------------------
831
832/// Dispatch a single [`RpcCommand`].
833///
834/// Returns `Some(response)` for synchronous commands. Returns `None` for
835/// `prompt` (response emitted asynchronously via preflight).
836#[allow(clippy::too_many_lines)]
837pub(crate) async fn handle_command<H>(
838    command: &RpcCommand,
839    host: &H,
840    state: &ServerState,
841) -> Option<RpcResponse>
842where
843    H: RpcSessionHost + ?Sized,
844{
845    let id = command.id().map(str::to_owned);
846
847    match command {
848        RpcCommand::Prompt {
849            message,
850            images,
851            streaming_behavior,
852            ..
853        } => {
854            // Wait until preflight enqueues the prompt response before returning,
855            // so a subsequent command's response cannot overtake it in the FIFO.
856            spawn_prompt(
857                id,
858                message.clone(),
859                images.clone().unwrap_or_default(),
860                *streaming_behavior,
861                host,
862                state.write_tx.clone(),
863            )
864            .await;
865            None
866        }
867
868        RpcCommand::Steer {
869            message, images, ..
870        } => match host
871            .steer(message.clone(), images.clone().unwrap_or_default())
872            .await
873        {
874            Ok(()) => Some(RpcResponse::ok(id, "steer")),
875            Err(e) => Some(RpcResponse::err(id, "steer", e)),
876        },
877
878        RpcCommand::FollowUp {
879            message, images, ..
880        } => match host
881            .follow_up(message.clone(), images.clone().unwrap_or_default())
882            .await
883        {
884            Ok(()) => Some(RpcResponse::ok(id, "follow_up")),
885            Err(e) => Some(RpcResponse::err(id, "follow_up", e)),
886        },
887
888        RpcCommand::Abort { .. } => {
889            host.abort().await;
890            Some(RpcResponse::ok(id, "abort"))
891        }
892
893        RpcCommand::NewSession { parent_session, .. } => {
894            match host.new_session(parent_session.clone()).await {
895                Ok(cancelled) => {
896                    if !cancelled {
897                        state.rebind(host).await;
898                    }
899                    Some(RpcResponse::ok_data(
900                        id,
901                        "new_session",
902                        RpcResponseData::Cancelled { cancelled },
903                    ))
904                }
905                Err(e) => Some(RpcResponse::err(id, "new_session", e)),
906            }
907        }
908
909        RpcCommand::GetState { .. } => {
910            let s = host.get_state().await;
911            Some(RpcResponse::ok_data(
912                id,
913                "get_state",
914                RpcResponseData::SessionState(s),
915            ))
916        }
917
918        RpcCommand::SetModel {
919            provider, model_id, ..
920        } => {
921            let models = host.get_available_models().await;
922            match models
923                .into_iter()
924                .find(|m| m.provider == *provider && m.id == *model_id)
925            {
926                Some(model) => match host.set_model(model.clone()).await {
927                    Ok(()) => Some(RpcResponse::ok_data(
928                        id,
929                        "set_model",
930                        RpcResponseData::Model(model),
931                    )),
932                    Err(e) => Some(RpcResponse::err(id, "set_model", e)),
933                },
934                None => Some(RpcResponse::err(
935                    id,
936                    "set_model",
937                    format!("Model not found: {provider}/{model_id}"),
938                )),
939            }
940        }
941
942        RpcCommand::CycleModel { .. } => {
943            let r = host.cycle_model().await;
944            let data = r.map(|x| {
945                RpcResponseData::CycleModel(Some(CycleModelData {
946                    model: x.model,
947                    thinking_level: x.thinking_level,
948                    is_scoped: x.is_scoped,
949                }))
950            });
951            Some(RpcResponse::ok_data(
952                id,
953                "cycle_model",
954                data.unwrap_or(RpcResponseData::CycleModel(None)),
955            ))
956        }
957
958        RpcCommand::GetAvailableModels { .. } => {
959            let models = host.get_available_models().await;
960            Some(RpcResponse::ok_data(
961                id,
962                "get_available_models",
963                RpcResponseData::AvailableModels { models },
964            ))
965        }
966
967        RpcCommand::SetThinkingLevel { level, .. } => {
968            if host.set_thinking_level(*level).await {
969                Some(RpcResponse::ok(id, "set_thinking_level"))
970            } else {
971                Some(RpcResponse::err(
972                    id,
973                    "set_thinking_level",
974                    "Failed to persist thinking level change",
975                ))
976            }
977        }
978
979        RpcCommand::CycleThinkingLevel { .. } => {
980            let level = host.cycle_thinking_level().await;
981            let data = level.map(|l| {
982                RpcResponseData::CycleThinkingLevel(Some(CycleThinkingLevelData { level: l }))
983            });
984            Some(RpcResponse::ok_data(
985                id,
986                "cycle_thinking_level",
987                data.unwrap_or(RpcResponseData::CycleThinkingLevel(None)),
988            ))
989        }
990
991        RpcCommand::SetSteeringMode { mode, .. } => {
992            host.set_steering_mode(*mode).await;
993            Some(RpcResponse::ok(id, "set_steering_mode"))
994        }
995
996        RpcCommand::SetFollowUpMode { mode, .. } => {
997            host.set_follow_up_mode(*mode).await;
998            Some(RpcResponse::ok(id, "set_follow_up_mode"))
999        }
1000
1001        RpcCommand::Compact {
1002            custom_instructions,
1003            ..
1004        } => match host.compact(custom_instructions.clone()).await {
1005            Ok(result) => Some(RpcResponse::ok_data(
1006                id,
1007                "compact",
1008                RpcResponseData::Compaction(result),
1009            )),
1010            Err(e) => Some(RpcResponse::err(id, "compact", e)),
1011        },
1012
1013        RpcCommand::SetAutoCompaction { enabled, .. } => {
1014            host.set_auto_compaction(*enabled).await;
1015            Some(RpcResponse::ok(id, "set_auto_compaction"))
1016        }
1017
1018        RpcCommand::SetAutoRetry { enabled, .. } => {
1019            host.set_auto_retry(*enabled).await;
1020            Some(RpcResponse::ok(id, "set_auto_retry"))
1021        }
1022
1023        RpcCommand::AbortRetry { .. } => {
1024            host.abort_retry().await;
1025            Some(RpcResponse::ok(id, "abort_retry"))
1026        }
1027
1028        RpcCommand::Bash {
1029            command: cmd,
1030            exclude_from_context,
1031            ..
1032        } => match host.execute_bash(cmd.clone(), *exclude_from_context).await {
1033            Ok(result) => Some(RpcResponse::ok_data(
1034                id,
1035                "bash",
1036                RpcResponseData::Bash(result),
1037            )),
1038            Err(e) => Some(RpcResponse::err(id, "bash", e)),
1039        },
1040
1041        RpcCommand::AbortBash { .. } => {
1042            host.abort_bash().await;
1043            Some(RpcResponse::ok(id, "abort_bash"))
1044        }
1045
1046        RpcCommand::GetSessionStats { .. } => {
1047            let session_stats = host.get_session_stats().await;
1048            Some(RpcResponse::ok_data(
1049                id,
1050                "get_session_stats",
1051                RpcResponseData::SessionStats(session_stats),
1052            ))
1053        }
1054
1055        RpcCommand::ExportHtml { output_path, .. } => {
1056            match host.export_to_html(output_path.clone()).await {
1057                Ok(path) => Some(RpcResponse::ok_data(
1058                    id,
1059                    "export_html",
1060                    RpcResponseData::ExportHtml { path },
1061                )),
1062                Err(e) => Some(RpcResponse::err(id, "export_html", e)),
1063            }
1064        }
1065
1066        RpcCommand::SwitchSession { session_path, .. } => {
1067            match host.switch_session(session_path.clone()).await {
1068                Ok(cancelled) => {
1069                    if !cancelled {
1070                        state.rebind(host).await;
1071                    }
1072                    Some(RpcResponse::ok_data(
1073                        id,
1074                        "switch_session",
1075                        RpcResponseData::Cancelled { cancelled },
1076                    ))
1077                }
1078                Err(e) => Some(RpcResponse::err(id, "switch_session", e)),
1079            }
1080        }
1081
1082        RpcCommand::Fork { entry_id, .. } => {
1083            match host.fork(entry_id.clone(), ForkPosition::Before).await {
1084                Ok(outcome) => {
1085                    if !outcome.cancelled {
1086                        state.rebind(host).await;
1087                    }
1088                    Some(RpcResponse::ok_data(
1089                        id,
1090                        "fork",
1091                        RpcResponseData::Fork {
1092                            text: outcome.selected_text.unwrap_or_default(),
1093                            cancelled: outcome.cancelled,
1094                        },
1095                    ))
1096                }
1097                Err(e) => Some(RpcResponse::err(id, "fork", e)),
1098            }
1099        }
1100
1101        RpcCommand::Clone { .. } => {
1102            let leaf = host.get_leaf_id().await;
1103            match leaf {
1104                None => Some(RpcResponse::err(
1105                    id,
1106                    "clone",
1107                    "Cannot clone session: no current entry selected",
1108                )),
1109                Some(leaf_id) => match host.fork(leaf_id, ForkPosition::At).await {
1110                    Ok(outcome) => {
1111                        if !outcome.cancelled {
1112                            state.rebind(host).await;
1113                        }
1114                        Some(RpcResponse::ok_data(
1115                            id,
1116                            "clone",
1117                            RpcResponseData::Cancelled {
1118                                cancelled: outcome.cancelled,
1119                            },
1120                        ))
1121                    }
1122                    Err(e) => Some(RpcResponse::err(id, "clone", e)),
1123                },
1124            }
1125        }
1126
1127        RpcCommand::GetForkMessages { .. } => {
1128            let messages = host.get_fork_messages().await;
1129            Some(RpcResponse::ok_data(
1130                id,
1131                "get_fork_messages",
1132                RpcResponseData::ForkMessages { messages },
1133            ))
1134        }
1135
1136        RpcCommand::GetEntries { since, .. } => {
1137            let entries = host.get_entries().await;
1138            let filtered = if let Some(since_id) = since {
1139                match entries
1140                    .iter()
1141                    .position(|e| e.id() == Some(since_id.as_str()))
1142                {
1143                    None => {
1144                        return Some(RpcResponse::err(
1145                            id,
1146                            "get_entries",
1147                            format!("Entry not found: {since_id}"),
1148                        ));
1149                    }
1150                    Some(i) => entries.into_iter().skip(i + 1).collect::<Vec<_>>(),
1151                }
1152            } else {
1153                entries
1154            };
1155            let leaf_id = host.get_leaf_id().await;
1156            Some(RpcResponse::ok_data(
1157                id,
1158                "get_entries",
1159                RpcResponseData::Entries {
1160                    entries: filtered,
1161                    leaf_id,
1162                },
1163            ))
1164        }
1165
1166        RpcCommand::GetTree { .. } => {
1167            let tree = host.get_tree().await;
1168            let leaf_id = host.get_leaf_id().await;
1169            Some(RpcResponse::ok_data(
1170                id,
1171                "get_tree",
1172                RpcResponseData::Tree { tree, leaf_id },
1173            ))
1174        }
1175
1176        RpcCommand::GetLastAssistantText { .. } => {
1177            let text = host.get_last_assistant_text().await;
1178            Some(RpcResponse::ok_data(
1179                id,
1180                "get_last_assistant_text",
1181                RpcResponseData::LastAssistantText { text },
1182            ))
1183        }
1184
1185        RpcCommand::SetSessionName { name, .. } => {
1186            let trimmed = name.trim();
1187            if trimmed.is_empty() {
1188                return Some(RpcResponse::err(
1189                    id,
1190                    "set_session_name",
1191                    "Session name cannot be empty",
1192                ));
1193            }
1194            match host.set_session_name(trimmed.to_owned()).await {
1195                Ok(()) => Some(RpcResponse::ok(id, "set_session_name")),
1196                Err(e) => Some(RpcResponse::err(id, "set_session_name", e)),
1197            }
1198        }
1199
1200        RpcCommand::GetMessages { .. } => {
1201            let messages = host.get_messages().await;
1202            Some(RpcResponse::ok_data(
1203                id,
1204                "get_messages",
1205                RpcResponseData::Messages { messages },
1206            ))
1207        }
1208
1209        RpcCommand::GetCommands { .. } => {
1210            let commands = host.get_commands().await;
1211            Some(RpcResponse::ok_data(
1212                id,
1213                "get_commands",
1214                RpcResponseData::Commands { commands },
1215            ))
1216        }
1217
1218        RpcCommand::Unknown {
1219            command_type: ct, ..
1220        } => Some(RpcResponse::err(id, ct, format!("Unknown command: {ct}"))),
1221    }
1222}
1223
1224/// Spawn the prompt run. Emits exactly one success on first preflight `true`,
1225/// or one error if preflight never fired.
1226///
1227/// The returned future resolves once the prompt future has either:
1228/// - fired preflight (success response already enqueued), or
1229/// - failed without preflight (error response enqueued).
1230///
1231/// Waiting here keeps subsequent command responses behind the prompt response
1232/// in the central write FIFO. Agent events still enqueue through the same
1233/// channel after preflight, preserving response-before-events ordering.
1234async fn spawn_prompt<H>(
1235    id: Option<String>,
1236    message: String,
1237    images: Vec<ImageContent>,
1238    streaming_behavior: Option<StreamingBehavior>,
1239    host: &H,
1240    write_tx: mpsc::UnboundedSender<WriteMessage>,
1241) where
1242    H: RpcSessionHost + ?Sized,
1243{
1244    let preflight_succeeded = Arc::new(AtomicBool::new(false));
1245    let (preflight_tx, preflight_rx) = tokio::sync::oneshot::channel::<()>();
1246    let preflight_notify = Arc::new(Mutex::new(Some(preflight_tx)));
1247
1248    let success_flag = Arc::clone(&preflight_succeeded);
1249    let success_tx = write_tx.clone();
1250    let success_id = id.clone();
1251    let notify_on_preflight = Arc::clone(&preflight_notify);
1252    let preflight: PreflightCallback = Arc::new(move |did_succeed: bool| {
1253        if did_succeed
1254            && success_flag
1255                .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
1256                .is_ok()
1257        {
1258            let response = RpcResponse::ok(success_id.clone(), "prompt");
1259            let _ = success_tx.send(WriteMessage::Line(to_jsonl(&response)));
1260        }
1261        // Unblock the dispatcher after preflight so later commands cannot
1262        // enqueue ahead of the prompt response.
1263        if let Some(tx) = notify_on_preflight
1264            .lock()
1265            .unwrap_or_else(std::sync::PoisonError::into_inner)
1266            .take()
1267        {
1268            let _ = tx.send(());
1269        }
1270    });
1271
1272    let prompt_future = host.prompt(message, images, streaming_behavior, preflight);
1273
1274    let error_flag = Arc::clone(&preflight_succeeded);
1275    let error_tx = write_tx;
1276    let error_id = id;
1277    let notify_on_error = Arc::clone(&preflight_notify);
1278
1279    tokio::spawn(async move {
1280        if let Err(msg) = prompt_future.await
1281            && !error_flag.load(Ordering::SeqCst)
1282        {
1283            let response = RpcResponse::err(error_id, "prompt", msg);
1284            let _ = error_tx.send(WriteMessage::Line(to_jsonl(&response)));
1285        }
1286        // If preflight never fired (host bug / panic path), still release the
1287        // dispatcher so the command loop cannot hang forever.
1288        if let Some(tx) = notify_on_error
1289            .lock()
1290            .unwrap_or_else(std::sync::PoisonError::into_inner)
1291            .take()
1292        {
1293            let _ = tx.send(());
1294        }
1295    });
1296
1297    let _ = preflight_rx.await;
1298}
1299
1300// ---------------------------------------------------------------------------
1301// Input line processing
1302// ---------------------------------------------------------------------------
1303
1304/// Outcome of processing one input line.
1305#[derive(Debug, PartialEq, Eq)]
1306pub enum LineOutcome {
1307    /// Command handled (or UI response routed).
1308    Done,
1309    /// Shutdown requested — stop reading.
1310    Shutdown,
1311}
1312
1313/// Parse and process one JSONL input line.
1314pub(crate) async fn process_input_line<H>(line: &str, host: &H, state: &ServerState) -> LineOutcome
1315where
1316    H: RpcSessionHost + ?Sized,
1317{
1318    let parsed: Value = match serde_json::from_str(line) {
1319        Ok(v) => v,
1320        Err(e) => {
1321            let response = RpcResponse::err(None, "parse", format!("Failed to parse command: {e}"));
1322            state.enqueue(to_jsonl(&response));
1323            return LineOutcome::Done;
1324        }
1325    };
1326
1327    // Route extension UI responses before command dispatch.
1328    if parsed.get("type").and_then(Value::as_str) == Some("extension_ui_response") {
1329        if let Ok(ui_resp) = serde_json::from_value::<RpcExtensionUiResponse>(parsed.clone()) {
1330            let _ = state.proxy.route_response(ui_resp);
1331        }
1332        return LineOutcome::Done;
1333    }
1334
1335    let command = match RpcCommand::parse_value(&parsed) {
1336        Ok(command) => command,
1337        Err(error) => {
1338            let response = RpcResponse::err(
1339                error.id,
1340                "parse",
1341                format!("Failed to parse command: {}", error.message),
1342            );
1343            state.enqueue(to_jsonl(&response));
1344            return LineOutcome::Done;
1345        }
1346    };
1347
1348    if let Some(resp) = handle_command(&command, host, state).await {
1349        state.enqueue(to_jsonl(&resp));
1350    }
1351
1352    if state.shutdown_requested.load(Ordering::SeqCst) {
1353        return LineOutcome::Shutdown;
1354    }
1355    LineOutcome::Done
1356}
1357
1358// ---------------------------------------------------------------------------
1359// stdin reader task
1360// ---------------------------------------------------------------------------
1361
1362enum LineMsg {
1363    Line(String),
1364    ReadError(String),
1365    Eof,
1366}
1367
1368async fn stdin_reader<R>(input: R, tx: mpsc::Sender<LineMsg>)
1369where
1370    R: AsyncRead + Unpin + Send + 'static,
1371{
1372    let mut reader = JsonlLineReader::new(input);
1373    loop {
1374        match reader.next_line().await {
1375            Ok(Some(line)) => {
1376                if tx.send(LineMsg::Line(line)).await.is_err() {
1377                    break;
1378                }
1379            }
1380            Ok(None) => {
1381                let _ = tx.send(LineMsg::Eof).await;
1382                break;
1383            }
1384            Err(error) => {
1385                let _ = tx.send(LineMsg::ReadError(error.to_string())).await;
1386                break;
1387            }
1388        }
1389    }
1390}
1391
1392// ---------------------------------------------------------------------------
1393// Event loop (testable)
1394// ---------------------------------------------------------------------------
1395
1396/// Run the RPC event loop until a shutdown condition is reached.
1397///
1398/// Returns the process exit code.
1399///
1400/// All stdout frames (responses, events, extension errors, prompt preflight)
1401/// flow through one writer actor. Ordered drain barriers provide backpressure
1402/// without making the input/signal loop responsible for stdout progress.
1403pub async fn run_rpc_loop<H, R>(host: H, sink: Arc<dyn RpcSink>, input: R) -> i32
1404where
1405    H: RpcSessionHost,
1406    R: AsyncRead + Unpin + Send + 'static,
1407{
1408    let (write_tx, write_rx) = mpsc::unbounded_channel::<WriteMessage>();
1409    let state = Arc::new(ServerState::new(
1410        Arc::clone(&sink),
1411        write_tx,
1412        ExtensionUiProxy::new(),
1413    ));
1414    let writer_task = tokio::spawn(writer_actor(write_rx, Arc::clone(&sink)));
1415
1416    let rebind_signal = Arc::clone(&state.signal);
1417    let rebind_flag = Arc::clone(&state.needs_rebind);
1418    host.set_rebind(Some(Arc::new(move || {
1419        let signal = Arc::clone(&rebind_signal);
1420        let flag = Arc::clone(&rebind_flag);
1421        Box::pin(async move {
1422            flag.store(true, Ordering::SeqCst);
1423            signal.notify_one();
1424        })
1425    })));
1426
1427    state.rebind(&host).await;
1428
1429    let (line_tx, mut line_rx) = mpsc::channel::<LineMsg>(64);
1430    tokio::spawn(stdin_reader(input, line_tx));
1431
1432    let exit_code = loop {
1433        tokio::select! {
1434            biased;
1435            () = state.signal.notified() => {
1436                if state.shutdown_requested.load(Ordering::SeqCst) {
1437                    break 0;
1438                }
1439                if state.needs_rebind.swap(false, Ordering::SeqCst) {
1440                    state.rebind(&host).await;
1441                }
1442            }
1443            msg = line_rx.recv() => {
1444                match msg {
1445                    Some(LineMsg::Line(line)) => {
1446                        let outcome = process_input_line(&line, &host, &state).await;
1447                        state.wait_for_output().await;
1448                        if outcome == LineOutcome::Shutdown {
1449                            break 0;
1450                        }
1451                    }
1452                    Some(LineMsg::ReadError(error)) => {
1453                        let response = RpcResponse::err(
1454                            None,
1455                            "transport",
1456                            format!("Failed to read stdin: {error}"),
1457                        );
1458                        state.enqueue(to_jsonl(&response));
1459                        break 1;
1460                    }
1461                    Some(LineMsg::Eof) | None => break 0,
1462                }
1463            }
1464        }
1465    };
1466
1467    state.cleanup(&host, exit_code).await;
1468    writer_task.abort();
1469    exit_code
1470}
1471
1472// ---------------------------------------------------------------------------
1473// Production entry point
1474// ---------------------------------------------------------------------------
1475
1476/// Production entry point. Takes over stdout, reads stdin, dispatches
1477/// commands, handles signals/EOF, and returns the exit code.
1478///
1479/// The caller (bootstrap) is responsible for `std::process::exit(code)`.
1480pub async fn run_rpc_mode<H>(host: H) -> i32
1481where
1482    H: RpcSessionHost,
1483{
1484    let _ = output_guard_mod::take_over_stdout();
1485    let sink: Arc<dyn RpcSink> = Arc::new(OutputGuardSink);
1486
1487    let (write_tx, write_rx) = mpsc::unbounded_channel::<WriteMessage>();
1488    let state = Arc::new(ServerState::new(
1489        Arc::clone(&sink),
1490        write_tx,
1491        ExtensionUiProxy::new(),
1492    ));
1493    let writer_task = tokio::spawn(writer_actor(write_rx, Arc::clone(&sink)));
1494
1495    let rs = Arc::clone(&state.signal);
1496    let rf = Arc::clone(&state.needs_rebind);
1497    host.set_rebind(Some(Arc::new(move || {
1498        let s = Arc::clone(&rs);
1499        let f = Arc::clone(&rf);
1500        Box::pin(async move {
1501            f.store(true, Ordering::SeqCst);
1502            s.notify_one();
1503        })
1504    })));
1505    state.rebind(&host).await;
1506
1507    let (signal_tx, mut signal_rx) = mpsc::channel::<i32>(1);
1508    spawn_signal_handlers(signal_tx);
1509
1510    let stdin = tokio::io::stdin();
1511    let (line_tx, mut line_rx) = mpsc::channel::<LineMsg>(64);
1512    tokio::spawn(stdin_reader(stdin, line_tx));
1513
1514    let exit_code = loop {
1515        tokio::select! {
1516            biased;
1517            code = signal_rx.recv() => {
1518                break code.unwrap_or(0);
1519            }
1520            () = state.signal.notified() => {
1521                if state.shutdown_requested.load(Ordering::SeqCst) {
1522                    break 0;
1523                }
1524                if state.needs_rebind.swap(false, Ordering::SeqCst) {
1525                    state.rebind(&host).await;
1526                }
1527            }
1528            msg = line_rx.recv() => {
1529                match msg {
1530                    Some(LineMsg::Line(line)) => {
1531                        let outcome = process_input_line(&line, &host, &state).await;
1532                        state.wait_for_output().await;
1533                        if outcome == LineOutcome::Shutdown {
1534                            break 0;
1535                        }
1536                    }
1537                    Some(LineMsg::ReadError(error)) => {
1538                        let response = RpcResponse::err(
1539                            None,
1540                            "transport",
1541                            format!("Failed to read stdin: {error}"),
1542                        );
1543                        state.enqueue(to_jsonl(&response));
1544                        break 1;
1545                    }
1546                    Some(LineMsg::Eof) | None => break 0,
1547                }
1548            }
1549        }
1550    };
1551
1552    state.cleanup(&host, exit_code).await;
1553    writer_task.abort();
1554    output_guard_mod::restore_stdout();
1555    exit_code
1556}
1557
1558async fn writer_actor(mut write_rx: mpsc::UnboundedReceiver<WriteMessage>, sink: Arc<dyn RpcSink>) {
1559    while let Some(message) = write_rx.recv().await {
1560        match message {
1561            WriteMessage::Line(line) => {
1562                let _ = sink.write_stdout(line).await;
1563            }
1564            WriteMessage::Drain(done) => {
1565                let _ = sink.backpressure().await;
1566                let _ = done.send(());
1567            }
1568        }
1569    }
1570}
1571
1572/// Spawn SIGTERM (→143) and SIGHUP (→129, unix-only) handlers.
1573fn spawn_signal_handlers(tx: mpsc::Sender<i32>) {
1574    #[cfg(unix)]
1575    {
1576        {
1577            let tx = tx.clone();
1578            tokio::spawn(async move {
1579                use tokio::signal::unix::{SignalKind, signal};
1580                if let Ok(mut sig) = signal(SignalKind::terminate()) {
1581                    sig.recv().await;
1582                    let _ = tx.send(143).await;
1583                }
1584            });
1585        }
1586        tokio::spawn(async move {
1587            use tokio::signal::unix::{SignalKind, signal};
1588            if let Ok(mut sig) = signal(SignalKind::hangup()) {
1589                sig.recv().await;
1590                // tx is moved here; SIGHUP is the last handler.
1591                let _ = tx.send(129).await;
1592            }
1593        });
1594    }
1595    #[cfg(not(unix))]
1596    {
1597        let _ = tx;
1598    }
1599}
1600
1601// ===========================================================================
1602// Tests
1603// ===========================================================================
1604
1605#[cfg(test)]
1606#[allow(clippy::unwrap_used, clippy::panic, clippy::expect_used)]
1607mod tests {
1608    use super::*;
1609    use crate::core::agent_session::extension::ExtensionBindings;
1610    use crate::core::agent_session_runtime::{ForkOutcome, ForkPosition};
1611    use crate::core::compaction::CompactionResult;
1612    use crate::core::sessions::SessionEntry;
1613    use crate::modes::rpc::types::{
1614        RpcSessionState, RpcSessionTreeNode, RpcSlashCommand, RpcSlashCommandSource, RpcSourceInfo,
1615        RpcSourceOrigin, RpcSourceScope, SessionStats, SessionStatsTokens,
1616    };
1617    use pi_agent::QueueMode;
1618    use pi_ai::{ImageContent, Model, ModelThinkingLevel};
1619    use pi_ext::client::HostClient;
1620    use pi_ext::protocol::{Frame, FrameKind, HelloAck, Method, decode_frame_str, encode_frame};
1621    use std::task::{Context, Poll};
1622    use tokio::io::ReadBuf;
1623    use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, DuplexStream};
1624
1625    // -----------------------------------------------------------------------
1626    // FakeRpcHost
1627    // -----------------------------------------------------------------------
1628
1629    #[derive(Clone)]
1630    struct FakeConfig {
1631        state: RpcSessionState,
1632        models: Vec<Model>,
1633        commands: Vec<RpcSlashCommand>,
1634        cycle_model_result: Option<ModelCycleResult>,
1635        cycle_thinking_result: Option<ModelThinkingLevel>,
1636        set_thinking_result: bool,
1637        compact_result: Option<Result<CompactionResult, String>>,
1638        bash_result: Option<Result<BashResult, String>>,
1639        fork_outcome: Result<ForkOutcome, String>,
1640        leaf_id: Option<String>,
1641        prompt_error: Option<String>,
1642        session_op_cancelled: bool,
1643    }
1644
1645    impl Default for FakeConfig {
1646        fn default() -> Self {
1647            Self {
1648                state: test_state(),
1649                models: vec![],
1650                commands: vec![],
1651                cycle_model_result: None,
1652                cycle_thinking_result: None,
1653                set_thinking_result: true,
1654                compact_result: None,
1655                bash_result: None,
1656                fork_outcome: Ok(ForkOutcome::default()),
1657                leaf_id: Some("leaf1".into()),
1658                prompt_error: None,
1659                session_op_cancelled: false,
1660            }
1661        }
1662    }
1663
1664    fn test_state() -> RpcSessionState {
1665        RpcSessionState {
1666            model: None,
1667            thinking_level: ModelThinkingLevel::Medium,
1668            is_streaming: false,
1669            is_compacting: false,
1670            steering_mode: QueueMode::All,
1671            follow_up_mode: QueueMode::OneAtATime,
1672            session_file: None,
1673            session_id: "test-session".into(),
1674            session_name: None,
1675            auto_compaction_enabled: true,
1676            message_count: 0,
1677            pending_message_count: 0,
1678        }
1679    }
1680
1681    fn test_stats() -> SessionStats {
1682        SessionStats {
1683            session_file: None,
1684            session_id: "test-session".into(),
1685            user_messages: 0,
1686            assistant_messages: 0,
1687            tool_calls: 0,
1688            tool_results: 0,
1689            total_messages: 0,
1690            tokens: SessionStatsTokens {
1691                input: 0,
1692                output: 0,
1693                cache_read: 0,
1694                cache_write: 0,
1695                total: 0,
1696            },
1697            cost: 0.0,
1698            context_usage: None,
1699        }
1700    }
1701
1702    struct FailingInput;
1703
1704    impl AsyncRead for FailingInput {
1705        fn poll_read(
1706            self: Pin<&mut Self>,
1707            _cx: &mut Context<'_>,
1708            _buf: &mut ReadBuf<'_>,
1709        ) -> Poll<io::Result<()>> {
1710            Poll::Ready(Err(io::Error::new(
1711                io::ErrorKind::ConnectionReset,
1712                "stdin transport failed",
1713            )))
1714        }
1715    }
1716
1717    #[derive(Clone)]
1718    struct FakeRpcHost {
1719        cfg: Arc<Mutex<FakeConfig>>,
1720        calls: Arc<Mutex<Vec<String>>>,
1721        disposed: Arc<AtomicBool>,
1722        events_tx: Arc<Mutex<Option<tokio::sync::mpsc::UnboundedSender<AgentSessionEvent>>>>,
1723        bindings: Arc<Mutex<Option<ExtensionBindings>>>,
1724        extension_runner: Arc<Mutex<Option<Arc<HostExtensionRunner>>>>,
1725    }
1726
1727    impl FakeRpcHost {
1728        fn new(cfg: FakeConfig) -> Self {
1729            Self {
1730                cfg: Arc::new(Mutex::new(cfg)),
1731                calls: Arc::new(Mutex::new(Vec::new())),
1732                disposed: Arc::new(AtomicBool::new(false)),
1733                events_tx: Arc::new(Mutex::new(None)),
1734                bindings: Arc::new(Mutex::new(None)),
1735                extension_runner: Arc::new(Mutex::new(None)),
1736            }
1737        }
1738
1739        fn set_extension_runner(&self, runner: Arc<HostExtensionRunner>) {
1740            *self.extension_runner.lock().unwrap() = Some(runner);
1741        }
1742        fn rec(&self, name: &str) {
1743            self.calls.lock().unwrap().push(name.to_owned());
1744        }
1745    }
1746
1747    impl RpcSessionHost for FakeRpcHost {
1748        fn prompt(
1749            &self,
1750            _msg: String,
1751            _img: Vec<ImageContent>,
1752            _sb: Option<StreamingBehavior>,
1753            preflight: PreflightCallback,
1754        ) -> BoxFuture<'static, Result<(), String>> {
1755            self.rec("prompt");
1756            let err = self.cfg.lock().unwrap().prompt_error.clone();
1757            let events_tx = Arc::clone(&self.events_tx);
1758            Box::pin(async move {
1759                if let Some(e) = err {
1760                    preflight(false);
1761                    Err(e)
1762                } else {
1763                    preflight(true);
1764                    // Emit an event synchronously immediately after preflight!
1765                    if let Some(tx) = events_tx.lock().unwrap().as_ref() {
1766                        let _ = tx.send(AgentSessionEvent::TurnStart);
1767                    }
1768                    Ok(())
1769                }
1770            })
1771        }
1772        fn steer(
1773            &self,
1774            _m: String,
1775            _i: Vec<ImageContent>,
1776        ) -> BoxFuture<'static, Result<(), String>> {
1777            self.rec("steer");
1778            Box::pin(async { Ok(()) })
1779        }
1780        fn follow_up(
1781            &self,
1782            _m: String,
1783            _i: Vec<ImageContent>,
1784        ) -> BoxFuture<'static, Result<(), String>> {
1785            self.rec("follow_up");
1786            Box::pin(async { Ok(()) })
1787        }
1788        fn abort(&self) -> BoxFuture<'static, ()> {
1789            self.rec("abort");
1790            Box::pin(async {})
1791        }
1792        fn get_state(&self) -> BoxFuture<'static, RpcSessionState> {
1793            self.rec("get_state");
1794            let cfg = Arc::clone(&self.cfg);
1795            Box::pin(async move { cfg.lock().unwrap().state.clone() })
1796        }
1797        fn get_available_models(&self) -> BoxFuture<'static, Vec<Model>> {
1798            self.rec("get_available_models");
1799            let cfg = Arc::clone(&self.cfg);
1800            Box::pin(async move { cfg.lock().unwrap().models.clone() })
1801        }
1802        fn set_model(&self, _m: Model) -> BoxFuture<'static, Result<(), String>> {
1803            self.rec("set_model");
1804            Box::pin(async { Ok(()) })
1805        }
1806        fn cycle_model(&self) -> BoxFuture<'static, Option<ModelCycleResult>> {
1807            self.rec("cycle_model");
1808            let cfg = Arc::clone(&self.cfg);
1809            Box::pin(async move { cfg.lock().unwrap().cycle_model_result.clone() })
1810        }
1811        fn set_thinking_level(&self, _l: ModelThinkingLevel) -> BoxFuture<'static, bool> {
1812            self.rec("set_thinking_level");
1813            let cfg = Arc::clone(&self.cfg);
1814            Box::pin(async move { cfg.lock().unwrap().set_thinking_result })
1815        }
1816        fn cycle_thinking_level(&self) -> BoxFuture<'static, Option<ModelThinkingLevel>> {
1817            self.rec("cycle_thinking_level");
1818            let cfg = Arc::clone(&self.cfg);
1819            Box::pin(async move { cfg.lock().unwrap().cycle_thinking_result })
1820        }
1821        fn set_steering_mode(&self, _m: QueueMode) -> BoxFuture<'static, ()> {
1822            self.rec("set_steering_mode");
1823            Box::pin(async {})
1824        }
1825        fn set_follow_up_mode(&self, _m: QueueMode) -> BoxFuture<'static, ()> {
1826            self.rec("set_follow_up_mode");
1827            Box::pin(async {})
1828        }
1829        fn compact(
1830            &self,
1831            _ci: Option<String>,
1832        ) -> BoxFuture<'static, Result<CompactionResult, String>> {
1833            self.rec("compact");
1834            let cfg = Arc::clone(&self.cfg);
1835            Box::pin(async move {
1836                cfg.lock()
1837                    .unwrap()
1838                    .compact_result
1839                    .clone()
1840                    .unwrap_or(Ok(test_compaction_result()))
1841            })
1842        }
1843        fn set_auto_compaction(&self, _e: bool) -> BoxFuture<'static, ()> {
1844            self.rec("set_auto_compaction");
1845            Box::pin(async {})
1846        }
1847        fn set_auto_retry(&self, _e: bool) -> BoxFuture<'static, ()> {
1848            self.rec("set_auto_retry");
1849            Box::pin(async {})
1850        }
1851        fn abort_retry(&self) -> BoxFuture<'static, ()> {
1852            self.rec("abort_retry");
1853            Box::pin(async {})
1854        }
1855        fn execute_bash(
1856            &self,
1857            _c: String,
1858            _e: Option<bool>,
1859        ) -> BoxFuture<'static, Result<BashResult, String>> {
1860            self.rec("bash");
1861            let cfg = Arc::clone(&self.cfg);
1862            Box::pin(async move {
1863                cfg.lock()
1864                    .unwrap()
1865                    .bash_result
1866                    .clone()
1867                    .unwrap_or(Ok(test_bash_result()))
1868            })
1869        }
1870        fn abort_bash(&self) -> BoxFuture<'static, ()> {
1871            self.rec("abort_bash");
1872            Box::pin(async {})
1873        }
1874        fn get_session_stats(&self) -> BoxFuture<'static, SessionStats> {
1875            self.rec("get_session_stats");
1876            Box::pin(async { test_stats() })
1877        }
1878        fn export_to_html(&self, _p: Option<String>) -> BoxFuture<'static, Result<String, String>> {
1879            self.rec("export_html");
1880            Box::pin(async { Ok("/tmp/out.html".into()) })
1881        }
1882        fn set_session_name(&self, _n: String) -> BoxFuture<'static, Result<(), String>> {
1883            self.rec("set_session_name");
1884            Box::pin(async { Ok(()) })
1885        }
1886        fn new_session(&self, _p: Option<String>) -> BoxFuture<'static, Result<bool, String>> {
1887            self.rec("new_session");
1888            let cfg = Arc::clone(&self.cfg);
1889            Box::pin(async move { Ok(cfg.lock().unwrap().session_op_cancelled) })
1890        }
1891        fn switch_session(&self, _p: String) -> BoxFuture<'static, Result<bool, String>> {
1892            self.rec("switch_session");
1893            let cfg = Arc::clone(&self.cfg);
1894            Box::pin(async move { Ok(cfg.lock().unwrap().session_op_cancelled) })
1895        }
1896        fn fork(
1897            &self,
1898            _e: String,
1899            _p: ForkPosition,
1900        ) -> BoxFuture<'static, Result<ForkOutcome, String>> {
1901            self.rec("fork");
1902            let cfg = Arc::clone(&self.cfg);
1903            Box::pin(async move { cfg.lock().unwrap().fork_outcome.clone() })
1904        }
1905        fn get_entries(&self) -> BoxFuture<'static, Vec<SessionEntry>> {
1906            self.rec("get_entries");
1907            Box::pin(async { vec![] })
1908        }
1909        fn get_leaf_id(&self) -> BoxFuture<'static, Option<String>> {
1910            self.rec("get_leaf_id");
1911            let cfg = Arc::clone(&self.cfg);
1912            Box::pin(async move { cfg.lock().unwrap().leaf_id.clone() })
1913        }
1914        fn get_tree(&self) -> BoxFuture<'static, Vec<RpcSessionTreeNode>> {
1915            self.rec("get_tree");
1916            Box::pin(async { vec![] })
1917        }
1918        fn get_fork_messages(&self) -> BoxFuture<'static, Vec<ForkMessage>> {
1919            self.rec("get_fork_messages");
1920            Box::pin(async { vec![] })
1921        }
1922        fn get_last_assistant_text(&self) -> BoxFuture<'static, Option<String>> {
1923            self.rec("get_last_assistant_text");
1924            Box::pin(async { None })
1925        }
1926        fn get_messages(&self) -> BoxFuture<'static, Vec<AgentMessage>> {
1927            self.rec("get_messages");
1928            Box::pin(async { vec![] })
1929        }
1930        fn get_commands(&self) -> BoxFuture<'static, Vec<RpcSlashCommand>> {
1931            self.rec("get_commands");
1932            let cfg = Arc::clone(&self.cfg);
1933            Box::pin(async move { cfg.lock().unwrap().commands.clone() })
1934        }
1935        fn subscribe(
1936            &self,
1937            listener: Arc<dyn Fn(&AgentSessionEvent) + Send + Sync>,
1938        ) -> Box<dyn Fn() + Send + Sync> {
1939            self.rec("subscribe");
1940            let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<AgentSessionEvent>();
1941            *self.events_tx.lock().unwrap() = Some(tx);
1942            tokio::spawn(async move {
1943                while let Some(event) = rx.recv().await {
1944                    listener(&event);
1945                }
1946            });
1947            Box::new(|| {})
1948        }
1949        fn register_backpressure_hook(
1950            &self,
1951            _hook: Arc<dyn Fn() -> BoxFuture<'static, ()> + Send + Sync>,
1952        ) -> Box<dyn Fn() + Send + Sync> {
1953            self.rec("register_backpressure_hook");
1954            Box::new(|| {})
1955        }
1956        fn bind_extensions_rpc(
1957            &self,
1958            bindings: ExtensionBindings,
1959        ) -> BoxFuture<'static, Result<(), String>> {
1960            self.rec("bind_extensions_rpc");
1961            *self.bindings.lock().unwrap() = Some(bindings);
1962            Box::pin(async { Ok(()) })
1963        }
1964        fn host_extension_runner(&self) -> Option<Arc<HostExtensionRunner>> {
1965            self.extension_runner.lock().unwrap().clone()
1966        }
1967        fn dispose(&self) -> BoxFuture<'static, ()> {
1968            self.rec("dispose");
1969            self.disposed.store(true, Ordering::SeqCst);
1970            Box::pin(async {})
1971        }
1972        fn set_rebind(&self, _cb: Option<RebindCallback>) {
1973            self.rec("set_rebind");
1974        }
1975    }
1976    fn test_compaction_result() -> CompactionResult {
1977        CompactionResult {
1978            summary: "Summary".into(),
1979            first_kept_entry_id: "entry1".into(),
1980            tokens_before: 1000,
1981            estimated_tokens_after: Some(500),
1982            details: None,
1983            from_hook: None,
1984        }
1985    }
1986    fn test_bash_result() -> BashResult {
1987        BashResult {
1988            output: "done".into(),
1989            exit_code: Some(0),
1990            cancelled: false,
1991            truncated: false,
1992            full_output_path: None,
1993        }
1994    }
1995    // -----------------------------------------------------------------------
1996    // Test helpers
1997    // -----------------------------------------------------------------------
1998
1999    fn make_state(
2000        sink: BufferSink,
2001    ) -> (
2002        Arc<ServerState>,
2003        BufferSink,
2004        mpsc::UnboundedReceiver<WriteMessage>,
2005    ) {
2006        let s = sink.clone();
2007        let (write_tx, write_rx) = mpsc::unbounded_channel::<WriteMessage>();
2008        let state = Arc::new(ServerState::new(
2009            Arc::new(sink) as Arc<dyn RpcSink>,
2010            write_tx,
2011            ExtensionUiProxy::new(),
2012        ));
2013        (state, s, write_rx)
2014    }
2015
2016    /// Drain all pending write-channel frames into the sink.
2017    async fn drain(sink: &BufferSink, write_rx: &mut mpsc::UnboundedReceiver<WriteMessage>) {
2018        while let Ok(message) = write_rx.try_recv() {
2019            match message {
2020                WriteMessage::Line(line) => {
2021                    let _ = sink.write_stdout(line).await;
2022                }
2023                WriteMessage::Drain(done) => {
2024                    let _ = sink.backpressure().await;
2025                    let _ = done.send(());
2026                }
2027            }
2028        }
2029    }
2030
2031    async fn dispatch(cmd_json: &str, cfg: FakeConfig) -> (Value, BufferSink) {
2032        let host = FakeRpcHost::new(cfg);
2033        let sink = BufferSink::new();
2034        let (state, sink_clone, mut write_rx) = make_state(sink);
2035        process_input_line(cmd_json, &host, &state).await;
2036        drain(&sink_clone, &mut write_rx).await;
2037        let lines = sink_clone.stdout_lines();
2038        assert!(!lines.is_empty(), "expected at least one response line");
2039        let resp: Value = serde_json::from_str(&lines[0]).unwrap();
2040        (resp, sink_clone)
2041    }
2042
2043    async fn dispatch_no_response(
2044        cmd_json: &str,
2045        cfg: FakeConfig,
2046    ) -> (BufferSink, mpsc::UnboundedReceiver<WriteMessage>) {
2047        let host = FakeRpcHost::new(cfg);
2048        let sink = BufferSink::new();
2049        let (state, sink_clone, mut write_rx) = make_state(sink);
2050        process_input_line(cmd_json, &host, &state).await;
2051        drain(&sink_clone, &mut write_rx).await;
2052        (sink_clone, write_rx)
2053    }
2054
2055    // -----------------------------------------------------------------------
2056    // Unknown command
2057    // -----------------------------------------------------------------------
2058
2059    #[tokio::test]
2060    async fn unknown_command_echoes_id_and_type() {
2061        let (resp, _) = dispatch(
2062            r#"{"type":"totally_unknown","id":"abc","foo":"bar"}"#,
2063            FakeConfig::default(),
2064        )
2065        .await;
2066        assert_eq!(resp["type"], "response");
2067        assert_eq!(resp["id"], "abc");
2068        assert_eq!(resp["command"], "totally_unknown");
2069        assert_eq!(resp["success"], false);
2070        assert_eq!(resp["error"], "Unknown command: totally_unknown");
2071    }
2072
2073    // -----------------------------------------------------------------------
2074    // Malformed JSON
2075    // -----------------------------------------------------------------------
2076
2077    #[tokio::test]
2078    async fn malformed_json_parse_error_no_id() {
2079        let (resp, _) = dispatch("{not valid json", FakeConfig::default()).await;
2080        assert_eq!(resp["command"], "parse");
2081        assert_eq!(resp["success"], false);
2082        assert!(resp.get("id").is_none() || resp["id"].is_null());
2083        assert!(
2084            resp["error"]
2085                .as_str()
2086                .unwrap()
2087                .starts_with("Failed to parse")
2088        );
2089    }
2090
2091    #[tokio::test]
2092    async fn field_validation_error_echoes_valid_id() {
2093        let (response, _) = dispatch(
2094            r#"{"type":"bash","id":"req-17","command":"true","excludeFromContext":"yes"}"#,
2095            FakeConfig::default(),
2096        )
2097        .await;
2098        assert_eq!(response["id"], "req-17");
2099        assert_eq!(response["command"], "parse");
2100        assert_eq!(response["success"], false);
2101        assert!(
2102            response["error"]
2103                .as_str()
2104                .unwrap()
2105                .contains("excludeFromContext")
2106        );
2107    }
2108
2109    // -----------------------------------------------------------------------
2110    // Prompt preflight semantics
2111    // -----------------------------------------------------------------------
2112
2113    #[tokio::test]
2114    async fn prompt_emits_success_once() {
2115        let (sink, mut write_rx) = dispatch_no_response(
2116            r#"{"type":"prompt","id":"p1","message":"hello"}"#,
2117            FakeConfig::default(),
2118        )
2119        .await;
2120        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
2121        drain(&sink, &mut write_rx).await;
2122        let lines = sink.stdout_lines();
2123        assert_eq!(lines.len(), 1, "exactly one success response");
2124        let resp: Value = serde_json::from_str(&lines[0]).unwrap();
2125        assert_eq!(resp["command"], "prompt");
2126        assert_eq!(resp["success"], true);
2127    }
2128
2129    #[tokio::test]
2130    async fn prompt_emits_error() {
2131        let cfg = FakeConfig {
2132            prompt_error: Some("No model".into()),
2133            ..FakeConfig::default()
2134        };
2135        let (sink, mut write_rx) =
2136            dispatch_no_response(r#"{"type":"prompt","id":"p2","message":"hi"}"#, cfg).await;
2137        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
2138        drain(&sink, &mut write_rx).await;
2139        let lines = sink.stdout_lines();
2140        assert_eq!(lines.len(), 1);
2141        let resp: Value = serde_json::from_str(&lines[0]).unwrap();
2142        assert_eq!(resp["success"], false);
2143        assert_eq!(resp["error"], "No model");
2144    }
2145
2146    // -----------------------------------------------------------------------
2147    // Simple success responses
2148    // -----------------------------------------------------------------------
2149
2150    #[tokio::test]
2151    async fn steer_success() {
2152        let (r, _) = dispatch(
2153            r#"{"type":"steer","id":"s1","message":"left"}"#,
2154            FakeConfig::default(),
2155        )
2156        .await;
2157        assert_eq!(r["command"], "steer");
2158        assert_eq!(r["success"], true);
2159    }
2160
2161    #[tokio::test]
2162    async fn follow_up_success() {
2163        let (r, _) = dispatch(
2164            r#"{"type":"follow_up","id":"f1","message":"next"}"#,
2165            FakeConfig::default(),
2166        )
2167        .await;
2168        assert_eq!(r["command"], "follow_up");
2169    }
2170
2171    #[tokio::test]
2172    async fn abort_success() {
2173        let (r, _) = dispatch(r#"{"type":"abort","id":"a1"}"#, FakeConfig::default()).await;
2174        assert_eq!(r["command"], "abort");
2175    }
2176
2177    #[tokio::test]
2178    async fn set_thinking_level_success() {
2179        let (r, _) = dispatch(
2180            r#"{"type":"set_thinking_level","id":"t1","level":"high"}"#,
2181            FakeConfig::default(),
2182        )
2183        .await;
2184        assert_eq!(r["command"], "set_thinking_level");
2185        assert_eq!(r["success"], true);
2186    }
2187
2188    #[tokio::test]
2189    async fn set_thinking_level_uncommitted_returns_error() {
2190        let (r, _) = dispatch(
2191            r#"{"type":"set_thinking_level","id":"t2","level":"high"}"#,
2192            FakeConfig {
2193                set_thinking_result: false,
2194                ..FakeConfig::default()
2195            },
2196        )
2197        .await;
2198        assert_eq!(r["command"], "set_thinking_level");
2199        assert_eq!(r["success"], false);
2200        assert_eq!(r["error"], "Failed to persist thinking level change");
2201    }
2202
2203    #[tokio::test]
2204    async fn set_steering_mode_success() {
2205        let (r, _) = dispatch(
2206            r#"{"type":"set_steering_mode","id":"sm1","mode":"all"}"#,
2207            FakeConfig::default(),
2208        )
2209        .await;
2210        assert_eq!(r["command"], "set_steering_mode");
2211    }
2212
2213    #[tokio::test]
2214    async fn set_follow_up_mode_success() {
2215        let (r, _) = dispatch(
2216            r#"{"type":"set_follow_up_mode","id":"fm1","mode":"one-at-a-time"}"#,
2217            FakeConfig::default(),
2218        )
2219        .await;
2220        assert_eq!(r["command"], "set_follow_up_mode");
2221    }
2222
2223    #[tokio::test]
2224    async fn set_auto_compaction_success() {
2225        let (r, _) = dispatch(
2226            r#"{"type":"set_auto_compaction","id":"ac1","enabled":true}"#,
2227            FakeConfig::default(),
2228        )
2229        .await;
2230        assert_eq!(r["command"], "set_auto_compaction");
2231    }
2232
2233    #[tokio::test]
2234    async fn set_auto_retry_success() {
2235        let (r, _) = dispatch(
2236            r#"{"type":"set_auto_retry","id":"ar1","enabled":false}"#,
2237            FakeConfig::default(),
2238        )
2239        .await;
2240        assert_eq!(r["command"], "set_auto_retry");
2241    }
2242
2243    #[tokio::test]
2244    async fn abort_retry_success() {
2245        let (r, _) = dispatch(
2246            r#"{"type":"abort_retry","id":"abr1"}"#,
2247            FakeConfig::default(),
2248        )
2249        .await;
2250        assert_eq!(r["command"], "abort_retry");
2251    }
2252
2253    #[tokio::test]
2254    async fn abort_bash_success() {
2255        let (r, _) = dispatch(r#"{"type":"abort_bash","id":"ab1"}"#, FakeConfig::default()).await;
2256        assert_eq!(r["command"], "abort_bash");
2257    }
2258
2259    // -----------------------------------------------------------------------
2260    // Data responses
2261    // -----------------------------------------------------------------------
2262
2263    #[tokio::test]
2264    async fn get_state_returns_snapshot() {
2265        let (r, _) = dispatch(r#"{"type":"get_state","id":"gs1"}"#, FakeConfig::default()).await;
2266        assert_eq!(r["data"]["sessionId"], "test-session");
2267        assert_eq!(r["data"]["thinkingLevel"], "medium");
2268    }
2269
2270    #[tokio::test]
2271    async fn get_session_stats_returns_data() {
2272        let (r, _) = dispatch(
2273            r#"{"type":"get_session_stats","id":"sst1"}"#,
2274            FakeConfig::default(),
2275        )
2276        .await;
2277        assert_eq!(r["data"]["sessionId"], "test-session");
2278    }
2279
2280    #[tokio::test]
2281    async fn export_html_returns_path() {
2282        let (r, _) = dispatch(
2283            r#"{"type":"export_html","id":"eh1","outputPath":"/tmp/x.html"}"#,
2284            FakeConfig::default(),
2285        )
2286        .await;
2287        assert_eq!(r["data"]["path"], "/tmp/out.html");
2288    }
2289
2290    #[tokio::test]
2291    async fn get_last_assistant_text_null() {
2292        let (r, _) = dispatch(
2293            r#"{"type":"get_last_assistant_text","id":"lat1"}"#,
2294            FakeConfig::default(),
2295        )
2296        .await;
2297        assert!(r["data"]["text"].is_null());
2298    }
2299
2300    #[tokio::test]
2301    async fn get_messages_empty() {
2302        let (r, _) = dispatch(
2303            r#"{"type":"get_messages","id":"gm1"}"#,
2304            FakeConfig::default(),
2305        )
2306        .await;
2307        assert_eq!(r["data"]["messages"].as_array().unwrap().len(), 0);
2308    }
2309
2310    #[tokio::test]
2311    async fn get_commands_returns_complete_catalog() {
2312        let source_info = RpcSourceInfo {
2313            path: "/tmp/resource".into(),
2314            source: "test".into(),
2315            scope: RpcSourceScope::Temporary,
2316            origin: RpcSourceOrigin::TopLevel,
2317            base_dir: None,
2318        };
2319        let commands = [
2320            ("ext-command", RpcSlashCommandSource::Extension),
2321            ("deploy", RpcSlashCommandSource::Prompt),
2322            ("skill:review", RpcSlashCommandSource::Skill),
2323        ]
2324        .into_iter()
2325        .map(|(name, source)| RpcSlashCommand {
2326            name: name.into(),
2327            description: Some(format!("{name} description")),
2328            source,
2329            source_info: source_info.clone(),
2330        })
2331        .collect();
2332        let (response, _) = dispatch(
2333            r#"{"type":"get_commands","id":"gc1"}"#,
2334            FakeConfig {
2335                commands,
2336                ..FakeConfig::default()
2337            },
2338        )
2339        .await;
2340        let catalog = response["data"]["commands"].as_array().unwrap();
2341        assert_eq!(catalog.len(), 3);
2342        assert_eq!(catalog[0]["source"], "extension");
2343        assert_eq!(catalog[1]["source"], "prompt");
2344        assert_eq!(catalog[2]["name"], "skill:review");
2345        assert_eq!(catalog[2]["source"], "skill");
2346    }
2347
2348    #[tokio::test]
2349    async fn bash_returns_result() {
2350        let (r, _) = dispatch(
2351            r#"{"type":"bash","id":"b1","command":"echo hi"}"#,
2352            FakeConfig::default(),
2353        )
2354        .await;
2355        assert_eq!(r["data"]["output"], "done");
2356    }
2357
2358    #[tokio::test]
2359    async fn compact_returns_result() {
2360        let (r, _) = dispatch(r#"{"type":"compact","id":"c1"}"#, FakeConfig::default()).await;
2361        assert_eq!(r["data"]["summary"], "Summary");
2362    }
2363
2364    #[tokio::test]
2365    async fn cycle_model_null() {
2366        let (r, _) = dispatch(
2367            r#"{"type":"cycle_model","id":"cm1"}"#,
2368            FakeConfig::default(),
2369        )
2370        .await;
2371        assert!(r["data"].is_null());
2372    }
2373
2374    #[tokio::test]
2375    async fn cycle_thinking_null() {
2376        let (r, _) = dispatch(
2377            r#"{"type":"cycle_thinking_level","id":"ct1"}"#,
2378            FakeConfig::default(),
2379        )
2380        .await;
2381        assert!(r["data"].is_null());
2382    }
2383
2384    #[tokio::test]
2385    async fn get_available_models_empty() {
2386        let (r, _) = dispatch(
2387            r#"{"type":"get_available_models","id":"gam1"}"#,
2388            FakeConfig::default(),
2389        )
2390        .await;
2391        assert_eq!(r["data"]["models"].as_array().unwrap().len(), 0);
2392    }
2393
2394    #[tokio::test]
2395    async fn get_fork_messages_empty() {
2396        let (r, _) = dispatch(
2397            r#"{"type":"get_fork_messages","id":"gfm1"}"#,
2398            FakeConfig::default(),
2399        )
2400        .await;
2401        assert!(r["data"]["messages"].is_array());
2402    }
2403
2404    #[tokio::test]
2405    async fn get_tree_empty() {
2406        let (r, _) = dispatch(r#"{"type":"get_tree","id":"gt1"}"#, FakeConfig::default()).await;
2407        assert!(r["data"]["tree"].is_array());
2408    }
2409
2410    // -----------------------------------------------------------------------
2411    // Error cases
2412    // -----------------------------------------------------------------------
2413
2414    #[tokio::test]
2415    async fn set_model_not_found() {
2416        let (r, _) = dispatch(
2417            r#"{"type":"set_model","id":"sm1","provider":"openai","modelId":"gpt-999"}"#,
2418            FakeConfig::default(),
2419        )
2420        .await;
2421        assert_eq!(r["success"], false);
2422        assert_eq!(r["error"], "Model not found: openai/gpt-999");
2423    }
2424    #[tokio::test]
2425    async fn clone_no_leaf() {
2426        let cfg = FakeConfig {
2427            leaf_id: None,
2428            ..Default::default()
2429        };
2430        let (r, _) = dispatch(r#"{"type":"clone","id":"cl1"}"#, cfg).await;
2431        assert_eq!(
2432            r["error"],
2433            "Cannot clone session: no current entry selected"
2434        );
2435    }
2436
2437    #[tokio::test]
2438    async fn set_session_name_empty() {
2439        let (r, _) = dispatch(
2440            r#"{"type":"set_session_name","id":"ssn1","name":"   "}"#,
2441            FakeConfig::default(),
2442        )
2443        .await;
2444        assert_eq!(r["error"], "Session name cannot be empty");
2445    }
2446
2447    #[tokio::test]
2448    async fn get_entries_since_not_found() {
2449        let (r, _) = dispatch(
2450            r#"{"type":"get_entries","id":"ge1","since":"nope"}"#,
2451            FakeConfig::default(),
2452        )
2453        .await;
2454        assert_eq!(r["error"], "Entry not found: nope");
2455    }
2456
2457    // -----------------------------------------------------------------------
2458    // Cancelled mutations
2459    // -----------------------------------------------------------------------
2460    #[tokio::test]
2461    async fn new_session_cancelled() {
2462        let cfg = FakeConfig {
2463            session_op_cancelled: true,
2464            ..Default::default()
2465        };
2466        let (r, _) = dispatch(r#"{"type":"new_session","id":"ns1"}"#, cfg).await;
2467        assert_eq!(r["data"]["cancelled"], true);
2468    }
2469
2470    #[tokio::test]
2471    async fn fork_returns_text() {
2472        let cfg = FakeConfig {
2473            fork_outcome: Ok(ForkOutcome {
2474                cancelled: false,
2475                selected_text: Some("fork here".into()),
2476            }),
2477            ..Default::default()
2478        };
2479        let (r, _) = dispatch(r#"{"type":"fork","id":"fk1","entryId":"e1"}"#, cfg).await;
2480        assert_eq!(r["data"]["text"], "fork here");
2481    }
2482
2483    #[tokio::test]
2484    async fn clone_with_leaf() {
2485        let (r, host) = dispatch(r#"{"type":"clone","id":"cl2"}"#, FakeConfig::default()).await;
2486        let _ = r;
2487        let _ = host;
2488    }
2489
2490    #[tokio::test]
2491    async fn switch_session_success() {
2492        let (r, _) = dispatch(
2493            r#"{"type":"switch_session","id":"sw1","sessionPath":"/tmp/s.jsonl"}"#,
2494            FakeConfig::default(),
2495        )
2496        .await;
2497        assert_eq!(r["data"]["cancelled"], false);
2498    }
2499
2500    // -----------------------------------------------------------------------
2501    // Extension UI routing
2502    // -----------------------------------------------------------------------
2503
2504    struct RpcHostPeer {
2505        read: BufReader<DuplexStream>,
2506        write: DuplexStream,
2507    }
2508
2509    impl RpcHostPeer {
2510        async fn read_frame(&mut self) -> Result<Frame, Box<dyn std::error::Error>> {
2511            let mut line = String::new();
2512            self.read.read_line(&mut line).await?;
2513            Ok(decode_frame_str(line.trim_end())?)
2514        }
2515
2516        async fn write_frame(&mut self, frame: &Frame) -> Result<(), Box<dyn std::error::Error>> {
2517            self.write.write_all(&encode_frame(frame)?).await?;
2518            self.write.flush().await?;
2519            Ok(())
2520        }
2521    }
2522
2523    async fn make_rpc_extension_runner()
2524    -> Result<(Arc<HostExtensionRunner>, RpcHostPeer), Box<dyn std::error::Error>> {
2525        let (client_stdout, host_stdout) = tokio::io::duplex(64 * 1024);
2526        let (host_stdin, client_stdin) = tokio::io::duplex(64 * 1024);
2527        let client = Arc::new(HostClient::connect_boxed(
2528            Box::new(client_stdin),
2529            Box::new(client_stdout),
2530            Box::new(tokio::io::empty()),
2531            None,
2532        ));
2533        let connect_client = Arc::clone(&client);
2534        let connect = tokio::spawn(async move {
2535            HostExtensionRunner::connect_with_cwd_and_trust(
2536                connect_client,
2537                Vec::new(),
2538                "/workspace",
2539                false,
2540                std::time::Duration::from_secs(1),
2541            )
2542            .await
2543        });
2544        let mut peer = RpcHostPeer {
2545            read: BufReader::new(host_stdin),
2546            write: host_stdout,
2547        };
2548        let hello = peer.read_frame().await?;
2549        peer.write_frame(&Frame::response(
2550            hello.id,
2551            Method::Hello,
2552            serde_json::to_value(HelloAck::local())?,
2553        ))
2554        .await?;
2555        let load = peer.read_frame().await?;
2556        assert_eq!(load.method, "extensions.load");
2557        peer.write_frame(&Frame::response(
2558            load.id,
2559            Method::Notify,
2560            serde_json::json!({
2561                "tools": [],
2562                "commands": [],
2563                "shortcuts": [],
2564                "flags": [],
2565                "renderers": [],
2566                "providers": [],
2567                "handlers": [],
2568                "errors": [],
2569                "terminalInput": false
2570            }),
2571        ))
2572        .await?;
2573        Ok((connect.await??, peer))
2574    }
2575
2576    #[tokio::test]
2577    async fn host_dialog_round_trips_through_rpc_stdout_and_stdin()
2578    -> Result<(), Box<dyn std::error::Error>> {
2579        let (runner, mut peer) = make_rpc_extension_runner().await?;
2580        let host = FakeRpcHost::new(FakeConfig::default());
2581        host.set_extension_runner(Arc::clone(&runner));
2582        let sink = BufferSink::new();
2583        let sink_arc = Arc::new(sink.clone()) as Arc<dyn RpcSink>;
2584        let (write_tx, write_rx) = mpsc::unbounded_channel::<WriteMessage>();
2585        let state = ServerState::new(sink_arc.clone(), write_tx, ExtensionUiProxy::new());
2586        let writer = tokio::spawn(writer_actor(write_rx, sink_arc));
2587        state.rebind(&host).await;
2588
2589        peer.write_frame(&Frame {
2590            id: 901,
2591            kind: FrameKind::Req,
2592            method: Method::Select.as_str().to_owned(),
2593            payload: serde_json::json!({
2594                "title": "Pick",
2595                "options": ["a", "b"],
2596                "timeoutMs": 1000
2597            }),
2598        })
2599        .await?;
2600
2601        let ui_request = tokio::time::timeout(std::time::Duration::from_secs(1), async {
2602            loop {
2603                state.wait_for_output().await;
2604                if let Some(line) = sink.stdout_lines().last().cloned()
2605                    && serde_json::from_str::<Value>(&line)
2606                        .ok()
2607                        .and_then(|value| {
2608                            value.get("type").and_then(Value::as_str).map(str::to_owned)
2609                        })
2610                        .as_deref()
2611                        == Some("extension_ui_request")
2612                {
2613                    break line;
2614                }
2615                tokio::task::yield_now().await;
2616            }
2617        })
2618        .await?;
2619        let request_json: Value = serde_json::from_str(&ui_request)?;
2620        assert_eq!(request_json["method"], "select");
2621        assert_eq!(request_json["title"], "Pick");
2622        let rpc_id = request_json["id"].as_str().ok_or("missing RPC UI id")?;
2623        let response = serde_json::json!({
2624            "type": "extension_ui_response",
2625            "id": rpc_id,
2626            "value": "b"
2627        })
2628        .to_string();
2629        assert_eq!(
2630            process_input_line(&response, &host, &state).await,
2631            LineOutcome::Done
2632        );
2633
2634        let host_response =
2635            tokio::time::timeout(std::time::Duration::from_secs(1), peer.read_frame()).await??;
2636        assert_eq!(host_response.kind, FrameKind::Res);
2637        assert_eq!(host_response.id, 901);
2638        assert_eq!(host_response.method, "select");
2639        assert_eq!(host_response.payload["value"], "b");
2640
2641        state.cleanup(&host, 0).await;
2642        writer.abort();
2643        runner.shutdown_once().await;
2644        Ok(())
2645    }
2646
2647    #[tokio::test]
2648    async fn ui_response_routes_to_proxy() {
2649        let host = FakeRpcHost::new(FakeConfig::default());
2650        let sink = BufferSink::new();
2651        let proxy = ExtensionUiProxy::new();
2652        let (req, rx) = proxy.create_dialog(|id| RpcExtensionUiRequest::Select {
2653            id: id.to_owned(),
2654            title: "Pick".into(),
2655            options: vec!["a".into()],
2656            timeout: None,
2657        });
2658        let pending_id = req.id().to_owned();
2659        let (write_tx, _write_rx) = mpsc::unbounded_channel::<WriteMessage>();
2660        let state = ServerState::new(Arc::new(sink.clone()) as Arc<dyn RpcSink>, write_tx, proxy);
2661        let resp_json =
2662            format!(r#"{{"type":"extension_ui_response","id":"{pending_id}","value":"picked"}}"#);
2663        process_input_line(&resp_json, &host, &state).await;
2664        assert!(sink.stdout_lines().is_empty());
2665        let resp = rx.await.unwrap();
2666        match resp {
2667            RpcExtensionUiResponse::Value { value, .. } => assert_eq!(value, "picked"),
2668            _ => panic!(),
2669        }
2670    }
2671
2672    #[tokio::test]
2673    async fn orphan_ui_response_dropped() {
2674        let host = FakeRpcHost::new(FakeConfig::default());
2675        let sink = BufferSink::new();
2676        let (state, _, _) = make_state(sink.clone());
2677        process_input_line(
2678            r#"{"type":"extension_ui_response","id":"orphan","value":"x"}"#,
2679            &host,
2680            &state,
2681        )
2682        .await;
2683        assert!(sink.stdout_lines().is_empty());
2684    }
2685
2686    // -----------------------------------------------------------------------
2687    // Queued prompt ordering
2688    // -----------------------------------------------------------------------
2689
2690    #[tokio::test]
2691    async fn prompt_then_steer_ordering() {
2692        let host = FakeRpcHost::new(FakeConfig::default());
2693        let sink = BufferSink::new();
2694        let (state, sink_clone, mut write_rx) = make_state(sink);
2695        process_input_line(
2696            r#"{"type":"prompt","id":"q1","message":"first"}"#,
2697            &host,
2698            &state,
2699        )
2700        .await;
2701        process_input_line(
2702            r#"{"type":"steer","id":"q2","message":"second"}"#,
2703            &host,
2704            &state,
2705        )
2706        .await;
2707        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
2708        drain(&sink_clone, &mut write_rx).await;
2709        let lines = sink_clone.stdout_lines();
2710        assert_eq!(lines.len(), 2);
2711        let r1: Value = serde_json::from_str(&lines[0]).unwrap();
2712        let r2: Value = serde_json::from_str(&lines[1]).unwrap();
2713        assert_eq!(r1["command"], "prompt");
2714        assert_eq!(r2["command"], "steer");
2715    }
2716    #[tokio::test]
2717    async fn prompt_response_precedes_agent_events() {
2718        let host = FakeRpcHost::new(FakeConfig::default());
2719        let sink = BufferSink::new();
2720        let (state, sink_clone, mut write_rx) = make_state(sink);
2721
2722        // Setup event subscriber exactly as the real rebind() does.
2723        // This ensures the host's emitted events go into the same write_tx queue.
2724        let event_tx = state.write_tx.clone();
2725        let unsub = host.subscribe(Arc::new(move |event: &AgentSessionEvent| {
2726            let _ = event_tx.send(WriteMessage::Line(to_jsonl(event)));
2727        }));
2728        *state.unsubscribe_events.lock().unwrap() = Some(unsub);
2729
2730        // Dispatch prompt. The FakeRpcHost will call preflight(true) and then
2731        // IMMEDIATELY emit TurnStart.
2732        process_input_line(
2733            r#"{"type":"prompt","id":"q3","message":"test"}"#,
2734            &host,
2735            &state,
2736        )
2737        .await;
2738
2739        // Wait for the spawned prompt task to run.
2740        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
2741        drain(&sink_clone, &mut write_rx).await;
2742
2743        let lines = sink_clone.stdout_lines();
2744        assert_eq!(
2745            lines.len(),
2746            2,
2747            "Expected exactly 2 frames: response and event"
2748        );
2749
2750        // First line MUST be the prompt success response (from preflight).
2751        let r1: Value = serde_json::from_str(&lines[0]).unwrap();
2752        assert_eq!(r1["type"], "response");
2753        assert_eq!(r1["command"], "prompt");
2754        assert_eq!(r1["success"], true);
2755
2756        // Second line MUST be the event (emitted right after preflight).
2757        let r2: Value = serde_json::from_str(&lines[1]).unwrap();
2758        assert_eq!(r2["type"], "turn_start");
2759    }
2760    // -----------------------------------------------------------------------
2761    // Shutdown after extension handler
2762    // -----------------------------------------------------------------------
2763
2764    #[tokio::test]
2765    async fn shutdown_flag_triggers_exit() {
2766        let host = FakeRpcHost::new(FakeConfig::default());
2767        let sink = BufferSink::new();
2768        let (state, _, _) = make_state(sink);
2769        // Simulate an extension invoking the RPC shutdown handler.
2770        state.shutdown_requested.store(true, Ordering::SeqCst);
2771        let outcome = process_input_line(r#"{"type":"get_state","id":"x"}"#, &host, &state).await;
2772        assert_eq!(outcome, LineOutcome::Shutdown);
2773    }
2774
2775    #[tokio::test]
2776    async fn extension_shutdown_wakes_idle_loop() {
2777        let host = FakeRpcHost::new(FakeConfig::default());
2778        let observer = host.clone();
2779        let sink = Arc::new(BufferSink::new()) as Arc<dyn RpcSink>;
2780        let (_input_writer, input_reader) = tokio::io::duplex(64);
2781        let loop_task = tokio::spawn(run_rpc_loop(host, sink, input_reader));
2782
2783        tokio::time::timeout(std::time::Duration::from_secs(1), async {
2784            loop {
2785                let handler = observer
2786                    .bindings
2787                    .lock()
2788                    .unwrap()
2789                    .as_ref()
2790                    .and_then(|bindings| bindings.shutdown_handler.clone());
2791                if let Some(handler) = handler {
2792                    handler();
2793                    break;
2794                }
2795                tokio::task::yield_now().await;
2796            }
2797        })
2798        .await
2799        .expect("extension bindings were not installed");
2800
2801        let code = tokio::time::timeout(std::time::Duration::from_secs(1), loop_task)
2802            .await
2803            .expect("idle RPC loop did not wake")
2804            .expect("RPC loop task panicked");
2805        assert_eq!(code, 0);
2806    }
2807
2808    // -----------------------------------------------------------------------
2809    // Event loop with EOF
2810    // -----------------------------------------------------------------------
2811
2812    #[tokio::test]
2813    async fn loop_eof_exit_zero() {
2814        let host = FakeRpcHost::new(FakeConfig::default());
2815        let sink = Arc::new(BufferSink::new()) as Arc<dyn RpcSink>;
2816        let input: &[u8] = b"";
2817        let code = run_rpc_loop(host, sink, input).await;
2818        assert_eq!(code, 0);
2819    }
2820
2821    #[tokio::test]
2822    async fn loop_stdin_read_error_is_protocol_visible_and_nonzero() {
2823        let host = FakeRpcHost::new(FakeConfig::default());
2824        let buffer = Arc::new(BufferSink::new());
2825        let sink = Arc::clone(&buffer) as Arc<dyn RpcSink>;
2826        let code = run_rpc_loop(host, sink, FailingInput).await;
2827        assert_eq!(code, 1);
2828        let lines = buffer.stdout_lines();
2829        assert_eq!(lines.len(), 1);
2830        let response: Value = serde_json::from_str(&lines[0]).unwrap();
2831        assert_eq!(response["type"], "response");
2832        assert_eq!(response["command"], "transport");
2833        assert_eq!(response["success"], false);
2834        assert!(
2835            response["error"]
2836                .as_str()
2837                .unwrap()
2838                .contains("stdin transport failed")
2839        );
2840    }
2841
2842    #[tokio::test]
2843    async fn loop_command_then_eof() {
2844        let host = FakeRpcHost::new(FakeConfig::default());
2845        let buf = BufferSink::new();
2846        let sink: Arc<dyn RpcSink> = Arc::new(buf.clone());
2847        let input: &[u8] = b"{\"type\":\"get_state\",\"id\":\"l1\"}\n";
2848        let code = run_rpc_loop(host, Arc::clone(&sink), input).await;
2849        assert_eq!(code, 0);
2850        assert_eq!(buf.stdout_lines().len(), 1);
2851    }
2852
2853    #[tokio::test]
2854    async fn command_dispatch_waits_for_writer_drain() {
2855        let host = FakeRpcHost::new(FakeConfig::default());
2856        let observer = host.clone();
2857        let gated = GatedSink::default();
2858        let sink = Arc::new(gated.clone()) as Arc<dyn RpcSink>;
2859        let input =
2860            &b"{\"type\":\"get_state\",\"id\":\"one\"}\n{\"type\":\"get_state\",\"id\":\"two\"}\n"
2861                [..];
2862        let loop_task = tokio::spawn(run_rpc_loop(host, sink, input));
2863
2864        gated.wait_for_write(1).await;
2865        assert_eq!(
2866            observer
2867                .calls
2868                .lock()
2869                .unwrap()
2870                .iter()
2871                .filter(|call| call.as_str() == "get_state")
2872                .count(),
2873            1,
2874            "second request dispatched before first response drained"
2875        );
2876        gated.release.notify_one();
2877
2878        gated.wait_for_write(2).await;
2879        gated.release.notify_one();
2880        let code = tokio::time::timeout(std::time::Duration::from_secs(1), loop_task)
2881            .await
2882            .expect("RPC loop did not finish")
2883            .expect("RPC loop task panicked");
2884        assert_eq!(code, 0);
2885        assert_eq!(
2886            observer
2887                .calls
2888                .lock()
2889                .unwrap()
2890                .iter()
2891                .filter(|call| call.as_str() == "get_state")
2892                .count(),
2893            2
2894        );
2895    }
2896
2897    #[tokio::test]
2898    async fn loop_disposes_on_exit() {
2899        let host = FakeRpcHost::new(FakeConfig::default());
2900        let sink = Arc::new(BufferSink::new()) as Arc<dyn RpcSink>;
2901        let input: &[u8] = b"";
2902        let _ = run_rpc_loop(host, sink, input).await;
2903        // host was moved; we can't check disposed flag after move.
2904        // Instead, verify via a shared flag.
2905    }
2906
2907    #[tokio::test]
2908    async fn loop_disposes_host() {
2909        let host = Arc::new(FakeRpcHost::new(FakeConfig::default()));
2910        let disposed = {
2911            let h = Arc::clone(&host);
2912            // We can't easily pass Arc<FakeRpcHost> to run_rpc_loop since it
2913            // takes H: RpcSessionHost by value. Instead, test rebind+dispose
2914            // via direct calls.
2915            let sink = Arc::new(BufferSink::new()) as Arc<dyn RpcSink>;
2916            let (write_tx, _) = mpsc::unbounded_channel::<WriteMessage>();
2917            let state = ServerState::new(sink, write_tx, ExtensionUiProxy::new());
2918            state.rebind(&*h).await;
2919            state.cleanup(&*h, 0).await;
2920            h.disposed.load(Ordering::SeqCst)
2921        };
2922        assert!(disposed);
2923    }
2924
2925    // -----------------------------------------------------------------------
2926    // BufferSink ordering
2927    // -----------------------------------------------------------------------
2928
2929    #[tokio::test]
2930    async fn buffer_sink_fifo_order() {
2931        let sink = BufferSink::new();
2932        sink.clone().write_stdout("a\n".into()).await.unwrap();
2933        sink.clone().write_stdout("b\n".into()).await.unwrap();
2934        sink.clone().write_stdout("c\n".into()).await.unwrap();
2935        assert_eq!(sink.stdout_lines(), vec!["a", "b", "c"]);
2936    }
2937
2938    // -----------------------------------------------------------------------
2939    // No-id command
2940    // -----------------------------------------------------------------------
2941
2942    #[tokio::test]
2943    async fn command_without_id() {
2944        let (r, _) = dispatch(r#"{"type":"get_state"}"#, FakeConfig::default()).await;
2945        assert!(r.get("id").is_none() || r["id"].is_null());
2946    }
2947
2948    // -----------------------------------------------------------------------
2949    // Rebind binds extensions
2950    // -----------------------------------------------------------------------
2951
2952    #[tokio::test]
2953    async fn rebind_binds_and_subscribes() {
2954        let host = FakeRpcHost::new(FakeConfig::default());
2955        let sink = Arc::new(BufferSink::new()) as Arc<dyn RpcSink>;
2956        let (write_tx, _) = mpsc::unbounded_channel::<WriteMessage>();
2957        let state = ServerState::new(sink, write_tx, ExtensionUiProxy::new());
2958        state.rebind(&host).await;
2959        let calls = host.calls.lock().unwrap();
2960        assert!(calls.contains(&"bind_extensions_rpc".to_owned()));
2961        assert!(calls.contains(&"subscribe".to_owned()));
2962        assert!(calls.contains(&"register_backpressure_hook".to_owned()));
2963    }
2964
2965    #[tokio::test]
2966    async fn rebind_extension_error_listener_preserves_structured_fields() {
2967        let host = FakeRpcHost::new(FakeConfig::default());
2968        let sink = Arc::new(BufferSink::new()) as Arc<dyn RpcSink>;
2969        let (write_tx, mut write_rx) = mpsc::unbounded_channel::<WriteMessage>();
2970        let state = ServerState::new(sink, write_tx, ExtensionUiProxy::new());
2971        state.rebind(&host).await;
2972
2973        let listener = host
2974            .bindings
2975            .lock()
2976            .unwrap()
2977            .as_ref()
2978            .and_then(|bindings| bindings.on_error.clone())
2979            .expect("RPC rebind must install extension error listener");
2980        listener("/workspace/ext.ts", "tool_call", "handler failed");
2981
2982        let WriteMessage::Line(line) = write_rx.recv().await.expect("extension error output")
2983        else {
2984            panic!("unexpected drain message");
2985        };
2986        let parsed: Value = serde_json::from_str(&line).unwrap();
2987        assert_eq!(parsed["extensionPath"], "/workspace/ext.ts");
2988        assert_eq!(parsed["event"], "tool_call");
2989        assert_eq!(parsed["error"], "handler failed");
2990    }
2991
2992    // -----------------------------------------------------------------------
2993    // ExtensionErrorOutput serialization
2994    // -----------------------------------------------------------------------
2995
2996    #[tokio::test]
2997    async fn extension_error_serializes() {
2998        let output = ExtensionErrorOutput::new("ext/path", "event_type", "boom");
2999        let line = to_jsonl(&output);
3000        let parsed: Value = serde_json::from_str(&line).unwrap();
3001        assert_eq!(parsed["type"], "extension_error");
3002        assert_eq!(parsed["extensionPath"], "ext/path");
3003        assert_eq!(parsed["event"], "event_type");
3004        assert_eq!(parsed["error"], "boom");
3005    }
3006    #[test]
3007    fn host_extension_error_wire_fields_are_recovered() {
3008        let output = map_extension_error_event(&ExtensionErrorEvent {
3009            code: "extension_error".to_owned(),
3010            message: "[/workspace/ext.ts] agent_start: handler failed".to_owned(),
3011            retryable: false,
3012            data: None,
3013        });
3014        let parsed: Value = serde_json::from_str(&to_jsonl(&output)).unwrap();
3015        assert_eq!(parsed["extensionPath"], "/workspace/ext.ts");
3016        assert_eq!(parsed["event"], "agent_start");
3017        assert_eq!(parsed["error"], "handler failed");
3018    }
3019
3020    #[test]
3021    fn structured_host_extension_error_data_takes_precedence() {
3022        let output = map_extension_error_event(&ExtensionErrorEvent {
3023            code: "extension_error".to_owned(),
3024            message: "legacy summary".to_owned(),
3025            retryable: false,
3026            data: Some(serde_json::json!({
3027                "extensionPath": "/workspace/structured.ts",
3028                "event": "tool_result",
3029                "error": "structured failure",
3030            })),
3031        });
3032        let parsed: Value = serde_json::from_str(&to_jsonl(&output)).unwrap();
3033        assert_eq!(parsed["extensionPath"], "/workspace/structured.ts");
3034        assert_eq!(parsed["event"], "tool_result");
3035        assert_eq!(parsed["error"], "structured failure");
3036    }
3037}