Skip to content

Commit c48ed95

Browse files
authored
feat(ui): merge Instances into a unified Devices page (#104561)
* chore(i18n): regenerate locale bundles for Devices rename * chore(i18n): regenerate locale bundles for Devices rename * chore(i18n): sync locale metadata for Devices rename * fix(gateway): stop tracking ephemeral control-plane clients in presence * feat(ui): merge Instances into a unified Devices page * docs: point Instances references at the unified Devices page * chore(i18n): reconcile locale metadata after rebase * fix(gateway): keep test-mode clients tracked in presence; refresh docs map
1 parent aee9382 commit c48ed95

74 files changed

Lines changed: 775 additions & 772 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/channels/pairing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ creates a device pairing request that must be approved.
113113
Use an already connected Control UI session with `operator.admin` access:
114114

115115
1. Open the Control UI and select **Nodes**.
116-
2. In **Nodes & devices**, click **Pair mobile device**.
116+
2. On the **Devices** page, click **Pair mobile device**.
117117
3. On your phone, open the OpenClaw app → **Settings****Gateway**.
118118
4. Scan the QR code or paste the setup code, then connect.
119119

docs/concepts/presence.md

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
summary: "How OpenClaw presence entries are produced, merged, and displayed"
33
read_when:
4-
- Debugging the Instances tab
4+
- Debugging live status on the Control UI Devices page
55
- Investigating duplicate or stale instance rows
66
- Changing gateway WS connect or system-event beacons
77
title: "Presence"
@@ -10,10 +10,10 @@ title: "Presence"
1010
OpenClaw "presence" is a lightweight, best-effort view of:
1111

1212
- the **Gateway** itself, and
13-
- **clients connected to the Gateway** (mac app, WebChat, CLI, etc.)
13+
- **user-visible clients connected to the Gateway** (mac app, WebChat, nodes, etc.)
1414

15-
Presence is used primarily to render the macOS app's **Instances** tab and to
16-
provide quick operator visibility.
15+
Presence renders live connection metadata in the Control UI **Devices** page
16+
and the macOS app's **Instances** tab.
1717

1818
## Presence fields (what shows up)
1919

@@ -44,10 +44,12 @@ even before any clients connect.
4444
Every WS client begins with a `connect` request. On successful handshake the
4545
Gateway upserts a presence entry for that connection.
4646

47-
#### Why one-off CLI commands do not show up
47+
#### Why ephemeral control-plane connections do not show up
4848

49-
The CLI often connects for short, one-off commands. To avoid spamming the
50-
Instances list, `client.mode === "cli"` is **not** turned into a presence entry.
49+
CLI commands, backend RPC clients, and probes often connect briefly. To avoid
50+
retaining that churn for the full presence TTL, clients in `cli`, `backend`,
51+
or `probe` mode are **not** turned into presence entries. Test-mode clients
52+
stay tracked because test suites use them as stand-ins for real clients.
5153

5254
### 3) `system-event` beacons
5355

@@ -65,10 +67,10 @@ Presence entries are stored in a single in-memory map, keyed case-insensitively
6567
by the first available of, in order: a paired device id, `connect.client.instanceId`,
6668
or the per-connection id as a last resort.
6769

68-
CLI clients are excluded from tracking entirely (see above), so their
69-
connection id never becomes a key. For every other client, the connection id
70-
fallback means a client that reconnects without a stable `instanceId` shows up
71-
as a **duplicate** row.
70+
Ephemeral control-plane clients are excluded from tracking entirely (see
71+
above), so their connection ids never become keys. For every other client, the
72+
connection id fallback means a client that reconnects without a stable
73+
`instanceId` shows up as a **duplicate** row.
7274

7375
## TTL and bounded size
7476

@@ -89,6 +91,12 @@ into the entry.
8991

9092
## Consumers
9193

94+
### Control UI Devices page
95+
96+
The **Devices** page joins `system-presence` with durable pairing and node
97+
records. It pins the Gateway self beacon first and uses matching device or
98+
instance ids for live platform, version, model, and input-recency metadata.
99+
92100
### macOS Instances tab
93101

94102
The macOS app renders the output of `system-presence` and applies a small status

docs/concepts/typing-indicators.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ Override mode or cadence per session:
6565

6666
<CardGroup cols={2}>
6767
<Card title="Presence" href="/concepts/presence" icon="signal">
68-
How the Gateway tracks connected clients and surfaces them in the macOS Instances tab.
68+
How the Gateway tracks connected clients for the Control UI Devices page and macOS Instances tab.
6969
</Card>
7070
<Card title="Streaming and chunking" href="/concepts/streaming" icon="bars-staggered">
7171
Outbound streaming behavior, chunk boundaries, and channel-specific delivery.

docs/docs_map.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2715,13 +2715,14 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
27152715
- H2: Producers (where presence comes from)
27162716
- H3: 1) Gateway self entry
27172717
- H3: 2) WebSocket connect
2718-
- H4: Why one-off CLI commands do not show up
2718+
- H4: Why ephemeral control-plane connections do not show up
27192719
- H3: 3) system-event beacons
27202720
- H3: 4) Node connects (role: node)
27212721
- H2: Merge + dedupe rules (why instanceId matters)
27222722
- H2: TTL and bounded size
27232723
- H2: Remote/tunnel caveat (loopback IPs)
27242724
- H2: Consumers
2725+
- H3: Control UI Devices page
27252726
- H3: macOS Instances tab
27262727
- H2: Debugging tips
27272728
- H2: Related

