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
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion agentmux-cef/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "agentmux-cef"
version = "0.33.56"
version = "0.33.58"
description = "AgentMux CEF Host — Pinned Chromium renderer replacing system WebView2"
authors = ["AgentMux Corp"]
edition = "2021"
Expand Down
18 changes: 13 additions & 5 deletions agentmux-cef/src/commands/drag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,15 +275,23 @@ pub fn open_window_at_position(state: &Arc<AppState>, args: &serde_json::Value)
let window_id = uuid::Uuid::new_v4();
let label = format!("window-{}", window_id.simple());

let win_w = 1200i32;
let win_h = 800i32;
// Use pane dimensions from the frontend if provided; fall back to defaults.
let win_w = args.get("width").and_then(|v| v.as_f64()).map(|v| (v as i32).max(400)).unwrap_or(1200);
let win_h = args.get("height").and_then(|v| v.as_f64()).map(|v| (v as i32).max(300)).unwrap_or(800);

// Position so cursor lands near top-center of title bar
let pos_x = ((screen_x - win_w as f64 / 2.0).max(0.0)) as i32;
let pos_y = ((screen_y - 16.0).max(0.0)) as i32;
// Cursor grab offset within the pane (where the user grabbed it).
// Positions the new window so the cursor lands at the same relative point.
// Falls back to top-center / title-bar-offset if not provided.
let grab_dx = args.get("grabOffsetX").and_then(|v| v.as_f64()).map(|v| v as i32).unwrap_or(win_w / 2);
let grab_dy = args.get("grabOffsetY").and_then(|v| v.as_f64()).map(|v| v as i32).unwrap_or(20);

let pos_x = ((screen_x as i32) - grab_dx).max(0);
let pos_y = ((screen_y as i32) - grab_dy).max(0);

tracing::info!(
label = %label, pos_x = %pos_x, pos_y = %pos_y,
win_w = %win_w, win_h = %win_h,
grab_dx = %grab_dx, grab_dy = %grab_dy,
workspace_id = %workspace_id,
"[dnd:cef] open_window_at_position"
);
Expand Down
1 change: 1 addition & 0 deletions agentmux-cef/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub mod providers;
pub mod drag;
pub mod clipboard;
pub mod stubs;
pub mod palette;

use std::sync::Arc;
use crate::state::AppState;
Expand Down
50 changes: 50 additions & 0 deletions agentmux-cef/src/commands/palette.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2026, AgentMux Corp.
// SPDX-License-Identifier: Apache-2.0
//
// `run_command` IPC handler — dispatches a command ID to the frontend registry.
//
// The Rust side does not own the command registry; it simply forwards the ID
// to the frontend via a CEF `CustomEvent`. The frontend's `commandRegistry.run(id)`
// handles validation and execution.

use std::sync::Arc;

use cef::{CefString, ImplBrowser, ImplFrame};

use crate::state::AppState;

