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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion app/src/server/server_api/factory.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
#[cfg(any(target_family = "wasm", test))]
use anyhow::Context;
use anyhow::{Result, anyhow};
use async_trait::async_trait;
use cynic::{MutationBuilder, QueryBuilder};
#[cfg(test)]
use mockall::automock;
#[cfg(any(target_family = "wasm", test))]
use serde::Deserialize;
use warp_graphql::mutations::delete_runner::{
DeleteRunner, DeleteRunnerInput, DeleteRunnerResult, DeleteRunnerVariables,
};
Expand All @@ -26,11 +30,20 @@ pub struct UpsertedRunner {
pub is_update: bool,
}

/// Client for the Factory GraphQL surface (runner CRUD).
#[cfg(any(target_family = "wasm", test))]
#[derive(Debug, Deserialize)]
struct FactoryAccessResponse {
allowed: bool,
}

/// Client for the Factory API surface.
#[cfg_attr(test, automock)]
#[cfg_attr(not(target_family = "wasm"), async_trait)]
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
pub trait FactoryClient: 'static + Send + Sync {
#[cfg(any(target_family = "wasm", test))]
/// Returns whether the current principal is in the Factory early-access rollout.
async fn has_factory_access(&self) -> Result<bool>;
/// Fetch all runners visible to the caller, optionally sorted.
async fn get_runners(&self, sort_by: Option<RunnerSortBy>) -> Result<Vec<Runner>>;