docs/platforms/ios.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Gateway has not been configured yet, run `openclaw onboard` first so setup-code
4343
creation has a token or password auth path.
4444

4545
2. Open the [Control UI](/web/control-ui), select **Nodes**, and click
46-
**Pair mobile device** in the **Nodes & devices** card.
46+
**Pair mobile device** on the **Devices** page.
4747

4848
3. In the iOS app, open **Settings** -> **Gateway**, scan the QR code (or paste
4949
the setup code), and connect.

docs/tools/exec.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ openclaw config get agents.list
154154
openclaw config set 'agents.list[0].tools.exec.node' "node-id-or-name"
155155
```
156156

157-
Control UI: the Nodes tab includes a small "Exec node binding" panel for the same settings.
157+
Control UI: the **Devices** page includes a small "Exec node binding" panel for the same settings.
158158

159159
## Session overrides (`/exec`)
160160

docs/web/control-ui.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ An already paired administrator can create the iOS/Android connection QR without
7070

7171
<Steps>
7272
<Step title="Open mobile pairing">
73-
Select **Nodes**, then click **Pair mobile device** in the **Nodes & devices** card.
73+
Select **Devices**, then click **Pair mobile device** in the **Devices** card.
7474
</Step>
7575
<Step title="Connect the phone">
7676
In the OpenClaw mobile app, open **Settings** → **Gateway** and scan the QR code. You can copy and paste the setup code instead.
@@ -180,21 +180,20 @@ A **Search** field at the top of the sidebar opens the command palette (⌘K). T
180180
- Activity tab with browser-local, redaction-first summaries of live tool activity from existing `session.tool` / tool event delivery.
181181

182182
</Accordion>
183-
<Accordion title="Channels, instances, sessions, dreams">
183+
<Accordion title="Channels, sessions, dreams">
184184
- Channels: built-in plus bundled/external plugin channels status, QR login, and per-channel config (`channels.status`, `web.login.*`, `config.patch`).
185185
- Channel probe refreshes keep the previous snapshot visible while slow provider checks finish, and label partial snapshots when a probe or audit exceeds its UI budget.
186-
- Instances: presence list and refresh (`system-presence`).
187186
- Sessions: list configured-agent sessions by default, pin frequent sessions, rename them, archive or restore inactive sessions, fall back from stale unconfigured agent session keys, and apply per-session model/thinking/fast/verbose/trace/reasoning overrides (`sessions.list`, `sessions.patch`). Pinned sessions sort above recent unpinned sessions; archived sessions live in the Sessions page's archived view and keep their transcripts. Rows show an unread dot for sessions with activity since their last read, with mark-unread/mark-read actions (`sessions.patch { unread }`), and a Fork action that branches the transcript into a new session (`sessions.create { parentSessionKey, fork: true }`). Overview tiles above the table summarize the loaded roster (session count, live runs, unread sessions, total tokens), each row carries a kind glyph with a live-run dot, status renders as a plain dot plus label, and the Tokens column shows a context-window usage meter when the session reports token and context sizes. Row management actions live in a per-row menu (kebab button or right-click) mirroring the sidebar's session menu, and the row drawer carries the agent runtime and run duration alongside the other session details.
188187
- Session grouping: a Group by control organizes the sessions table into sections by custom groups, channel, kind, agent, or date. Custom groups persist per session via `sessions.patch` (`category`), so sessions started from message channels (Discord, Telegram, WhatsApp, ...) can be categorized too; assign groups by dragging rows onto a section, or with the per-row group selector, and create groups with the New group action.
189188
- Dreams: dreaming status, enable/disable toggle, and Dream Diary reader (`doctor.memory.status`, `doctor.memory.dreamDiary`, `config.patch`).
190189

191190
</Accordion>
192-
<Accordion title="Cron, tasks, plugins, skills, nodes, exec approvals">
191+
<Accordion title="Cron, tasks, plugins, skills, devices, exec approvals">
193192
- Cron jobs: list/add/edit/run/enable/disable plus run history (`cron.*`).
194193
- Tasks: live active and recent background task ledger with linked sessions and cancellation (`tasks.*`).
195194
- Plugins: browse the installed inventory and curated store, search ClawHub, install and remove plugin code, and enable or disable installed plugins (`plugins.*`); MCP server rows edit `mcp.servers` through the config methods.
196195
- Skills: status, enable/disable, install, API key updates (`skills.*`).
197-
- Nodes: one **Nodes & devices** inventory that joins paired device records with the node catalog (`node.list`, `device.pair.list`) — one entry per machine with roles, live link status, tokens, and capabilities. Duplicate pairings of the same client collapse into an expandable group, and **Clean up N stale** bulk-removes superseded pairings that are offline and were auto-approved (silent local or trusted-CIDR), so affected clients re-pair without user action. Entries can be removed (`node.pair.remove`, `device.pair.remove`), device pairing and node re-approvals handled inline (`device.pair.*`, `node.pair.approve`/`reject`), and mobile setup codes created from the same card.
196+
- Devices: one inventory joins paired device records, the node catalog, and live presence (`device.pair.list`, `node.list`, `system-presence`). The Gateway host is pinned first; paired clients show connection status, roles, tokens, capabilities, and commands. Duplicate pairings collapse into an expandable group, and **Clean up N stale** bulk-removes admin-confirmed offline duplicates that were auto-approved (silent local, trusted-CIDR, or SSH-verified) or predate approval provenance. Entries can be removed (`node.pair.remove`, `device.pair.remove`), device pairing and node re-approvals handled inline (`device.pair.*`, `node.pair.approve`/`reject`), and mobile setup codes created from the same card.
198197
- Exec approvals: edit gateway or node allowlists and ask policy for `exec host=gateway/node` (`exec.approvals.*`).
199198

200199
</Accordion>

scripts/control-ui-mock-dev.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -959,6 +959,49 @@ async function createChatPickerScenario(): Promise<ControlUiMockGatewayScenario>
959959
},
960960
],
961961
},
962+
"system-presence": [
963+
{
964+
host: "gateway-mock.local",
965+
ip: "192.168.1.10",
966+
version: "2026.6.11",
967+
platform: "macos 26.5.2",
968+
deviceFamily: "Mac",
969+
modelIdentifier: "Mac14,12",
970+
lastInputSeconds: 42,
971+
mode: "gateway",
972+
reason: "self",
973+
instanceId: "mock-gateway-instance",
974+
text: "Gateway: gateway-mock.local (192.168.1.10) · app 2026.6.11 · mode gateway · reason self",
975+
ts: baseTime,
976+
},
977+
{
978+
host: "Mac Studio",
979+
ip: "192.168.1.11",
980+
version: "2026.6.11",
981+
platform: "macos 26.5.2",
982+
deviceFamily: "Mac",
983+
modelIdentifier: "Mac15,14",
984+
lastInputSeconds: 177,
985+
mode: "node",
986+
reason: "periodic",
987+
deviceId: "a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90",
988+
instanceId: "a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90",
989+
roles: ["node"],
990+
text: "Node: Mac Studio (192.168.1.11) · app 2026.6.11 · last input 177s ago · mode node · reason periodic",
991+
ts: baseTime - 30_000,
992+
},
993+
{
994+
host: "openclaw-control-ui",
995+
version: "2026.6.11",
996+
platform: "macos 26.5.2",
997+
mode: "webchat",
998+
reason: "connect",
999+
roles: ["operator"],
1000+
instanceId: "mock-unpaired-webchat",
1001+
text: "Node: openclaw-control-ui · mode webchat",
1002+
ts: baseTime - 10_000,
1003+
},
1004+
],
9621005
"agents.files.get": {
9631006
cases: workspaceFileCases,
9641007
},

src/gateway/server/ws-connection/message-handler.post-connect-health.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,7 @@ describe("attachGatewayWsMessageHandler post-connect health refresh", () => {
625625
"hello_payload_prepared",
626626
"ready",
627627
]);
628+
expect(upsertPresenceMock).not.toHaveBeenCalled();
628629
});
629630

630631
it("does not mark local backend self-pairing clients as approval runtimes", async () => {

src/gateway/server/ws-connection/message-handler.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ import { roleScopesAllow } from "../../../shared/operator-scope-compat.js";
110110
import { recordRemoteNodeInfo, refreshRemoteNodeBins } from "../../../skills/runtime/remote.js";
111111
import {
112112
isBrowserOperatorUiClient,
113-
isGatewayCliClient,
113+
isEphemeralGatewayClient,
114114
isOperatorUiClient,
115115
isWebchatClient,
116116
} from "../../../utils/message-channel.js";
@@ -2152,7 +2152,9 @@ export function attachGatewayWsMessageHandler(params: GatewayWsMessageHandlerPar
21522152
connectParams.permissions = reconciliation.effectivePermissions;
21532153
}
21542154

2155-
const shouldTrackPresence = !isGatewayCliClient(connectParams.client);
2155+
// Presence lists user-visible clients/nodes. Ephemeral control-plane connections
2156+
// (CLI, backend RPC probes, tests) churn for the full TTL and stay excluded.
2157+
const shouldTrackPresence = !isEphemeralGatewayClient(connectParams.client);
21562158
const clientId = connectParams.client.id;
21572159
const instanceId = connectParams.client.instanceId;
21582160
const presenceKey = shouldTrackPresence ? (device?.id ?? instanceId ?? connId) : undefined;

0 commit comments

Comments
 (0)