Skip to repository content
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T00:58:28.863Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitRevision diff
a2cf2010 → 051d63eadiff --git a/OMEGA_DELTAS.md b/OMEGA_DELTAS.md
index f4dab94524..2f87d17ad7 100644
--- a/OMEGA_DELTAS.md
+++ b/OMEGA_DELTAS.md
@@ -2383,3 +2383,40 @@ than it sounds, because the harness omega#81's acceptance sentence names —
23832383 - **Scope.** This is the **source** half. `0.2.0-rc17` and every earlier
23842384 candidate carry the refusal in their shipped bytes; the packaged half needs
23852385 the next candidate.
2386+### OMEGA-DELTA-0046 — Exo threads have a native conversation workspace
2387+
2388+- **Before this change:** An Exo thread used the standard transcript and
2389+ composer, but its Exo-specific interface was one compressed disclosure row.
2390+ The row did not show the observed harness state, the current turn state, or
2391+ the exact authority boundary. A user could not inspect the Exo lane as one
2392+ coherent system.
2393+- **Omega now:** An Exo thread has a dedicated workspace header and runtime
2394+ inspector. The transcript and composer remain the standard Omega components.
2395+ Text, tool calls, tool results, errors, completion, queued prompts, and
2396+ cancellation therefore keep one rendering and control path.
2397+- **The inspector is a projection, not a second source of truth.** It reads the
2398+ exact observation that gates each turn. It shows the agent, conversation,
2399+ executor, model, provider disclosure, ACP transport, turn state, source pin,
2400+ binary digest, tool modules and digests, writable mounts, network state, and
2401+ the last one-turn authority receipt. Each terminal turn also shows the
2402+ durable Exo session, turn, and latest event references that Exo returned,
2403+ whether or not that turn needed self-modification authority. If observation
2404+ fails, the inspector reports that failure and does not invent runtime facts.
2405+- **The controls use existing authority.** Refresh runs the read-only Exo
2406+ observation. Stop uses the existing ACP cancellation path. The one-turn
2407+ self-modification control appears only when the observed capability set needs
2408+ it, and it uses the existing exact-draft confirmation and receipt path.
2409+ Omega does not add an Exo configuration command, listener, proxy, or Full
2410+ Auto route.
2411+- **The layout is responsive and native.** A wide window puts the inspector
2412+ beside the transcript. A narrow window puts a bounded inspector above it.
2413+ The interface uses the active Omega theme tokens and standard controls. It
2414+ does not add a separate dark theme or a web surface.
2415+- **Enforced by:**
2416+ `an_exo_thread_has_a_live_workspace_and_exact_runtime_inspector` in
2417+ `crates/omega_deltas/`, the Exo inspection and turn-state tests in
2418+ `crates/agent_ui/`, the real Exo acceptance path `drives_a_real_exo`, and
2419+ `omega_exo_workspace_wide` plus `omega_exo_workspace_narrow` in the native
2420+ Metal visual runner. The visual path starts the shipped ACP transport, sends
2421+ one real turn, requires the reply and all three durable references, and then
2422+ records both layouts.
diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs
index 353391c30a..fae06f1792 100644
--- a/crates/agent_ui/src/conversation_view/thread_view.rs
+++ b/crates/agent_ui/src/conversation_view/thread_view.rs
@@ -41,6 +41,7 @@ use language_model::{
4141 LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, Speed,
4242 };
4343 use notifications::status_toast::StatusToast;
44+use omega_exo_lane::ObservedExoCapabilityState;
4445 use omega_front_door::{ExecutorClass, ExecutorDisclosure, ExecutorPin, PinGesture};
4546 use settings::{update_settings_file, update_settings_file_with_completion};
4647 use ui::{
@@ -658,6 +659,8 @@ pub struct ThreadView {
658659 /// `OMEGA-DELTA-0030`. Polls the engine for the linked run. Present only
659660 /// on threads an engine lane executed.
660661 _omega_run_link_task: Option<Task<()>>,
662+ exo_inspector_expanded: bool,
663+ _exo_inspection_task: Option<Task<()>>,
661664 }
662665 impl Focusable for ThreadView {
663666 fn focus_handle(&self, cx: &App) -> FocusHandle {
@@ -1039,6 +1042,8 @@ impl ThreadView {
10391042 _sandbox_status_refresh_task: None,
10401043 omega_run_records: None,
10411044 _omega_run_link_task: None,
1045+ exo_inspector_expanded: true,
1046+ _exo_inspection_task: None,
10421047 hovered_edited_file_buttons: None,
10431048 in_flight_prompt: None,
10441049 message_editor,
@@ -12146,21 +12151,583 @@ impl ThreadView {
1214612151 )
1214712152 }
1214812153
12154+ fn exo_connection(
12155+ &self,
12156+ cx: &App,
12157+ ) -> Option<Rc<crate::omega_exo_connection::ExoHarnessConnection>> {
12158+ self.thread
12159+ .read(cx)
12160+ .connection()
12161+ .clone()
12162+ .downcast::<crate::omega_exo_connection::ExoHarnessConnection>()
12163+ }
12164+
12165+ fn refresh_exo_inspection(&mut self, cx: &mut Context<Self>) {
12166+ if self._exo_inspection_task.is_some() {
12167+ return;
12168+ }
12169+ let Some(exo) = self.exo_connection(cx) else {
12170+ return;
12171+ };
12172+ let refresh = exo.refresh_inspection(cx);
12173+ self._exo_inspection_task = Some(cx.spawn(async move |this, cx| {
12174+ let result = refresh.await;
12175+ if let Err(error) = this.update(cx, |this, cx| {
12176+ this._exo_inspection_task = None;
12177+ cx.notify();
12178+ }) {
12179+ log::debug!("omega#95: Exo inspector closed during refresh: {error}");
12180+ return;
12181+ }
12182+ if let Err(error) = result {
12183+ log::warn!("omega#95: Exo inspection refresh failed: {error}");
12184+ }
12185+ }));
12186+ cx.notify();
12187+ }
12188+
12189+ fn ensure_exo_inspection(&mut self, cx: &mut Context<Self>) {
12190+ let needs_refresh = self.exo_connection(cx).is_some_and(|exo| {
12191+ exo.inspection().phase == crate::omega_exo_connection::ExoInspectionPhase::NotLoaded
12192+ });
12193+ if needs_refresh {
12194+ self.refresh_exo_inspection(cx);
12195+ }
12196+ }
12197+
12198+ fn authorize_exo_self_modification(
12199+ &mut self,
12200+ exo: Rc<crate::omega_exo_connection::ExoHarnessConnection>,
12201+ window: &mut Window,
12202+ cx: &mut Context<Self>,
12203+ ) {
12204+ let (session_id, prompt) = {
12205+ let thread = self.thread.read(cx);
12206+ let prompt = thread
12207+ .draft_prompt()
12208+ .filter(|prompt| !prompt.is_empty())
12209+ .map(<[acp::ContentBlock]>::to_vec);
12210+ (thread.session_id().clone(), prompt)
12211+ };
12212+ let Some(prompt) = prompt else {
12213+ log::warn!("omega#95: a self-modification grant needs a non-empty draft");
12214+ return;
12215+ };
12216+ let objective = crate::omega_exo_connection::exo_prompt_objective(&prompt);
12217+ let Ok(turn_ref) = crate::omega_exo_connection::exo_turn_ref(&session_id, &prompt) else {
12218+ log::error!("omega#95: failed to bind the Exo draft to a turn");
12219+ return;
12220+ };
12221+ window
12222+ .spawn(cx, async move |cx| {
12223+ let request = exo.self_modification_request(objective, turn_ref).await?;
12224+ let detail = format!(
12225+ "This grant expires in 60 seconds and applies only to this exact draft, Exo \
12226+ generation, source tree, binary, tool modules, and read-write mounts.\n\n\
12227+ Exact capabilities:\n{:#?}",
12228+ request.capabilities
12229+ );
12230+ let answer = cx
12231+ .prompt(
12232+ PromptLevel::Warning,
12233+ "Allow this Exo agent to modify itself for one turn?",
12234+ Some(&detail),
12235+ &["Authorize one turn", "Cancel"],
12236+ )
12237+ .await?;
12238+ if answer == 0 {
12239+ exo.confirm_self_modification(request)?;
12240+ }
12241+ anyhow::Ok(())
12242+ })
12243+ .detach_and_log_err(cx);
12244+ }
12245+
12246+ fn exo_status_color(phase: crate::omega_exo_connection::ExoTurnPhase) -> Color {
12247+ use crate::omega_exo_connection::ExoTurnPhase;
12248+
12249+ match phase {
12250+ ExoTurnPhase::Idle | ExoTurnPhase::Completed => Color::Success,
12251+ ExoTurnPhase::Inspecting | ExoTurnPhase::Working => Color::Accent,
12252+ ExoTurnPhase::Cancelling => Color::Warning,
12253+ ExoTurnPhase::Cancelled => Color::Muted,
12254+ ExoTurnPhase::Refused | ExoTurnPhase::Failed => Color::Error,
12255+ }
12256+ }
12257+
12258+ fn exo_reference(value: &str) -> String {
12259+ const VISIBLE: usize = 12;
12260+ if value.chars().count() <= VISIBLE {
12261+ value.to_owned()
12262+ } else {
12263+ format!("{}…", value.chars().take(VISIBLE).collect::<String>())
12264+ }
12265+ }
12266+
12267+ fn render_exo_field(
12268+ label: &'static str,
12269+ value: impl Into<SharedString>,
12270+ tooltip: Option<SharedString>,
12271+ cx: &App,
12272+ ) -> AnyElement {
12273+ let value = value.into();
12274+ let field_id = format!("omega-exo-field-{label}-{value}");
12275+ h_flex()
12276+ .w_full()
12277+ .min_w_0()
12278+ .gap_1()
12279+ .child(
12280+ Label::new(label)
12281+ .size(LabelSize::XSmall)
12282+ .color(Color::Muted),
12283+ )
12284+ .child(div().flex_1())
12285+ .child(
12286+ div()
12287+ .id(field_id)
12288+ .min_w_0()
12289+ .max_w(px(210.))
12290+ .when_some(tooltip, |this, tooltip| {
12291+ this.tooltip(Tooltip::text(tooltip))
12292+ })
12293+ .child(
12294+ Label::new(value)
12295+ .size(LabelSize::XSmall)
12296+ .color(Color::Default)
12297+ .truncate(),
12298+ ),
12299+ )
12300+ .border_b_1()
12301+ .border_color(cx.theme().colors().border_variant.opacity(0.5))
12302+ .py_0p5()
12303+ .into_any_element()
12304+ }
12305+
12306+ fn render_exo_section(title: &'static str, rows: Vec<AnyElement>, cx: &App) -> AnyElement {
12307+ v_flex()
12308+ .w_full()
12309+ .gap_0p5()
12310+ .pb_2()
12311+ .child(
12312+ Label::new(title)
12313+ .size(LabelSize::Small)
12314+ .color(Color::Default),
12315+ )
12316+ .child(
12317+ v_flex()
12318+ .w_full()
12319+ .border_t_1()
12320+ .border_color(cx.theme().colors().border_variant)
12321+ .children(rows),
12322+ )
12323+ .into_any_element()
12324+ }
12325+
12326+ fn render_exo_header(
12327+ &self,
12328+ exo: Rc<crate::omega_exo_connection::ExoHarnessConnection>,
12329+ cx: &mut Context<Self>,
12330+ ) -> AnyElement {
12331+ let disclosure = self.executor_disclosure(cx);
12332+ let turn = exo.turn();
12333+ let is_generating = self.thread.read(cx).status() == ThreadStatus::Generating;
12334+ let inspector_open = self.exo_inspector_expanded;
12335+
12336+ h_flex()
12337+ .id("omega-exo-workspace-header")
12338+ .w_full()
12339+ .min_w_0()
12340+ .flex_wrap()
12341+ .px_2()
12342+ .py_1()
12343+ .gap_1()
12344+ .border_b_1()
12345+ .border_color(cx.theme().colors().border_variant)
12346+ .bg(cx.theme().colors().panel_background)
12347+ .child(
12348+ Icon::new(IconName::BoltOutlined)
12349+ .size(IconSize::Small)
12350+ .color(Color::Accent),
12351+ )
12352+ .child(
12353+ v_flex()
12354+ .min_w_0()
12355+ .gap_0p5()
12356+ .child(
12357+ Label::new("Exo workspace")
12358+ .size(LabelSize::Small)
12359+ .color(Color::Default),
12360+ )
12361+ .child(
12362+ Label::new(disclosure.label())
12363+ .size(LabelSize::XSmall)
12364+ .color(Color::Muted)
12365+ .truncate(),
12366+ ),
12367+ )
12368+ .child(div().flex_1())
12369+ .child(
12370+ h_flex()
12371+ .gap_0p5()
12372+ .child(
12373+ Icon::new(IconName::Circle)
12374+ .size(IconSize::XSmall)
12375+ .color(Self::exo_status_color(turn.phase)),
12376+ )
12377+ .child(
12378+ Label::new(turn.phase.label())
12379+ .size(LabelSize::XSmall)
12380+ .color(Self::exo_status_color(turn.phase)),
12381+ ),
12382+ )
12383+ .when(is_generating, |row| {
12384+ row.child(
12385+ Button::new("omega-exo-cancel-turn", "Stop")
12386+ .label_size(LabelSize::XSmall)
12387+ .style(ButtonStyle::Tinted(TintColor::Error))
12388+ .on_click(cx.listener(|this, _, _, cx| this.cancel_generation(cx))),
12389+ )
12390+ })
12391+ .child(
12392+ Button::new("omega-exo-toggle-inspector", "Inspector")
12393+ .label_size(LabelSize::XSmall)
12394+ .toggle_state(inspector_open)
12395+ .selected_style(ButtonStyle::Outlined)
12396+ .selected_label_color(Color::Default)
12397+ .on_click(cx.listener(|this, _, _, cx| {
12398+ this.exo_inspector_expanded = !this.exo_inspector_expanded;
12399+ cx.notify();
12400+ })),
12401+ )
12402+ .child(self.render_executor_pin(cx))
12403+ .into_any_element()
12404+ }
12405+
12406+ fn render_exo_inspector(
12407+ &self,
12408+ exo: Rc<crate::omega_exo_connection::ExoHarnessConnection>,
12409+ compact: bool,
12410+ cx: &mut Context<Self>,
12411+ ) -> AnyElement {
12412+ let inspection = exo.inspection();
12413+ let turn = exo.turn();
12414+ let config = exo.config();
12415+ let receipt = exo.tier_c_receipt();
12416+ let is_refreshing =
12417+ inspection.phase == crate::omega_exo_connection::ExoInspectionPhase::Refreshing;
12418+ let capabilities = inspection
12419+ .observed
12420+ .as_ref()
12421+ .map(ObservedExoCapabilityState::requested_capabilities)
12422+ .unwrap_or_default();
12423+ let needs_authority = !capabilities.is_empty();
12424+ let has_draft = self
12425+ .thread
12426+ .read(cx)
12427+ .draft_prompt()
12428+ .is_some_and(|prompt| !prompt.is_empty());
12429+ let exo_for_authorize = exo.clone();
12430+
12431+ let identity_rows = vec![
12432+ Self::render_exo_field("Agent", config.agent.clone(), None, cx),
12433+ Self::render_exo_field("Conversation", config.conversation.clone(), None, cx),
12434+ Self::render_exo_field(
12435+ "Executor",
12436+ inspection.identity.as_ref().map_or_else(
12437+ || "Not observed".into(),
12438+ |identity| identity.executor.clone(),
12439+ ),
12440+ None,
12441+ cx,
12442+ ),
12443+ Self::render_exo_field(
12444+ "Model",
12445+ inspection
12446+ .identity
12447+ .as_ref()
12448+ .and_then(|identity| identity.model.clone())
12449+ .unwrap_or_else(|| "Not observed".to_owned()),
12450+ None,
12451+ cx,
12452+ ),
12453+ Self::render_exo_field(
12454+ "Provider",
12455+ inspection
12456+ .identity
12457+ .as_ref()
12458+ .and_then(|identity| identity.provider.clone())
12459+ .unwrap_or_else(|| "Not disclosed".to_owned()),
12460+ None,
12461+ cx,
12462+ ),
12463+ Self::render_exo_field("Transport", "ACP v1 · stdio", None, cx),
12464+ ];
12465+
12466+ let mut runtime_rows = Vec::new();
12467+ runtime_rows.push(Self::render_exo_field(
12468+ "Turn",
12469+ turn.phase.label(),
12470+ turn.detail.clone().map(Into::into),
12471+ cx,
12472+ ));
12473+ for (label, reference) in [
12474+ ("Exo session", turn.exo_session_id.as_ref()),
12475+ ("Exo turn", turn.exo_turn_id.as_ref()),
12476+ ("Latest event", turn.latest_event_id.as_ref()),
12477+ ] {
12478+ if let Some(reference) = reference {
12479+ runtime_rows.push(Self::render_exo_field(
12480+ label,
12481+ Self::exo_reference(reference),
12482+ Some(reference.clone().into()),
12483+ cx,
12484+ ));
12485+ }
12486+ }
12487+ if let Some(observed) = inspection.observed.as_ref() {
12488+ runtime_rows.push(Self::render_exo_field(
12489+ "Source commit",
12490+ Self::exo_reference(&observed.source_commit),
12491+ Some(observed.source_commit.clone().into()),
12492+ cx,
12493+ ));
12494+ runtime_rows.push(Self::render_exo_field(
12495+ "Source tree",
12496+ Self::exo_reference(&observed.source_tree),
12497+ Some(observed.source_tree.clone().into()),
12498+ cx,
12499+ ));
12500+ runtime_rows.push(Self::render_exo_field(
12501+ "Binary",
12502+ Self::exo_reference(&observed.binary_digest),
12503+ Some(observed.binary_digest.clone().into()),
12504+ cx,
12505+ ));
12506+ runtime_rows.push(Self::render_exo_field(
12507+ "Generation",
12508+ observed.generation.to_string(),
12509+ None,
12510+ cx,
12511+ ));
12512+ } else {
12513+ runtime_rows.push(Self::render_exo_field(
12514+ "Inspection",
12515+ match inspection.phase {
12516+ crate::omega_exo_connection::ExoInspectionPhase::NotLoaded => "Not loaded",
12517+ crate::omega_exo_connection::ExoInspectionPhase::Refreshing => "Reading Exo",
12518+ crate::omega_exo_connection::ExoInspectionPhase::Ready => "Ready",
12519+ crate::omega_exo_connection::ExoInspectionPhase::Unavailable => "Unavailable",
12520+ },
12521+ inspection.error.clone().map(Into::into),
12522+ cx,
12523+ ));
12524+ }
12525+
12526+ let mut capability_rows =
12527+ vec![
12528+ Self::render_exo_field(
12529+ "Agent-authored tools",
12530+ inspection.observed.as_ref().map_or("Unknown", |observed| {
12531+ if observed.agent_authored_tools {
12532+ "Enabled"
12533+ } else {
12534+ "Off"
12535+ }
12536+ }),
12537+ None,
12538+ cx,
12539+ ),
12540+ Self::render_exo_field(
12541+ "Network",
12542+ inspection.networking.map_or("Unknown", |networking| {
12543+ if networking { "Enabled" } else { "Off" }
12544+ }),
12545+ None,
12546+ cx,
12547+ ),
12548+ ];
12549+ if let Some(observed) = inspection.observed.as_ref() {
12550+ capability_rows.push(Self::render_exo_field(
12551+ "Tool modules",
12552+ observed.tool_modules.len().to_string(),
12553+ None,
12554+ cx,
12555+ ));
12556+ capability_rows.extend(observed.tool_modules.iter().map(|module| {
12557+ Self::render_exo_field(
12558+ "Module",
12559+ module.path.clone(),
12560+ Some(format!("{}\n{}", module.path, module.digest).into()),
12561+ cx,
12562+ )
12563+ }));
12564+ capability_rows.push(Self::render_exo_field(
12565+ "Read-write mounts",
12566+ observed.read_write_mounts.len().to_string(),
12567+ None,
12568+ cx,
12569+ ));
12570+ capability_rows.extend(observed.read_write_mounts.iter().map(|mount| {
12571+ let mapping = format!("{} → {}", mount.host_path, mount.mount_path);
12572+ Self::render_exo_field("Mount", mapping.clone(), Some(mapping.into()), cx)
12573+ }));
12574+ }
12575+
12576+ let authority_rows = if let Some(receipt) = receipt {
12577+ vec![
12578+ Self::render_exo_field("Outcome", receipt.outcome, None, cx),
12579+ Self::render_exo_field(
12580+ "Exo session",
12581+ receipt
12582+ .exo_session_id
12583+ .as_deref()
12584+ .map_or_else(|| "Pending".to_owned(), Self::exo_reference),
12585+ receipt.exo_session_id.map(Into::into),
12586+ cx,
12587+ ),
12588+ Self::render_exo_field(
12589+ "Exo turn",
12590+ receipt
12591+ .exo_turn_id
12592+ .as_deref()
12593+ .map_or_else(|| "Pending".to_owned(), Self::exo_reference),
12594+ receipt.exo_turn_id.map(Into::into),
12595+ cx,
12596+ ),
12597+ Self::render_exo_field(
12598+ "Latest event",
12599+ receipt
12600+ .latest_event_id
12601+ .as_deref()
12602+ .map_or_else(|| "Pending".to_owned(), Self::exo_reference),
12603+ receipt.latest_event_id.map(Into::into),
12604+ cx,
12605+ ),
12606+ Self::render_exo_field(
12607+ "Verification",
12608+ receipt.verification.clone(),
12609+ Some(receipt.verification.into()),
12610+ cx,
12611+ ),
12612+ ]
12613+ } else {
12614+ vec![Self::render_exo_field(
12615+ "Authority",
12616+ if needs_authority {
12617+ "Required for this agent"
12618+ } else {
12619+ "Not required"
12620+ },
12621+ None,
12622+ cx,
12623+ )]
12624+ };
12625+
12626+ v_flex()
12627+ .id("omega-exo-inspector")
12628+ .h_full()
12629+ .when(!compact, |this| this.w(px(336.)).min_w(px(288.)))
12630+ .when(compact, |this| this.w_full().max_h(px(300.)))
12631+ .border_l_1()
12632+ .when(compact, |this| this.border_l_0().border_b_1())
12633+ .border_color(cx.theme().colors().border_variant)
12634+ .bg(cx.theme().colors().surface_background)
12635+ .child(
12636+ h_flex()
12637+ .w_full()
12638+ .px_2()
12639+ .py_1()
12640+ .gap_1()
12641+ .border_b_1()
12642+ .border_color(cx.theme().colors().border_variant)
12643+ .child(
12644+ Label::new("Runtime inspector")
12645+ .size(LabelSize::Small)
12646+ .color(Color::Default),
12647+ )
12648+ .child(div().flex_1())
12649+ .child(
12650+ IconButton::new("omega-exo-refresh-inspection", IconName::RotateCw)
12651+ .disabled(is_refreshing)
12652+ .tooltip(Tooltip::text(if is_refreshing {
12653+ "Reading Exo runtime"
12654+ } else {
12655+ "Refresh Exo runtime"
12656+ }))
12657+ .on_click(cx.listener(|this, _, _, cx| {
12658+ this.refresh_exo_inspection(cx);
12659+ })),
12660+ ),
12661+ )
12662+ .child(
12663+ v_flex()
12664+ .id("omega-exo-inspector-scroll")
12665+ .flex_1()
12666+ .overflow_y_scroll()
12667+ .px_2()
12668+ .pt_2()
12669+ .children([
12670+ Self::render_exo_section("Identity", identity_rows, cx),
12671+ Self::render_exo_section("Runtime", runtime_rows, cx),
12672+ Self::render_exo_section("Capabilities", capability_rows, cx),
12673+ Self::render_exo_section("Authority receipt", authority_rows, cx),
12674+ ])
12675+ .child(
12676+ v_flex()
12677+ .w_full()
12678+ .gap_1()
12679+ .pb_2()
12680+ .when(needs_authority, |this| {
12681+ this.child(
12682+ Button::new(
12683+ "omega-exo-authorize-self-modification",
12684+ "Authorize this draft",
12685+ )
12686+ .style(ButtonStyle::Tinted(TintColor::Warning))
12687+ .label_size(LabelSize::XSmall)
12688+ .disabled(!has_draft)
12689+ .tooltip(Tooltip::text(if has_draft {
12690+ "Authorize one exact self-modifying Exo turn"
12691+ } else {
12692+ "Write the exact draft before authorizing it"
12693+ }))
12694+ .on_click(cx.listener(
12695+ move |this, _, window, cx| {
12696+ this.authorize_exo_self_modification(
12697+ exo_for_authorize.clone(),
12698+ window,
12699+ cx,
12700+ );
12701+ },
12702+ )),
12703+ )
12704+ })
12705+ .when(!needs_authority, |this| {
12706+ this.child(
12707+ Label::new(
12708+ "This agent has no observed self-modification capability. \
12709+ Normal turns need no additional grant.",
12710+ )
12711+ .size(LabelSize::XSmall)
12712+ .color(Color::Muted),
12713+ )
12714+ }),
12715+ ),
12716+ )
12717+ .into_any_element()
12718+ }
12719+
1214912720 /// The executor line, rendered from the record.
1215012721 ///
1215112722 /// Unconditional, and above the entries rather than beside them, because
1215212723 /// omega#77's falsifier is a thread surface that shows work without naming
1215312724 /// its executor — including an empty thread, which is about to.
12154- fn render_executor_disclosure(&self, cx: &mut Context<Self>) -> impl IntoElement {
12725+ fn render_executor_disclosure(&self, cx: &mut Context<Self>) -> AnyElement {
1215512726 let disclosure = self.executor_disclosure(cx);
12156- let exo = self
12157- .thread
12158- .read(cx)
12159- .connection()
12160- .clone()
12161- .downcast::<crate::omega_exo_connection::ExoHarnessConnection>();
12162- let receipt = exo.as_ref().and_then(|exo| exo.tier_c_receipt());
12163- let thread = self.thread.clone();
12727+ if let Some(exo) = self.exo_connection(cx) {
12728+ return self.render_exo_header(exo, cx);
12729+ }
12730+
1216412731 h_flex()
1216512732 .w_full()
1216612733 .px_2()
@@ -12179,77 +12746,8 @@ impl ThreadView {
1217912746 .color(Color::Muted),
1218012747 )
1218112748 .child(div().flex_1())
12182- .when_some(receipt, |row, receipt| {
12183- row.child(
12184- Label::new(format!(
12185- "self-modification {} · generation {} · event {}",
12186- receipt.outcome,
12187- receipt.observed.generation,
12188- receipt.latest_event_id.as_deref().unwrap_or("pending")
12189- ))
12190- .size(LabelSize::XSmall)
12191- .color(Color::Muted),
12192- )
12193- })
12194- .when_some(exo, |row, exo| {
12195- row.child(
12196- Button::new(
12197- "omega-exo-authorize-self-modification",
12198- "Authorize one self-modifying turn",
12199- )
12200- .label_size(LabelSize::XSmall)
12201- .color(Color::Warning)
12202- .on_click(move |_, window, cx| {
12203- let (session_id, prompt) = {
12204- let thread = thread.read(cx);
12205- let prompt = thread
12206- .draft_prompt()
12207- .filter(|prompt| !prompt.is_empty())
12208- .map(<[acp::ContentBlock]>::to_vec);
12209- (thread.session_id().clone(), prompt)
12210- };
12211- let Some(prompt) = prompt else {
12212- log::warn!(
12213- "omega#87: a self-modification grant needs a non-empty draft"
12214- );
12215- return;
12216- };
12217- let objective = crate::omega_exo_connection::exo_prompt_objective(&prompt);
12218- let Ok(turn_ref) =
12219- crate::omega_exo_connection::exo_turn_ref(&session_id, &prompt)
12220- else {
12221- log::error!("omega#87: failed to bind the Exo draft to a turn");
12222- return;
12223- };
12224- let exo = Rc::clone(&exo);
12225- window
12226- .spawn(cx, async move |cx| {
12227- let request =
12228- exo.self_modification_request(objective, turn_ref).await?;
12229- let detail = format!(
12230- "This grant expires in 60 seconds and applies only to this \
12231- exact draft, Exo generation, source tree, binary, tool \
12232- modules, and read-write mounts.\n\nExact capabilities:\n{:#?}",
12233- request.capabilities
12234- );
12235- let answer = cx
12236- .prompt(
12237- PromptLevel::Warning,
12238- "Allow this Exo agent to modify itself for one turn?",
12239- Some(&detail),
12240- &["Authorize one turn", "Cancel"],
12241- )
12242- .await?;
12243- if answer == 0 {
12244- exo.confirm_self_modification(request)?;
12245- }
12246- anyhow::Ok(())
12247- })
12248- .detach_and_log_err(cx);
12249- }),
12250- )
12251- })
1225212749 .child(self.render_executor_pin(cx))
12750+ .into_any_element()
1225312751 }
1225412752
1225512753 /// `OMEGA-DELTA-0035`. The pin, as a gesture rather than a mode.
@@ -12341,13 +12839,14 @@ impl Render for ThreadView {
1234112839 // engine what its run is doing, because the engine is the only thing
1234212840 // entitled to say.
1234312841 self.ensure_omega_run_link_refresh(cx);
12842+ self.ensure_exo_inspection(cx);
1234412843
1234512844 let has_messages = self.list_state.item_count() > 0;
1234612845 let list_state = self.list_state.clone();
12846+ let exo = self.exo_connection(cx);
12847+ let compact_exo_layout = window.viewport_size().width < px(960.);
1234712848
1234812849 let conversation = v_flex()
12349- // OMEGA-DELTA-0021. Before anything the thread produced.
12350- .child(self.render_executor_disclosure(cx))
1235112850 // OMEGA-DELTA-0030. The run behind the line, where there is one.
1235212851 .children(self.render_omega_run_link(cx))
1235312852 .when(self.resumed_without_history, |this| {
@@ -12364,6 +12863,28 @@ impl Render for ThreadView {
1236412863 this.into_any()
1236512864 }
1236612865 });
12866+ let conversation = match (exo, self.exo_inspector_expanded) {
12867+ (Some(exo), true) => {
12868+ let inspector = self.render_exo_inspector(exo, compact_exo_layout, cx);
12869+ if compact_exo_layout {
12870+ v_flex()
12871+ .flex_1()
12872+ .size_full()
12873+ .child(inspector)
12874+ .child(conversation)
12875+ .into_any_element()
12876+ } else {
12877+ h_flex()
12878+ .flex_1()
12879+ .size_full()
12880+ .min_w_0()
12881+ .child(conversation)
12882+ .child(inspector)
12883+ .into_any_element()
12884+ }
12885+ }
12886+ _ => conversation.into_any_element(),
12887+ };
1236712888
1236812889 v_flex()
1236912890 .key_context("AcpThread")
@@ -12673,6 +13194,8 @@ impl Render for ThreadView {
1267313194 .flatten(),
1267413195 |this, bar| this.child(bar),
1267513196 )
13197+ // OMEGA-DELTA-0021. Before anything the thread produced.
13198+ .child(self.render_executor_disclosure(cx))
1267613199 .child(conversation)
1267713200 .children(self.render_multi_root_callout(cx))
1267813201 .children(self.render_activity_bar(window, cx))
@@ -12900,6 +13423,19 @@ mod tests {
1290013423 ))
1290113424 }
1290213425
13426+ #[test]
13427+ fn exo_runtime_references_stay_distinguishable_in_the_inspector() {
13428+ assert_eq!(ThreadView::exo_reference("short"), "short");
13429+ assert_eq!(
13430+ ThreadView::exo_reference("cd7c0d29db869e953fb7261d8390ca93007d36a6"),
13431+ "cd7c0d29db86…"
13432+ );
13433+ assert_ne!(
13434+ ThreadView::exo_reference("cd7c0d29db869e953fb7261d8390ca93007d36a6"),
13435+ ThreadView::exo_reference("c61846e3f44daaf445930d1a499432ca9b069306")
13436+ );
13437+ }
13438+
1290313439 #[test]
1290413440 fn test_leading_native_command_matches_bare_and_with_remainder() {
1290513441 let commands = [native_command("compact"), mcp_command("deploy")];
diff --git a/crates/agent_ui/src/omega_exo_connection.rs b/crates/agent_ui/src/omega_exo_connection.rs
index 93beb3120c..9d4d76c652 100644
--- a/crates/agent_ui/src/omega_exo_connection.rs
+++ b/crates/agent_ui/src/omega_exo_connection.rs
@@ -166,6 +166,88 @@ struct ExoDriver {
166166 generation: u64,
167167 }
168168
169+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
170+pub enum ExoInspectionPhase {
171+ NotLoaded,
172+ Refreshing,
173+ Ready,
174+ Unavailable,
175+}
176+
177+#[derive(Clone, Debug, PartialEq, Eq)]
178+pub struct ExoInspectionSnapshot {
179+ pub phase: ExoInspectionPhase,
180+ pub observed: Option<ObservedExoCapabilityState>,
181+ pub identity: Option<ExoLaneIdentity>,
182+ pub networking: Option<bool>,
183+ pub refreshed_at_ms: Option<u64>,
184+ pub error: Option<String>,
185+}
186+
187+impl Default for ExoInspectionSnapshot {
188+ fn default() -> Self {
189+ Self {
190+ phase: ExoInspectionPhase::NotLoaded,
191+ observed: None,
192+ identity: None,
193+ networking: None,
194+ refreshed_at_ms: None,
195+ error: None,
196+ }
197+ }
198+}
199+
200+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
201+pub enum ExoTurnPhase {
202+ Idle,
203+ Inspecting,
204+ Working,
205+ Cancelling,
206+ Completed,
207+ Cancelled,
208+ Refused,
209+ Failed,
210+}
211+
212+impl ExoTurnPhase {
213+ #[must_use]
214+ pub const fn label(self) -> &'static str {
215+ match self {
216+ Self::Idle => "Ready",
217+ Self::Inspecting => "Checking runtime",
218+ Self::Working => "Working",
219+ Self::Cancelling => "Cancelling",
220+ Self::Completed => "Completed",
221+ Self::Cancelled => "Cancelled",
222+ Self::Refused => "Refused",
223+ Self::Failed => "Failed",
224+ }
225+ }
226+}
227+
228+#[derive(Clone, Debug, PartialEq, Eq)]
229+pub struct ExoTurnSnapshot {
230+ pub phase: ExoTurnPhase,
231+ pub detail: Option<String>,
232+ pub exo_session_id: Option<String>,
233+ pub exo_turn_id: Option<String>,
234+ pub latest_event_id: Option<String>,
235+ pub updated_at_ms: u64,
236+}
237+
238+impl Default for ExoTurnSnapshot {
239+ fn default() -> Self {
240+ Self {
241+ phase: ExoTurnPhase::Idle,
242+ detail: None,
243+ exo_session_id: None,
244+ exo_turn_id: None,
245+ latest_event_id: None,
246+ updated_at_ms: now_ms(),
247+ }
248+ }
249+}
250+
169251 #[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
170252 pub struct ExoTierCReceipt {
171253 pub schema: String,
@@ -190,6 +272,8 @@ pub struct ExoHarnessConnection {
190272 pending_grant: RefCell<Option<ExoSelfModificationGrant>>,
191273 tier_c_receipt: Rc<RefCell<Option<ExoTierCReceipt>>>,
192274 active_tier_c_turn: Rc<Cell<bool>>,
275+ inspection: Rc<RefCell<ExoInspectionSnapshot>>,
276+ turn: Rc<RefCell<ExoTurnSnapshot>>,
193277 }
194278
195279 impl ExoHarnessConnection {
@@ -212,6 +296,8 @@ impl ExoHarnessConnection {
212296 pending_grant: RefCell::new(None),
213297 tier_c_receipt: Rc::new(RefCell::new(previous_receipt)),
214298 active_tier_c_turn: Rc::new(Cell::new(false)),
299+ inspection: Rc::new(RefCell::new(ExoInspectionSnapshot::default())),
300+ turn: Rc::new(RefCell::new(ExoTurnSnapshot::default())),
215301 }
216302 }
217303
@@ -236,6 +322,58 @@ impl ExoHarnessConnection {
236322 self.tier_c_receipt.borrow().clone()
237323 }
238324
325+ #[must_use]
326+ pub fn inspection(&self) -> ExoInspectionSnapshot {
327+ self.inspection.borrow().clone()
328+ }
329+
330+ #[must_use]
331+ pub fn turn(&self) -> ExoTurnSnapshot {
332+ self.turn.borrow().clone()
333+ }
334+
335+ fn set_turn(&self, phase: ExoTurnPhase, detail: Option<String>) {
336+ *self.turn.borrow_mut() = ExoTurnSnapshot {
337+ phase,
338+ detail,
339+ exo_session_id: None,
340+ exo_turn_id: None,
341+ latest_event_id: None,
342+ updated_at_ms: now_ms(),
343+ };
344+ }
345+
346+ pub fn refresh_inspection(&self, cx: &mut App) -> Task<Result<()>> {
347+ {
348+ let mut inspection = self.inspection.borrow_mut();
349+ inspection.phase = ExoInspectionPhase::Refreshing;
350+ inspection.error = None;
351+ }
352+ let driver = self.driver.clone();
353+ let inspection = self.inspection.clone();
354+ cx.spawn(async move |_| match driver.observe().await {
355+ Ok(observed) => {
356+ *inspection.borrow_mut() = ExoInspectionSnapshot {
357+ phase: ExoInspectionPhase::Ready,
358+ observed: Some(observed.observed),
359+ identity: Some(observed.identity),
360+ networking: Some(observed.networking),
361+ refreshed_at_ms: Some(now_ms()),
362+ error: None,
363+ };
364+ Ok(())
365+ }
366+ Err(error) => {
367+ let message = error.to_string();
368+ let mut snapshot = inspection.borrow_mut();
369+ snapshot.phase = ExoInspectionPhase::Unavailable;
370+ snapshot.refreshed_at_ms = Some(now_ms());
371+ snapshot.error = Some(message.clone());
372+ Err(anyhow!(message))
373+ }
374+ })
375+ }
376+
239377 /// Observe the exact Exo capability state for a dedicated confirmation
240378 /// dialog. This does not mint authority.
241379 pub async fn self_modification_request(
@@ -275,6 +413,8 @@ impl ExoHarnessConnection {
275413
276414 struct ObservedTurn {
277415 observed: ObservedExoCapabilityState,
416+ identity: ExoLaneIdentity,
417+ networking: bool,
278418 }
279419
280420 fn now_ms() -> u64 {
@@ -285,6 +425,34 @@ fn now_ms() -> u64 {
285425 })
286426 }
287427
428+fn set_turn_snapshot(turn: &RefCell<ExoTurnSnapshot>, phase: ExoTurnPhase, detail: Option<String>) {
429+ *turn.borrow_mut() = ExoTurnSnapshot {
430+ phase,
431+ detail,
432+ exo_session_id: None,
433+ exo_turn_id: None,
434+ latest_event_id: None,
435+ updated_at_ms: now_ms(),
436+ };
437+}
438+
439+fn set_terminal_turn_snapshot(
440+ turn: &RefCell<ExoTurnSnapshot>,
441+ phase: ExoTurnPhase,
442+ detail: Option<String>,
443+ response: &acp::PromptResponse,
444+) {
445+ let meta = response.meta.as_ref();
446+ *turn.borrow_mut() = ExoTurnSnapshot {
447+ phase,
448+ detail,
449+ exo_session_id: meta_value(meta, "exo.session_id"),
450+ exo_turn_id: meta_value(meta, "exo.turn_id"),
451+ latest_event_id: meta_value(meta, "exo.latest_event_id"),
452+ updated_at_ms: now_ms(),
453+ };
454+}
455+
288456 fn resolve_module_host_path(module: &str, mounts: &[ExoMount]) -> Option<PathBuf> {
289457 let path = std::path::Path::new(module);
290458 if path.is_file() {
@@ -482,6 +650,8 @@ impl ExoDriver {
482650 tool_modules,
483651 read_write_mounts,
484652 },
653+ identity,
654+ networking: agent.networking,
485655 })
486656 }
487657
@@ -686,14 +856,45 @@ impl AgentConnection for ExoHarnessConnection {
686856 params: acp::PromptRequest,
687857 cx: &mut App,
688858 ) -> Task<Result<acp::PromptResponse>> {
859+ self.set_turn(
860+ ExoTurnPhase::Inspecting,
861+ Some("Checking the pinned Exo runtime before this turn".to_owned()),
862+ );
689863 let driver = Rc::clone(&self.driver);
690864 let acp = Rc::clone(&self.acp);
691865 let pending_grant = self.pending_grant.take();
692866 let receipt_cell = self.tier_c_receipt.clone();
693867 let active_tier_c_turn = self.active_tier_c_turn.clone();
868+ let inspection_cell = self.inspection.clone();
869+ let turn_cell = self.turn.clone();
694870 cx.spawn(async move |cx| {
695871 let turn_ref = exo_turn_ref(¶ms.session_id, ¶ms.prompt)?;
696- let observed = driver.preflight().await?;
872+ let observed = match driver.preflight().await {
873+ Ok(observed) => observed,
874+ Err(error) => {
875+ let message = error.to_string();
876+ {
877+ let mut inspection = inspection_cell.borrow_mut();
878+ inspection.phase = ExoInspectionPhase::Unavailable;
879+ inspection.refreshed_at_ms = Some(now_ms());
880+ inspection.error = Some(message.clone());
881+ }
882+ set_turn_snapshot(
883+ &turn_cell,
884+ ExoTurnPhase::Failed,
885+ Some(format!("Runtime check failed: {message}")),
886+ );
887+ return Err(error);
888+ }
889+ };
890+ *inspection_cell.borrow_mut() = ExoInspectionSnapshot {
891+ phase: ExoInspectionPhase::Ready,
892+ observed: Some(observed.observed.clone()),
893+ identity: Some(observed.identity.clone()),
894+ networking: Some(observed.networking),
895+ refreshed_at_ms: Some(now_ms()),
896+ error: None,
897+ };
697898 let capabilities = observed.observed.requested_capabilities();
698899 let authority_receipt = if capabilities.is_empty() {
699900 None
@@ -708,6 +909,11 @@ impl AgentConnection for ExoHarnessConnection {
708909 );
709910 *receipt_cell.borrow_mut() = Some(receipt.clone());
710911 persist_tier_c_receipt(receipt).await?;
912+ set_turn_snapshot(
913+ &turn_cell,
914+ ExoTurnPhase::Refused,
915+ Some(message.to_owned()),
916+ );
711917 bail!("{message}");
712918 };
713919 let requested_objective = Some(grant.request().objective.clone());
@@ -725,6 +931,11 @@ impl AgentConnection for ExoHarnessConnection {
725931 );
726932 *receipt_cell.borrow_mut() = Some(receipt.clone());
727933 persist_tier_c_receipt(receipt).await?;
934+ set_turn_snapshot(
935+ &turn_cell,
936+ ExoTurnPhase::Refused,
937+ Some(message.clone()),
938+ );
728939 bail!("{message}");
729940 }
730941 }
@@ -755,8 +966,34 @@ impl AgentConnection for ExoHarnessConnection {
755966 }
756967 }
757968 }
969+ set_turn_snapshot(
970+ &turn_cell,
971+ ExoTurnPhase::Working,
972+ Some("Streaming this turn over ACP".to_owned()),
973+ );
758974 let prompt = cx.update(|cx| acp.prompt(params, cx));
759975 let response = prompt.await;
976+ match &response {
977+ Ok(response) => match response.stop_reason {
978+ acp::StopReason::Cancelled => set_terminal_turn_snapshot(
979+ &turn_cell,
980+ ExoTurnPhase::Cancelled,
981+ Some("Exo confirmed cancellation".to_owned()),
982+ response,
983+ ),
984+ _ => set_terminal_turn_snapshot(
985+ &turn_cell,
986+ ExoTurnPhase::Completed,
987+ Some("Exo completed the streamed turn".to_owned()),
988+ response,
989+ ),
990+ },
991+ Err(error) => set_turn_snapshot(
992+ &turn_cell,
993+ ExoTurnPhase::Failed,
994+ Some(error.to_string()),
995+ ),
996+ }
760997 if is_tier_c_turn {
761998 if let Some(receipt) = receipt_cell.borrow_mut().as_mut() {
762999 match &response {
@@ -800,6 +1037,10 @@ impl AgentConnection for ExoHarnessConnection {
8001037 }
8011038
8021039 fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
1040+ self.set_turn(
1041+ ExoTurnPhase::Cancelling,
1042+ Some("Waiting for Exo to confirm cancellation".to_owned()),
1043+ );
8031044 let cancelled_pending_grant = self.pending_grant.borrow_mut().take().map(|grant| {
8041045 let request = grant.request();
8051046 refused_tier_c_receipt(
@@ -873,6 +1114,66 @@ mod tests {
8731114 assert_eq!(config.root.as_str(), "/opt/exo/.exo");
8741115 }
8751116
1117+ #[test]
1118+ fn an_unread_inspector_claims_no_runtime_facts() {
1119+ let inspection = ExoInspectionSnapshot::default();
1120+ assert_eq!(inspection.phase, ExoInspectionPhase::NotLoaded);
1121+ assert_eq!(inspection.observed, None);
1122+ assert_eq!(inspection.identity, None);
1123+ assert_eq!(inspection.networking, None);
1124+ assert_eq!(inspection.refreshed_at_ms, None);
1125+ assert_eq!(inspection.error, None);
1126+ }
1127+
1128+ #[test]
1129+ fn every_exo_turn_phase_has_a_distinct_visible_label() {
1130+ let phases = [
1131+ ExoTurnPhase::Idle,
1132+ ExoTurnPhase::Inspecting,
1133+ ExoTurnPhase::Working,
1134+ ExoTurnPhase::Cancelling,
1135+ ExoTurnPhase::Completed,
1136+ ExoTurnPhase::Cancelled,
1137+ ExoTurnPhase::Refused,
1138+ ExoTurnPhase::Failed,
1139+ ];
1140+ let labels = phases.map(ExoTurnPhase::label);
1141+ for (index, label) in labels.iter().enumerate() {
1142+ assert!(!label.is_empty());
1143+ assert!(
1144+ !labels[..index].contains(label),
1145+ "duplicate Exo turn label: {label}"
1146+ );
1147+ }
1148+ }
1149+
1150+ #[test]
1151+ fn a_turn_snapshot_records_terminal_state_and_detail() {
1152+ let turn = RefCell::new(ExoTurnSnapshot::default());
1153+ let response = acp::PromptResponse::new(acp::StopReason::EndTurn).meta(
1154+ [
1155+ ("exo.session_id".into(), serde_json::json!("session-1")),
1156+ ("exo.turn_id".into(), serde_json::json!("turn-1")),
1157+ ("exo.latest_event_id".into(), serde_json::json!("event-1")),
1158+ ]
1159+ .into_iter()
1160+ .collect::<serde_json::Map<String, serde_json::Value>>(),
1161+ );
1162+ set_terminal_turn_snapshot(
1163+ &turn,
1164+ ExoTurnPhase::Completed,
1165+ Some("durable event observed".into()),
1166+ &response,
1167+ );
1168+ let turn = turn.borrow();
1169+ assert_eq!(turn.phase, ExoTurnPhase::Completed);
1170+ assert_eq!(turn.detail.as_deref(), Some("durable event observed"));
1171+ assert_eq!(turn.exo_session_id.as_deref(), Some("session-1"));
1172+ assert_eq!(turn.exo_turn_id.as_deref(), Some("turn-1"));
1173+ assert_eq!(turn.latest_event_id.as_deref(), Some("event-1"));
1174+ assert!(turn.updated_at_ms > 0);
1175+ }
1176+
8761177 /// A machine with no Exo has no lane. This is the ordinary case, and it
8771178 /// must not be an error.
8781179 #[test]
@@ -1057,11 +1358,12 @@ mod tests {
10571358 .expect("a session on the Exo lane");
10581359
10591360 let session_id = thread.read_with(cx, |thread, _| thread.session_id().clone());
1060- let marker = "OMEGA-EXO-ACP-STREAM";
1361+ let tool_marker = "OMEGA-EXO-TOOL";
1362+ let marker = "OMEGA-EXO-PANE-READY";
10611363 let request = acp::PromptRequest::new(
10621364 session_id,
10631365 vec![acp::ContentBlock::Text(acp::TextContent::new(format!(
1064- "Reply with exactly the word {marker} and nothing else."
1366+ "Run one tool that prints {tool_marker}, then reply with exactly {marker}."
10651367 )))],
10661368 );
10671369 let response = cx
@@ -1075,6 +1377,10 @@ mod tests {
10751377 rendered.contains(marker),
10761378 "the Exo reply did not reach the thread: {rendered}"
10771379 );
1380+ assert!(
1381+ rendered.contains(tool_marker),
1382+ "the Exo tool result did not reach the thread: {rendered}"
1383+ );
10781384
10791385 let disclosure = thread.read_with(cx, |thread, cx| thread.omega_executor_disclosure(cx));
10801386 assert!(disclosure.is_coherent(), "{disclosure:?}");
@@ -1085,6 +1391,18 @@ mod tests {
10851391 .downcast::<ExoHarnessConnection>()
10861392 .expect("the configured lane is Exo");
10871393 let identity = exo.identity().expect("Exo told the lane who it is");
1394+ let inspection = exo.inspection();
1395+ assert_eq!(inspection.phase, ExoInspectionPhase::Ready);
1396+ assert_eq!(inspection.identity.as_ref(), Some(&identity));
1397+ assert!(
1398+ inspection.observed.is_some(),
1399+ "the workspace inspector did not retain the turn's exact preflight"
1400+ );
1401+ let turn = exo.turn();
1402+ assert_eq!(turn.phase, ExoTurnPhase::Completed, "{turn:?}");
1403+ assert!(turn.exo_session_id.is_some(), "{turn:?}");
1404+ assert!(turn.exo_turn_id.is_some(), "{turn:?}");
1405+ assert!(turn.latest_event_id.is_some(), "{turn:?}");
10881406 assert!(disclosure.agent_id.starts_with("exo/"), "{disclosure:?}");
10891407 // The parts that only the Exo arm of `classify_connection` can supply.
10901408 // Without them this assertion set passes on the shared external-agent
diff --git a/crates/omega_deltas/src/omega_deltas.rs b/crates/omega_deltas/src/omega_deltas.rs
index ee58b45193..6eaa9e4e8c 100644
--- a/crates/omega_deltas/src/omega_deltas.rs
+++ b/crates/omega_deltas/src/omega_deltas.rs
@@ -73,6 +73,7 @@ pub const ENFORCED_DELTAS: &[&str] = &[
7373 "OMEGA-DELTA-0043",
7474 "OMEGA-DELTA-0044",
7575 "OMEGA-DELTA-0045",
76+ "OMEGA-DELTA-0046",
7677 ];
7778
7879 /// OMEGA-DELTA-0036. The uninstall script embedded in the shipped `cli`.
@@ -218,6 +219,9 @@ pub const THREAD_ENTRY_PATH: &str = "crates/acp_thread/src/acp_thread.rs";
218219 pub const SYSTEM_NOTE_REFUSAL: &str =
219220 "Agent threads do not expose an owner-visible system-note authority.";
220221
222+/// OMEGA-DELTA-0046. The native Metal proof for the Exo workspace.
223+pub const VISUAL_TEST_RUNNER_PATH: &str = "crates/zed/src/visual_test_runner.rs";
224+
221225 /// OMEGA-DELTA-0030. The typed start command sent to `omega-effectd`.
222226 pub const FULL_AUTO_DISPATCH_PATH: &str = "crates/full_auto_ui/src/dispatch.rs";
223227
@@ -6703,6 +6707,77 @@ mod tests {
67036707 disclosure_path.display()
67046708 );
67056709 }
6710+
6711+ // ------ OMEGA-DELTA-0046
6712+
6713+ /// OMEGA-DELTA-0046. An Exo thread is a usable workspace, not one label.
6714+ ///
6715+ /// The workspace must keep Omega's standard transcript and composer. Its
6716+ /// inspector must project facts from the same preflight that gates a turn.
6717+ /// Its controls must reuse the existing cancel and exact one-turn authority
6718+ /// paths. This source check catches the cheap failure modes: a mock panel,
6719+ /// a second message implementation, or controls that only look active.
6720+ #[test]
6721+ fn an_exo_thread_has_a_live_workspace_and_exact_runtime_inspector() {
6722+ let thread_path = repository_path(THREAD_VIEW_PATH);
6723+ let thread = std::fs::read_to_string(&thread_path)
6724+ .unwrap_or_else(|error| panic!("cannot read {}: {error}", thread_path.display()));
6725+ for token in [
6726+ "omega-exo-workspace-header",
6727+ "omega-exo-inspector",
6728+ "Runtime inspector",
6729+ "render_entries(cx)",
6730+ "render_message_editor(window, cx)",
6731+ "cancel_generation",
6732+ "refresh_exo_inspection",
6733+ "authorize_exo_self_modification",
6734+ "ObservedExoCapabilityState::requested_capabilities",
6735+ ] {
6736+ assert!(
6737+ thread.contains(token),
6738+ "OMEGA-DELTA-0046: the Exo workspace lost `{token}`"
6739+ );
6740+ }
6741+
6742+ let connection_path = repository_path(EXO_CONNECTION_PATH);
6743+ let connection = std::fs::read_to_string(&connection_path)
6744+ .unwrap_or_else(|error| panic!("cannot read {}: {error}", connection_path.display()));
6745+ for token in [
6746+ "ExoInspectionSnapshot",
6747+ "ExoTurnPhase",
6748+ "driver.observe().await",
6749+ "driver.preflight().await",
6750+ "acp.prompt(params",
6751+ "meta_value(meta, \"exo.session_id\")",
6752+ "meta_value(meta, \"exo.turn_id\")",
6753+ "meta_value(meta, \"exo.latest_event_id\")",
6754+ ] {
6755+ assert!(
6756+ connection.contains(token),
6757+ "OMEGA-DELTA-0046: the Exo workspace state lost `{token}`"
6758+ );
6759+ }
6760+
6761+ let visual_path = repository_path(VISUAL_TEST_RUNNER_PATH);
6762+ let visual = std::fs::read_to_string(&visual_path)
6763+ .unwrap_or_else(|error| panic!("cannot read {}: {error}", visual_path.display()));
6764+ for token in [
6765+ "OMEGA_EXO_VISUAL_ONLY",
6766+ "run_omega_exo_visual_tests",
6767+ "omega_exo_workspace_wide",
6768+ "omega_exo_workspace_narrow",
6769+ "the real Exo visual turn failed",
6770+ "turn.exo_session_id.is_some()",
6771+ "turn.exo_turn_id.is_some()",
6772+ "turn.latest_event_id.is_some()",
6773+ ] {
6774+ assert!(
6775+ visual.contains(token),
6776+ "OMEGA-DELTA-0046: the real Exo visual proof lost `{token}`"
6777+ );
6778+ }
6779+ }
6780+
67066781 // ---------------------------------------------------------------------
67076782 // OMEGA-DELTA-0041 — Omega Agent served over ACP on a loopback socket
67086783 // ---------------------------------------------------------------------
diff --git a/crates/zed/src/visual_test_runner.rs b/crates/zed/src/visual_test_runner.rs
index 9e87966647..82b1dbcd4e 100644
--- a/crates/zed/src/visual_test_runner.rs
+++ b/crates/zed/src/visual_test_runner.rs
@@ -124,6 +124,7 @@ use {
124124 acp_thread::{AgentConnection, StubAgentConnection},
125125 agent_client_protocol::schema::v1 as acp,
126126 agent_servers::{AgentServer, AgentServerDelegate},
127+ agent_ui::Agent,
127128 anyhow::{Context as _, Result},
128129 assets::Assets,
129130 editor::display_map::DisplayRow,
@@ -420,6 +421,28 @@ fn run_visual_tests(project_path: PathBuf, update_baseline: bool) -> Result<()>
420421 let mut failed = 0;
421422 let mut updated = 0;
422423
424+ // `OMEGA_EXO_VISUAL_ONLY=1` runs the real Exo conversation workspace
425+ // proof and nothing else. The lane file must name an isolated Exo install.
426+ // This path connects over the shipped ACP stdio transport and sends a real
427+ // turn before it captures the wide and narrow layouts.
428+ #[cfg(feature = "visual-tests")]
429+ if std::env::var("OMEGA_EXO_VISUAL_ONLY").is_ok() {
430+ println!("\n--- Omega: real Exo conversation workspace ---");
431+ let outcome = run_omega_exo_visual_tests(app_state.clone(), &mut cx, update_baseline);
432+ teardown_shared_window(workspace_window, &mut cx);
433+ return match outcome {
434+ Ok(TestResult::Passed) => {
435+ println!("\u{2713} omega_exo_workspace: PASSED");
436+ Ok(())
437+ }
438+ Ok(TestResult::BaselineUpdated(_)) => {
439+ println!("\u{2713} omega_exo_workspace: Baselines updated");
440+ Ok(())
441+ }
442+ Err(error) => Err(error.context("omega_exo_workspace")),
443+ };
444+ }
445+
423446 // `OMEGA_VISUAL_ONLY=1` runs Omega's own suite and nothing else.
424447 //
425448 // This runner has no committed baselines for any of the inherited Zed
@@ -728,7 +751,10 @@ fn run_visual_tests(project_path: PathBuf, update_baseline: bool) -> Result<()>
728751 updated += 1;
729752 }
730753 Err(e) => {
731- eprintln!("\u{2717} external_agent_harness_maintenance: FAILED - {}", e);
754+ eprintln!(
755+ "\u{2717} external_agent_harness_maintenance: FAILED - {}",
756+ e
757+ );
732758 failed += 1;
733759 }
734760 }
@@ -2174,7 +2200,9 @@ fn run_omega_agent_visual_tests_inner(
21742200 ..Default::default()
21752201 },
21762202 |window, cx| {
2177- cx.new(|cx| Workspace::new(None, project.clone(), app_state.clone(), window, cx))
2203+ cx.new(|cx| {
2204+ Workspace::new(None, project.clone(), app_state.clone(), window, cx)
2205+ })
21782206 },
21792207 )
21802208 })
@@ -2768,7 +2796,9 @@ fn run_omega_restart_visual_tests_inner(
27682796 ..Default::default()
27692797 },
27702798 |window, cx| {
2771- cx.new(|cx| Workspace::new(None, project.clone(), app_state.clone(), window, cx))
2799+ cx.new(|cx| {
2800+ Workspace::new(None, project.clone(), app_state.clone(), window, cx)
2801+ })
27722802 },
27732803 )
27742804 })
@@ -2965,6 +2995,321 @@ impl AgentServer for StubAgentServer {
29652995 }
29662996 }
29672997
2998+/// A visual-test server that connects to one real, explicitly named Exo lane.
2999+///
3000+/// The server stores only the lane path so it remains `Send`, as
3001+/// [`AgentServer`] requires. The connection itself stays on the GPUI thread.
3002+#[derive(Clone)]
3003+#[cfg(target_os = "macos")]
3004+struct ExoVisualAgentServer {
3005+ lane_path: PathBuf,
3006+}
3007+
3008+#[cfg(target_os = "macos")]
3009+impl AgentServer for ExoVisualAgentServer {
3010+ fn logo(&self) -> ui::IconName {
3011+ ui::IconName::OmegaAssistant
3012+ }
3013+
3014+ fn agent_id(&self) -> AgentId {
3015+ "Exo".into()
3016+ }
3017+
3018+ fn connect(
3019+ &self,
3020+ _delegate: AgentServerDelegate,
3021+ project: Entity<Project>,
3022+ cx: &mut App,
3023+ ) -> gpui::Task<gpui::Result<Rc<dyn AgentConnection>>> {
3024+ let lane_path = self.lane_path.clone();
3025+ let agent_server_store = project.read(cx).agent_server_store().downgrade();
3026+ cx.spawn(async move |cx| {
3027+ agent_ui::omega_exo_connection::connect_configured_lane(
3028+ &lane_path,
3029+ project,
3030+ agent_server_store,
3031+ cx,
3032+ )
3033+ .await?
3034+ .ok_or_else(|| anyhow::anyhow!("the Exo visual lane file is not configured"))
3035+ })
3036+ }
3037+
3038+ fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
3039+ self
3040+ }
3041+}
3042+
3043+#[cfg(all(target_os = "macos", feature = "visual-tests"))]
3044+fn run_omega_exo_visual_tests(
3045+ app_state: Arc<AppState>,
3046+ cx: &mut VisualTestAppContext,
3047+ update_baseline: bool,
3048+) -> Result<TestResult> {
3049+ let lane_path = PathBuf::from(
3050+ std::env::var("OMEGA_EXO_VISUAL_LANE_FILE")
3051+ .context("set OMEGA_EXO_VISUAL_LANE_FILE to an isolated Exo lane file")?,
3052+ );
3053+ anyhow::ensure!(
3054+ lane_path.is_file(),
3055+ "the Exo visual lane does not exist: {}",
3056+ lane_path.display()
3057+ );
3058+
3059+ // Use independent native windows instead of resizing one hidden Metal
3060+ // window. The macOS platform does not dispatch a bounds callback for that
3061+ // test-only resize, so a second screenshot can silently retain the first
3062+ // viewport. Two fresh windows prove both responsive branches directly.
3063+ let wide = run_omega_exo_visual_capture(
3064+ app_state.clone(),
3065+ cx,
3066+ lane_path.clone(),
3067+ "omega_exo_workspace_wide",
3068+ size(px(1320.), px(860.)),
3069+ update_baseline,
3070+ )?;
3071+ let narrow = run_omega_exo_visual_capture(
3072+ app_state,
3073+ cx,
3074+ lane_path,
3075+ "omega_exo_workspace_narrow",
3076+ size(px(720.), px(900.)),
3077+ update_baseline,
3078+ )?;
3079+
3080+ for result in [&wide, &narrow] {
3081+ if let TestResult::BaselineUpdated(path) = result {
3082+ return Ok(TestResult::BaselineUpdated(path.clone()));
3083+ }
3084+ }
3085+ Ok(TestResult::Passed)
3086+}
3087+
3088+#[cfg(all(target_os = "macos", feature = "visual-tests"))]
3089+fn run_omega_exo_visual_capture(
3090+ app_state: Arc<AppState>,
3091+ cx: &mut VisualTestAppContext,
3092+ lane_path: PathBuf,
3093+ test_name: &'static str,
3094+ window_size: gpui::Size<gpui::Pixels>,
3095+ update_baseline: bool,
3096+) -> Result<TestResult> {
3097+ use agent_ui::AgentPanel;
3098+
3099+ let project_dir = tempfile::tempdir()?.keep().canonicalize()?;
3100+ let project = cx.update(|cx| {
3101+ Project::local(
3102+ app_state.client.clone(),
3103+ app_state.node_runtime.clone(),
3104+ app_state.user_store.clone(),
3105+ app_state.languages.clone(),
3106+ app_state.fs.clone(),
3107+ None,
3108+ project::LocalProjectFlags {
3109+ init_worktree_trust: false,
3110+ ..Default::default()
3111+ },
3112+ cx,
3113+ )
3114+ });
3115+ let add_worktree = project.update(cx, |project, cx| {
3116+ project.find_or_create_worktree(&project_dir, true, cx)
3117+ });
3118+ cx.background_executor.allow_parking();
3119+ cx.foreground_executor
3120+ .block_test(add_worktree)
3121+ .context("failed to add the Exo visual worktree")?;
3122+ cx.background_executor.forbid_parking();
3123+ cx.run_until_parked();
3124+
3125+ let bounds = Bounds {
3126+ origin: point(px(-10_000.), px(-10_000.)),
3127+ size: window_size,
3128+ };
3129+ let workspace_window: WindowHandle<Workspace> = cx
3130+ .update(|cx| {
3131+ cx.open_window(
3132+ WindowOptions {
3133+ window_bounds: Some(WindowBounds::Windowed(bounds)),
3134+ focus: false,
3135+ show: true,
3136+ ..Default::default()
3137+ },
3138+ |window, cx| {
3139+ cx.new(|cx| {
3140+ Workspace::new(None, project.clone(), app_state.clone(), window, cx)
3141+ })
3142+ },
3143+ )
3144+ })
3145+ .context("failed to open the Exo workspace window")?;
3146+ cx.run_until_parked();
3147+
3148+ let (weak_workspace, async_window_cx) = workspace_window
3149+ .update(cx, |workspace, window, cx| {
3150+ (workspace.weak_handle(), window.to_async(cx))
3151+ })
3152+ .context("failed to get the Exo workspace handle")?;
3153+ cx.background_executor.allow_parking();
3154+ let panel = cx
3155+ .foreground_executor
3156+ .block_test(AgentPanel::load(weak_workspace, async_window_cx))
3157+ .context("failed to load the Exo Agent panel")?;
3158+ cx.background_executor.forbid_parking();
3159+
3160+ workspace_window
3161+ .update(cx, |workspace, window, cx| {
3162+ workspace.add_panel(panel.clone(), window, cx);
3163+ workspace.open_panel::<AgentPanel>(window, cx);
3164+ panel.update(cx, |panel, cx| {
3165+ use workspace::dock::Panel as _;
3166+ panel.set_zoomed(true, window, cx);
3167+ });
3168+ })
3169+ .context("failed to open the Exo Agent panel")?;
3170+ cx.run_until_parked();
3171+
3172+ // Keep capture failures inside this closure so the real ACP connection is
3173+ // always removed below. Otherwise an early assertion or screenshot error
3174+ // masks its own cause with GPUI's leaked ElicitationStore check.
3175+ let capture = (|| -> Result<TestResult> {
3176+ let server: Rc<dyn AgentServer> = Rc::new(ExoVisualAgentServer { lane_path });
3177+ cx.update_window(workspace_window.into(), |_, window, cx| {
3178+ panel.update(cx, |panel, cx| {
3179+ panel.open_external_thread_with_server(server, window, cx);
3180+ });
3181+ })?;
3182+
3183+ // Connecting the child process and completing ACP initialization
3184+ // require real I/O. Poll the visible state for at most five seconds;
3185+ // one `run_until_parked` can return before stdio initialization wakes
3186+ // the foreground executor.
3187+ cx.background_executor.allow_parking();
3188+ for _ in 0..100 {
3189+ cx.run_until_parked();
3190+ if cx.read(|cx| {
3191+ panel
3192+ .read(cx)
3193+ .active_thread_view_for_tests()
3194+ .is_some_and(|view| view.read(cx).active_thread().is_some())
3195+ }) {
3196+ break;
3197+ }
3198+ std::thread::sleep(Duration::from_millis(50));
3199+ }
3200+ cx.background_executor.forbid_parking();
3201+
3202+ let thread_view = cx
3203+ .read(|cx| panel.read(cx).active_thread_view_for_tests().cloned())
3204+ .ok_or_else(|| anyhow::anyhow!("the Exo workspace has no active thread"))?;
3205+ let thread = cx
3206+ .read(|cx| {
3207+ thread_view
3208+ .read(cx)
3209+ .active_thread()
3210+ .map(|active| active.read(cx).thread.clone())
3211+ })
3212+ .ok_or_else(|| anyhow::anyhow!("the Exo workspace thread is not available"))?;
3213+
3214+ let marker = "OMEGA-EXO-PANE-READY";
3215+ let prompt =
3216+ format!("Run one tool that prints OMEGA-EXO-TOOL, then reply with exactly {marker}.");
3217+ let send = thread.update(cx, |thread, cx| thread.send(vec![prompt.into()], cx));
3218+ cx.background_executor.allow_parking();
3219+ let send_result = cx.foreground_executor.block_test(send);
3220+ cx.background_executor.forbid_parking();
3221+ send_result.context("the real Exo visual turn failed")?;
3222+ cx.run_until_parked();
3223+
3224+ let (transcript, turn) = cx.read(|cx| {
3225+ let thread = thread.read(cx);
3226+ let connection = thread
3227+ .connection()
3228+ .clone()
3229+ .downcast::<agent_ui::omega_exo_connection::ExoHarnessConnection>()
3230+ .expect("the visual thread must retain its Exo connection");
3231+ (thread.to_markdown(cx), connection.turn())
3232+ });
3233+ anyhow::ensure!(
3234+ transcript.contains(marker),
3235+ "the real Exo response did not reach the transcript:\n{transcript}"
3236+ );
3237+ anyhow::ensure!(
3238+ transcript.contains("Tool Call: shell"),
3239+ "the real Exo tool call did not reach the transcript:\n{transcript}"
3240+ );
3241+ anyhow::ensure!(
3242+ transcript.contains("OMEGA-EXO-TOOL"),
3243+ "the real Exo tool result did not reach the transcript:\n{transcript}"
3244+ );
3245+ anyhow::ensure!(
3246+ turn.phase == agent_ui::omega_exo_connection::ExoTurnPhase::Completed,
3247+ "the real Exo turn did not complete: {turn:?}"
3248+ );
3249+ anyhow::ensure!(
3250+ turn.exo_session_id.is_some()
3251+ && turn.exo_turn_id.is_some()
3252+ && turn.latest_event_id.is_some(),
3253+ "the real Exo turn did not return durable references: {turn:?}"
3254+ );
3255+
3256+ let actual_size = cx.update_window(workspace_window.into(), |_, window, _cx| {
3257+ window.viewport_size()
3258+ })?;
3259+ anyhow::ensure!(
3260+ actual_size == window_size,
3261+ "{test_name} opened at {actual_size:?}, expected {window_size:?}"
3262+ );
3263+
3264+ run_visual_test(test_name, workspace_window.into(), cx, update_baseline)
3265+ })();
3266+
3267+ // Drop every owner of the real ACP connection before the runner's leaked
3268+ // entity check. The panel's connection store retains a successful custom
3269+ // connection after its thread and window close. Replacing that cached
3270+ // entry first lets the Exo child and its ElicitationStore terminate.
3271+ panel.update(cx, |panel, cx| {
3272+ panel.connection_store().update(cx, |store, cx| {
3273+ store.restart_connection(
3274+ Agent::Custom { id: "Exo".into() },
3275+ Rc::new(StubAgentServer::new(StubAgentConnection::new())),
3276+ cx,
3277+ );
3278+ });
3279+ });
3280+ cx.run_until_parked();
3281+
3282+ workspace_window
3283+ .update(cx, |workspace, window, cx| {
3284+ workspace.remove_panel(&panel, window, cx);
3285+ let project = workspace.project().clone();
3286+ project.update(cx, |project, cx| {
3287+ let worktree_ids: Vec<_> = project
3288+ .worktrees(cx)
3289+ .map(|worktree| worktree.read(cx).id())
3290+ .collect();
3291+ for id in worktree_ids {
3292+ project.remove_worktree(id, cx);
3293+ }
3294+ });
3295+ })
3296+ .log_err();
3297+ cx.run_until_parked();
3298+ drop(panel);
3299+ drop(project);
3300+ cx.update_window(workspace_window.into(), |_, window, _cx| {
3301+ window.remove_window();
3302+ })
3303+ .log_err();
3304+ cx.run_until_parked();
3305+ for _ in 0..15 {
3306+ cx.advance_clock(Duration::from_millis(100));
3307+ cx.run_until_parked();
3308+ }
3309+
3310+ capture
3311+}
3312+
29683313 #[cfg(all(target_os = "macos", feature = "visual-tests"))]
29693314 fn run_agent_thread_view_test(
29703315 app_state: Arc<AppState>,
@@ -4578,11 +4923,11 @@ fn run_external_agent_maintenance_visual_tests(
45784923 update_baseline: bool,
45794924 ) -> Result<TestResult> {
45804925 use ::collections::HashMap;
4926+ use gpui::UpdateGlobal as _;
45814927 use project::agent_registry_store::{
45824928 AgentRegistryStore, RegistryAgent, RegistryAgentMetadata, RegistryBinaryAgent,
45834929 RegistryTargetConfig,
45844930 };
4585- use gpui::UpdateGlobal as _;
45864931 use settings::SettingsStore;
45874932
45884933 const HEALTHY: &str = "omega-visual-harness";
@@ -4741,8 +5086,7 @@ fn run_external_agent_maintenance_visual_tests(
47415086 // has to be allowed to park while it waits for them. This test runs last.
47425087 cx.executor().allow_parking();
47435088
4744- let outcome: Arc<std::sync::Mutex<Option<Result<()>>>> =
4745- Arc::new(std::sync::Mutex::new(None));
5089+ let outcome: Arc<std::sync::Mutex<Option<Result<()>>>> = Arc::new(std::sync::Mutex::new(None));
47465090 let setup = cx.update(|cx| {
47475091 let outcome = outcome.clone();
47485092 cx.background_spawn(async move {
diff --git a/crates/zed/test_fixtures/visual_tests/omega_exo_workspace_narrow.png b/crates/zed/test_fixtures/visual_tests/omega_exo_workspace_narrow.png
new file mode 100644
index 0000000000..03437abecc
Binary files /dev/null and b/crates/zed/test_fixtures/visual_tests/omega_exo_workspace_narrow.png differ
diff --git a/crates/zed/test_fixtures/visual_tests/omega_exo_workspace_wide.png b/crates/zed/test_fixtures/visual_tests/omega_exo_workspace_wide.png
new file mode 100644
index 0000000000..7185c43886
Binary files /dev/null and b/crates/zed/test_fixtures/visual_tests/omega_exo_workspace_wide.png differ