Skip to main content

Overview

Jarvis uses a binary split tree to manage pane layout within the application window. Every visible pane occupies a leaf node in the tree, and interior nodes describe how their two children are divided—either horizontally (side by side) or vertically (top and bottom).
The tiling system lives in the jarvis-tiling crate and is intentionally decoupled from rendering and platform windowing.

Layout Configuration

Layout settings control the visual appearance of the tiling grid:

Core Layout Settings

layout.panel_gap
u32
default:6
Gap between panels in pixels (valid range: 1–20)
layout.border_radius
u32
default:8
Border radius in pixels (valid range: 0–20)
layout.padding
u32
default:10
Inner pane padding in pixels (valid range: 0–40)
layout.max_panels
u32
default:5
Maximum number of panels (valid range: 1–10)
layout.default_panel_width
f64
Default panel width as fraction of screen (valid range: 0.3–1.0)
layout.scrollbar_width
u32
default:3
Scrollbar width in pixels (valid range: 1–10)
layout.border_width
f64
Panel border width in pixels (valid range: 0.0–3.0)
layout.outer_padding
u32
default:0
Screen-edge padding in pixels (valid range: 0–40)
layout.inactive_opacity
f64
Opacity for unfocused panels (valid range: 0.0–1.0)

Configuration Example

config.toml
[layout]
panel_gap = 8
border_radius = 12
padding = 12
max_panels = 3
default_panel_width = 0.5
border_width = 1.0
outer_padding = 10
inactive_opacity = 0.7

The Split Tree

The layout is represented by a tree structure:
Window (800 x 600)
+-------------------------------------------+
|    Tree root: Split(H, 0.5)              |
|     /                   \                |
|  Leaf(1)         Split(V, 0.5)           |
|                   /          \           |
|               Leaf(2)     Leaf(3)        |
+-------------------------------------------+

Visual result:
+------------------+------------------+
|                  |                  |
|                  |    Pane 2        |
|    Pane 1        |                  |
|                  +------------------+
|                  |                  |
|                  |    Pane 3        |
+------------------+------------------+
pub enum SplitNode {
    Leaf { pane_id: u32 },
    Split {
        direction: Direction,   // Horizontal or Vertical
        ratio: f64,             // 0.0 .. 1.0, how much space the first child gets
        first: Box<SplitNode>,
        second: Box<SplitNode>,
    },
}

pub enum Direction {
    Horizontal,  // Children placed side by side (left | right)
    Vertical,    // Children placed top over bottom (top / bottom)
}

Splitting Panes

Manual Split

You can split panes horizontally (side by side) or vertically (top/bottom):
  • Horizontal split (SplitHorizontal): places panes side by side
  • Vertical split (SplitVertical): places panes top and bottom
[keybinds]
split_vertical = "Cmd+D"
split_horizontal = "Cmd+Shift+D"

Auto-Split Logic

The auto_split_direction() method chooses the split direction based on the focused pane’s aspect ratio:
  • If width >= heightHorizontal (split side by side)
  • If width < heightVertical (split top/bottom)
The max_panels configuration value caps the total number of panes that can exist simultaneously.

Split Behavior

When a split is performed:
1

Replace leaf

The currently focused leaf is replaced by a Split node
2

Create children

The original pane becomes the first child, a new pane becomes the second child
3

Set ratio

The ratio is set to 0.5 (equal split)
4

Update focus

Focus moves to the new pane
5

Cancel zoom

If the manager was in zoom mode, zoom is cancelled

Focus Management

Focus Operations

focus_next()

Move focus to the next pane in depth-first order, wrapping around

focus_prev()

Move focus to the previous pane, wrapping from first to last

focus_direction(dir)

Move focus to the neighbor in a specific direction (doesn’t wrap)

focus_pane(id)

Focus a specific pane by its numeric ID

Focus Indicators

When layout.inactive_opacity < 1.0, unfocused panes are rendered with reduced opacity. The layout.border_width setting controls whether pane borders are visible (default 0.0 means no border).
config.toml
[layout]
inactive_opacity = 0.7
border_width = 1.0

[effects]
inactive_pane_dim = true
dim_opacity = 0.6

Zoom Mode

Zoom mode makes a single pane fill the entire content area, hiding all other panes.

Behavior

  • Toggle on: zoom_toggle() sets zoomed = Some(focused_id)
  • Toggle off: calling zoom_toggle() again sets zoomed = None
  • Zooming requires at least 2 panes
  • Any split operation automatically cancels zoom
  • Closing a pane cancels zoom
When zoomed, only the zoomed pane is positioned; all other panes remain at their previous positions (off-screen).

Resizing Panes

Pane sizes are controlled by the ratio field on Split nodes.

Keyboard Resize

Use keyboard shortcuts to resize the focused pane:
config.toml
# Resize actions (not in keybinds section)
# These are triggered via ResizePane action with direction
Each resize step adjusts the ratio by 5%. The ratio is clamped to [0.1, 0.9], preventing any pane from being resized to invisibility.

Mouse Drag Resize

Mouse drag resize is a three-phase interaction:
1

Hit testing (cursor move)

On cursor movement, the system:
  • Computes all split borders from the current tree
  • Hit-tests the cursor position (6-pixel hit zone)
  • Changes cursor icon to ColResize or RowResize when hovering a border
2

Drag start (mouse button press)

