@log0u7/opencode-mesh 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,14 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.6.0] - 2026-11-06
9
+
10
+ ### Added
11
+
12
+ - Live mesh sidebar: the TUI part polls a `mesh.snapshot` RPC (registered
13
+ server-side) every 15s and renders the node, one line per paired peer and
14
+ the active lock count. Hidden on v1 hosts and while the server is down.
15
+
8
16
  ## [0.5.1] - 2026-11-06
9
17
 
10
18
  ### Fixed
package/README.md CHANGED
@@ -12,6 +12,34 @@ Each node advertises itself over mDNS, pairs with explicit operator approval (pe
12
12
 
13
13
  `mesh_lock` is **local to your machine**: it is enforced between OpenCode sessions sharing this host's data directory (`~/.local/share/opencode-mesh/`). Locks are not part of the wire protocol, so a peer on another node does not see or respect them. Remote nodes coordinate through messages (`mesh_send`), not locks.
14
14
 
15
+ ## Why
16
+
17
+ One OpenCode instance is one brain. When a task outgrows a single session, or you want to parallelise across machines, sessions need to talk to each other, claim files without stepping on each other, and workers you can spawn and reap. opencode-mesh adds that layer:
18
+
19
+ - **Same machine:** sessions coordinate through a local registry - messages (`mesh_send` / `mesh_inbox`) and exclusive file claims (`mesh_lock`).
20
+ - **Across the room:** nodes advertise over mDNS and pair with explicit operator approval (per-peer bearer tokens, deny by default). Tailscale IPs are picked up automatically for cross-subnet pools.
21
+ - **Headless workers:** `mesh_spawn` starts an isolated OpenCode worktree per worker, tracks its lifecycle, captures its logs, and cleans up on dispose.
22
+
23
+ ```mermaid
24
+ flowchart LR
25
+ subgraph lan["LAN or Tailscale tailnet"]
26
+ N1["Node A - OpenCode + mesh"]
27
+ N2["Node B - OpenCode + mesh"]
28
+ N3["Node C - OpenCode + mesh"]
29
+ end
30
+ N1 <-- "mDNS discovery (node id + fingerprint only)" --> N2
31
+ N1 <-- "HTTP + per-peer bearer tokens (mesh_send / mesh_inbox)" --> N2
32
+ N2 <-- "mDNS + HTTP" --> N3
33
+ N1 <-- "mDNS + HTTP" --> N3
34
+ ```
35
+
36
+ ## Highlights
37
+
38
+ - **Spawn headless workers in one call.** `mesh_spawn(task, name)` starts an isolated OpenCode worktree per worker; `mesh_workers`, `mesh_worker_logs`, `mesh_worker_stop` and `mesh_worker_remove` track and reap them. A pool of N workers costs N worktrees, not N mesh nodes.
39
+ - **Deny by default.** Every endpoint requires a per-peer bearer token; pairing offers expire after 5 minutes; both sides must run `mesh_pair`; every mail carries an Ed25519 proof, so a leaked offer cannot install or rotate a live credential.
40
+ - **File claims between sessions.** `mesh_lock` gives exclusive claims on paths, enforced between sessions on the same machine, so two agents never edit the same file.
41
+ - **Deliberately simple wire protocol.** mDNS for discovery, bearer tokens for auth, two HTTP routes (`/mail`, `/status`), plain HTTP over loopback and trusted networks.
42
+
15
43
  ## Install
16
44
 
17
45
  Register the plugin in `opencode.json` (project or global `~/.config/opencode/opencode.json`):
@@ -26,7 +54,7 @@ Restart OpenCode. The package is installed automatically at startup into opencod
26
54
 
27
55
  ```json
28
56
  {
29
- "plugin": ["@log0u7/opencode-mesh@0.4.1"]
57
+ "plugin": ["@log0u7/opencode-mesh@0.5.1"]
30
58
  }
31
59
  ```
32
60
 
@@ -75,7 +103,7 @@ mesh_unlock(path="src/api.ts")
75
103
 
76
104
  | Tool | Args | Description |
77
105
  |---|---|---|
78
- | `mesh_status` | : local session registry, own address, paired peers, active locks. |
106
+ | `mesh_status` | | Local session registry, own address, paired peers, active locks. |
79
107
  | `mesh_pair` | `node_id?`, `token?`, `address?` | No args: emit a pairing offer (5 min ttl). With args: complete the peer's offer. |
80
108
  | `mesh_send` | `peer`, `subject`, `body` | Deliver a message to a peer's mailbox. |
81
109
  | `mesh_inbox` | `ack?` | Fetch unread messages; ack marks them read. |
@@ -86,6 +114,17 @@ mesh_unlock(path="src/api.ts")
86
114
 
87
115
  Spawn headless workers in isolated opencode worktrees (native worktree API), track them, and clean up:
88
116
 
117
+ ```mermaid
118
+ flowchart TB
119
+ O["Orchestrator session"]
120
+ subgraph workers["Headless workers, one worktree each"]
121
+ W1["Worker parser"]
122
+ W2["Worker tests"]
123
+ end
124
+ O -- "mesh_spawn(task, name)" --> workers
125
+ workers -- "mesh_workers / mesh_worker_logs / mesh_worker_stop / mesh_worker_remove" --> O
126
+ ```
127
+
89
128
  ```
90
129
  mesh_spawn(task="implement the parser", name="parser") # -> worker_id, worktree, branch
91
130
  mesh_workers() # lifecycle states, session ids
@@ -110,6 +149,22 @@ The plugin ships a TUI part (auto-loaded by OpenCode): a toast on load, a `/mesh
110
149
  - Tailnet addresses are surfaced by `mesh_status` when `tailscale` runs, for cross-subnet pools.
111
150
  - Messages travel over plain HTTP: there is no TLS layer. Loopback and trusted networks only.
112
151
 
152
+ The pairing handshake (both sides must run `mesh_pair`; a leaked offer alone installs nothing):
153
+
154
+ ```mermaid
155
+ sequenceDiagram
156
+ autonumber
157
+ participant A as Node A
158
+ participant B as Node B
159
+ A->>A: mesh_pair() emits an offer (5 min TTL)
160
+ Note over A,B: The operator relays the offer out of band (chat, not the mesh)
161
+ B->>A: mesh_pair(node_id, token, address, pk) completes the offer
162
+ B->>B: mesh_pair() emits its own offer
163
+ Note over A,B: Relayed out of band again
164
+ A->>B: mesh_pair(node_id, token, address, pk) completes it
165
+ Note over A,B: Pairing complete: per-peer bearer tokens on both sides
166
+ ```
167
+
113
168
  ## Development
114
169
 
115
170
  Requires Node >= 22.5 (`mise install`) and pnpm (corepack).
@@ -1 +1 @@
1
- {"version":3,"file":"plugin-v2.d.ts","sourceRoot":"","sources":["../src/plugin-v2.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AA2B1C,eAAO,MAAM,MAAM,eA6EjB,CAAC"}
1
+ {"version":3,"file":"plugin-v2.d.ts","sourceRoot":"","sources":["../src/plugin-v2.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AA6B1C,eAAO,MAAM,MAAM,eAyFjB,CAAC"}
package/dist/plugin-v2.js CHANGED
@@ -2,12 +2,14 @@ import { Plugin } from "@opencode/plugin";
2
2
  import { Bonjour } from "bonjour-service";
3
3
  import { openMeshDb } from "./lib/db.js";
4
4
  import { advertiseMesh, browseMesh } from "./lib/discovery.js";
5
- import { upsertDiscoveredPeer } from "./lib/peers.js";
5
+ import { listLocks } from "./lib/locks.js";
6
+ import { listPeers, upsertDiscoveredPeer } from "./lib/peers.js";
6
7
  import { loadOrCreateIdentity } from "./lib/identity.js";
7
8
  import { nodeSpawn, stopAllWorkers } from "./lib/spawn.js";
8
9
  import { v2WorktreeDomainClient } from "./lib/worktree.js";
9
10
  import { reconcileWorkers } from "./lib/workers.js";
10
11
  import { startMeshServer } from "./lib/serve.js";
12
+ import { MeshRpc, snapshotFromMesh } from "./rpc.js";
11
13
  import { buildToolSpecs } from "./tools.js";
12
14
  import { defaultDataDir, parseOptions } from "./options.js";
13
15
  const DEFAULT_PORT = 4399;
@@ -83,10 +85,20 @@ export const MeshV2 = Plugin.define({
83
85
  });
84
86
  }
85
87
  });
