@timlaing/opencode-tunnel-switch 1.4.3 → 1.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.
Files changed (3) hide show
  1. package/README.md +7 -5
  2. package/package.json +3 -3
  3. package/src/tui.ts +22 -9
package/README.md CHANGED
@@ -7,9 +7,11 @@ The external [ssh-tunnel-switch](https://github.com/timlaing/ssh-tunnel-switch)
7
7
  After a successful tunnel switch, the plugin requests that OpenCode retry the current model request after one second, continuing the existing work automatically. Status display runs independently so a slow or failed notification cannot delay continuation. If a switch returns 429 (for example, another switch is already in progress), only the current work request is retried after `Retry-After`; the switch is not retried in that retry sequence. A local cooldown similarly delays the work request until it expires. Other switch failures stop the retry, and OpenCode's retry limit still applies.
8
8
 
9
9
  The OpenCode session sidebar also includes a **Tunnel** section with connection
10
- state, server, ID, start time, uptime, proxy address and API availability. It
11
- refreshes every five seconds; `/tunnel-refresh` refreshes immediately. Failed
12
- checks mark previous details stale. Reload OpenCode after updating the plugin.
10
+ state, server, start time, uptime, proxy address and API availability. While
11
+ collapsed, the header still names the tunnel and its reachability, for example
12
+ `▶ Tunnel (fra1 · reachable)`. It refreshes every five seconds;
13
+ `/tunnel-refresh` refreshes immediately. Failed checks mark previous details
14
+ stale. Reload OpenCode after updating the plugin.
13
15
  The TUI uses the same API options from the terminal machine, so remote-server
14
16
  setups need an API address reachable from that machine.
15
17
 
@@ -69,7 +71,7 @@ Keep registry credentials in your npm user configuration, not the repository. Co
69
71
  "$schema": "https://opencode.ai/config.json",
70
72
  "plugin": [
71
73
  [
72
- "@timlaing/opencode-tunnel-switch@1.4.3",
74
+ "@timlaing/opencode-tunnel-switch@1.5.0",
73
75
  {
74
76
  "apiIp": "127.0.0.1",
75
77
  "apiPort": 8765,
@@ -85,7 +87,7 @@ The REST token is separate from GitHub registry credentials. Proxy IP/port are d
85
87
  ## Serializer dependency
86
88
 
87
89
  OpenTUI currently requires a Solid version that constrains Seroval to a vulnerable
88
- release range. This checkout overrides Seroval to `^1.6.9` and audits that resolved
90
+ release range. This checkout overrides Seroval to `~1.6.11` and audits that resolved
89
91
  tree. npm applies overrides only from the installing root project: applications
90
92
  installing this package must also enforce a patched Seroval version in their root
91
93
  dependency configuration. This package's override does not patch OpenCode's own
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timlaing/opencode-tunnel-switch",
3
- "version": "1.4.3",
3
+ "version": "1.6.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./index.ts",
@@ -16,7 +16,7 @@
16
16
  "verify:published": "node scripts/verify-published-package.mjs"
17
17
  },
18
18
  "dependencies": {
19
- "@opencode/plugin": "2.0.32",
19
+ "@opencode/plugin": "2.1.4",
20
20
  "zod": "^4.0.0"
21
21
  },
22
22
  "devDependencies": {
@@ -49,6 +49,6 @@
49
49
  "solid-js": "^1.9.12"
50
50
  },
51
51
  "overrides": {
52
- "seroval": "^1.6.9"
52
+ "seroval": "~1.6.11"
53
53
  }
54
54
  }
package/src/tui.ts CHANGED
@@ -100,11 +100,13 @@ export default Plugin.define({
100
100
  const positive = themed(["text", "feedback", "success", "base"], "#a6e3a1");
101
101
  const warning = themed(["text", "feedback", "warning", "base"], "#f9e2af");
102
102
  const negative = themed(["text", "feedback", "error", "base"], "#f38ba8");
103
- // A row: white label left, value flush right (grey or state-coloured),
104
- // matching the MCP sidebar's label/value layout.
103
+ // A row: bold title in the theme's base colour, two-space indented and
104
+ // without a colon; value flush right (muted or state-coloured), matching
105
+ // the sibling sidebars' label/value layout.
105
106
  const line = (label: string, value: string | (() => string), colour?: string, suffix?: string) =>
106
107
  jsx("box", { flexDirection: "row", gap: 1, minWidth: 0, get children() { return [
107
- jsx("text", { fg: base, wrapMode: "none", truncate: true, flexGrow: 1, flexShrink: 1, minWidth: 0, children: `${label}:` }),
108
+ jsx("text", { fg: base, wrapMode: "none", truncate: true, flexGrow: 1, flexShrink: 1, minWidth: 0,
109
+ get children() { return jsx("b", { children: ` ${label}` }); } }),
108
110
  jsx("text", { wrapMode: "none", truncate: true, flexShrink: 0, get children() {
109
111
  const children = [jsx("span", {
110
112
  style: { fg: colour ?? muted },
@@ -114,10 +116,20 @@ export default Plugin.define({
114
116
  return children;
115
117
  } }),
116
118
  ]; } });
117
- // Muted summary shown beside "Tunnel" while the sidebar is collapsed.
118
- const collapsedSummary = (checked: number | undefined, available: boolean): string => {
119
- if (!checked) return " (checking…)";
120
- return available ? " (reachable)" : " (unavailable)";
119
+ // Muted summary shown beside "Tunnel" while the sidebar is collapsed,
120
+ // naming the tunnel (its configured server) once one has been reported.
121
+ // The name is capped so a long hostname cannot push the reachability
122
+ // state out of the collapsed header.
123
+ /** Keep a server name within 20 columns, elided with an ellipsis. */
124
+ const clipName = (name: string): string => (name.length <= 20 ? name : `${name.slice(0, 19)}…`);
125
+ const collapsedSummary = (name: string | null | undefined, checked: number | undefined,
126
+ available: boolean): string => {
127
+ let reach: string;
128
+ if (!checked) reach = "checking…";
129
+ else if (available) reach = "reachable";
130
+ else reach = "unavailable";
131
+ if (!name) return ` (${reach})`;
132
+ return ` (${clipName(name)} · ${reach})`;
121
133
  };
122
134
  // Feedback colour for a tunnel state.
123
135
  const stateColour = (state: TunnelStatus["state"]): string => {
@@ -153,7 +165,7 @@ export default Plugin.define({
153
165
  jsx("text", { fg: base, get children() {
154
166
  const children = [jsx("b", { children: "Tunnel" })];
155
167
  if (!open) children.push(jsx("span", { style: { fg: muted },
156
- children: collapsedSummary(checked, available) }));
168
+ children: collapsedSummary(status?.server, checked, available) }));
157
169
  return children;
158
170
  } }),
159
171
  ]; } });
@@ -172,7 +184,8 @@ export default Plugin.define({
172
184
  line("Proxy", status.proxy),
173
185
  );
174
186
  }
175
- lines.push(line("Checked", () => formatAge(checked, clock())));
187
+ // The relative age stays muted (the default value colour) between polls.
188
+ lines.push(line("Checked", () => formatAge(checked, clock()), muted));
176
189
  return [header, ...lines];
177
190
  } });
178
191
  } catch {