@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 +20 -0
- package/README.md +57 -2
- package/dist/plugin-v2.d.ts.map +1 -1
- package/dist/plugin-v2.js +13 -1
- package/dist/plugin-v2.js.map +1 -1
- package/dist/rpc.d.ts +79 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +73 -0
- package/dist/rpc.js.map +1 -0
- package/dist/tui.js +163 -8
- package/dist/tui.tsx +121 -9
- package/package.json +1 -1
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.
|
|
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` |
|
|
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).
|
package/dist/plugin-v2.d.ts.map
CHANGED
|
@@ -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;
|
|
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 {
|
|
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
|
};
|
package/dist/plugin-v2.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-v2.js","sourceRoot":"","sources":["../src/plugin-v2.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAE1C,OAAO,EAAE,UAAU,EAAe,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,UAAU,EAAwB,MAAM,oBAAoB,CAAC;AACrF,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;
|
|
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
|
package/dist/rpc.js.map
ADDED
|
@@ -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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
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.
|
|
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",
|