88
+ // Live snapshot for the TUI sidebar: one compact read over the mesh state.
89
+ const rpc = await ctx.rpc.register(MeshRpc, {
90
+ snapshot: async () => snapshotFromMesh({
91
+ identity,
92
+ peers: listPeers(db),
93
+ locks: listLocks(db, { now: Date.now() }),
94
+ server: `${address}:${port}`,
95
+ }),
96
+ });
86
97
  const cleanup = async () => {
87
98
  stopAllWorkers(db);
88
99
  advertiseHandle?.close();
89
100
  browseHandle?.close();
101
+ rpc.dispose();
90
102
  await new Promise((resolve) => server.close(() => resolve()));
91
103
  db.close();
92
104
  };
@@ -1 +1 @@
1
- {"version":3,"file":"plugin-v2.js","sourceRoot":"","sources":["../src/plugin-v2.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"plugin-v2.js","sourceRoot":"","sources":["../src/plugin-v2.ts"],"names":[],"mappings":"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"}
package/dist/rpc.d.ts ADDED
@@ -0,0 +1,77 @@
1
+ export type MeshSnapshot = {
2
+ node_id: string;
3
+ hostname: string;
4
+ /** Paired peers only: discovery alone never makes a peer sendable. */
5
+ peers: Array<{
6
+ node_id: string;
7
+ hostname: string;
8
+ }>;
9
+ active_locks: number;
10
+ };
11
+ /** Structural input: the mesh context (identity, listPeers, listLocks) satisfies this. */
12
+ export type SnapshotInput = {
13
+ identity: {
14
+ node_id: string;
15
+ hostname: string;
16
+ };
17
+ peers: ReadonlyArray<{
18
+ node_id: string;
19
+ hostname: string;
20
+ paired: boolean;
21
+ }>;
22
+ locks: ReadonlyArray<unknown>;
23
+ server: string;
24
+ };
25
+ export declare function snapshotFromMesh(data: SnapshotInput): MeshSnapshot;
26
+ /** Sidebar lines: the node, one line per paired peer, the active lock count. */
27
+ export declare function formatMeshLines(s: MeshSnapshot): string[];
28
+ export declare const MeshRpc: {
29
+ readonly id: "mesh";
30
+ readonly methods: {
31
+ readonly snapshot: {
32
+ readonly input: {
33
+ readonly type: "object";
34
+ readonly properties: {};
35
+ readonly additionalProperties: false;
36
+ };
37
+ readonly output: {
38
+ readonly type: "object";
39
+ readonly properties: {
40
+ readonly node_id: {
41
+ readonly type: "string";
42
+ readonly description: "This node's id";
43
+ };
44
+ readonly hostname: {
45
+ readonly type: "string";
46
+ readonly description: "This node's hostname";
47
+ };
48
+ readonly peers: {
49
+ readonly type: "array";
50
+ readonly items: {
51
+ readonly type: "object";
52
+ readonly properties: {
53
+ readonly node_id: {
54
+ readonly type: "string";
55
+ };
56
+ readonly hostname: {
57
+ readonly type: "string";
58
+ };
59
+ };
60
+ readonly required: readonly ["node_id", "hostname"];
61
+ readonly additionalProperties: false;
62
+ };
63
+ readonly description: "Paired peers";
64
+ };
65
+ readonly active_locks: {
66
+ readonly type: "number";
67
+ readonly description: "Active local file locks";
68
+ };
69
+ };
70
+ readonly required: readonly ["node_id", "hostname", "peers", "active_locks"];
71
+ readonly additionalProperties: false;
72
+ };
73
+ };
74
+ };
75
+ readonly events: {};
76
+ };
77
+ //# sourceMappingURL=rpc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rpc.d.ts","sourceRoot":"","sources":["../src/rpc.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,KAAK,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpD,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,0FAA0F;AAC1F,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,KAAK,EAAE,aAAa,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAC7E,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;IAC9B,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,aAAa,GAAG,YAAY,CASlE;AAED,gFAAgF;AAChF,wBAAgB,eAAe,CAAC,CAAC,EAAE,YAAY,GAAG,MAAM,EAAE,CAczD;AAED,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+BlB,CAAC"}
package/dist/rpc.js ADDED
@@ -0,0 +1,61 @@
1
+ import { Rpc } from "@opencode/plugin/rpc";
2
+ export function snapshotFromMesh(data) {
3
+ return {
4
+ node_id: data.identity.node_id,
5
+ hostname: data.identity.hostname,
6
+ peers: data.peers
7
+ .filter((p) => p.paired)
8
+ .map((p) => ({ node_id: p.node_id, hostname: p.hostname })),
9
+ active_locks: data.locks.length,
10
+ };
11
+ }
12
+ /** Sidebar lines: the node, one line per paired peer, the active lock count. */
13
+ export function formatMeshLines(s) {
14
+ const self = s.hostname.length > 0 ? s.hostname : s.node_id;
15
+ const lines = [`mesh: ${self}`];
16
+ if (s.peers.length === 0) {
17
+ lines.push("no paired peers");
18
+ }
19
+ else {
20
+ for (const peer of s.peers) {
21
+ lines.push(`↔ ${peer.hostname.length > 0 ? peer.hostname : peer.node_id}`);
22
+ }
23
+ }
24
+ if (s.active_locks > 0) {
25
+ lines.push(`${s.active_locks} active lock(s)`);
26
+ }
27
+ return lines;
28
+ }
29
+ export const MeshRpc = Rpc.define({
30
+ id: "mesh",
31
+ methods: {
32
+ snapshot: {
33
+ input: { type: "object", properties: {}, additionalProperties: false },
34
+ output: {
35
+ type: "object",
36
+ properties: {
37
+ node_id: { type: "string", description: "This node's id" },
38
+ hostname: { type: "string", description: "This node's hostname" },
39
+ peers: {
40
+ type: "array",
41
+ items: {
42
+ type: "object",
43
+ properties: {
44
+ node_id: { type: "string" },
45
+ hostname: { type: "string" },
46
+ },
47
+ required: ["node_id", "hostname"],
48
+ additionalProperties: false,
49
+ },
50
+ description: "Paired peers",
51
+ },
52
+ active_locks: { type: "number", description: "Active local file locks" },
53
+ },
54
+ required: ["node_id", "hostname", "peers", "active_locks"],
55
+ additionalProperties: false,
56
+ },
57
+ },
58
+ },
59
+ events: {},
60
+ });
61
+ //# sourceMappingURL=rpc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rpc.js","sourceRoot":"","sources":["../src/rpc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAC;AAsB3C,MAAM,UAAU,gBAAgB,CAAC,IAAmB;IAClD,OAAO;QACL,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAC,OAAO;QAC9B,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,QAAQ;QAChC,KAAK,EAAE,IAAI,CAAC,KAAK;aACd,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;aACvB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC7D,YAAY,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM;KAChC,CAAC;AACJ,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,eAAe,CAAC,CAAe;IAC7C,MAAM,IAAI,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;IAC5D,MAAM,KAAK,GAAG,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;IAChC,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;IAChC,CAAC;SAAM,CAAC;QACN,KAAK,MAAM,IAAI,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC;YAC3B,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QAC7E,CAAC;IACH,CAAC;IACD,IAAI,CAAC,CAAC,YAAY,GAAG,CAAC,EAAE,CAAC;QACvB,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,YAAY,iBAAiB,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,CAAC,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC;IAChC,EAAE,EAAE,MAAM;IACV,OAAO,EAAE;QACP,QAAQ,EAAE;YACR,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,EAAE,oBAAoB,EAAE,KAAK,EAAE;YACtE,MAAM,EAAE;gBACN,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE;oBACV,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,gBAAgB,EAAE;oBAC1D,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,sBAAsB,EAAE;oBACjE,KAAK,EAAE;wBACL,IAAI,EAAE,OAAO;wBACb,KAAK,EAAE;4BACL,IAAI,EAAE,QAAQ;4BACd,UAAU,EAAE;gCACV,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gCAC3B,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;6BAC7B;4BACD,QAAQ,EAAE,CAAC,SAAS,EAAE,UAAU,CAAC;4BACjC,oBAAoB,EAAE,KAAK;yBAC5B;wBACD,WAAW,EAAE,cAAc;qBAC5B;oBACD,YAAY,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,yBAAyB,EAAE;iBACzE;gBACD,QAAQ,EAAE,CAAC,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,cAAc,CAAC;gBAC1D,oBAAoB,EAAE,KAAK;aAC5B;SACF;KACF;IACD,MAAM,EAAE,EAAE;CACX,CAAC,CAAC"}
package/dist/tui.js CHANGED
@@ -1,6 +1,12 @@
1
+ import { insert as _$insert } from "@opentui/solid";
2
+ import { createElement as _$createElement } from "@opentui/solid";
3
+ import { createComponent as _$createComponent } from "@opentui/solid";
1
4
  /** @jsxImportSource @opentui/solid */
2
5
 
3
6
  import { Plugin } from "@opencode/plugin/tui";
7
+ import { For, Show, createSignal } from "solid-js";
8
+ import { MeshRpc, formatMeshLines } from "./rpc.js";
9
+ const REFRESH_MS = 15_000;
4
10
  export default Plugin.define({
5
11
  id: "@log0u7/opencode-mesh.tui",
6
12
  setup(context) {
@@ -28,9 +34,45 @@ export default Plugin.define({
28
34
  }
29
35
  }]
30
36
  }));
31
- return context.ui.slot({
37
+
38
+ // Live sidebar: poll the server-side snapshot over RPC. On a v1 host (no
39
+ // RPC) or while the server is unreachable, the slot stays hidden.
40
+ const [lines, setLines] = createSignal(null);
41
+ const mesh = context.client.rpc(MeshRpc);
42
+ const refresh = async () => {
43
+ try {
44
+ const snap = await mesh.snapshot({});
45
+ setLines(formatMeshLines(snap));
46
+ } catch {
47
+ setLines(null);
48
+ }
49
+ };
50
+ void refresh();
51
+ const timer = setInterval(() => void refresh(), REFRESH_MS);
52
+ const unregister = context.ui.slot({
32
53
  append: "sidebar.content",
33
- render: () => null
54
+ render: () => _$createComponent(Show, {
55
+ get when() {
56
+ return lines();
57
+ },
58
+ keyed: true,
59
+ children: current => (() => {
60
+ var _el$ = _$createElement("box");
61
+ _$insert(_el$, _$createComponent(For, {
62
+ each: current,
63
+ children: line => (() => {
64
+ var _el$2 = _$createElement("text");
65
+ _$insert(_el$2, line);
66
+ return _el$2;
67
+ })()
68
+ }));
69
+ return _el$;
70
+ })()
71
+ })
34
72
  });
73
+ return () => {
74
+ clearInterval(timer);
75
+ unregister();
76
+ };
35
77
  }
36
78
  });
