@log0u7/opencode-mesh 0.5.1 → 0.7.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,26 @@ 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.7.0] - 2026-11-06
9
+
10
+ ### Added
11
+
12
+ - Themed mesh sidebar: bold header and muted peer lines using the active
13
+ theme's text colors.
14
+ - Sidebar collapse: the `/mesh` toggle keybind persists its state in durable
15
+ storage, so the collapsed sidebar survives TUI restarts.
16
+ - Event refresh: the sidebar refreshes immediately (debounced) on server
17
+ events, with the 15s polling as fallback.
18
+ - `/mesh` now opens a dialog with the full snapshot instead of a toast.
19
+
20
+ ## [0.6.0] - 2026-11-06
21
+
22
+ ### Added
23
+
24
+ - Live mesh sidebar: the TUI part polls a `mesh.snapshot` RPC (registered
25
+ server-side) every 15s and renders the node, one line per paired peer and
26
+ the active lock count. Hidden on v1 hosts and while the server is down.
27
+
8
28
  ## [0.5.1] - 2026-11-06
9
29
 
10
30
  ### 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,79 @@
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
+ /** Collapsible sidebar: when collapsed, one header line with the peer count (plus locks). */
29
+ export declare function formatMeshSidebar(s: MeshSnapshot, collapsed: boolean): string[];
30
+ export declare const MeshRpc: {
31
+ readonly id: "mesh";
32
+ readonly methods: {
33
+ readonly snapshot: {
34
+ readonly input: {
35
+ readonly type: "object";
36
+ readonly properties: {};
37
+ readonly additionalProperties: false;
38
+ };
39
+ readonly output: {
40
+ readonly type: "object";
41
+ readonly properties: {
42
+ readonly node_id: {
43
+ readonly type: "string";
44
+ readonly description: "This node's id";
45
+ };
46
+ readonly hostname: {
47
+ readonly type: "string";
48
+ readonly description: "This node's hostname";
49
+ };
50
+ readonly peers: {
51
+ readonly type: "array";
52
+ readonly items: {
53
+ readonly type: "object";
54
+ readonly properties: {
55
+ readonly node_id: {
56
+ readonly type: "string";
57
+ };
58
+ readonly hostname: {
59
+ readonly type: "string";
60
+ };
61
+ };
62
+ readonly required: readonly ["node_id", "hostname"];
63
+ readonly additionalProperties: false;
64
+ };
65
+ readonly description: "Paired peers";
66
+ };
67
+ readonly active_locks: {
68
+ readonly type: "number";
69
+ readonly description: "Active local file locks";
70
+ };
71
+ };
72
+ readonly required: readonly ["node_id", "hostname", "peers", "active_locks"];
73
+ readonly additionalProperties: false;
74
+ };
75
+ };
76
+ };
77
+ readonly events: {};
78
+ };
79
+ //# 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,6FAA6F;AAC7F,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,YAAY,EAAE,SAAS,EAAE,OAAO,GAAG,MAAM,EAAE,CAU/E;AAED,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+BlB,CAAC"}
package/dist/rpc.js ADDED
@@ -0,0 +1,73 @@
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
+ /** Collapsible sidebar: when collapsed, one header line with the peer count (plus locks). */
30
+ export function formatMeshSidebar(s, collapsed) {
31
+ if (!collapsed) {
32
+ return formatMeshLines(s);
33
+ }
34
+ const self = s.hostname.length > 0 ? s.hostname : s.node_id;
35
+ const lines = [`mesh: ${self} (${s.peers.length} peers)`];
36
+ if (s.active_locks > 0) {
37
+ lines.push(`${s.active_locks} active lock(s)`);
38
+ }
39
+ return lines;
40
+ }
41
+ export const MeshRpc = Rpc.define({
42
+ id: "mesh",
43
+ methods: {
44
+ snapshot: {
45
+ input: { type: "object", properties: {}, additionalProperties: false },
46
+ output: {
47
+ type: "object",
48
+ properties: {
49
+ node_id: { type: "string", description: "This node's id" },
50
+ hostname: { type: "string", description: "This node's hostname" },
51
+ peers: {
52
+ type: "array",
53
+ items: {
54
+ type: "object",
55
+ properties: {
56
+ node_id: { type: "string" },
57
+ hostname: { type: "string" },
58
+ },
59
+ required: ["node_id", "hostname"],
60
+ additionalProperties: false,
61
+ },
62
+ description: "Paired peers",
63
+ },
64
+ active_locks: { type: "number", description: "Active local file locks" },
65
+ },
66
+ required: ["node_id", "hostname", "peers", "active_locks"],
67
+ additionalProperties: false,
68
+ },
69
+ },
70
+ },
71
+ events: {},
72
+ });
73
+ //# sourceMappingURL=rpc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rpc.js","sourceRoot":"","sources":["../src/rpc.ts"],"names":[],"mappings":"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"}
package/dist/tui.js CHANGED
@@ -1,14 +1,149 @@
1
+ import { memo as _$memo } from "@opentui/solid";
2
+ import { createTextNode as _$createTextNode } from "@opentui/solid";
3
+ import { insertNode as _$insertNode } from "@opentui/solid";
4
+ import { effect as _$effect } from "@opentui/solid";
5
+ import { setProp as _$setProp } from "@opentui/solid";
6
+ import { insert as _$insert } from "@opentui/solid";
7
+ import { createElement as _$createElement } from "@opentui/solid";
8
+ import { createComponent as _$createComponent } from "@opentui/solid";
1
9
  /** @jsxImportSource @opentui/solid */