Expand All @@ -47,6 +60,34 @@ pub trait FactoryClient: 'static + Send + Sync {
#[cfg_attr(not(target_family = "wasm"), async_trait)]
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
impl FactoryClient for ServerApi {
#[cfg(any(target_family = "wasm", test))]
async fn has_factory_access(&self) -> Result<bool> {
let auth_token = self
.get_or_refresh_access_token()
.await
.context("Failed to get access token for Factory access request")?;
let url = format!(
"{}/api/v1/factory/access",
crate::ChannelState::server_root_url()
);
let mut request = self.http_client().get(&url);
if let Some(token) = auth_token.as_bearer_token() {
request = request.bearer_auth(token);
}
let response = request
.send()
.await
.with_context(|| format!("Failed to send Factory access request to {url}"))?;
if !response.status().is_success() {
self.observe_iap_challenge(&response);
return Err(Self::error_from_response(response).await);
}
let response: FactoryAccessResponse = response
.json()
.await
.with_context(|| format!("Failed to deserialize Factory access response from {url}"))?;
Ok(response.allowed)
}
async fn get_runners(&self, sort_by: Option<RunnerSortBy>) -> Result<Vec<Runner>> {
let operation = GetRunners::build(GetRunnersVariables {
request_context: get_request_context(),
Expand Down Expand Up @@ -97,3 +138,7 @@ impl FactoryClient for ServerApi {
}
}
}

#[cfg(test)]
#[path = "factory_tests.rs"]
mod tests;
48 changes: 48 additions & 0 deletions app/src/server/server_api/factory_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use std::sync::Arc;

use futures::executor::block_on;

use super::FactoryClient;
use crate::ChannelState;
use crate::auth::auth_state::AuthState;
use crate::server::server_api::ServerApi;
use crate::server::telemetry::TelemetryApi;

fn server_api_with_bearer_token() -> ServerApi {
let (event_sender, _) = async_channel::unbounded();
let auth_state = Arc::new(AuthState::new_logged_out_for_test());
auth_state.set_remote_server_bearer_token("factory-access-token".to_string());
ServerApi::new_with_parts(
Arc::new(http_client::Client::new_for_test()),
auth_state,
event_sender,
None,
None,
TelemetryApi::new(),
)
}

fn factory_access_response(body: &str) -> anyhow::Result<bool> {
let _request = {
let mut server = ChannelState::mock_server();
server
.mock("GET", "/api/v1/factory/access")
.match_header("authorization", "Bearer factory-access-token")
.with_status(200)
.with_body(body)
.create()
};

block_on(server_api_with_bearer_token().has_factory_access())
}

#[test]
fn factory_access_request_uses_authenticated_endpoint_and_decodes_rollout_state() {
assert!(factory_access_response(r#"{"allowed":true}"#).unwrap());
assert!(!factory_access_response(r#"{"allowed":false}"#).unwrap());
}

#[test]
fn factory_access_request_rejects_malformed_response() {
assert!(factory_access_response(r#"{"access":true}"#).is_err());
}
126 changes: 99 additions & 27 deletions app/src/workspace/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,8 @@ use crate::server::cloud_objects::update_manager::{
use crate::server::ids::{ObjectUid, ServerId, SyncId};
use crate::server::network_log_pane_manager::NetworkLogPaneManager;
use crate::server::server_api::ai::AIClient;
#[cfg(any(target_family = "wasm", test))]
use crate::server::server_api::factory::FactoryClient;
use crate::server::server_api::{ServerApi, ServerApiProvider, ServerTime};
use crate::server::telemetry::{
AddTabWithShellSource, AnonymousUserSignupEntrypoint, CloseTarget, EnvVarTelemetryMetadata,
Expand Down Expand Up @@ -473,8 +475,8 @@ use crate::util::openable_file_type::{
};
use crate::util::traffic_lights::{TrafficLightMouseStates, TrafficLightSide, traffic_light_data};
use crate::util::truncation::truncate_from_end;
#[cfg(target_family = "wasm")]
use crate::view_components::action_button::ActionButton;
#[cfg(any(target_family = "wasm", test))]
use crate::view_components::action_button::{ActionButton, SecondaryTheme};
use crate::view_components::callout_bubble::{
CalloutArrowDirection, CalloutArrowPosition, CalloutBubbleConfig, render_callout_bubble,
};
Expand Down Expand Up @@ -862,6 +864,34 @@ enum SimplifiedWasmTabBarContent {
ConversationTranscript { task_id: Option<AmbientAgentTaskId> },
}

#[cfg(any(target_family = "wasm", test))]
#[derive(Clone, Debug, PartialEq, Eq)]
struct SimplifiedWasmProductNavigation {
action_label: &'static str,
destination: String,
}

#[cfg(any(target_family = "wasm", test))]
impl SimplifiedWasmProductNavigation {
fn from_factory_access(allowed: bool) -> Self {
if allowed {
Self {
action_label: "View Factory",
destination: ChannelState::server_root_url().into_owned(),
}
} else {
Self {
action_label: "View in Oz",
destination: format!("{}/runs", ChannelState::oz_root_url()),
}
}
}

fn open_action(&self) -> WorkspaceAction {
WorkspaceAction::OpenLink(self.destination.clone())
}
}

type RemoteUploadId = (TerminalPaneId, FileUploadId);
type WorkspaceMenuHandles = (
ViewHandle<Menu<WorkspaceAction>>,
Expand Down Expand Up @@ -1156,8 +1186,10 @@ pub struct Workspace {
wasm_nux_dialog: ViewHandle<WasmNUXDialog>,
#[cfg(target_family = "wasm")]
open_in_warp_button: ViewHandle<ActionButton>,
#[cfg(target_family = "wasm")]
view_cloud_runs_button: ViewHandle<ActionButton>,
#[cfg(any(target_family = "wasm", test))]
view_product_button: ViewHandle<ActionButton>,
#[cfg(any(target_family = "wasm", test))]
simplified_wasm_product_navigation: SimplifiedWasmProductNavigation,
#[cfg(target_family = "wasm")]
transcript_info_button: ViewHandle<ActionButton>,
#[cfg(target_family = "wasm")]
Expand Down Expand Up @@ -1220,6 +1252,45 @@ pub struct Workspace {
}

impl Workspace {
#[cfg(any(target_family = "wasm", test))]
fn build_view_product_button(
navigation: SimplifiedWasmProductNavigation,
ctx: &mut ViewContext<Self>,
) -> ViewHandle<ActionButton> {
let label = navigation.action_label;
let action = navigation.open_action();
ctx.add_typed_action_view(|_| {
ActionButton::new(label, SecondaryTheme).on_click(move |ctx| {
let action = action.clone();
ctx.dispatch_typed_action(action);
})
})
}

#[cfg(any(target_family = "wasm", test))]
fn fetch_factory_access_with_client(
&mut self,
factory_client: Arc<dyn FactoryClient>,
ctx: &mut ViewContext<Self>,
) {
ctx.spawn(
async move { factory_client.has_factory_access().await },
|workspace, result, ctx| match result {
Ok(allowed) => {
let navigation = SimplifiedWasmProductNavigation::from_factory_access(allowed);
workspace.view_product_button =
Self::build_view_product_button(navigation.clone(), ctx);
workspace.simplified_wasm_product_navigation = navigation;
ctx.notify();
}
Err(error) => {
log::warn!(
"Failed to check Factory access; keeping legacy Oz navigation: {error:#}"
);
}
},
);
}
/// Whether this workspace was opened directly against a shared session, cloud
/// conversation, or similar deep-linked content.
pub(crate) fn opened_from_content_deep_link(&self) -> bool {
Expand Down Expand Up @@ -3259,8 +3330,13 @@ impl Workspace {
#[cfg(target_family = "wasm")]
let transcript_info_button = Self::build_transcript_info_button(ctx);

#[cfg(target_family = "wasm")]
let view_cloud_runs_button = Self::build_view_cloud_runs_button(ctx);
#[cfg(any(target_family = "wasm", test))]
let simplified_wasm_product_navigation =
SimplifiedWasmProductNavigation::from_factory_access(false);

#[cfg(any(target_family = "wasm", test))]
let view_product_button =
Self::build_view_product_button(simplified_wasm_product_navigation.clone(), ctx);

#[cfg(target_family = "wasm")]
let transcript_details_panel = Self::build_transcript_details_panel(ctx);
Expand Down Expand Up @@ -3515,8 +3591,10 @@ impl Workspace {
open_in_warp_button,
#[cfg(target_family = "wasm")]
transcript_info_button,
#[cfg(target_family = "wasm")]
view_cloud_runs_button,
#[cfg(any(target_family = "wasm", test))]
view_product_button,
#[cfg(any(target_family = "wasm", test))]
simplified_wasm_product_navigation,
#[cfg(target_family = "wasm")]
transcript_details_panel,
tab_fixed_width: None,
Expand Down Expand Up @@ -3569,6 +3647,8 @@ impl Workspace {
// any) read from `GlobalResourceHandles`. Subsequent updates are
// pushed by `subscribe_to_settings_errors` and `dismiss_workspace_banner`.
ws.sync_settings_error_state_into_settings_pane(ctx);
#[cfg(target_family = "wasm")]
ws.fetch_factory_access(ctx);

let weak_handle = ctx.handle();
WorkspaceRegistry::handle(ctx).update(ctx, |registry, _| {
Expand Down Expand Up @@ -20865,7 +20945,7 @@ impl Workspace {
.with_main_axis_size(MainAxisSize::Max);
let bg_color = blended_colors::neutral_1(appearance.theme());

// Left: Warp logo - clickable to link to warp.dev
let open_product_action = self.simplified_wasm_product_navigation.open_action();
let warp_logo = Hoverable::new(self.mouse_states.warp_logo.clone(), |_state| {
ConstrainedBox::new(
warp_core::ui::Icon::Warp
Expand All @@ -20876,25 +20956,18 @@ impl Workspace {
.with_width(24.)
.finish()
})
.on_click(|ctx, _, _| {
ctx.dispatch_typed_action(WorkspaceAction::OpenLink("https://warp.dev".to_owned()));
.on_click(move |ctx, _, _| {
let open_product_action = open_product_action.clone();
ctx.dispatch_typed_action(open_product_action);
})
.with_cursor(Cursor::PointingHand)
.finish();
tab_bar.add_child(warp_logo);

// Right: Info button + "View all cloud runs" button (for ambient agent sessions) + "Open in Warp" button
let mut right_row = Flex::row()
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_main_axis_size(MainAxisSize::Min);

// Extract task_id from conversation transcripts and shared sessions
let task_id = match content_type {
SimplifiedWasmTabBarContent::ConversationTranscript { task_id }
| SimplifiedWasmTabBarContent::SharedSession { task_id } => task_id,
SimplifiedWasmTabBarContent::WarpDriveObject => None,
};

// Show info button for conversation transcripts and shared sessions (if there's content to display)
let should_show_info_button =
!matches!(content_type, SimplifiedWasmTabBarContent::WarpDriveObject)
Expand All @@ -20912,15 +20985,14 @@ impl Workspace {
.with_margin_right(8.)
.finish(),
);
}

// Add "View all cloud runs" button when task_id exists (with 4px gap)
if task_id.is_some() {
right_row.add_child(
Container::new(ChildView::new(&self.view_cloud_runs_button).finish())
.with_margin_right(4.)
.finish(),
);
}
if !matches!(content_type, SimplifiedWasmTabBarContent::WarpDriveObject) {
right_row.add_child(
Container::new(ChildView::new(&self.view_product_button).finish())
.with_margin_right(4.)
.finish(),
);
}

// Hide "Open in Warp" button on mobile devices
Expand Down
23 changes: 5 additions & 18 deletions app/src/workspace/view/wasm_view.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
//! WASM-only view functions for the Workspace.

use warp_core::channel::ChannelState;
use warpui::elements::{ChildView, Element};
use warpui::{AppContext, SingletonEntity, ViewContext, ViewHandle};

Expand All @@ -10,23 +9,17 @@ use crate::ai::agent_conversations_model::AgentConversationsModel;
use crate::ai::conversation_details_panel::{
ConversationDetailsData, ConversationDetailsPanel, ConversationDetailsPanelEvent,
};
use crate::server::server_api::ServerApiProvider;
use crate::terminal::TerminalView;
use crate::ui_components::icons;
use crate::uri::browser_url_handler::parse_current_url;
use crate::view_components::action_button::{
ActionButton, ButtonSize, NakedTheme, PrimaryTheme, SecondaryTheme,
};
use crate::view_components::action_button::{ActionButton, ButtonSize, NakedTheme, PrimaryTheme};
use crate::wasm_nux_dialog::{WasmNUXDialog, WasmNUXDialogEvent};
use crate::workspace::action::WorkspaceAction;
use crate::workspace::view::{NotebookSource, OpenWarpDriveObjectSettings, Workspace};

const TRANSCRIPT_PANEL_WIDTH: f32 = 280.0;

/// Builds the OZ runs URL for viewing all cloud runs.
fn build_oz_runs_url() -> String {
format!("{}/runs", ChannelState::oz_root_url())
}

impl Workspace {
pub(super) fn build_wasm_nux_dialog(ctx: &mut ViewContext<Self>) -> ViewHandle<WasmNUXDialog> {
let wasm_nux_dialog = ctx.add_typed_action_view(|_| WasmNUXDialog::new());
Expand Down Expand Up @@ -54,15 +47,9 @@ impl Workspace {
})
}

pub(super) fn build_view_cloud_runs_button(
ctx: &mut ViewContext<Self>,
) -> ViewHandle<ActionButton> {
let url = build_oz_runs_url();
ctx.add_typed_action_view(|_ctx| {
ActionButton::new("View all cloud runs", SecondaryTheme).on_click(move |ctx| {
ctx.dispatch_typed_action(WorkspaceAction::OpenLink(url.clone()));
})
})
pub(super) fn fetch_factory_access(&mut self, ctx: &mut ViewContext<Self>) {
let factory_client = ServerApiProvider::as_ref(ctx).get_factory_client();
self.fetch_factory_access_with_client(factory_client, ctx);
}

pub(super) fn build_transcript_info_button(
Expand Down
Loading
Loading