@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 +8 -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 +77 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +61 -0
- package/dist/rpc.js.map +1 -0
- package/dist/tui.js +44 -2
- package/dist/tui.tsx +34 -2
- package/package.json +1 -1
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.
|
|
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,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
|
package/dist/rpc.js.map
ADDED
|
@@ -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
|
-
|
|
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: () =>
|
|
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
|
-
|
|
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: () =>
|
|
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.
|
|
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",
|