2
10
 
3
11
  import { Plugin } from "@opencode/plugin/tui";
12
+ import { For, Show, createSignal } from "solid-js";
13
+ import { MeshRpc, formatMeshSidebar } from "./rpc.js";
14
+ const REFRESH_MS = 15_000;
15
+ /** quota-style event refresh: debounced immediate refresh after activity. */
16
+ const EVENT_DEBOUNCE_MS = 150;
17
+ function themedSidebar(theme, lines) {
18
+ return _$createComponent(Show, {
19
+ get when() {
20
+ return lines();
21
+ },
22
+ keyed: true,
23
+ children: current => (() => {
24
+ var _el$ = _$createElement("box");
25
+ _$insert(_el$, _$createComponent(For, {
26
+ each: current,
27
+ children: (line, index) => (() => {
28
+ var _el$2 = _$createElement("text");
29
+ _$setProp(_el$2, "wrapMode", "none");
30
+ _$insert(_el$2, line);
31
+ _$effect(_$p => _$setProp(_el$2, "fg", index() === 0 ? theme.text.base : theme.text.muted, _$p));
32
+ return _el$2;
33
+ })()
34
+ }));
35
+ return _el$;
36
+ })()
37
+ });
38
+ }
39
+ function dialogBody(theme, snapshot) {
40
+ const self = snapshot.hostname.length > 0 ? snapshot.hostname : snapshot.node_id;
41
+ const peers = snapshot.peers.map(p => p.hostname.length > 0 ? p.hostname : p.node_id);
42
+ return (() => {
43
+ var _el$3 = _$createElement("box"),
44
+ _el$4 = _$createElement("text"),
45
+ _el$5 = _$createElement("b"),
46
+ _el$7 = _$createElement("text"),
47
+ _el$8 = _$createTextNode(`Node: `),
48
+ _el$9 = _$createElement("text"),
49
+ _el$0 = _$createTextNode(`Paired peers: `),
50
+ _el$1 = _$createElement("text"),
51
+ _el$10 = _$createTextNode(`Active locks: `);
52
+ _$insertNode(_el$3, _el$4);
53
+ _$insertNode(_el$3, _el$7);
54
+ _$insertNode(_el$3, _el$9);
55
+ _$insertNode(_el$3, _el$1);
56
+ _$setProp(_el$3, "gap", 1);
57
+ _$setProp(_el$3, "paddingLeft", 2);
58
+ _$setProp(_el$3, "paddingRight", 2);
59
+ _$setProp(_el$3, "paddingBottom", 1);
60
+ _$insertNode(_el$4, _el$5);
61
+ _$insertNode(_el$5, _$createTextNode(`Mesh — coordination`));
62
+ _$insertNode(_el$7, _el$8);
63
+ _$insert(_el$7, self, null);
64
+ _$insertNode(_el$9, _el$0);
65
+ _$insert(_el$9, (() => {
66
+ var _c$ = _$memo(() => peers.length === 0);
67
+ return () => _c$() ? "none" : peers.join(", ");
68
+ })(), null);
69
+ _$insertNode(_el$1, _el$10);
70
+ _$insert(_el$1, () => snapshot.active_locks, null);
71
+ _$effect(_p$ => {
72
+ var _v$ = theme.text.base,
73
+ _v$2 = theme.text.muted,
74
+ _v$3 = theme.text.muted,
75
+ _v$4 = theme.text.muted;
76
+ _v$ !== _p$.e && (_p$.e = _$setProp(_el$4, "fg", _v$, _p$.e));
77
+ _v$2 !== _p$.t && (_p$.t = _$setProp(_el$7, "fg", _v$2, _p$.t));
78
+ _v$3 !== _p$.a && (_p$.a = _$setProp(_el$9, "fg", _v$3, _p$.a));
79
+ _v$4 !== _p$.o && (_p$.o = _$setProp(_el$1, "fg", _v$4, _p$.o));
80
+ return _p$;
81
+ }, {
82
+ e: undefined,
83
+ t: undefined,
84
+ a: undefined,
85
+ o: undefined
86
+ });
87
+ return _el$3;
88
+ })();
89
+ }
4
90
  export default Plugin.define({
5
91
  id: "@log0u7/opencode-mesh.tui",
6
92
  setup(context) {
93
+ const theme = context.theme;
7
94
  context.ui.toast.show({
8
95
  message: "opencode-mesh loaded",
9
96
  variant: "info",
10
97
  duration: 2000
11
98
  });
99
+
100
+ // Durable collapse state: persisted to disk, survives restarts and hot reloads.
101
+ const [store, mutate] = context.storage.store("mesh-sidebar", {
102
+ initial: {
103
+ collapsed: false
104
+ }
105
+ });
106
+ const [snapshot, setSnapshot] = createSignal(null);
107
+ const [lines, setLines] = createSignal(null);
108
+
109
+ // Live sidebar: poll the server-side snapshot over RPC. On a v1 host (no
110
+ // RPC) or while the server is unreachable, the slot stays hidden.
111
+ const mesh = context.client.rpc(MeshRpc);
112
+ const refresh = async () => {
113
+ try {
114
+ const snap = await mesh.snapshot({});
115
+ setSnapshot(snap);
116
+ setLines(formatMeshSidebar(snap, store.collapsed));
117
+ } catch {
118
+ setLines(null);
119
+ }
120
+ };
121
+ void refresh();
122
+ const timer = setInterval(() => void refresh(), REFRESH_MS);
123
+
124
+ // quota-style event refresh: debounced immediate refresh after activity.
125
+ let debounce;
126
+ const schedule = () => {
127
+ if (debounce) clearTimeout(debounce);
128
+ debounce = setTimeout(() => void refresh(), EVENT_DEBOUNCE_MS);
129
+ };
130
+ // context.client.event is an AsyncIterable of V2Event, not an EventEmitter.
131
+ const events = context.client.event.subscribe();
132
+ void (async () => {
133
+ try {
134
+ for await (const _event of events) {
135
+ schedule();
136
+ }
137
+ } catch {
138
+ // stream ended or errored: polling fallback stays active.
139
+ }
140
+ })();
141
+ const unregister = context.ui.slot({
142
+ append: "sidebar.content",
143
+ render: () => themedSidebar(theme, lines)
144
+ });
145
+
146
+ // Keymap layers are disposed with the plugin; no manual disposer here.
12
147
  context.keymap.layer(() => ({
13
148
  mode: "global",
14
149
  commands: [{
@@ -20,17 +155,37 @@ export default Plugin.define({
20
155
  },
21
156
  palette: true,
22
157
  run: async () => {
23
- context.ui.toast.show({
24
- title: "Mesh",
25
- message: "Multi-session coordination available",
26
- variant: "info"
158
+ await refresh();
159
+ const current = snapshot();
160
+ if (!current) {
161
+ context.ui.toast.show({
162
+ title: "Mesh",
163
+ message: "Server unreachable",
164
+ variant: "info"
165
+ });
166
+ return;
167
+ }
168
+ context.ui.dialog.show(() => dialogBody(theme, current));
169
+ context.ui.dialog.set({
170
+ size: "medium"
27
171
  });
28
172
  }
173
+ }, {
174
+ id: "mesh.toggleSidebar",
175
+ title: "Toggle mesh sidebar",
176
+ group: "Mesh",
177
+ run: async () => {
178
+ await mutate(draft => {
179
+ draft.collapsed = !draft.collapsed;
180
+ });
181
+ void refresh();
182
+ }
29
183
  }]
30
184
  }));
31
- return context.ui.slot({
32
- append: "sidebar.content",
33
- render: () => null
34
- });
185
+ return () => {
186
+ clearTimeout(debounce);
187
+ clearInterval(timer);
188
+ unregister();
189
+ };
35
190
  }
36
191
  });
package/dist/tui.tsx CHANGED
@@ -1,12 +1,105 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
2
 
3
3
  import { Plugin } from "@opencode/plugin/tui";
4
+ import type { JSX } from "@opentui/solid";
5
+ import { For, Show, createSignal } from "solid-js";
6
+
7
+ type Context = Plugin.Context;
8
+ type Theme = Context["theme"];
9
+
10
+ import { MeshRpc, formatMeshSidebar, type MeshSnapshot } from "./rpc.js";
11
+
12
+ const REFRESH_MS = 15_000;
13
+ /** quota-style event refresh: debounced immediate refresh after activity. */
14
+ const EVENT_DEBOUNCE_MS = 150;
15
+
16
+ function themedSidebar(theme: Theme, lines: () => string[] | null): JSX.Element {
17
+ return (
18
+ <Show when={lines()} keyed>
19
+ {(current) => (
20
+ <box>
21
+ <For each={current}>
22
+ {(line, index) => (
23
+ <text fg={index() === 0 ? theme.text.base : theme.text.muted} wrapMode="none">
24
+ {line}
25
+ </text>
26
+ )}
27
+ </For>
28
+ </box>
29
+ )}
30
+ </Show>
31
+ );
32
+ }
33
+
34
+ function dialogBody(theme: Theme, snapshot: MeshSnapshot): JSX.Element {
35
+ const self = snapshot.hostname.length > 0 ? snapshot.hostname : snapshot.node_id;
36
+ const peers = snapshot.peers.map((p) => (p.hostname.length > 0 ? p.hostname : p.node_id));
37
+ return (
38
+ <box gap={1} paddingLeft={2} paddingRight={2} paddingBottom={1}>
39
+ <text fg={theme.text.base}>
40
+ <b>Mesh — coordination</b>
41
+ </text>
42
+ <text fg={theme.text.muted}>Node: {self}</text>
43
+ <text fg={theme.text.muted}>
44
+ Paired peers: {peers.length === 0 ? "none" : peers.join(", ")}
45
+ </text>
46
+ <text fg={theme.text.muted}>Active locks: {snapshot.active_locks}</text>
47
+ </box>
48
+ );
49
+ }
4
50
 
5
51
  export default Plugin.define({
6
52
  id: "@log0u7/opencode-mesh.tui",
7
- setup(context) {
53
+ setup(context: Context) {
54
+ const theme = context.theme;
8
55
  context.ui.toast.show({ message: "opencode-mesh loaded", variant: "info", duration: 2000 });
9
56
 
57
+ // Durable collapse state: persisted to disk, survives restarts and hot reloads.
58
+ const [store, mutate] = context.storage.store("mesh-sidebar", {
59
+ initial: { collapsed: false },
60
+ });
61
+ const [snapshot, setSnapshot] = createSignal<MeshSnapshot | null>(null);
62
+ const [lines, setLines] = createSignal<string[] | null>(null);
63
+
64
+ // Live sidebar: poll the server-side snapshot over RPC. On a v1 host (no
65
+ // RPC) or while the server is unreachable, the slot stays hidden.
66
+ const mesh = context.client.rpc(MeshRpc);
67
+ const refresh = async () => {
68
+ try {
69
+ const snap = (await mesh.snapshot({})) as MeshSnapshot;
70
+ setSnapshot(snap);
71
+ setLines(formatMeshSidebar(snap, store.collapsed));
72
+ } catch {
73
+ setLines(null);
74
+ }
75
+ };
76
+ void refresh();
77
+ const timer = setInterval(() => void refresh(), REFRESH_MS);
78
+
79
+ // quota-style event refresh: debounced immediate refresh after activity.
80
+ let debounce: ReturnType<typeof setTimeout> | undefined;
81
+ const schedule = () => {
82
+ if (debounce) clearTimeout(debounce);
83
+ debounce = setTimeout(() => void refresh(), EVENT_DEBOUNCE_MS);
84
+ };
85
+ // context.client.event is an AsyncIterable of V2Event, not an EventEmitter.
86
+ const events = context.client.event.subscribe();
87
+ void (async () => {
88
+ try {
89
+ for await (const _event of events) {
90
+ schedule();
91
+ }
92
+ } catch {
93
+ // stream ended or errored: polling fallback stays active.
94
+ }
95
+ })();
96
+
97
+ const unregister = context.ui.slot({
98
+ append: "sidebar.content",
99
+ render: () => themedSidebar(theme, lines),
100
+ });
101
+
102
+ // Keymap layers are disposed with the plugin; no manual disposer here.
10
103
  context.keymap.layer(() => ({
11
104
  mode: "global",
12
105
  commands: [
@@ -17,19 +110,38 @@ export default Plugin.define({
17
110
  slash: { name: "mesh" },
18
111
  palette: true,
19
112
  run: async () => {
20
- context.ui.toast.show({
21
- title: "Mesh",
22
- message: "Multi-session coordination available",
23
- variant: "info",
113
+ await refresh();
114
+ const current = snapshot();
115
+ if (!current) {
116
+ context.ui.toast.show({
117
+ title: "Mesh",
118
+ message: "Server unreachable",
119
+ variant: "info",
120
+ });
121
+ return;
122
+ }
123
+ context.ui.dialog.show(() => dialogBody(theme, current));
124
+ context.ui.dialog.set({ size: "medium" });
125
+ },
126
+ },
127
+ {
128
+ id: "mesh.toggleSidebar",
129
+ title: "Toggle mesh sidebar",
130
+ group: "Mesh",
131
+ run: async () => {
132
+ await mutate((draft) => {
133
+ draft.collapsed = !draft.collapsed;
24
134
  });
135
+ void refresh();
25
136
  },
26
137
  },
27
138
  ],
28
139
  }));
29
140
 
30
- return context.ui.slot({
31
- append: "sidebar.content",
32
- render: () => null,
33
- });
141
+ return () => {
142
+ clearTimeout(debounce);
143
+ clearInterval(timer);
144
+ unregister();
145
+ };
34
146
  },
35
147
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@log0u7/opencode-mesh",
3
- "version": "0.5.1",
3
+ "version": "0.7.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",