/// Dispatch a command palette ID to the frontend of the target window.
///
/// Args:
/// `id` — stable command ID (e.g. `"open:terminal"`)
/// `windowLabel` — (optional) which window to target; defaults to `"main"`
pub fn run_command(state: &Arc<AppState>, args: &serde_json::Value) -> Result<serde_json::Value, String> {
let id = args["id"]
.as_str()
.ok_or_else(|| "run_command: missing 'id' field".to_string())?;

let window_label = args["windowLabel"].as_str().unwrap_or("main");

let js = format!(
"window.dispatchEvent(new CustomEvent('agentmux-run-command', {{ detail: {{ id: {:?} }} }}));",
id
);

let browsers = state.browsers.lock();
let browser = browsers
.get(window_label)
.or_else(|| browsers.values().next());

if let Some(browser) = browser {
if let Some(frame) = browser.main_frame() {
let code = CefString::from(js.as_str());
let url = CefString::from("");
frame.execute_java_script(Some(&code), Some(&url), 0);
tracing::debug!(id = %id, window = %window_label, "[palette] dispatched run_command");
}
} else {
tracing::warn!(id = %id, "[palette] run_command: no browser available");
}

Ok(serde_json::Value::Null)
}
3 changes: 3 additions & 0 deletions agentmux-cef/src/ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,9 @@ async fn route_command(
"open_in_editor" => commands::platform::open_in_editor(args),
"copy_file_to_dir" => commands::providers::copy_file_to_dir(args),

// ---- Command palette ----
"run_command" => commands::palette::run_command(state, args),

// ---- Unknown command ----
_ => Err(format!("Unknown command: {}", cmd)),
}
Expand Down
2 changes: 1 addition & 1 deletion agentmux-launcher/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "agentmux-launcher"
version = "0.33.56"
version = "0.33.58"
description = "AgentMux Launcher — tiny exe that sets DLL path then spawns the CEF host"
authors = ["AgentMux Corp"]
edition = "2021"
Expand Down
2 changes: 1 addition & 1 deletion agentmux-srv/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "agentmux-srv"
version = "0.33.56"
version = "0.33.58"
edition = "2021"
description = "AgentMux Rust backend server"

Expand Down
2 changes: 1 addition & 1 deletion agentmux-wsh/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "agentmux-wsh"
version = "0.33.56"
version = "0.33.58"
edition = "2021"
description = "Shell integration CLI for AgentMux"

Expand Down
2 changes: 1 addition & 1 deletion frontend/app/drag/CrossWindowDragMonitor.platform.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
// at build time by Vite's platformResolve plugin.
// Do NOT import this file directly in application code.
export { CrossWindowDragMonitor, setCurrentDragPayload, getCurrentDragPayload } from "./CrossWindowDragMonitor.win32";
export type { DragItemPayload } from "./CrossWindowDragMonitor.win32";
export type { DragItemPayload, GrabOffset, PaneRect } from "./CrossWindowDragMonitor.win32";
2 changes: 1 addition & 1 deletion frontend/app/drag/CrossWindowDragMonitor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
// Import from this file directly (e.g. `import { setCurrentDragPayload } from "@/app/drag/CrossWindowDragMonitor"`)
// and Vite's platformResolve plugin will pick the right .win32/.darwin/.linux variant.
export { CrossWindowDragMonitor, setCurrentDragPayload, getCurrentDragPayload } from "./CrossWindowDragMonitor.platform";
export type { DragItemPayload } from "./CrossWindowDragMonitor.platform";
export type { DragItemPayload, GrabOffset, PaneRect } from "./CrossWindowDragMonitor.platform";
19 changes: 15 additions & 4 deletions frontend/app/drag/CrossWindowDragMonitor.win32.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ import type { JSX } from "solid-js";
import type { LayoutNode } from "@/layout/lib/types";

// Shared drag state set by TileLayout / TabBar drag handlers
export type PaneRect = { x: number; y: number; w: number; h: number };
export type GrabOffset = { dx: number; dy: number };

export type DragItemPayload =
| { kind: "tile"; node: LayoutNode }
| { kind: "tile"; node: LayoutNode; paneRect?: PaneRect; grabOffset?: GrabOffset }
| { kind: "tab"; tabId: string; workspaceId: string; isPinned: boolean };

