Skip to content

POC: Embed external app windows in dock panel#43

Draft
eatnug wants to merge 2 commits intomainfrom
app-in-app
Draft

POC: Embed external app windows in dock panel#43
eatnug wants to merge 2 commits intomainfrom
app-in-app

Conversation

@eatnug
Copy link
Member

@eatnug eatnug commented Feb 27, 2026

Summary

  • Adds AppPane type that embeds external macOS app windows (e.g. Figma) inside Tide's editor panel tabs
  • Uses Accessibility API (AXUIElement) for cross-process window positioning/resizing
  • Uses CGS private APIs for window level manipulation to keep embedded windows above Tide
  • Panel picker (Cmd+Shift+P) now includes app launch options

Known Limitations

  • Z-ordering is fragile — relies on lowering Tide's CGS window level, which can break across focus changes
  • Apps with minimum window sizes (e.g. Figma 900pt width) overflow the panel area
  • Window discovery fails when target app has no visible windows
  • CGS SetWindowLevel has no effect on cross-process windows on modern macOS

Status

POC / Do not merge — exploring feasibility of native window embedding via macOS private APIs. The approach has fundamental limitations around z-ordering and cross-process window control.

Test plan

  • Cmd+Shift+P → select Figma → app launches and window repositions into panel
  • Resize Tide window → embedded window follows
  • Switch dock tabs → embedded window hides/shows
  • Close app tab (Cmd+W) → embedded window returns to normal
  • Quit embedded app externally → "quit" placeholder shown

🤖 Generated with Claude Code

Adds AppPane type that launches external macOS apps (e.g. Figma) and
repositions their windows into Tide's editor panel area using the
Accessibility API (AXUIElement) for positioning/resizing and CGS private
APIs for window level manipulation.

Known limitations:
- Z-ordering relies on lowering Tide's window level via CGS, which is
  fragile across focus changes
- Apps with minimum window sizes (e.g. Figma 900pt) overflow the panel
- Window discovery fails when the target app has no visible windows

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request explores the feasibility of integrating external macOS application windows directly into Tide's UI as dock panel tabs. It introduces core functionality for launching, embedding, and managing these external app windows, providing a proof-of-concept for a more unified workspace experience. The changes encompass new UI elements, event handling, and platform-specific macOS API interactions, while also highlighting significant technical challenges and limitations inherent in this approach.