package/dist/tui.tsx CHANGED
@@ -1,6 +1,10 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
2
 
3
3
  import { Plugin } from "@opencode/plugin/tui";
4
+ import { For, Show, createSignal } from "solid-js";
5
+ import { MeshRpc, formatMeshLines, type MeshSnapshot } from "./rpc.js";
6
+
7
+ const REFRESH_MS = 15_000;
4
8
 
5
9
  export default Plugin.define({
6
10
  id: "@log0u7/opencode-mesh.tui",
@@ -27,9 +31,37 @@ export default Plugin.define({
27
31
  ],
28
32
  }));
29
33
 
30
- return context.ui.slot({
34
+ // Live sidebar: poll the server-side snapshot over RPC. On a v1 host (no
35
+ // RPC) or while the server is unreachable, the slot stays hidden.
36
+ const [lines, setLines] = createSignal<string[] | null>(null);
37
+ const mesh = context.client.rpc(MeshRpc);
38
+ const refresh = async () => {
39
+ try {
40
+ const snap = (await mesh.snapshot({})) as MeshSnapshot;
41
+ setLines(formatMeshLines(snap));
42
+ } catch {
43
+ setLines(null);
44
+ }
45
+ };
46
+ void refresh();
47
+ const timer = setInterval(() => void refresh(), REFRESH_MS);
48
+
49
+ const unregister = context.ui.slot({
31
50
  append: "sidebar.content",
32
- render: () => null,
51
+ render: () => (
52
+ <Show when={lines()} keyed>
53
+ {(current) => (
54
+ <box>
55
+ <For each={current}>{(line) => <text>{line}</text>}</For>
56
+ </box>
57
+ )}
58
+ </Show>
59
+ ),
33
60
  });
61
+
62
+ return () => {
63
+ clearInterval(timer);
64
+ unregister();
65
+ };
34
66
  },
35
67
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@log0u7/opencode-mesh",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Multi-session coordination for OpenCode: session coordination on one machine and a peer pool across LAN or Tailscale, with pairing, mailbox, and worktree locks.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",