// Module-level drag state so TileLayout/TabBar can set it before dragend fires
Expand Down Expand Up @@ -190,7 +193,7 @@ async function handleCrossWindowDragEnd(payload: DragItemPayload, sourceWindow:
await performCrossWindowDrop(dragType, dragPayloadForApi, workspace.oid, activeTabId);
await api.completeCrossDrag(dragId, targetWindow, cursorPoint.x, cursorPoint.y);
} else if (!targetWindow) {
await performTearOff(dragType, dragPayloadForApi, workspace.oid, activeTabId, cursorPoint.x, cursorPoint.y);
await performTearOff(dragType, dragPayloadForApi, workspace.oid, activeTabId, cursorPoint.x, cursorPoint.y, payload);
await api.completeCrossDrag(dragId, null, cursorPoint.x, cursorPoint.y);
try { await api.releaseDragCapture(); } catch {}
} else {
Expand All @@ -216,9 +219,13 @@ async function performTearOff(
sourceWsId: string,
sourceTabId: string,
screenX: number,
screenY: number
screenY: number,
dragPayload: DragItemPayload | null
) {
const api = getApi();
const paneRect = dragPayload?.kind === "tile" ? dragPayload.paneRect : undefined;
const grabOffset = dragPayload?.kind === "tile" ? dragPayload.grabOffset : undefined;

if (dragType === "pane" && payload.blockId) {
const newWsId = await WorkspaceService.TearOffBlock(payload.blockId, sourceTabId, sourceWsId, true);
if (newWsId) {
Expand All @@ -234,7 +241,11 @@ async function performTearOff(
} as LayoutTreeDeleteNodeAction);
}
}
await api.openWindowAtPosition(screenX, screenY, newWsId);
await api.openWindowAtPosition(
screenX, screenY, newWsId,
paneRect?.w, paneRect?.h,
grabOffset?.dx, grabOffset?.dy
);
}
} else if (dragType === "tab" && payload.tabId) {
const newWsId = await WorkspaceService.TearOffTab(payload.tabId, sourceWsId);
Expand Down
137 changes: 137 additions & 0 deletions frontend/app/modals/command-palette.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Copyright 2026, AgentMux Corp.
// SPDX-License-Identifier: Apache-2.0

.command-palette-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 900;
}

.command-palette-container {
position: fixed;
top: 20%;
left: 50%;
transform: translateX(-50%);
width: 560px;
max-height: 480px;
background: var(--modal-bg-color, #1e1e1e);
border: 1px solid var(--border-color, #444);
border-radius: 8px;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.6);
z-index: 901;
display: flex;
flex-direction: column;
overflow: hidden;

// Ensure command palette is above all other content
isolation: isolate;
}

.command-palette-input-row {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
border-bottom: 1px solid var(--border-color, #444);
flex-shrink: 0;
}

.command-palette-search-icon {
color: var(--secondary-text-color, #888);
font-size: 13px;
flex-shrink: 0;
}

.command-palette-input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: var(--main-text-color, #e0e0e0);
font-size: 14px;
font-family: var(--base-font, inherit);
line-height: 1.5;

&::placeholder {
color: var(--secondary-text-color, #666);
}
}

.command-palette-esc-hint {
font-size: 10px;
color: var(--secondary-text-color, #666);
background: var(--panel-bg-color, #2a2a2a);
border: 1px solid var(--border-color, #444);
border-radius: 3px;
padding: 1px 5px;
flex-shrink: 0;
font-family: var(--base-font, inherit);
}

.command-palette-list {
overflow-y: auto;
flex: 1;
padding: 4px 0;

&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--border-color, #555);
border-radius: 3px;
}
}

.command-palette-item {
display: flex;
align-items: center;
gap: 10px;
padding: 7px 14px;
cursor: pointer;
user-select: none;

&.selected {
background: var(--highlight-bg-color, #2d4a6e);
}

&:hover:not(.selected) {
background: var(--hover-bg-color, #2a2a2a);
}
}

.command-palette-item-icon {
font-size: 13px;
color: var(--secondary-text-color, #888);
width: 16px;
text-align: center;
flex-shrink: 0;
}

.command-palette-item-label {
flex: 1;
font-size: 13px;
color: var(--main-text-color, #e0e0e0);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.command-palette-item-category {
font-size: 11px;
color: var(--secondary-text-color, #666);
flex-shrink: 0;
text-transform: uppercase;
letter-spacing: 0.04em;
}

.command-palette-empty {
padding: 24px 14px;
text-align: center;
color: var(--secondary-text-color, #666);
font-size: 13px;
font-style: italic;
}
Loading