Highlights

  • External App Embedding: Introduced an AppPane type to embed external macOS application windows (e.g., Figma) directly within Tide's editor panel tabs, leveraging macOS Accessibility API (AXUIElement) for positioning and resizing.
  • Window Level Manipulation: Utilized CGS private APIs to manage window levels, attempting to keep embedded windows above Tide's main window. However, this approach has known limitations regarding z-ordering and cross-process window control on modern macOS.
  • Panel Picker Integration: The Cmd+Shift+P shortcut now opens a 'Panel Picker' that includes options to launch and embed external applications, alongside existing options for new editor or browser panes.
  • Known Limitations Identified: The pull request explicitly notes several limitations, including fragile z-ordering, potential window overflow due to minimum app window sizes, failure in window discovery if the target app has no visible windows, and ineffectiveness of CGS SetWindowLevel for cross-process windows on newer macOS versions.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • Cargo.lock
    • Added libc dependency for platform-specific system calls.
  • crates/tide-app/src/action/mod.rs
    • Updated keyboard input handling to include App panes.
    • Updated scroll input handling to include App panes.
    • Added PanelPicker global action to open the new panel selection UI.
    • Updated pane closing logic to include App panes.
  • crates/tide-app/src/action/pane_lifecycle.rs
    • Imported PanelPickerAction and PanelPickerState.
    • Added open_app_pane function to launch and embed external applications.
    • Modified close_editor_panel_tab to immediately close App panes without dirty checks.
    • Modified force_close_editor_panel_tab to destroy AppPane instances.
    • Modified close_pane to destroy AppPane instances.
    • Added functions to open, close, and execute actions from the PanelPicker.
  • crates/tide-app/src/app_pane.rs
    • Added new module defining the AppPane struct and its associated AppPaneState enum for managing external application windows.
  • crates/tide-app/src/event_handler/keyboard.rs
    • Added keyboard event handling for the PanelPicker overlay.
    • Updated search functionality to include App panes in ignored types.
  • crates/tide-app/src/event_handler/mouse.rs
    • Updated mouse event handling to include App panes in ignored types for selection and scroll.
  • crates/tide-app/src/event_handler/search.rs
    • Updated search functionality to ignore App panes.
  • crates/tide-app/src/event_handler/text_routing.rs
    • Added PanelPicker as a TextInputTarget.
    • Updated text_input_target to prioritize PanelPicker and consume input for App panes.
    • Added text input handling for the PanelPicker.
    • Updated handle_text_input to include App panes in ignored types.
  • crates/tide-app/src/event_loop.rs
    • Added handling for the PlatformEvent::WindowMoved to synchronize app pane frames.
    • Modified IME proxy logic to skip App panes.
  • crates/tide-app/src/header.rs
    • Added rendering logic for AppPane titles in the panel header.
  • crates/tide-app/src/layout_compute.rs
    • Added sync_app_pane_frames call to the main layout computation.
    • Implemented sync_app_pane_frames to reposition and manage embedded app windows based on Tide's layout.
  • crates/tide-app/src/main.rs
    • Added app_pane module import.
    • Added panel_picker field to the App struct.
    • Initialized panel_picker to None in the App::new constructor.
  • crates/tide-app/src/pane.rs
    • Added App(AppPane) variant to the PaneKind enum.
  • crates/tide-app/src/rendering/cursor.rs
    • Updated cursor rendering to ignore App panes.
  • crates/tide-app/src/rendering/grid.rs
    • Updated grid rendering to ignore App panes, as they render natively.
  • crates/tide-app/src/rendering/overlays.rs
    • Added render_panel_picker function call to render the panel picker overlay.
    • Implemented render_panel_picker to display the panel selection UI.
  • crates/tide-app/src/ui.rs
    • Updated pane_title function to retrieve titles for App panes.
  • crates/tide-app/src/ui_state.rs
    • Added PanelPickerAction, PanelPickerEntry, and PanelPickerState structs to manage the panel selection UI.
  • crates/tide-app/src/update.rs
    • Added update_app_panes call to the main update loop.
    • Implemented update_app_panes to manage the lifecycle and state transitions of embedded app windows, including launching, window discovery, and checking if the app is still alive.
  • crates/tide-input/src/lib.rs
    • Added PanelPicker variant to the GlobalAction enum.
    • Updated GlobalAction trait implementations to include PanelPicker.
    • Added Cmd+Shift+P keybinding for GlobalAction::PanelPicker.
  • crates/tide-platform/Cargo.toml
    • Added libc dependency.
  • crates/tide-platform/src/lib.rs
    • Added WindowMoved variant to PlatformEvent enum.
  • crates/tide-platform/src/macos/cgs.rs
    • Added new module implementing macOS Core Graphics Services (CGS) private APIs and Accessibility API (AXUIElement) for window embedding, including functions for permission checks, window positioning/sizing, and app launching/discovery.
  • crates/tide-platform/src/macos/mod.rs
    • Added cgs module import.
  • crates/tide-platform/src/macos/view.rs
    • Implemented windowDidMove delegate method to emit PlatformEvent::WindowMoved.
Activity
  • The pull request was created as a Proof of Concept (POC) to explore the feasibility of embedding external application windows.
  • The author explicitly marked the PR as 'POC / Do not merge', indicating it's for exploration rather than immediate integration.
  • The description outlines a test plan, suggesting the author has performed initial manual testing of the new functionality.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This is an impressive proof-of-concept for embedding external application windows. The use of Accessibility APIs and private CGS functions is well-encapsulated. My review focuses on a few areas that could be improved for maintainability and efficiency, particularly around code duplication and polling mechanisms. These suggestions are aimed at strengthening the design if this POC were to be developed into a full feature.

Comment on lines 290 to +295
if let Some(PaneKind::Browser(bp)) = self.panes.get_mut(&tab_id) {
bp.destroy();
}
if let Some(PaneKind::App(ap)) = self.panes.get_mut(&tab_id) {
ap.destroy();
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This destruction logic for BrowserPane and AppPane is duplicated in a few places (here and in close_specific_pane). To improve maintainability, consider adding a destroy method to the PaneKind enum that handles this logic internally. This would centralize the cleanup logic for all pane types that require it.

For example, you could add this to pane.rs:

impl PaneKind {
    pub fn destroy(&mut self) {
        match self {
            PaneKind::Browser(bp) => bp.destroy(),
            PaneKind::App(ap) => ap.destroy(),
            _ => {}
        }
    }
}

Then the call sites would be simplified to:

if let Some(pane) = self.panes.get_mut(&tab_id) {
    pane.destroy();
}

Comment on lines +829 to +839
let app_ids: Vec<tide_core::PaneId> = self
.panes
.iter()
.filter_map(|(&id, pk)| {
if matches!(pk, PaneKind::App(_)) {
Some(id)
} else {
None
}
})
.collect();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Collecting app_ids into a Vec on each layout computation introduces a small but frequent allocation. You could iterate directly over the panes to avoid this, which would be more efficient.

For example:

for (id, pane) in self.panes.iter_mut() {
    if let PaneKind::App(ap) = pane {
        let is_active = active_app_id == Some(*id);
        // ... existing logic from inside your loop ...
    }
}

Comment on lines +343 to +353
let app_ids: Vec<tide_core::PaneId> = self
.panes
.iter()
.filter_map(|(&id, pk)| {
if matches!(pk, PaneKind::App(_)) {
Some(id)
} else {
None
}
})
.collect();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Collecting app_ids into a Vec on each update cycle introduces a small but frequent allocation. You can iterate directly over self.panes.values_mut() to make this more efficient.

For example:

let mut needs_layout = false;
for pane in self.panes.values_mut() {
    if let PaneKind::App(ap) = pane {
        // ... existing logic from inside your loop ...
        // You will need to get the pane id if you need it.
    }
}
if needs_layout {
    self.sync_app_pane_frames();
}

Comment on lines +477 to +493
// Poll for app to start (up to 2 seconds)
for attempt in 0..20 {
std::thread::sleep(std::time::Duration::from_millis(100));
let running_apps2: Retained<AnyObject> = msg_send_id![
objc2::runtime::AnyClass::get("NSRunningApplication").unwrap(),
runningApplicationsWithBundleIdentifier: &*ns_bundle_id
];
let count2: usize = msg_send![&*running_apps2, count];
if count2 > 0 {
let app: Retained<AnyObject> = msg_send_id![&*running_apps2, objectAtIndex: 0usize];
let pid: i32 = msg_send![&*app, processIdentifier];
if pid > 0 {
log::info!("launch_or_find_app: started after {}ms, pid={}", (attempt + 1) * 100, pid);
return Some(pid as u32);
}
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This polling loop to wait for the app to start is functional for a proof-of-concept, but it's not very efficient and has a fixed timeout. A more robust and idiomatic macOS approach would be to use notifications.

Consider using NSWorkspace notifications, specifically NSWorkspaceDidLaunchApplicationNotification. You can add an observer for this notification to be called back when the application has finished launching, which would eliminate the need for sleeping and polling.

- Add Pencil (dev.pencil.desktop) as an embeddable app option
- Call sync_app_pane_frames() every frame so embedded windows hide/show
  correctly when switching dock tabs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant