@log0u7/opencode-mesh 0.6.0 → 0.7.1

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,27 @@ 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.1] - 2026-11-07
9
+
10
+ ### Fixed
11
+
12
+ - The TUI no longer crashes on hosts whose storage layer rejects scoped npm
13
+ plugin ids (`TypeError: Invalid storage segment`, anomalyco/opencode#62634).
14
+ The sidebar falls back to in-memory collapse state: functional, but not
15
+ persisted across TUI restarts until the host-side fix lands.
16
+
17
+ ## [0.7.0] - 2026-11-06
18
+
19
+ ### Added
20
+
21
+ - Themed mesh sidebar: bold header and muted peer lines using the active
22
+ theme's text colors.
23
+ - Sidebar collapse: the `/mesh` toggle keybind persists its state in durable
24
+ storage, so the collapsed sidebar survives TUI restarts.
25
+ - Event refresh: the sidebar refreshes immediately (debounced) on server
26
+ events, with the 15s polling as fallback.
27
+ - `/mesh` now opens a dialog with the full snapshot instead of a toast.
28
+
8
29
  ## [0.6.0] - 2026-11-06
9
30
 
10
31
  ### Added
package/dist/rpc.d.ts CHANGED
@@ -25,6 +25,8 @@ export type SnapshotInput = {
25
25
  export declare function snapshotFromMesh(data: SnapshotInput): MeshSnapshot;
26
26
  /** Sidebar lines: the node, one line per paired peer, the active lock count. */
27
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[];
28
30
  export declare const MeshRpc: {
29
31
  readonly id: "mesh";
30
32
  readonly methods: {
package/dist/rpc.d.ts.map CHANGED
@@ -1 +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"}
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 CHANGED
@@ -26,6 +26,18 @@ export function formatMeshLines(s) {
26
26
  }
27
27
  return lines;
28
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
+ }
29
41
  export const MeshRpc = Rpc.define({
30
42
  id: "mesh",
31
43
  methods: {
package/dist/rpc.js.map CHANGED
@@ -1 +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"}
1
+ {"version":3,"file":"rpc.js","sourceRoot":"","sources":["../src/rpc.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import { Plugin } from "@opencode/plugin/tui";
2
+ type SidebarState = {
3
+ collapsed: boolean;
4
+ };
5
+ /** Collapse state for the mesh sidebar: durable via the host's TUI storage when
6
+ * available, in-memory fallback when it is not. The host composes storage keys
7
+ * as `plugin.<id>.<key>` and its segment() validation rejects scoped npm ids
8
+ * (`@scope/name.tui`), so `storage.store()` throws on first use for any plugin
9
+ * installed from a scoped package (anomalyco/opencode#62634). The upstream fix
10
+ * removes the throw; until then the fallback keeps the sidebar functional, at
11
+ * the cost of not persisting the collapse state across TUI restarts.
12
+ * Returns the same `[store, mutate]` shape as `storage.store` either way. */
13
+ export declare function createSidebarState(storage: Plugin.Context["storage"]): readonly [{
14
+ readonly collapsed: boolean;
15
+ }, (mutation: (draft: SidebarState) => void) => Promise<void>];
16
+ export {};
17
+ //# sourceMappingURL=sidebar-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sidebar-state.d.ts","sourceRoot":"","sources":["../src/sidebar-state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAG9C,KAAK,YAAY,GAAG;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,CAAC;AAE3C;;;;;;;6EAO6E;AAC7E,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC;wBAYzC,OAAO;cAClB,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,EAG/D"}
@@ -0,0 +1,26 @@
1
+ import { Plugin } from "@opencode/plugin/tui";
2
+ import { createStore, produce } from "solid-js/store";
3
+ /** Collapse state for the mesh sidebar: durable via the host's TUI storage when
4
+ * available, in-memory fallback when it is not. The host composes storage keys
5
+ * as `plugin.<id>.<key>` and its segment() validation rejects scoped npm ids
6
+ * (`@scope/name.tui`), so `storage.store()` throws on first use for any plugin
7
+ * installed from a scoped package (anomalyco/opencode#62634). The upstream fix
8
+ * removes the throw; until then the fallback keeps the sidebar functional, at
9
+ * the cost of not persisting the collapse state across TUI restarts.
10
+ * Returns the same `[store, mutate]` shape as `storage.store` either way. */
11
+ export function createSidebarState(storage) {
12
+ try {
13
+ return storage.store("mesh-sidebar", { initial: { collapsed: false } });
14
+ }
15
+ catch {
16
+ const [store, setStore] = createStore({ collapsed: false });
17
+ return [
18
+ store,
19
+ (mutation) => {
20
+ setStore(produce(mutation));
21
+ return Promise.resolve();
22
+ },
23
+ ];
24
+ }
25
+ }
26
+ //# sourceMappingURL=sidebar-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sidebar-state.js","sourceRoot":"","sources":["../src/sidebar-state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAItD;;;;;;;6EAO6E;AAC7E,MAAM,UAAU,kBAAkB,CAAC,OAAkC;IACnE,IAAI,CAAC;QACH,OAAO,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,EAAE,OAAO,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;IAC1E,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,WAAW,CAAe,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;QAC1E,OAAO;YACL,KAAK;YACL,CAAC,QAAuC,EAAE,EAAE;gBAC1C,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;gBAC5B,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;YAC3B,CAAC;SAIF,CAAC;IACJ,CAAC;AACH,CAAC"}
package/dist/tui.js CHANGED
@@ -1,3 +1,8 @@
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";
1
6
  import { insert as _$insert } from "@opentui/solid";
2
7
  import { createElement as _$createElement } from "@opentui/solid";
3
8
  import { createComponent as _$createComponent } from "@opentui/solid";
@@ -5,72 +10,179 @@ import { createComponent as _$createComponent } from "@opentui/solid";
5
10
 
6
11
  import { Plugin } from "@opencode/plugin/tui";
7
12
  import { For, Show, createSignal } from "solid-js";
8
- import { MeshRpc, formatMeshLines } from "./rpc.js";
13
+ import { MeshRpc, formatMeshSidebar } from "./rpc.js";
14
+ import { createSidebarState } from "./sidebar-state.js";
9
15
  const REFRESH_MS = 15_000;
16
+ /** quota-style event refresh: debounced immediate refresh after activity. */
17
+ const EVENT_DEBOUNCE_MS = 150;
18
+ function themedSidebar(theme, lines) {
19
+ return _$createComponent(Show, {
20
+ get when() {
21
+ return lines();
22
+ },
23
+ keyed: true,
24
+ children: current => (() => {
25
+ var _el$ = _$createElement("box");
26
+ _$insert(_el$, _$createComponent(For, {
27
+ each: current,
28
+ children: (line, index) => (() => {
29
+ var _el$2 = _$createElement("text");
30
+ _$setProp(_el$2, "wrapMode", "none");
31
+ _$insert(_el$2, line);
32
+ _$effect(_$p => _$setProp(_el$2, "fg", index() === 0 ? theme.text.base : theme.text.muted, _$p));
33
+ return _el$2;
34
+ })()
35
+ }));
36
+ return _el$;
37
+ })()
38
+ });
39
+ }
40
+ function dialogBody(theme, snapshot) {
41
+ const self = snapshot.hostname.length > 0 ? snapshot.hostname : snapshot.node_id;
42
+ const peers = snapshot.peers.map(p => p.hostname.length > 0 ? p.hostname : p.node_id);
43
+ return (() => {
44
+ var _el$3 = _$createElement("box"),
45
+ _el$4 = _$createElement("text"),
46
+ _el$5 = _$createElement("b"),
47
+ _el$7 = _$createElement("text"),
48
+ _el$8 = _$createTextNode(`Node: `),
49
+ _el$9 = _$createElement("text"),
50
+ _el$0 = _$createTextNode(`Paired peers: `),
51
+ _el$1 = _$createElement("text"),
52
+ _el$10 = _$createTextNode(`Active locks: `);
53
+ _$insertNode(_el$3, _el$4);
54
+ _$insertNode(_el$3, _el$7);
55
+ _$insertNode(_el$3, _el$9);
56
+ _$insertNode(_el$3, _el$1);
57
+ _$setProp(_el$3, "gap", 1);
58
+ _$setProp(_el$3, "paddingLeft", 2);
59
+ _$setProp(_el$3, "paddingRight", 2);
60
+ _$setProp(_el$3, "paddingBottom", 1);
61
+ _$insertNode(_el$4, _el$5);
62
+ _$insertNode(_el$5, _$createTextNode(`Mesh — coordination`));
63
+ _$insertNode(_el$7, _el$8);
64
+ _$insert(_el$7, self, null);
65
+ _$insertNode(_el$9, _el$0);
66
+ _$insert(_el$9, (() => {
67
+ var _c$ = _$memo(() => peers.length === 0);
68
+ return () => _c$() ? "none" : peers.join(", ");
69
+ })(), null);
70
+ _$insertNode(_el$1, _el$10);
71
+ _$insert(_el$1, () => snapshot.active_locks, null);
72
+ _$effect(_p$ => {
73
+ var _v$ = theme.text.base,
74
+ _v$2 = theme.text.muted,
75
+ _v$3 = theme.text.muted,
76
+ _v$4 = theme.text.muted;
77
+ _v$ !== _p$.e && (_p$.e = _$setProp(_el$4, "fg", _v$, _p$.e));
78
+ _v$2 !== _p$.t && (_p$.t = _$setProp(_el$7, "fg", _v$2, _p$.t));
79
+ _v$3 !== _p$.a && (_p$.a = _$setProp(_el$9, "fg", _v$3, _p$.a));
80
+ _v$4 !== _p$.o && (_p$.o = _$setProp(_el$1, "fg", _v$4, _p$.o));
81
+ return _p$;
82
+ }, {
83
+ e: undefined,
84
+ t: undefined,
85
+ a: undefined,
86
+ o: undefined
87
+ });
88
+ return _el$3;
89
+ })();
90
+ }
10
91
  export default Plugin.define({
11
92
  id: "@log0u7/opencode-mesh.tui",
12
93
  setup(context) {
94
+ const theme = context.theme;
13
95
  context.ui.toast.show({
14
96
  message: "opencode-mesh loaded",
15
97
  variant: "info",
16
98
  duration: 2000
17
99
  });
18
- context.keymap.layer(() => ({
19
- mode: "global",
20
- commands: [{
21
- id: "mesh.status",
22
- title: "Mesh status",
23
- group: "Mesh",
24
- slash: {
25
- name: "mesh"
26
- },
27
- palette: true,
28
- run: async () => {
29
- context.ui.toast.show({
30
- title: "Mesh",
31
- message: "Multi-session coordination available",
32
- variant: "info"
33
- });
34
- }
35
- }]
36
- }));
100
+
101
+ // Durable collapse state when the host allows it (scoped plugin ids hit a
102
+ // host-side segment validation error — anomalyco/opencode#62634); in-memory
103
+ // fallback otherwise: sidebar works, collapse not persisted across restarts.
104
+ const [store, mutate] = createSidebarState(context.storage);
105
+ const [snapshot, setSnapshot] = createSignal(null);
106
+ const [lines, setLines] = createSignal(null);
37
107
 
38
108
  // Live sidebar: poll the server-side snapshot over RPC. On a v1 host (no
39
109
  // RPC) or while the server is unreachable, the slot stays hidden.
40
- const [lines, setLines] = createSignal(null);
41
110
  const mesh = context.client.rpc(MeshRpc);
42
111
  const refresh = async () => {
43
112
  try {
44
113
  const snap = await mesh.snapshot({});
45
- setLines(formatMeshLines(snap));
114
+ setSnapshot(snap);
115
+ setLines(formatMeshSidebar(snap, store.collapsed));
46
116
  } catch {
47
117
  setLines(null);
48
118
  }
49
119
  };
50
120
  void refresh();
51
121
  const timer = setInterval(() => void refresh(), REFRESH_MS);
122
+
123
+ // quota-style event refresh: debounced immediate refresh after activity.
124
+ let debounce;
125
+ const schedule = () => {
126
+ if (debounce) clearTimeout(debounce);
127
+ debounce = setTimeout(() => void refresh(), EVENT_DEBOUNCE_MS);
128
+ };
129
+ // context.client.event is an AsyncIterable of V2Event, not an EventEmitter.
130
+ const events = context.client.event.subscribe();
131
+ void (async () => {
132
+ try {
133
+ for await (const _event of events) {
134
+ schedule();
135
+ }
136
+ } catch {
137
+ // stream ended or errored: polling fallback stays active.
138
+ }
139
+ })();
52
140
  const unregister = context.ui.slot({
53
141
  append: "sidebar.content",
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
- })
142
+ render: () => themedSidebar(theme, lines)
72
143
  });
144
+
145
+ // Keymap layers are disposed with the plugin; no manual disposer here.
146
+ context.keymap.layer(() => ({
147
+ mode: "global",
148
+ commands: [{
149
+ id: "mesh.status",
150
+ title: "Mesh status",
151
+ group: "Mesh",
152
+ slash: {
153
+ name: "mesh"
154
+ },
155
+ palette: true,
156
+ run: async () => {
157
+ await refresh();
158
+ const current = snapshot();
159
+ if (!current) {
160
+ context.ui.toast.show({
161
+ title: "Mesh",
162
+ message: "Server unreachable",
163
+ variant: "info"
164
+ });
165
+ return;
166
+ }
167
+ context.ui.dialog.show(() => dialogBody(theme, current));
168
+ context.ui.dialog.set({
169
+ size: "medium"
170
+ });
171
+ }
172
+ }, {
173
+ id: "mesh.toggleSidebar",
174
+ title: "Toggle mesh sidebar",
175
+ group: "Mesh",
176
+ run: async () => {
177
+ await mutate(draft => {
178
+ draft.collapsed = !draft.collapsed;
179
+ });
180
+ void refresh();
181
+ }
182
+ }]
183
+ }));
73
184
  return () => {
185
+ clearTimeout(debounce);
74
186
  clearInterval(timer);
75
187
  unregister();
76
188
  };
package/dist/tui.tsx CHANGED
@@ -1,44 +1,75 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
2
 
3
3
  import { Plugin } from "@opencode/plugin/tui";
4
+ import type { JSX } from "@opentui/solid";
4
5
  import { For, Show, createSignal } from "solid-js";
5
- import { MeshRpc, formatMeshLines, type MeshSnapshot } from "./rpc.js";
6
+
7
+ type Context = Plugin.Context;
8
+ type Theme = Context["theme"];
9
+
10
+ import { MeshRpc, formatMeshSidebar, type MeshSnapshot } from "./rpc.js";
11
+ import { createSidebarState } from "./sidebar-state.js";
6
12
 
7
13
  const REFRESH_MS = 15_000;
14
+ /** quota-style event refresh: debounced immediate refresh after activity. */
15
+ const EVENT_DEBOUNCE_MS = 150;
16
+
17
+ function themedSidebar(theme: Theme, lines: () => string[] | null): JSX.Element {
18
+ return (
19
+ <Show when={lines()} keyed>
20
+ {(current) => (
21
+ <box>
22
+ <For each={current}>
23
+ {(line, index) => (
24
+ <text fg={index() === 0 ? theme.text.base : theme.text.muted} wrapMode="none">
25
+ {line}
26
+ </text>
27
+ )}
28
+ </For>
29
+ </box>
30
+ )}
31
+ </Show>
32
+ );
33
+ }
34
+
35
+ function dialogBody(theme: Theme, snapshot: MeshSnapshot): JSX.Element {
36
+ const self = snapshot.hostname.length > 0 ? snapshot.hostname : snapshot.node_id;
37
+ const peers = snapshot.peers.map((p) => (p.hostname.length > 0 ? p.hostname : p.node_id));
38
+ return (
39
+ <box gap={1} paddingLeft={2} paddingRight={2} paddingBottom={1}>
40
+ <text fg={theme.text.base}>
41
+ <b>Mesh — coordination</b>
42
+ </text>
43
+ <text fg={theme.text.muted}>Node: {self}</text>
44
+ <text fg={theme.text.muted}>
45
+ Paired peers: {peers.length === 0 ? "none" : peers.join(", ")}
46
+ </text>
47
+ <text fg={theme.text.muted}>Active locks: {snapshot.active_locks}</text>
48
+ </box>
49
+ );
50
+ }
8
51
 
9
52
  export default Plugin.define({
10
53
  id: "@log0u7/opencode-mesh.tui",
11
- setup(context) {
54
+ setup(context: Context) {
55
+ const theme = context.theme;
12
56
  context.ui.toast.show({ message: "opencode-mesh loaded", variant: "info", duration: 2000 });
13
57
 
14
- context.keymap.layer(() => ({
15
- mode: "global",
16
- commands: [
17
- {
18
- id: "mesh.status",
19
- title: "Mesh status",
20
- group: "Mesh",
21
- slash: { name: "mesh" },
22
- palette: true,
23
- run: async () => {
24
- context.ui.toast.show({
25
- title: "Mesh",
26
- message: "Multi-session coordination available",
27
- variant: "info",
28
- });
29
- },
30
- },
31
- ],
32
- }));
58
+ // Durable collapse state when the host allows it (scoped plugin ids hit a
59
+ // host-side segment validation error — anomalyco/opencode#62634); in-memory
60
+ // fallback otherwise: sidebar works, collapse not persisted across restarts.
61
+ const [store, mutate] = createSidebarState(context.storage);
62
+ const [snapshot, setSnapshot] = createSignal<MeshSnapshot | null>(null);
63
+ const [lines, setLines] = createSignal<string[] | null>(null);
33
64
 
34
65
  // Live sidebar: poll the server-side snapshot over RPC. On a v1 host (no
35
66
  // RPC) or while the server is unreachable, the slot stays hidden.
36
- const [lines, setLines] = createSignal<string[] | null>(null);
37
67
  const mesh = context.client.rpc(MeshRpc);
38
68
  const refresh = async () => {
39
69
  try {
40
70
  const snap = (await mesh.snapshot({})) as MeshSnapshot;
41
- setLines(formatMeshLines(snap));
71
+ setSnapshot(snap);
72
+ setLines(formatMeshSidebar(snap, store.collapsed));
42
73
  } catch {
43
74
  setLines(null);
44
75
  }
@@ -46,20 +77,70 @@ export default Plugin.define({
46
77
  void refresh();
47
78
  const timer = setInterval(() => void refresh(), REFRESH_MS);
48
79
 
80
+ // quota-style event refresh: debounced immediate refresh after activity.
81
+ let debounce: ReturnType<typeof setTimeout> | undefined;
82
+ const schedule = () => {
83
+ if (debounce) clearTimeout(debounce);
84
+ debounce = setTimeout(() => void refresh(), EVENT_DEBOUNCE_MS);
85
+ };
86
+ // context.client.event is an AsyncIterable of V2Event, not an EventEmitter.
87
+ const events = context.client.event.subscribe();
88
+ void (async () => {
89
+ try {
90
+ for await (const _event of events) {
91
+ schedule();
92
+ }
93
+ } catch {
94
+ // stream ended or errored: polling fallback stays active.
95
+ }
96
+ })();
97
+
49
98
  const unregister = context.ui.slot({
50
99
  append: "sidebar.content",
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
- ),
100
+ render: () => themedSidebar(theme, lines),
60
101
  });
61
102
 
103
+ // Keymap layers are disposed with the plugin; no manual disposer here.
104
+ context.keymap.layer(() => ({
105
+ mode: "global",
106
+ commands: [
107
+ {
108
+ id: "mesh.status",
109
+ title: "Mesh status",
110
+ group: "Mesh",
111
+ slash: { name: "mesh" },
112
+ palette: true,
113
+ run: async () => {
114
+ await refresh();
115
+ const current = snapshot();
116
+ if (!current) {
117
+ context.ui.toast.show({
118
+ title: "Mesh",
119
+ message: "Server unreachable",
120
+ variant: "info",
121
+ });
122
+ return;
123
+ }
124
+ context.ui.dialog.show(() => dialogBody(theme, current));
125
+ context.ui.dialog.set({ size: "medium" });
126
+ },
127
+ },
128
+ {
129
+ id: "mesh.toggleSidebar",
130
+ title: "Toggle mesh sidebar",
131
+ group: "Mesh",
132
+ run: async () => {
133
+ await mutate((draft) => {
134
+ draft.collapsed = !draft.collapsed;
135
+ });
136
+ void refresh();
137
+ },
138
+ },
139
+ ],
140
+ }));
141
+
62
142
  return () => {
143
+ clearTimeout(debounce);
63
144
  clearInterval(timer);
64
145
  unregister();
65
146
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@log0u7/opencode-mesh",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
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",