How ChatMCP Handles Window Management on Desktop Platforms Using `window_manager`

ChatMCP uses the window_manager Flutter plugin to initialize frameless windows with custom sizes, enables toolbar dragging via DragToMoveArea, and exposes minimize, maximize, and close controls through platform-guarded UI widgets.

ChatMCP is an open-source Flutter-based chat client that leverages the window_manager plugin to deliver native desktop window experiences on Windows, macOS, and Linux. When running on desktop platforms, the application hijacks the native title bar, implements custom draggable toolbars, and provides programmatic control over window states. This article examines the exact implementation details found in the repository's Dart source files.

Desktop Platform Detection and Initialization

Before invoking any desktop-specific APIs, ChatMCP determines the current platform using utility constants defined in lib/utils/platform.dart.

Checking for Desktop Environments

The application defines a boolean flag that evaluates to true only when running natively on Windows, macOS, or Linux:

final bool kIsDesktop = !kIsWeb && (kIsLinux || kIsWindows || kIsMacOS);

This kIsDesktop constant guards all desktop-specific logic throughout the UI, ensuring mobile and web builds never invoke unsupported native methods.

Early Initialization in main.dart

When kIsDesktop is true, lib/main.dart initializes the window manager before any widgets render:

if (kIsDesktop) {
  await wm.windowManager.ensureInitialized();
  
  final wm.WindowOptions windowOptions = wm.WindowOptions(
    size: Size(1200, 800),
    minimumSize: Size(400, 600),
    center: true,
    backgroundColor: Colors.transparent,
    titleBarStyle: wm.TitleBarStyle.hidden,
    windowButtonVisibility: true,
  );

  await wm.windowManager.waitUntilReadyToShow(windowOptions, () async {
    try {
      await wm.windowManager.show();
      await wm.windowManager.focus();
    } catch (e) {
      Logger.root.warning('Window initialization error: $e');
    }
  });
}

The ensureInitialized() method loads the native bindings, while waitUntilReadyToShow delays the window presentation until the specified options are applied.

Configuring Frameless Window Behavior

ChatMCP opts for a custom title bar by hiding the native OS chrome and implementing its own controls.

WindowOptions and Hidden Title Bars

The WindowOptions constructor in main.dart configures several critical parameters:

  • size: Size(1200, 800) — Sets the default window dimensions on launch.
  • minimumSize: Size(400, 600) — Prevents the user from resizing below usable thresholds.
  • titleBarStyle: wm.TitleBarStyle.hidden — Removes the native title bar, allowing Flutter to draw a custom toolbar.
  • backgroundColor: Colors.transparent — Enables seamless blending with custom frame designs.

Showing and Focusing the Window

Inside the waitUntilReadyToShow callback, the application explicitly calls wm.windowManager.show() to make the window visible and wm.windowManager.focus() to bring it to the foreground. A brief delay ensures the native window completes its setup before the UI appears.

Implementing Draggable Custom Chrome

Without a native title bar, users need an alternative way to move the window. ChatMCP solves this by making the top toolbar a draggable zone.

DragToMoveArea for Frameless Windows

In lib/page/layout/widgets/top_toolbar.dart, the toolbar conditionally wraps its content with DragToMoveArea:

child: kIsDesktop
    ? wm.DragToMoveArea(child: _buildToolbarContent(context))
    : _buildToolbarContent(context),

The DragToMoveArea widget intercepts mouse drag events and forwards them to the native window manager, allowing users to reposition the frameless window by dragging the custom toolbar.

Double-Tap Maximization Gesture

The toolbar also supports a double-tap gesture to toggle between maximized and restored states:

onDoubleTap: () async {
  if (kIsDesktop) {
    bool isMaximized = await wm.windowManager.isMaximized();
    if (isMaximized) {
      await wm.windowManager.unmaximize();
    } else {
      await wm.windowManager.maximize();
    }
  }
},

This logic checks the current window state asynchronously before invoking the appropriate API.