When pressing the mouse button over a border, the system records:
  • The border being dragged
  • The start position
3

Drag update (cursor move while dragging)

While dragging, each cursor movement:
  • Computes the pixel delta from the drag start
  • Converts pixels to a ratio delta
  • Calls adjust_ratio_between() to update the split node
  • Repositions webviews
4

Drag end (mouse button release)

Clears the drag state and resets the cursor

Pane Stacks (Tabs)

A single leaf position in the tree can host multiple panes as a stack (tabbed interface).

Stack Operations

push_to_stack(kind, title)
Add a new pane of the given kind to the stack at the focused leaf position
cycle_stack_next()
Advance to the next tab, wrapping around
cycle_stack_prev()
Go to the previous tab, wrapping around
Only the active pane in the stack is visible; others are preserved in memory.

Opacity Configuration

Transparency settings for different UI layers:
opacity.background
f64
Background layer opacity (valid range: 0.0–1.0)
opacity.panel
f64
Panel opacity (valid range: 0.0–1.0)
opacity.orb
f64
Orb visualizer opacity (valid range: 0.0–1.0)
opacity.hex_grid
f64
Hex grid background opacity (valid range: 0.0–1.0)
opacity.hud
f64
HUD overlay opacity (valid range: 0.0–1.0)

Opacity Example

config.toml
[opacity]
background = 0.95
panel = 0.9
hex_grid = 0.5

UI Chrome: Tab Bar and Status Bar

UI chrome elements surround the content area and affect the viewport available for tiling.

Tab Bar

The tab bar sits at the top of the window with a default height of 32 pixels.

Status Bar

The status bar sits at the bottom of the window with a default height of 24 pixels.
status_bar.enabled
bool
default:true
Show the status bar
status_bar.height
u32
default:28
Status bar height in pixels (valid range: 20–48)
status_bar.show_panel_buttons
bool
default:true
Show panel toggle buttons (left side)
status_bar.show_online_count
bool
default:true
Show online user count (right side)
status_bar.bg
string
default:"rgba(24,24,37,0.95)"
Background color (CSS color string)

Chrome Layout

+========================================+
|  Tab Bar (32px)   [Tab1] [Tab2] [Tab3] |
+========================================+
|                                        |
|        Content Area (tiling)           |
|                                        |
|    +------------------+-----------+    |
|    |                  |           |    |
|    |    Pane 1        |  Pane 2   |    |
|    +------------------+-----------+    |
+========================================+
|  Status Bar (24px)  [left] [ctr] [rgt] |
+========================================+

Auto-Open Panels

Configure panels to open automatically when Jarvis starts:
config.toml
[[auto_open.panels]]
kind = "terminal"
command = "claude"
title = "Claude Code"

[[auto_open.panels]]
kind = "terminal"
title = "Terminal"

[[auto_open.panels]]
kind = "chat"
title = "Chat"

Panel Kinds

Terminal

Interactive terminal emulator (xterm.js)

Chat

Chat interface pane

Assistant

AI assistant panel

WebView

General-purpose web content pane

Auto-Open Configuration

kind
enum
default:"terminal"
Panel type: terminal, assistant, chat, settings, presence
command
string
Command to run (empty = $SHELL)
args
string[]
default:"[]"
Arguments for the command
title
string
Panel title
working_directory
string
Working directory (empty = $HOME)
To disable auto-open entirely, set an empty array:
[auto_open]
panels = []

Multi-Panel Workspace Example

config.toml
[layout]
max_panels = 5
panel_gap = 4
default_panel_width = 0.5
border_width = 1.0
outer_padding = 8
inactive_opacity = 0.7

[effects]
inactive_pane_dim = true
dim_opacity = 0.5

[[auto_open.panels]]
kind = "terminal"
title = "Build"
working_directory = "/projects/my-app"

[[auto_open.panels]]
kind = "terminal"
title = "Server"
command = "npm"
args = ["run", "dev"]

[[auto_open.panels]]
kind = "assistant"
title = "AI"

[[auto_open.panels]]
kind = "chat"
title = "Chat"

Advanced: Layout Engine

The layout engine converts the split tree into concrete pixel rectangles:
  1. Apply outer padding — The viewport is inset by outer_padding on all four sides
  2. Recurse through the tree — At each Split node:
    • Subtract the gap from the available dimension
    • Multiply the remaining space by ratio to get the first child’s size
    • The second child gets the remainder
    • The gap is placed between the two children
  3. Leaf nodes emit (pane_id, bounds) directly

Gap and Padding Calculation

Viewport: 800 x 600, outer_padding = 10, gap = 8

+---------- 800 ----------+
|  padding = 10            |
|  +--- 780 x 580 ------+ |
|  |        |  gap  |    | |
|  | Pane 1 | (8px) | P2 | |
|  |  386   |       |386 | |
|  +--------+-------+----+ |
+--------------------------+

Available = 780 - 8 = 772
Each pane = 772 * 0.5 = 386

Troubleshooting

Check:
  • Current panel count < layout.max_panels
  • Keybinds are correctly configured
  • Not in zoom mode (splits cancel zoom)
Adjust layout.panel_gap (valid range: 1–20 pixels)
Increase layout.inactive_opacity or disable effects.inactive_pane_dim
  • Check that you’re dragging on the split border (6-pixel hit zone)
  • Ratios are clamped to [0.1, 0.9] to prevent invisible panes