Reorder tabs by dragging, and pop the browser view into its own window
Build App / compute-version (pull_request) Successful in 3s
Build App / build-macos (pull_request) Successful in 2m31s
Build App / build-linux (pull_request) Successful in 5m35s
Build App / build-windows (pull_request) Successful in 6m9s
Build App / create-tag (pull_request) Skipped
Build App / sync-to-github (pull_request) Skipped
Build App / compute-version (pull_request) Successful in 3s
Build App / build-macos (pull_request) Successful in 2m31s
Build App / build-linux (pull_request) Successful in 5m35s
Build App / build-windows (pull_request) Successful in 6m9s
Build App / create-tag (pull_request) Skipped
Build App / sync-to-github (pull_request) Skipped
Two things the UI couldn't do: rearrange the tab strip, and watch the browser while working somewhere else. **Drag to reorder.** `moveTab`/`moveActiveTab` on the store, HTML5 drag on the strip with a marker showing where the drop lands, `Ctrl+Shift+←/→` for the same thing without a mouse. Reordering deliberately does not select what it moves, so a drag aimed at a background tab doesn't yank the main area away from a terminal mid-run. A tab being renamed is not draggable — a draggable ancestor swallows the mouse-drag that selects text in its input. **Pop the browser view out.** `browser_view/popout.rs` opens the view's existing token-bearing loopback URL as a second OS window, with a "Keep on top" toggle so it can float above the app. Window-only: the viewer, the proxy and the container are untouched, so popping out and back interrupts nothing. Three things it rests on: - No capability lists that window, so it has no IPC surface — right for a page served out of a container, and it must stay that way. - The app CSP is irrelevant to it: `frame-src` constrains what the app's document may *embed*, and this is a top-level document. The port range and the token gate are what actually protect it, unchanged. - The window is owned by the session, so the supervisor's teardown closes it. A window onto a viewer that no longer exists is worse than none. The pane drops its iframe while popped out — two viewers can both *drive* the browser, and two cursors on one page is not a feature. `lib.rs`'s `on_window_event` is now guarded on `label() == "main"`. It fires for every window and its body stops every container and exits, so without the guard closing a pop-out would quit the app. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
use crate::browser_view::install::{self, BrowserSetupOutcome};
|
||||
use crate::browser_view::{manager, BrowserViewStatus};
|
||||
use crate::browser_view::{manager, popout, BrowserViewState, BrowserViewStatus};
|
||||
use crate::AppState;
|
||||
|
||||
/// Turn the pane on or off for a project.
|
||||
@@ -97,6 +97,67 @@ pub async fn install_browser_view_browser(
|
||||
install::install_browser(&app_handle, &project_id, &container_id, target).await
|
||||
}
|
||||
|
||||
/// Detach the view into a window of its own, or raise the one already open.
|
||||
///
|
||||
/// Host-side and window-only: the viewer keeps running exactly as it was, and
|
||||
/// this touches neither the container nor the proxy. Requires a *live* view,
|
||||
/// because a window with nothing behind it is not worth opening — the pane
|
||||
/// only offers the button in that state, and this enforces it.
|
||||
#[tauri::command]
|
||||
pub async fn open_browser_view_popout(
|
||||
project_id: String,
|
||||
always_on_top: bool,
|
||||
app_handle: AppHandle,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
let status = manager().status(&project_id).await;
|
||||
let (BrowserViewState::Running, Some(url)) = (status.state, status.url.as_deref()) else {
|
||||
return Err(
|
||||
"The browser view isn't running. Start it before opening it in its own window."
|
||||
.to_string(),
|
||||
);
|
||||
};
|
||||
|
||||
let name = state
|
||||
.projects_store
|
||||
.get(&project_id)
|
||||
.map(|p| p.name)
|
||||
.unwrap_or_else(|| "Triple-C".to_string());
|
||||
|
||||
popout::open(&app_handle, &project_id, &name, url, always_on_top)
|
||||
}
|
||||
|
||||
/// Close the pop-out, putting the view back in the tab. No-op if it is closed.
|
||||
#[tauri::command]
|
||||
pub async fn close_browser_view_popout(
|
||||
project_id: String,
|
||||
app_handle: AppHandle,
|
||||
) -> Result<(), String> {
|
||||
popout::close(&app_handle, &project_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether the pop-out is open — asked on tab open, since a window can outlive
|
||||
/// the pane that spawned it.
|
||||
#[tauri::command]
|
||||
pub async fn is_browser_view_popout_open(
|
||||
project_id: String,
|
||||
app_handle: AppHandle,
|
||||
) -> Result<bool, String> {
|
||||
Ok(popout::is_open(&app_handle, &project_id))
|
||||
}
|
||||
|
||||
/// Pin the pop-out above other windows, so it can be watched while working in
|
||||
/// the main one.
|
||||
#[tauri::command]
|
||||
pub async fn set_browser_view_popout_always_on_top(
|
||||
project_id: String,
|
||||
on_top: bool,
|
||||
app_handle: AppHandle,
|
||||
) -> Result<(), String> {
|
||||
popout::set_always_on_top(&app_handle, &project_id, on_top)
|
||||
}
|
||||
|
||||
/// The project's container, or a sentence saying why there isn't one.
|
||||
///
|
||||
/// Every command here needs a *running* container, and every one of them used
|
||||
|
||||
Reference in New Issue
Block a user