Custom Window Control Buttons

Since the native title bar is hidden, ChatMCP provides its own minimize, maximize, and close buttons in lib/page/layout/widgets/window_controls.dart.

The WindowControls Widget Implementation

The widget defines three control buttons with distinct behaviors:

Minimize Button:

_WindowButton(
  icon: Icons.remove,
  onPressed: () async => await wm.windowManager.minimize(),
  tooltip: l10n.minimize,
),

Maximize/Restore Toggle:

_WindowButton(
  icon: Icons.crop_square,
  onPressed: () async {
    bool isMaximized = await wm.windowManager.isMaximized();
    if (isMaximized) {
      await wm.windowManager.unmaximize();
    } else {
      await wm.windowManager.maximize();
    }
  },
  tooltip: l10n.maximize,
),

Close Button:

_WindowButton(
  icon: Icons.close,
  onPressed: () async => await wm.windowManager.close(),
  tooltip: l10n.close,
  isCloseButton: true,
),

State-Aware Maximize and Restore

The maximize button dynamically changes behavior based on the window's current state. When isMaximized() returns true, the button calls unmaximize() to restore the previous size; otherwise, it calls maximize() to fill the screen.

Additional Window Management Techniques

Beyond the core setup, window_manager enables runtime manipulation of window properties.

Runtime Window Resizing

Developers can adjust window dimensions programmatically after initialization:

import 'package:window_manager/window_manager.dart' as wm;

Future<void> resizeWindow() async {
  await wm.windowManager.setSize(const Size(1600, 900));
}

Taskbar Visibility Control

To hide the window from the taskbar or dock:

await wm.windowManager.setSkipTaskbar(true);

Listening for Window Events

The plugin supports event listeners for state changes:

wm.windowManager.addListener(MyWindowListener());

class MyWindowListener extends wm.WindowListener {
  @override
  void onWindowFocus() {
    debugPrint('Window gained focus');
  }
  
  @override
  void onWindowBlur() {
    debugPrint('Window lost focus');
  }
}

Summary

  • ChatMCP initializes window_manager early in lib/main.dart only when kIsDesktop is true, using WindowOptions to configure a 1200×800 frameless window centered on screen.
  • The DragToMoveArea widget wraps the toolbar in lib/page/layout/widgets/top_toolbar.dart, enabling users to drag the frameless window by its custom chrome.
  • Double-tapping the toolbar checks isMaximized() to toggle asynchronously between maximize() and unmaximize() states.
  • The WindowControls widget provides explicit minimize, maximize/restore, and close buttons that invoke corresponding window_manager APIs.
  • Runtime window manipulation leverages setSize(), setSkipTaskbar(), and event listeners for dynamic desktop window behavior.

Frequently Asked Questions

Why does ChatMCP hide the native title bar on desktop?

ChatMCP hides the native title bar using TitleBarStyle.hidden in WindowOptions to implement a custom frameless design that matches the Flutter UI theme. This allows the application to draw its own gradient backgrounds and custom toolbar widgets while maintaining native window controls through the window_manager API.

How does ChatMCP allow dragging a window without a title bar?

The application wraps the toolbar content with the DragToMoveArea widget from the window_manager package, as seen in lib/page/layout/widgets/top_toolbar.dart. This widget forwards mouse drag events directly to the native window manager, enabling standard window repositioning behavior despite the absence of native chrome.

Can I change the default window size in ChatMCP?

Yes, the default size is defined in lib/main.dart within the WindowOptions constructor as Size(1200, 800). You can modify these dimensions before building, or call wm.windowManager.setSize() at runtime to dynamically resize the window based on user preferences or screen constraints.

What happens if window_manager fails to initialize?

The initialization logic in lib/main.dart wraps the show() and focus() calls inside a try-catch block that logs warnings via Logger.root.warning. This defensive programming ensures that if the native window manager fails to bind, the application continues running rather than crashing, though the window may appear with default system styling.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →