Files
orca/notes/multi-host-orca-plan.html
T
2026-06-11 12:33:57 -07:00

2194 lines
78 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Making Remote Hosts First Class in Orca</title>
<style>
:root {
color-scheme: dark;
--background: #111111;
--foreground: #f4f4f5;
--muted: #1c1c1f;
--muted-foreground: #a1a1aa;
--card: #161618;
--border: #2a2a2f;
--accent: #242429;
--ring: #d4d4d8;
--success: #7dd3a7;
--warning: #f4c56a;
--danger: #f08c8c;
--info: #8ab4f8;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--background);
color: var(--foreground);
font-family:
Geist,
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
letter-spacing: 0.01em;
line-height: 1.5;
}
main {
max-width: 1120px;
margin: 0 auto;
padding: 48px 24px 72px;
}
header {
border-bottom: 1px solid var(--border);
padding-bottom: 28px;
margin-bottom: 32px;
}
.eyebrow {
color: var(--muted-foreground);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.05em;
text-transform: uppercase;
}
h1 {
font-size: 36px;
line-height: 1.1;
margin: 10px 0 14px;
letter-spacing: 0;
}
h2 {
font-size: 22px;
margin: 36px 0 14px;
letter-spacing: 0;
}
h3 {
font-size: 15px;
margin: 22px 0 8px;
letter-spacing: 0;
}
p {
margin: 0 0 12px;
}
a {
color: var(--info);
}
code {
border: 1px solid var(--border);
border-radius: 6px;
background: var(--muted);
color: var(--foreground);
font-family:
"SFMono-Regular",
Consolas,
"Liberation Mono",
monospace;
font-size: 0.92em;
padding: 1px 5px;
}
.summary {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
margin-top: 24px;
}
.panel {
background: var(--card);
border: 1px solid var(--border);
border-radius: 8px;
padding: 16px;
}
.panel strong {
display: block;
margin-bottom: 6px;
}
.muted {
color: var(--muted-foreground);
}
.callout {
border: 1px solid var(--border);
border-left: 3px solid var(--info);
background: color-mix(in srgb, var(--info) 8%, var(--background));
border-radius: 8px;
padding: 16px;
margin: 18px 0;
}
.decision {
border-left-color: var(--success);
background: color-mix(in srgb, var(--success) 8%, var(--background));
}
.warning {
border-left-color: var(--warning);
background: color-mix(in srgb, var(--warning) 8%, var(--background));
}
.danger {
border-left-color: var(--danger);
background: color-mix(in srgb, var(--danger) 8%, var(--background));
}
ul,
ol {
margin: 0 0 16px 22px;
padding: 0;
}
li {
margin: 7px 0;
}
table {
width: 100%;
border-collapse: collapse;
margin: 16px 0 24px;
overflow: hidden;
border: 1px solid var(--border);
border-radius: 8px;
}
th,
td {
border-bottom: 1px solid var(--border);
padding: 10px 12px;
text-align: left;
vertical-align: top;
font-size: 14px;
}
th {
background: var(--muted);
color: var(--foreground);
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
tr:last-child td {
border-bottom: 0;
}
.tag {
display: inline-flex;
align-items: center;
border: 1px solid var(--border);
border-radius: 999px;
color: var(--muted-foreground);
font-size: 12px;
padding: 2px 8px;
margin: 0 6px 6px 0;
white-space: nowrap;
}
.phase {
display: grid;
grid-template-columns: 108px 1fr;
gap: 16px;
border-top: 1px solid var(--border);
padding: 18px 0;
}
.phase:first-of-type {
border-top: 0;
}
.phase-label {
color: var(--muted-foreground);
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.phase h3 {
margin-top: 0;
}
.footer {
color: var(--muted-foreground);
border-top: 1px solid var(--border);
margin-top: 40px;
padding-top: 16px;
font-size: 13px;
}
.mock-shell {
display: grid;
grid-template-columns: 272px minmax(0, 1fr);
min-height: 680px;
overflow: hidden;
border: 1px solid var(--border);
border-radius: 8px;
background: #0a0a0a;
}
.mock-sidebar {
position: relative;
background: #171717;
border-right: 1px solid rgb(255 255 255 / 0.07);
color: #fafafa;
overflow: hidden;
}
.mock-nav {
padding: 12px 10px 8px;
border-bottom: 1px solid rgb(255 255 255 / 0.07);
}
.mock-nav-row {
display: flex;
align-items: center;
gap: 8px;
height: 24px;
color: #a1a1a1;
font-size: 12px;
}
.mock-sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
height: 32px;
padding: 8px 10px 4px 12px;
}
.mock-sidebar-title {
color: rgb(161 161 161 / 0.8);
font-size: 12px;
font-weight: 600;
}
.mock-icon-buttons {
display: inline-flex;
gap: 6px;
}
.mock-icon-button {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border: 0;
border-radius: 6px;
background: transparent;
color: #a1a1a1;
font-size: 14px;
}
.mock-badge-dot {
position: absolute;
top: 1px;
right: 1px;
min-width: 12px;
height: 12px;
border-radius: 999px;
background: #fafafa;
color: #0a0a0a;
font-size: 9px;
font-weight: 700;
line-height: 12px;
text-align: center;
}
.mock-scope-strip {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin: 4px 8px 8px;
padding: 7px 8px;
border: 1px solid rgb(255 255 255 / 0.07);
border-radius: 8px;
background: color-mix(in srgb, #fafafa 5%, #171717);
}
.mock-scope-title {
display: flex;
min-width: 0;
align-items: center;
gap: 7px;
font-size: 12px;
font-weight: 600;
}
.mock-scope-title span:last-child {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mock-scope-meta {
color: #a1a1a1;
font-size: 11px;
white-space: nowrap;
}
.mock-list {
padding: 0 6px 12px;
}
.mock-host-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
height: 28px;
margin-top: 6px;
padding: 0 6px;
color: #a1a1a1;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.mock-host-left {
display: flex;
min-width: 0;
align-items: center;
gap: 6px;
}
.mock-host-left span:last-child {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mock-status-pill {
display: inline-flex;
align-items: center;
gap: 4px;
border-radius: 999px;
color: #a1a1a1;
font-size: 10px;
font-weight: 600;
letter-spacing: 0;
text-transform: none;
}
.mock-status-light {
width: 6px;
height: 6px;
border-radius: 999px;
background: var(--success);
}
.mock-status-light.warn {
background: var(--warning);
}
.mock-card {
margin: 3px 0;
padding: 7px 8px;
border: 1px solid transparent;
border-radius: 7px;
background: transparent;
}
.mock-card.active {
border-color: rgb(255 255 255 / 0.07);
background: color-mix(in srgb, #fafafa 10%, transparent);
box-shadow: 0 1px 2px color-mix(in srgb, #fafafa 3%, transparent);
}
.mock-card.child {
margin-left: 14px;
background: color-mix(in srgb, #404040 10%, #171717);
box-shadow: inset 0 0 0 1px color-mix(in srgb, #404040 16%, transparent);
}
.mock-card-top {
display: flex;
align-items: center;
gap: 7px;
min-width: 0;
}
.mock-card-status {
width: 7px;
height: 7px;
flex: 0 0 auto;
border-radius: 999px;
background: var(--success);
}
.mock-card-status.yellow {
background: var(--warning);
}
.mock-card-status.gray {
background: #737373;
}
.mock-card-title {
overflow: hidden;
color: #fafafa;
font-size: 12px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.mock-card-subtitle {
overflow: hidden;
margin-top: 3px;
padding-left: 14px;
color: #a1a1a1;
font-size: 11px;
text-overflow: ellipsis;
white-space: nowrap;
}
.mock-agent-row {
margin: 6px 0 0 14px;
padding: 5px 6px;
border-radius: 6px;
background: color-mix(in srgb, #404040 18%, transparent);
color: #d4d4d8;
font-size: 11px;
}
.mock-workspace-surface {
position: relative;
background:
linear-gradient(90deg, rgb(255 255 255 / 0.025), transparent 40%),
#0a0a0a;
padding: 18px;
}
.mock-workspace-tabs {
display: flex;
gap: 6px;
border-bottom: 1px solid var(--border);
padding-bottom: 10px;
}
.mock-tab {
border: 1px solid var(--border);
border-radius: 7px;
background: var(--card);
color: var(--muted-foreground);
font-size: 12px;
padding: 5px 10px;
}
.mock-tab.active {
color: var(--foreground);
background: color-mix(in srgb, var(--foreground) 8%, var(--card));
}
.mock-terminal {
margin-top: 22px;
color: #d4d4d8;
font-family:
"SFMono-Regular",
Consolas,
"Liberation Mono",
monospace;
font-size: 13px;
}
.mock-terminal-line {
margin: 7px 0;
}
.mock-popover {
position: absolute;
top: 58px;
left: 58px;
width: 288px;
overflow: hidden;
border: 1px solid rgb(255 255 255 / 0.13);
border-radius: 12px;
background: rgb(10 10 10 / 0.98);
box-shadow: 0 18px 40px rgb(0 0 0 / 0.45);
color: #fafafa;
z-index: 2;
}
.mock-menu-label {
padding: 12px 16px 7px;
color: #a1a1a1;
font-size: 12px;
font-weight: 700;
}
.mock-segmented {
display: grid;
grid-template-columns: repeat(4, 1fr);
margin: 0 12px 10px;
overflow: hidden;
border: 1px solid rgb(255 255 255 / 0.16);
border-radius: 9px;
}
.mock-segmented span {
display: flex;
align-items: center;
justify-content: center;
height: 28px;
border-left: 1px solid rgb(255 255 255 / 0.1);
color: #fafafa;
font-size: 12px;
font-weight: 600;
}
.mock-segmented span:first-child {
border-left: 0;
}
.mock-segmented .selected {
background: rgb(255 255 255 / 0.11);
}
.mock-menu-separator {
height: 1px;
background: rgb(255 255 255 / 0.07);
margin: 7px 8px;
}
.mock-menu-row {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 32px;
gap: 12px;
padding: 0 16px;
color: #fafafa;
font-size: 13px;
font-weight: 500;
}
.mock-menu-row .right {
color: #a1a1a1;
font-size: 12px;
white-space: nowrap;
}
.mock-menu-row.host {
min-height: 38px;
}
.mock-menu-row.selected-host {
background: rgb(255 255 255 / 0.08);
}
.mock-host-menu-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 10px 12px 8px;
}
.mock-host-menu-top strong {
font-size: 12px;
}
.mock-host-menu-top span {
color: #a1a1a1;
font-size: 11px;
}
.mock-host-select {
display: flex;
min-width: 0;
flex-direction: column;
}
.mock-host-select strong {
overflow: hidden;
font-size: 13px;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
.mock-host-select span {
color: #a1a1a1;
font-size: 11px;
}
.mock-check {
color: var(--success);
font-size: 12px;
font-weight: 700;
}
.mock-check.hidden {
opacity: 0;
}
.mock-host-filter-row {
display: grid;
grid-template-columns: 18px 1fr auto;
align-items: center;
gap: 8px;
min-height: 48px;
margin: 0 8px;
border-radius: 8px;
color: #fafafa;
font-size: 13px;
padding: 5px 8px;
}
.mock-host-filter-row.selected {
background: rgb(255 255 255 / 0.12);
}
.mock-host-filter-main {
min-width: 0;
}
.mock-host-filter-title {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
font-weight: 600;
}
.mock-host-filter-title span:last-child {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mock-host-filter-detail {
margin-top: 1px;
overflow: hidden;
color: #a1a1a1;
font-size: 11px;
text-overflow: ellipsis;
white-space: nowrap;
}
.mock-host-filter-meta {
color: #a1a1a1;
font-size: 10px;
font-weight: 600;
white-space: nowrap;
}
.mock-host-filter-meta.blocked {
color: var(--warning);
}
.mock-toggle {
position: relative;
width: 24px;
height: 14px;
border-radius: 999px;
background: rgb(255 255 255 / 0.24);
}
.mock-toggle.on {
background: #fafafa;
}
.mock-toggle::after {
position: absolute;
top: 2px;
left: 2px;
width: 10px;
height: 10px;
border-radius: 999px;
background: #0a0a0a;
content: "";
}
.mock-toggle.on::after {
transform: translateX(10px);
}
.mock-search {
display: flex;
align-items: center;
gap: 8px;
height: 34px;
margin: 4px 12px 8px;
border: 1px solid rgb(255 255 255 / 0.14);
border-radius: 8px;
color: #a1a1a1;
font-size: 13px;
padding: 0 10px;
}
.mock-project-row {
display: flex;
align-items: center;
gap: 9px;
height: 32px;
margin: 0 8px;
border-radius: 8px;
color: #fafafa;
font-size: 13px;
font-weight: 600;
padding: 0 14px;
}
.mock-project-row.selected {
background: rgb(255 255 255 / 0.14);
}
.mock-swatch {
width: 7px;
height: 7px;
background: #ff4d57;
}
.mock-swatch.gray {
background: #737373;
}
.mock-swatch.green {
background: #22c55e;
}
.mock-swatch.blue {
background: #60a5fa;
}
@media (max-width: 780px) {
main {
padding: 32px 16px 48px;
}
h1 {
font-size: 28px;
}
.summary,
.phase,
.mock-shell {
grid-template-columns: 1fr;
}
.mock-workspace-surface {
min-height: 280px;
}
}
</style>
</head>
<body>
<main>
<header>
<div class="eyebrow">Issue #4697 Product and Architecture Memo</div>
<h1>Making VMs, remote servers, and SSH workspaces first class in Orca</h1>
<p class="muted">
Users want one desktop Orca client to control local work plus multiple remote hosts
concurrently, without losing terminals or mentally switching into a different app.
</p>
<div class="summary">
<div class="panel">
<strong>Recommended model</strong>
One app window, many first-class execution hosts, grouped in the workspace sidebar.
</div>
<div class="panel">
<strong>Core architecture move</strong>
Replace global server switching with host-scoped routing and per-host session slices.
</div>
<div class="panel">
<strong>Settings principle</strong>
Client settings stay local; host settings are explicit and contextual.
</div>
</div>
</header>
<section>
<h2>Executive Recommendation</h2>
<div class="callout decision">
Build a <strong>single-window, host-aware workbench</strong>. Local, SSH targets, VMs,
and paired Orca runtime servers should appear as execution hosts. Selecting a workspace
changes focus; it must not tear down other hosts' terminals.
</div>
<p>
The current destructive behavior comes from treating <code>activeRuntimeEnvironmentId</code>
as a global app mode. That model does not satisfy concurrent remote work. It should become
connection management and default selection, not the identity of the whole workbench.
</p>
<p>
Multiple app windows can be a later power-user feature, but should not be the first design.
A window-per-host approach fights the existing single-renderer architecture and weakens the
product goal of one client coordinating work across machines.
</p>
<p>
The product should feel configurable, not split into separate modes. A local-only user sees
today's Orca. A remote-heavy user can choose <code>All hosts</code> to operate like a
command center, or choose one host so the client feels almost entirely like that machine.
Those are two views over the same host registry and session model.
</p>
<p>
Do not design around a minimal MVP boundary here. The intended destination is the full
end-to-end model: concurrent local, SSH, VM-like, and paired-runtime hosts with safe
host-owned routing. Implementation can still be phased for review and risk control, but
the design should describe the complete product.
</p>
<h3>Vocabulary</h3>
<table>
<thead>
<tr>
<th>Term</th>
<th>Meaning</th>
<th>Examples</th>
</tr>
</thead>
<tbody>
<tr>
<td>Host</td>
<td>The machine or backend that owns execution, files, terminals, ports, and agents.</td>
<td>Local Mac, Windows VM over SSH, Linux builder, paired Orca runtime server.</td>
</tr>
<tr>
<td>VM</td>
<td>
A loose product term for a remote machine. It may be backed by SSH today, a paired
runtime server, or future Orca-provided cloud infrastructure.
</td>
<td>Windows dev VM, Linux cloud builder, future Orca-hosted environment.</td>
</tr>
<tr>
<td>Visible host filter</td>
<td>Which hosts the sidebar shows. This is presentation/filtering, not teardown.</td>
<td>All hosts, Local Mac + Windows VM, Linux Builder only.</td>
</tr>
<tr>
<td>Default host</td>
<td>The host preselected for new workspace creation when a flow needs a target.</td>
<td>Local Mac remains the default while Windows VM is also visible.</td>
</tr>
<tr>
<td>Execution owner</td>
<td>The host identity attached to a repo/worktree/tab/PTY operation.</td>
<td><code>ssh:target-1</code>, <code>runtime:env-a</code>, <code>local</code>.</td>
</tr>
</tbody>
</table>
</section>
<section>
<h2>What We Learned</h2>
<h3>Current behavior</h3>
<ul>
<li>Desktop Settings are backed by the local Electron main process store.</li>
<li>
<code>activeRuntimeEnvironmentId</code> is stored as a local client setting, but it acts
like a global workbench switch.
</li>
<li>
Switching runtime environments currently resets runtime-scoped renderer state and can
close remote terminals/browser pages.
</li>
<li>
SSH already has partial multi-host shape: repos carry <code>connectionId</code>, SSH
connection state is target-keyed, and remote workspace snapshots are keyed by target.
</li>
<li>
The renderer still has one live <code>WorkspaceSessionState</code>, so runtime servers
are not first-class concurrent peers yet.
</li>
</ul>
<h3>Core product requirement</h3>
<p>
A user must be able to start work on one host, switch attention to another host, then come
back without losing terminals, browser tabs, agent status, file context, or workspace
ordering. Host focus is a viewing and default-target choice. It is not permission to close
work on the old host.
</p>
<h3>Agent feedback</h3>
<table>
<thead>
<tr>
<th>Agent</th>
<th>Main recommendation</th>
<th>Useful nuance</th>
</tr>
</thead>
<tbody>
<tr>
<td>Cursor</td>
<td>Unify local, SSH, runtime, and VMs under an <code>ExecutionHostId</code>.</td>
<td>Partition session state by host and phase the refactor around routing keys.</td>
</tr>
<tr>
<td>Claude</td>
<td>Make runtime environments per-worktree, like SSH is already per-target.</td>
<td>
Keep local/runtime/SSH transport types distinct internally for now, but converge
them in the product model.
</td>
</tr>
</tbody>
</table>
</section>
<section>
<h2>Product Model</h2>
<p>
Orca should present every machine or remote backend as a <strong>host</strong>. A host can
be local, an SSH target, a paired Orca runtime server, or a VM profile backed by either SSH
or runtime RPC.
</p>
<p>
"VM" should remain a user-facing label, not a required separate transport in v1. Today a VM
can be an SSH target or a remote runtime server. Later, if Orca offers managed cloud
machines as a revenue source, those machines should slot into the same host model instead
of creating a parallel workspace concept.
</p>
<div>
<span class="tag">local</span>
<span class="tag">ssh:windows-vm</span>
<span class="tag">runtime:prod-builder</span>
<span class="tag">vm:win-only-product</span>
</div>
<h3>Sidebar behavior</h3>
<ul>
<li>Group workspaces by host in the sidebar.</li>
<li>Each host header shows label, health, reconnect state, and active agent count.</li>
<li>
Users choose visible hosts with a searchable multi-select menu modeled after the
existing Projects selector: sticky <code>All hosts</code>, search, checked rows, and
status metadata.
</li>
<li>
Selecting one visible host is the focused-host experience. Selecting multiple hosts is a
custom command-center view. Neither mode disconnects hidden hosts.
</li>
<li>New workspace defaults to the focused host, but existing workspaces keep their host.</li>
<li>Searching and Cmd+J should work across hosts, with host labels visible in results.</li>
<li>
Hide the host visibility UI entirely when there are no remote hosts. Local-only users
should keep today's sidebar.
</li>
</ul>
<h3>Host actions</h3>
<ul>
<li><code>Manage Host</code>: open a host details panel.</li>
<li><code>Reconnect</code> / <code>Disconnect</code>: connection lifecycle only.</li>
<li><code>Rename</code>: local display label.</li>
<li><code>Remove</code>: forget the host and clean up host-scoped local state safely.</li>
<li><code>Open in New Window</code>: optional later feature, not MVP.</li>
</ul>
<h3>Two Valid User Modes</h3>
<table>
<thead>
<tr>
<th>Mode</th>
<th>User intent</th>
<th>Experience</th>
</tr>
</thead>
<tbody>
<tr>
<td>All hosts</td>
<td>"I want my local and remote work visible together."</td>
<td>
Sidebar shows host sections. Cmd+J/search can cross hosts. New work defaults to the
last focused host or an explicit host picker.
</td>
</tr>
<tr>
<td>One visible host</td>
<td>"Make this Orca client feel like the server."</td>
<td>
Sidebar shows only that host. Terminals/files/agents route to that host. Other hosts
keep running in the background and can be restored by changing scope.
</td>
</tr>
<tr>
<td>Selected hosts</td>
<td>"Show my Mac plus the remote machines relevant to this task."</td>
<td>
Sidebar shows only checked host sections, with host health still visible. This is a
persistent presentation filter, not an ownership or teardown operation.
</td>
</tr>
</tbody>
</table>
<h3>Primary user journeys</h3>
<table>
<thead>
<tr>
<th>Journey</th>
<th>Expected behavior</th>
<th>Implementation implication</th>
</tr>
</thead>
<tbody>
<tr>
<td>Add an SSH VM</td>
<td>
User saves a target, Orca shows it as a host, and new work can be created there.
</td>
<td>
Saved SSH targets become host registry entries with <code>ssh:&lt;targetId&gt;</code>
execution identity.
</td>
</tr>
<tr>
<td>Switch to Windows VM</td>
<td>
Sidebar narrows to Windows workspaces. Existing local terminals and agents keep
running.
</td>
<td>
Host visibility updates view/default state only. It must not call terminal/browser
close paths.
</td>
</tr>
<tr>
<td>Create a workspace from All hosts</td>
<td>
Orca asks for or infers a target host, then binds the repo/worktree to that host
forever unless explicitly moved.
</td>
<td>
Creation flows use focused/default host as input. Later operations use owner host.
</td>
</tr>
<tr>
<td>Open Settings while scoped to a remote host</td>
<td>
Settings still clearly says whether the user is editing client settings or that
host's details.
</td>
<td>
Host focus must never silently reinterpret local client settings as remote settings.
</td>
</tr>
<tr>
<td>Reconnect after SSH auth expires</td>
<td>
Remote workspaces remain visible as unavailable/deferred until the user reauths.
</td>
<td>
Session hydration and sidebar rendering cannot require an active SSH tunnel.
</td>
</tr>
</tbody>
</table>
</section>
<section>
<h2>Sidebar Mock</h2>
<p>
Hosts should behave as a visibility filter above the existing organization controls. The
existing <code>Group by</code>, <code>Sort by</code>, filters, and project filter still
apply inside the visible host set.
</p>
<div class="callout">
There is no separate "Focused Host Mode" toggle. Showing only <code>Local Mac</code>,
<code>Windows VM</code>, or any other single host is the focused experience. Showing
<code>All hosts</code> or a checked subset is the command-center experience. New workspace
creation should still expose the target host explicitly when more than one host is visible.
</div>
<div class="mock-shell" aria-label="Mock Orca sidebar with host visibility menu">
<aside class="mock-sidebar">
<div class="mock-nav">
<div class="mock-nav-row">☰ Tasks</div>
<div class="mock-nav-row">◌ Automations</div>
<div class="mock-nav-row">⌕ Search</div>
</div>
<div class="mock-sidebar-header">
<div class="mock-sidebar-title">Workspaces</div>
<div class="mock-icon-buttons">
<div class="mock-icon-button">☷<span class="mock-badge-dot">2</span></div>
<div class="mock-icon-button">▦</div>
<div class="mock-icon-button">+</div>
</div>
</div>
<div class="mock-scope-strip">
<div class="mock-scope-title">
<span>◎</span>
<span>Hosts</span>
</div>
<div class="mock-scope-meta">3 of 4 · 1 offline ▸</div>
</div>
<div class="mock-list">
<div class="mock-host-header">
<div class="mock-host-left">
<span>▾</span>
<span>Local Mac</span>
</div>
<div class="mock-status-pill"><span class="mock-status-light"></span>2 agents</div>
</div>
<div class="mock-card active">
<div class="mock-card-top">
<span class="mock-card-status"></span>
<span class="mock-card-title">Control multiple Orca servers concurrently</span>
</div>
<div class="mock-card-subtitle">orca · Jinwoo-H/feature-control-multiple...</div>
<div class="mock-agent-row">Claude done · Cursor working · Codex working</div>
</div>
<div class="mock-card child">
<div class="mock-card-top">
<span class="mock-card-status yellow"></span>
<span class="mock-card-title">debug-claude-tui-terminal-read-blank</span>
</div>
<div class="mock-card-subtitle">child workspace · Codex working</div>
</div>
<div class="mock-host-header">
<div class="mock-host-left">
<span>▾</span>
<span>Windows VM</span>
</div>
<div class="mock-status-pill"><span class="mock-status-light"></span>Connected</div>
</div>
<div class="mock-card">
<div class="mock-card-top">
<span class="mock-card-status"></span>
<span class="mock-card-title">Win-only packaging fix</span>
</div>
<div class="mock-card-subtitle">desktop-app · PowerShell · 1 live port</div>
</div>
<div class="mock-card">
<div class="mock-card-top">
<span class="mock-card-status gray"></span>
<span class="mock-card-title">default branch</span>
</div>
<div class="mock-card-subtitle">desktop-app · main</div>
</div>
<div class="mock-host-header">
<div class="mock-host-left">
<span>▸</span>
<span>Linux Builder</span>
</div>
<div class="mock-status-pill"><span class="mock-status-light warn"></span>Reconnecting</div>
</div>
</div>
<div class="mock-popover">
<div class="mock-host-menu-top">
<strong>Hosts</strong>
<span>3 of 4 visible</span>
</div>
<div class="mock-search">⌕ Search hosts...</div>
<div class="mock-menu-row selected-host">
<span class="mock-check">✓</span>
<span>All hosts</span>
</div>
<div class="mock-host-filter-row selected">
<span class="mock-check">✓</span>
<div class="mock-host-filter-main">
<div class="mock-host-filter-title">
<span class="mock-status-light"></span>
<span>Local Mac</span>
</div>
<div class="mock-host-filter-detail">/Users/jinwoohong · default for new work</div>
</div>
<span class="mock-host-filter-meta">2 agents</span>
</div>
<div class="mock-host-filter-row selected">
<span class="mock-check">✓</span>
<div class="mock-host-filter-main">
<div class="mock-host-filter-title">
<span class="mock-status-light"></span>
<span>Windows VM</span>
</div>
<div class="mock-host-filter-detail">ssh://dev-win · 2 workspaces · PowerShell</div>
</div>
<span class="mock-host-filter-meta">Connected</span>
</div>
<div class="mock-host-filter-row selected">
<span class="mock-check">✓</span>
<div class="mock-host-filter-main">
<div class="mock-host-filter-title">
<span class="mock-status-light warn"></span>
<span>Linux Builder</span>
</div>
<div class="mock-host-filter-detail">runtime:prod-builder · cached workspaces visible</div>
</div>
<span class="mock-host-filter-meta blocked">Reconnecting</span>
</div>
<div class="mock-host-filter-row">
<span class="mock-check hidden">✓</span>
<div class="mock-host-filter-main">
<div class="mock-host-filter-title">
<span class="mock-status-light warn"></span>
<span>Old staging server</span>
</div>
<div class="mock-host-filter-detail">ssh://staging · update needed</div>
</div>
<span class="mock-host-filter-meta blocked">Blocked</span>
</div>
<div class="mock-menu-row">
<span>Manage hosts...</span>
<span class="right">›</span>
</div>
<div class="mock-menu-separator"></div>
<div class="mock-menu-label">Group by</div>
<div class="mock-segmented">
<span>None</span>
<span class="selected">Status</span>
<span>PR</span>
<span>Project</span>
</div>
<div class="mock-menu-separator"></div>
<div class="mock-menu-row">
<span>Sort by</span>
<span class="right">Manual ›</span>
</div>
<div class="mock-menu-row">
<span>Card layout</span>
<span class="right">Detailed ›</span>
</div>
<div class="mock-menu-row">
<span>Show properties</span>
<span class="right">6 ›</span>
</div>
<div class="mock-menu-separator"></div>
<div class="mock-menu-label">Filters</div>
<div class="mock-menu-row">
<span>☾ Hide sleeping</span>
<span class="mock-toggle on"></span>
</div>
<div class="mock-menu-row">
<span>⌘ Hide default branch</span>
<span class="mock-toggle"></span>
</div>
<div class="mock-menu-separator"></div>
<div class="mock-menu-label">Projects</div>
<div class="mock-search">⌕ Filter projects...</div>
<div class="mock-project-row selected"><span class="mock-swatch gray"></span>noqa</div>
<div class="mock-project-row"><span class="mock-swatch"></span>orca</div>
<div class="mock-project-row"><span class="mock-swatch green"></span>desktop-app</div>
<div class="mock-project-row"><span class="mock-swatch blue"></span>server-tools</div>
</div>
</aside>
<div class="mock-workspace-surface">
<div class="mock-workspace-tabs">
<span class="mock-tab active">feature-control-multi...</span>
<span class="mock-tab">Cursor opinion...</span>
<span class="mock-tab">Claude opinion...</span>
</div>
<div class="mock-terminal">
<div class="mock-terminal-line">
$ visible hosts: <span class="muted">Local Mac, Windows VM, Linux Builder</span>
</div>
<div class="mock-terminal-line">
Local Mac and remote workspaces stay alive while the sidebar changes focus.
</div>
<div class="mock-terminal-line">
Selecting <code>Windows VM</code> would hide local workspaces without closing them.
</div>
</div>
</div>
</div>
<h3>How this maps to the real sidebar</h3>
<ul>
<li>
Add <code>Hosts</code> above the current <code>Group by</code> control in
<code>SidebarWorkspaceOptionsMenu</code>, but only when at least one non-local host
exists. Use the same searchable multi-select popover pattern as the existing Projects
selector.
</li>
<li>
Keep <code>Group by</code> as <code>None / Status / PR / Project</code>; do not add
Host there because users still need status/project grouping inside a host.
</li>
<li>
Add a compact host summary strip below the <code>Workspaces</code> header when more
than one host exists. Hide it for local-only users.
</li>
<li>
In <code>All hosts</code>, render host headers above the existing grouped workspace
rows. When one or more specific hosts are selected, hide unchecked host sections without
disconnecting them.
</li>
<li>
Host header menus own connection actions: manage, reconnect, disconnect, rename, remove,
and later open in a separate window.
</li>
</ul>
</section>
<section>
<h2>Settings Model</h2>
<div class="callout">
The Settings page should be explicit: users are either editing the local client, a host,
or a repo/worktree. Do not let host focus silently change what a setting means.
</div>
<table>
<thead>
<tr>
<th>Scope</th>
<th>Examples</th>
<th>Storage / routing</th>
</tr>
</thead>
<tbody>
<tr>
<td>Client settings</td>
<td>Theme, keybindings, language, window layout, telemetry, global UI preferences.</td>
<td>Existing local settings store.</td>
</tr>
<tr>
<td>Host settings</td>
<td>
Host label, connection details, auth/account selection, detected agents, shell
capability, server version, health, default clone location.
</td>
<td>Local host registry plus host RPC for server-owned facts.</td>
</tr>
<tr>
<td>Repo/worktree settings</td>
<td>Base ref, source-control AI recipe, tabs, layouts, per-worktree state.</td>
<td>Repo/worktree records tagged with host identity.</td>
</tr>
</tbody>
</table>
<h3>Host Details surface</h3>
<p>
Do not build a second giant Settings app. Add a focused <strong>Host Details</strong>
panel reachable from each host header and from Settings &gt; Servers.
</p>
<ul>
<li>Overview: status, latency/health, server version, protocol compatibility.</li>
<li>Connection: SSH/runtime endpoint, credentials, reconnect policy.</li>
<li>Agents: detected Codex/Claude/Cursor/Gemini/OpenCode on that host.</li>
<li>Terminal: default shell and host-specific terminal capabilities.</li>
<li>Storage: default workspace/clone directory, disk/port/resource status.</li>
<li>Danger zone: disconnect, remove, clear cached host session.</li>
</ul>
<h3>Effective settings rule</h3>
<p>
For ambiguous preferences, use inheritance:
<code>effective = host override ?? client default</code>. Show when a host is overriding
the client default, and provide a clear reset action.
</p>
<p>
In the settings navigation, keep client settings as the default top-level destination.
Add a <code>Servers / Hosts</code> area for saved local, SSH, runtime, and VM hosts. From
the sidebar, <code>Manage Host</code> should deep-link directly to the selected host
details, not change the meaning of the entire Settings page.
</p>
<p>
For settings that can vary by host, a scoped selector is enough: a dropdown such as
<code>Apply to: Client default / Local Mac / Windows VM / Linux Builder</code>. This is
similar to Windows settings surfaces that let users choose whether a setting applies to
Windows or WSL. The key rule is that the selected settings scope must be visible in the
control itself, not inferred from sidebar focus.
</p>
</section>
<section>
<h2>How SSH Fits</h2>
<div class="callout decision">
SSH should be treated as a first-class host type. It is not a side feature. It is the
closest existing implementation to the desired multi-host model.
</div>
<p>
SSH already has many of the properties this design needs: target-keyed connection state,
repo-level <code>connectionId</code>, per-target remote workspace snapshots, SSH-aware PTY
IDs, remote file operations, and port forwarding. The new model should give those pieces a
clearer product wrapper rather than replacing them.
</p>
<h3>Host identity</h3>
<p>Conceptually, host IDs should look like this:</p>
<ul>
<li><code>local</code></li>
<li><code>ssh:&lt;targetId&gt;</code></li>
<li><code>runtime:&lt;environmentId&gt;</code></li>
</ul>
<p>
An SSH connection is both a saved connection profile and an execution host. The connection
profile stores how to reach the machine. The execution host identity is what repos,
worktrees, PTYs, agents, browser sessions, and file operations route through.
</p>
<p>
A VM is not a special case here. A Windows VM reached over SSH is
<code>ssh:&lt;targetId&gt;</code>. A cloud VM running a paired Orca runtime is
<code>runtime:&lt;environmentId&gt;</code>. A future Orca-managed VM should still expose a
host ID and adapter rather than leaking cloud-provider details into workspace routing.
</p>
<h3>SSH lifecycle</h3>
<table>
<thead>
<tr>
<th>State</th>
<th>Sidebar representation</th>
<th>User action</th>
</tr>
</thead>
<tbody>
<tr>
<td>Saved, never connected</td>
<td>Host appears as available to connect, with no live workspaces yet.</td>
<td>Connect, edit details, or create workspace on host.</td>
</tr>
<tr>
<td>Connected</td>
<td>Host section shows health, active workspaces, live ports, and agent count.</td>
<td>Create/open work, launch terminals, manage host, disconnect.</td>
</tr>
<tr>
<td>Needs auth</td>
<td>Host section remains visible with a clear auth-needed status.</td>
<td>Enter passphrase/password or edit credentials.</td>
</tr>
<tr>
<td>Offline/reconnecting</td>
<td>Previously known workspaces remain visible but disabled where necessary.</td>
<td>Retry, inspect error, edit connection, or remove host.</td>
</tr>
<tr>
<td>Removed</td>
<td>Host disappears after confirmation; cached host sessions are cleaned safely.</td>
<td>Explicit destructive action only.</td>
</tr>
</tbody>
</table>
<h3>SSH in the sidebar</h3>
<p>
A saved SSH target appears as a host section, not merely as a small badge on a project.
The transport can still be visible in secondary text:
</p>
<ul>
<li><code>Windows VM · SSH · Connected · 2 agents</code></li>
<li><code>Linux Builder · SSH · Reconnecting</code></li>
<li><code>Remote QA Box · SSH · Needs passphrase</code></li>
</ul>
<h3>Host Details for SSH</h3>
<ul>
<li>Connection label, hostname, username, port, and auth method.</li>
<li>Reconnect status, last error, passphrase/password prompts.</li>
<li>Remote default clone/workspace directory.</li>
<li>Detected shells, installed coding agents, and terminal capabilities.</li>
<li>Remote ports, relay capability, server/relay version, and health.</li>
<li>Danger actions: disconnect, remove host, clear cached sessions.</li>
</ul>
<h3>Keep adapters distinct</h3>
<p>
The UI and routing language should converge, but the implementation should not force SSH
and paired runtime servers into one transport too early. Use an adapter shape:
</p>
<ul>
<li><code>{ kind: 'local' }</code></li>
<li><code>{ kind: 'ssh', targetId }</code></li>
<li><code>{ kind: 'runtime', environmentId }</code></li>
</ul>
<p>
Features ask "which host owns this worktree?" and dispatch through the right adapter. That
preserves SSH-specific filesystem, PTY, relay, and port-forward behavior while making the
product feel coherent.
</p>
<h3>SSH-specific risks</h3>
<ul>
<li>Passphrase-protected targets may be deferred; do not hide their restored workspaces.</li>
<li>Windows SSH hosts use different path and shell semantics; never assume local POSIX paths.</li>
<li>Relay capabilities may differ by version; Host Details should surface unsupported features.</li>
<li>Remote PTY session IDs must remain connection-scoped so reconnects do not attach the wrong host.</li>
<li>SSH targets may not run the same Orca relay or agent versions as local; gate features by capability.</li>
</ul>
</section>
<section>
<h2>Architecture Direction</h2>
<h3>Introduce execution host identity</h3>
<p>
Add a normalized host/routing key that can represent local, SSH, runtime, and future VM
hosts.
</p>
<ul>
<li><code>local</code></li>
<li><code>ssh:&lt;targetId&gt;</code></li>
<li><code>runtime:&lt;environmentId&gt;</code></li>
</ul>
<h3>Bind work to hosts</h3>
<ul>
<li>Repos carry host identity.</li>
<li>Worktrees derive host identity from repo.</li>
<li>Terminal/browser/editor operations route by worktree host, not by global app mode.</li>
<li>New workspace creation uses focused/default host only as an initial value.</li>
</ul>
<h3>Data ownership rules</h3>
<table>
<thead>
<tr>
<th>Data</th>
<th>Owner</th>
<th>Rule</th>
</tr>
</thead>
<tbody>
<tr>
<td>Theme, keybindings, window/sidebar layout</td>
<td>Client</td>
<td>Always local. Host focus must not change what these settings mean.</td>
</tr>
<tr>
<td>Host registry and labels</td>
<td>Client</td>
<td>Local list of known hosts, with runtime/SSH-specific connection fields.</td>
</tr>
<tr>
<td>Paired runtime server configuration</td>
<td>Client</td>
<td>
The desktop client owns the saved server entry, connection policy, and user-facing
settings for that server. The server reports execution facts and capabilities.
</td>
</tr>
<tr>
<td>Repos and worktrees</td>
<td>Host</td>
<td>Every repo/worktree must carry or derive host identity.</td>
</tr>
<tr>
<td>Terminal/browser handles</td>
<td>Host</td>
<td>IDs must be host-scoped and routed through the owner host.</td>
</tr>
<tr>
<td>Agent detection and account availability</td>
<td>Host or client depending on runtime</td>
<td>Show source clearly. Installed tools on a VM are not the same as local tools.</td>
</tr>
</tbody>
</table>
<h3>Source of truth rules</h3>
<ul>
<li>
The local client owns the list of known hosts, display labels, sidebar scope, and UI
preferences.
</li>
<li>
The owning host owns execution facts: filesystem, shell, git state, running terminals,
live browser sessions, ports, and installed agents.
</li>
<li>
Repo/worktree records must include enough host identity to route without consulting the
current sidebar scope.
</li>
<li>
Cached remote snapshots are client-side projections of host-owned state. They can render
offline UI, but they are not permission to mutate remote state while disconnected.
</li>
</ul>
<h3>Partition session state</h3>
<p>
Move from one global session blob toward host-partitioned session slices. The renderer can
still render one unified workbench, but persistence and routing should be host-aware.
</p>
<ul>
<li><code>workspaceSessionsByHost.local</code></li>
<li><code>workspaceSessionsByHost.ssh:windows-vm</code></li>
<li><code>workspaceSessionsByHost.runtime:prod-builder</code></li>
</ul>
<h3>Terminal and session identity direction</h3>
<p>
Existing SSH PTY IDs already point toward the right model:
<code>src/shared/ssh-pty-id.ts</code> embeds the SSH connection ID into app-visible PTY
IDs so relay-local IDs cannot collide after restore. Runtime and local sessions should
converge on the same principle.
</p>
<ul>
<li>
Prefer structured ownership at API boundaries: <code>{ hostId, sessionId }</code> for
new code and persisted records.
</li>
<li>
Keep string wrappers for compatibility where existing tab/layout maps require string
keys, but make them parseable and host-scoped.
</li>
<li>
Do not let cleanup paths infer runtime ownership from
<code>getActiveRuntimeTarget(get().settings)</code>. A terminal stop/kill must use the
owner host of the worktree, tab, or PTY being closed.
</li>
<li>
Audit maps such as <code>ptyIdsByTabId</code>, <code>terminalLayoutsByTabId</code>,
browser page maps, and tab order maps for assumptions that IDs are globally unique.
</li>
</ul>
<h3>Stop using global active runtime for mutations</h3>
<p>
<code>activeRuntimeEnvironmentId</code> should not decide where a terminal, file operation,
git action, browser page, or agent launch goes. The owning worktree should decide.
</p>
<h3>Compatibility bridge</h3>
<p>
During migration, keep a compatibility layer where <code>activeRuntimeEnvironmentId</code>
can still provide a default host for legacy flows. New code should ask for host identity
from the repo/worktree/tab owner first, then fall back to the focused/default host only for
creation flows.
</p>
<p>
Existing remote-server users should not experience a jarring migration. Their current
active runtime becomes the focused/default host. The main visible change is additive: they
may now also see local-machine workspaces when using <code>All hosts</code>, and can narrow
back to the remote server by selecting only that host.
</p>
<h3>Version skew and capability gating</h3>
<p>
Multi-host Orca must assume hosts can run different Orca builds. A new desktop client may
connect to an old runtime server, an old desktop client may connect to a newer server, and
two remote hosts may expose different feature sets at the same time.
</p>
<p>
The current runtime compatibility foundation is already the right shape:
<code>src/shared/protocol-version.ts</code> declares protocol versions and capabilities,
<code>src/shared/protocol-compat.ts</code> evaluates whether client/server protocol ranges
overlap, and <code>runtime.status.get</code> returns optional protocol and capability
fields. The host model should promote that from a hidden preflight into visible per-host
state.
</p>
<p>
The important rule is that <strong>Orca app versions are diagnostic labels, not the
compatibility contract</strong>. A desktop client at app version 1.8.0 should be allowed
to use a server at app version 1.7.2 if their runtime protocol ranges overlap. Exact app
version equality would make remote hosts unnecessarily brittle, especially for SSH boxes,
long-lived VMs, and future cloud-hosted machines that may update on a different cadence
from the user's laptop.
</p>
<p>
Compatibility should be modeled as host state, not app state. Each host registry entry
needs a verdict such as <code>compatible</code>, <code>server-too-old</code>,
<code>client-too-old</code>, <code>unknown</code>, or <code>degraded</code>, plus the
capabilities that were actually advertised by that host. The sidebar, Settings, worktree
actions, terminal launch, browser actions, hosted review polling, and background agents
should all ask the owning host for this verdict before mutating anything.
</p>
<h4>Handshake model</h4>
<ol>
<li>
When a host connects, call its cheapest status endpoint first, such as
<code>runtime.status.get</code> for runtime servers or the SSH relay status equivalent
for SSH hosts.
</li>
<li>
Record <code>appVersion</code>, <code>runtimeProtocolVersion</code>,
<code>minCompatibleRuntimeClientVersion</code>, <code>capabilities</code>,
platform, relay version, health, and last successful check time on the host registry
entry.
</li>
<li>
Evaluate protocol compatibility per host. A host is hard-blocked only when the client is
below the server's minimum client protocol, or the server is below the client's minimum
server protocol.
</li>
<li>
Evaluate optional features from <code>capabilities</code>. Missing capabilities disable
only the specific feature, not the whole host.
</li>
<li>
Cache the verdict per host and re-check on reconnect, explicit refresh, server update,
relay reinstall, or status-version change.
</li>
</ol>
<table>
<thead>
<tr>
<th>Scenario</th>
<th>Expected behavior</th>
<th>UI treatment</th>
</tr>
</thead>
<tbody>
<tr>
<td>New client, old compatible server</td>
<td>
Connect normally. Older servers may omit newer fields; clients must treat missing
optional fields as unavailable, not corrupt. Enable only features advertised by the
server's <code>capabilities</code>.
</td>
<td>
Host is healthy. Newer unsupported features are hidden or disabled with a short
"requires newer server" explanation.
</td>
</tr>
<tr>
<td>New client, too-old server</td>
<td>
Block runtime RPCs after the status handshake reports <code>server-too-old</code>.
Keep cached host sidebar/session state visible where possible, but do not launch
terminals, agents, git mutations, browser actions, or file writes against that host.
</td>
<td>
Host header shows <code>Update server</code>. Host Details shows server protocol,
required protocol, and update guidance.
</td>
</tr>
<tr>
<td>Old client, newer server</td>
<td>
Server should allow the client if the client's protocol is within the server's
supported range. Otherwise the server reports <code>client-too-old</code> before
accepting mutating calls.
</td>
<td>
Host header shows <code>Update client</code> when blocked. Do not attempt partial
writes after a hard incompatibility verdict.
</td>
</tr>
<tr>
<td>Mixed compatible hosts</td>
<td>
Each host gets its own status and capability set. One old host must not downgrade
the UI for another newer host.
</td>
<td>
Disable unsupported actions only inside the owning host/workspace. Keep other hosts
fully functional.
</td>
</tr>
<tr>
<td>SSH relay or agent-hook skew</td>
<td>
Treat relay/agent-hook versions as host capabilities. Terminal/file basics can work
while newer agent attribution, port detection, orchestration, or browser features
are unavailable.
</td>
<td>
Host Details lists relay version, detected agents, missing capabilities, and repair
actions.
</td>
</tr>
</tbody>
</table>
<ul>
<li>Never require exact app-version equality between client and server.</li>
<li>Use protocol range overlap for hard allow/block decisions.</li>
<li>Use capabilities for additive feature availability.</li>
<li>
Treat missing protocol fields as protocol 0 so pre-contract servers produce a clear
update-server path instead of mysterious runtime failures.
</li>
<li>
Prefer read-only cached visibility for incompatible hosts. Users should still understand
that the host exists and why it is unavailable.
</li>
<li>
Cache compatibility per host, not globally, and invalidate it on reconnect, server
update, or status version change.
</li>
<li>
Host Details should expose app version, protocol version, minimum compatible peer
version, and capabilities for debugging.
</li>
</ul>
<h4>Operational contract for mismatched versions</h4>
<p>
The client should treat every remote host as an independent compatibility island. A new
client connected to an old server, an old client connected to a new server, and a fully
current local host can all coexist in the same sidebar. Compatibility must be checked at
the host boundary before the host is allowed to receive mutating commands.
</p>
<ol>
<li>
<strong>Status-only bootstrap:</strong> the first call to a remote host must be a
backwards-compatible status handshake. It should not depend on the full runtime API
being compatible. If even the status endpoint is unavailable, classify the host as
<code>unknown</code> or <code>needs update</code> and keep cached read-only state visible.
</li>
<li>
<strong>Hard compatibility verdict:</strong> compare the client's protocol range with
the host's reported protocol range. If they do not overlap, the host remains visible but
disabled for terminals, agents, git writes, file writes, browser actions, and settings
mutations.
</li>
<li>
<strong>Soft capability verdict:</strong> if the protocol range is compatible, use the
host's advertised capabilities to enable or disable individual features. This is how a
new desktop client can safely work with an older but still compatible server.
</li>
<li>
<strong>Per-command guard:</strong> every host-owned mutation should check the host
compatibility/capability verdict at dispatch time, not only when rendering the button.
UI hiding prevents confusion; dispatch guards prevent stale UI or background workflows
from writing to an incompatible host.
</li>
<li>
<strong>Update direction:</strong> when blocked, the host registry should preserve a
reason code such as <code>server-too-old</code>, <code>client-too-old</code>, or
<code>missing-capability</code>. The sidebar badge and Host Details page can then say
exactly whether the user should update the desktop client, update/reinstall the remote
server or relay, or simply accept that one optional feature is unavailable on that host.
</li>
<li>
<strong>Owner-scoped fallback:</strong> if an older server cannot answer a newer API,
the client may fall back only for the host that owns the request, and only to a known
compatible older endpoint. It must not silently reroute the request to the focused host
or to the local machine.
</li>
</ol>
<div class="callout warning">
A mismatched remote host should never force the whole Orca client into a degraded mode.
In <code>All hosts</code>, incompatible hosts are visible with blocked actions while
compatible hosts continue working normally. When the visible host filter contains only an
incompatible host, the main view should show the blocked state for that host and offer
repair/update actions instead of silently falling back to local execution.
</div>
<h3>Implementation inventory to start from</h3>
<table>
<thead>
<tr>
<th>Area</th>
<th>What to inspect</th>
<th>Desired direction</th>
</tr>
</thead>
<tbody>
<tr>
<td>Runtime switching</td>
<td><code>activeRuntimeEnvironmentId</code>, <code>getActiveRuntimeTarget</code></td>
<td>Use only as focused/default host compatibility, not mutation routing.</td>
</tr>
<tr>
<td>SSH ownership</td>
<td><code>connectionId</code>, SSH target maps, remote workspace projections</td>
<td>Promote existing target-keyed concepts into host registry entries.</td>
</tr>
<tr>
<td>PTY and browser sessions</td>
<td>Terminal IDs, browser page IDs, relay/session IDs</td>
<td>Namespace or validate by execution host before attach, close, or mutate.</td>
</tr>
<tr>
<td>Sidebar state</td>
<td>Workspace filters, grouping, project filters, selected workspace</td>
<td>
Add the Hosts visibility filter above existing organization controls; keep other
filters inside the visible host set.
</td>
</tr>
<tr>
<td>Settings</td>
<td>Local settings store, runtime settings, SSH target editing</td>
<td>Separate client settings from Host Details and inherited host overrides.</td>
</tr>
</tbody>
</table>
</section>
<section>
<h2>Phased Plan</h2>
<div class="phase">
<div class="phase-label">Phase 0</div>
<div>
<h3>Design the host identity layer</h3>
<p>
Define <code>ExecutionHostId</code>, host helpers, migration notes, and a grep map of
call sites that currently read <code>activeRuntimeEnvironmentId</code>.
</p>
</div>
</div>
<div class="phase">
<div class="phase-label">Phase 1</div>
<div>
<h3>Separate connection management from workbench focus</h3>
<p>
Keep saved servers in Settings, but stop presenting one exclusive active server as
the whole app mode. Add focused/default host state for new work only.
</p>
</div>
</div>
<div class="phase">
<div class="phase-label">Phase 2</div>
<div>
<h3>Keep terminals alive across local/server focus changes</h3>
<p>
Preserve runtime terminal/browser handles when changing focus. Do not close remote
resources unless the user disconnects or removes a host.
</p>
</div>
</div>
<div class="phase">
<div class="phase-label">Phase 3</div>
<div>
<h3>Support concurrent local plus runtime and SSH hosts</h3>
<p>
Route repo/worktree/terminal operations by owner host. The full destination is grouped
local, SSH, and runtime hosts in one sidebar, with the visible host set as a filter.
</p>
</div>
</div>
<div class="phase">
<div class="phase-label">Phase 4</div>
<div>
<h3>Generalize to many runtime servers</h3>
<p>
Use host-keyed connection maps, per-host health, and per-host session persistence.
Mirror the per-target discipline SSH already has.
</p>
</div>
</div>
<div class="phase">
<div class="phase-label">Phase 5</div>
<div>
<h3>Unify local, SSH, runtime, and VM UI</h3>
<p>
Show host-grouped workspaces in the sidebar. Make SSH targets and paired runtime
servers feel like peers while preserving transport-specific implementation.
</p>
</div>
</div>
<div class="phase">
<div class="phase-label">Phase 6</div>
<div>
<h3>Add optional detached host windows</h3>
<p>
Only after the host registry and session partitioning are solid, allow a host to be
opened in another window for power users.
</p>
</div>
</div>
</section>
<section>
<h2>Risks</h2>
<div class="callout warning">
The main engineering risk is not UI. It is preventing work from being routed to the wrong
machine once multiple hosts are alive at the same time.
</div>
<ul>
<li>
<strong>ID collisions:</strong> repo, worktree, tab, browser, and PTY IDs from many
hosts may coexist. IDs must be namespaced or validated by host.
</li>
<li>
<strong>Routing mistakes:</strong> file, git, terminal, browser, and agent operations
must route from worktree ownership, not a global active setting.
</li>
<li>
<strong>Session corruption:</strong> mixed-host state in one session blob can overwrite
or mis-hydrate remote work.
</li>
<li>
<strong>Protocol skew:</strong> different hosts may run different Orca versions and
capabilities. Compatibility and feature availability must be evaluated per host.
</li>
<li>
<strong>Security boundaries:</strong> auth/account settings and command execution must
not leak between hosts.
</li>
<li>
<strong>Resource use:</strong> many live hosts means many terminals, browser pages, and
subscriptions. Do not optimize this first, but leave room for later throttling, sleep,
and resource-budget policies.
</li>
<li>
<strong>UX ambiguity:</strong> users must always know which host owns a workspace and
which settings scope they are editing.
</li>
</ul>
</section>
<section>
<h2>Immediate Follow-ups</h2>
<ol>
<li>
Write a design doc for <code>ExecutionHostId</code>, host registry, session
partitioning, and migration.
</li>
<li>
Audit every <code>activeRuntimeEnvironmentId</code> read and classify it as default
host, owner host, or client setting.
</li>
<li>
Prototype a host-grouped sidebar with local plus one remote server.
</li>
<li>
Add a Host Details panel for saved servers without duplicating the full Settings page.
</li>
<li>
Preserve remote terminals when changing focus between local and remote contexts.
</li>
<li>
Fix the terminal-read bug where Claude Code's visible full-screen TUI output can appear
as blank lines through <code>orca terminal read</code>.
</li>
<li>
Add operator safeguards: do not close live agent terminals solely because transcript
output is quiet or blank.
</li>
</ol>
</section>
<section>
<h2>Acceptance Criteria</h2>
<ul>
<li>
A local-only user with no saved remote hosts sees no host visibility strip and no extra
sidebar complexity.
</li>
<li>
A user can connect Local Mac plus at least one SSH or runtime host and see both in one
sidebar under <code>All hosts</code>.
</li>
<li>
Selecting one host, multiple hosts, or sticky <code>All hosts</code> changes visible
workspaces and creation defaults without closing terminals, browser pages, or agents on
other hosts.
</li>
<li>
Settings always makes the edited scope visible: client, host, or repo/worktree.
</li>
<li>
SSH targets with expired auth or offline status still show restored workspaces and a
clear reconnect path.
</li>
<li>
A host with an incompatible server version is visible but blocked from unsafe mutations,
with clear update guidance in the host header and Host Details.
</li>
<li>
Optional features are enabled per host based on capabilities; one older host does not
disable newer features on another host.
</li>
<li>
File, git, terminal, browser, and agent actions route by the workspace owner host, not
by the current visible host filter.
</li>
<li>
Removing or disconnecting a host is the only path that intentionally tears down that
host's live remote resources.
</li>
</ul>
</section>
<section>
<h2>Agent Implementation Notes</h2>
<p>
A future agent picking this up should treat this as a multi-step architecture change, not a
sidebar-only feature. The UI can ship incrementally, but the safety boundary is routing.
</p>
<ul>
<li>
Start with a call-site inventory for <code>activeRuntimeEnvironmentId</code>,
<code>getActiveRuntimeTarget</code>, <code>connectionId</code>, and runtime/SSH PTY ID
helpers.
</li>
<li>
Classify each call site as: client setting, creation default, owner-host mutation,
status display, cache key, or compatibility fallback.
</li>
<li>
Add tests before broad rewrites. Regressions here can destroy terminals or route writes
to the wrong machine.
</li>
<li>
Preserve SSH behavior while making it more visible. Do not regress passphrase-deferred
reconnects, Windows remote paths, or remote port forwarding.
</li>
<li>
Avoid destructive switching in prototypes. A host focus change should never call close
on old host terminals unless explicitly testing disconnect/remove behavior.
</li>
<li>
If UI changes are made, verify local-only users still see the current simple sidebar
with no host-scope chrome.
</li>
</ul>
</section>
<section>
<h2>Resolved Decisions and Remaining Unknowns</h2>
<h3>Resolved decisions</h3>
<ul>
<li>VM is a loose host label, usually backed by SSH or paired runtime.</li>
<li>The design target is full end-to-end implementation, not an intentionally narrow MVP.</li>
<li>
In <code>All hosts</code>, new workspace creation should use the focused/default host
but make the target host visible and changeable.
</li>
<li>
Host-varying settings can use a visible host dropdown rather than a duplicate Settings app.
</li>
<li>The desktop client owns paired runtime server configuration.</li>
<li>
Existing active-runtime users migrate to a focused/default host and can still narrow the
app back to that server.
</li>
<li>
Background host resource policy can wait, as long as routing and teardown semantics leave
room for future throttling.
</li>
</ul>
<h3>Remaining unknowns</h3>
<ul>
<li>
Exact read-only behavior for offline hosts: recommended default is show cached sidebar,
tabs, terminal history, and metadata; disable filesystem/git/terminal mutations until
reconnect; allow local-only metadata edits such as rename/remove.
</li>
<li>
Exact shape of the host-scoped session ID migration: structured
<code>{ hostId, sessionId }</code> should be the destination, with parseable string
wrappers where legacy maps require string keys.
</li>
<li>Which status signals define "quiet but alive" for agent terminals?</li>
<li>
How future Orca-managed cloud hosts are provisioned, billed, and surfaced. They should
still conform to the same host registry and execution-owner model.
</li>
</ul>
</section>
<div class="footer">
Created from the issue #4697 investigation and agent reviews. Recommended next artifact:
a design doc with concrete type definitions, persistence migration, and routing call-site
inventory.
</div>
</main>
</body>
</html>