codehost 0.18.1 → 0.19.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
@@ -1,3 +1,17 @@
1
+ # [0.19.0](https://github.com/snomiao/codehost/compare/v0.18.2...v0.19.0) (2026-06-11)
2
+
3
+
4
+ ### Features
5
+
6
+ * **web:** GitHub-URL header/title for the open workspace; agent chips link into the agent-yes console ([83d62bc](https://github.com/snomiao/codehost/commit/83d62bc2ab3e70722f8a4f0f817541bea7cf69a2))
7
+
8
+ ## [0.18.2](https://github.com/snomiao/codehost/compare/v0.18.1...v0.18.2) (2026-06-11)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **signaling:** recover instantly when a throttled tab wakes (visibility/focus/online) ([d459132](https://github.com/snomiao/codehost/commit/d45913267f053ee1fee062bae550cfe60a193207))
14
+
1
15
  ## [0.18.1](https://github.com/snomiao/codehost/compare/v0.18.0...v0.18.1) (2026-06-11)
2
16
 
3
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codehost",
3
- "version": "0.18.1",
3
+ "version": "0.19.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -53,6 +53,7 @@ export class SignalingClient {
53
53
  private ws: WebSocket | null = null;
54
54
  private closed = false;
55
55
  private reconnectDelay = 1000;
56
+ private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
56
57
  private heartbeat: ReturnType<typeof setInterval> | null = null;
57
58
  /** Fires STABLE_MS after a socket opens; only then is the backoff reset. */
58
59
  private stableTimer: ReturnType<typeof setTimeout> | null = null;
@@ -65,9 +66,53 @@ export class SignalingClient {
65
66
 
66
67
  connect(): void {
67
68
  this.closed = false;
69
+ this.attachWakeListeners();
68
70
  this.open();
69
71
  }
70
72
 
73
+ // ---- background-tab recovery -------------------------------------------
74
+ // Chrome throttles timers in hidden tabs to minutes, so the backoff retry
75
+ // (and the connect-timeout abort) may be arbitrarily far away even though a
76
+ // fresh socket would connect in milliseconds. When the tab becomes visible /
77
+ // focused / back online, recover NOW instead of waiting for a timer.
78
+
79
+ private onWake = (): void => {
80
+ if (this.closed) return;
81
+ const state = this.ws?.readyState;
82
+ if (state === 1 /* OPEN */) return;
83
+ if (state === 0 /* CONNECTING */) {
84
+ // Stuck handshake: abort — onclose reschedules, and timers run normally
85
+ // now that the tab is active.
86
+ try {
87
+ this.ws?.close();
88
+ } catch {
89
+ // ignore
90
+ }
91
+ return;
92
+ }
93
+ // Closed and waiting out the (throttled) backoff: skip the wait.
94
+ if (this.reconnectTimer != null) {
95
+ this.clearReconnectTimer();
96
+ this.open();
97
+ }
98
+ };
99
+
100
+ private attachWakeListeners(): void {
101
+ const doc = (globalThis as { document?: EventTarget }).document;
102
+ doc?.addEventListener("visibilitychange", this.onWake);
103
+ const win = (globalThis as { window?: EventTarget }).window;
104
+ win?.addEventListener("focus", this.onWake);
105
+ win?.addEventListener("online", this.onWake);
106
+ }
107
+
108
+ private detachWakeListeners(): void {
109
+ const doc = (globalThis as { document?: EventTarget }).document;
110
+ doc?.removeEventListener("visibilitychange", this.onWake);
111
+ const win = (globalThis as { window?: EventTarget }).window;
112
+ win?.removeEventListener("focus", this.onWake);
113
+ win?.removeEventListener("online", this.onWake);
114
+ }
115
+
71
116
  private roomUrl(): string {
72
117
  const base = this.opts.url.replace(/\/+$/, "");
73
118
  return `${base}/room/${encodeURIComponent(this.opts.token)}`;
@@ -172,11 +217,20 @@ export class SignalingClient {
172
217
  private scheduleReconnect(): void {
173
218
  const delay = this.reconnectDelay;
174
219
  this.reconnectDelay = Math.min(delay * 2, 15000);
175
- setTimeout(() => {
220
+ this.clearReconnectTimer();
221
+ this.reconnectTimer = setTimeout(() => {
222
+ this.reconnectTimer = null;
176
223
  if (!this.closed) this.open();
177
224
  }, delay);
178
225
  }
179
226
 
227
+ private clearReconnectTimer(): void {
228
+ if (this.reconnectTimer != null) {
229
+ clearTimeout(this.reconnectTimer);
230
+ this.reconnectTimer = null;
231
+ }
232
+ }
233
+
180
234
  sendSignal(to: string, data: unknown): void {
181
235
  const msg: ClientMessage = { type: "signal", to, data };
182
236
  this.ws?.send(JSON.stringify(msg));
@@ -194,6 +248,8 @@ export class SignalingClient {
194
248
 
195
249
  close(): void {
196
250
  this.closed = true;
251
+ this.detachWakeListeners();
252
+ this.clearReconnectTimer();
197
253
  this.stopHeartbeat();
198
254
  this.clearStableTimer();
199
255
  try {
@@ -58,6 +58,18 @@ function folderQuery(folder?: string): string {
58
58
  return folder ? `?folder=${encodeURIComponent(folder)}` : "";
59
59
  }
60
60
 
61
+ /** Human label for a connected workspace: its GitHub-style URL when the share
62
+ * path is repo-shaped (/gh/owner/repo -> github.com/owner/repo, /git/<host>/…
63
+ * -> <host>/…), else the deep-link path as-is. */
64
+ function shareLabel(path: string | null): string | null {
65
+ if (!path) return null;
66
+ const gh = path.match(/^\/gh\/(.+)$/);
67
+ if (gh) return `github.com/${gh[1]}`;
68
+ const git = path.match(/^\/git\/(.+)$/);
69
+ if (git) return git[1];
70
+ return path;
71
+ }
72
+
61
73
  /**
62
74
  * Find which of the user's saved rooms hosts a server matching a token-less deep
63
75
  * link. Opens a short-lived viewer connection to each candidate room in
@@ -292,6 +304,13 @@ export function Discovery() {
292
304
  tryAutoConnect();
293
305
  }, [serversByRoom, tokens]);
294
306
 
307
+ // Mirror the open workspace into the tab title (GitHub-style URL), so tabs
308
+ // read as "github.com/owner/repo/tree/main — codehost", not all "Codehost".
309
+ useEffect(() => {
310
+ const label = connState === "connected" ? shareLabel(sharePathRef.current) : null;
311
+ document.title = label ? `${label} — codehost` : "Codehost";
312
+ }, [connState]);
313
+
295
314
  // Keep the connection in sync with the URL as servers come and go: reconnect
296
315
  // when the workspace named by the address bar (re)appears in a room — covers a
297
316
  // daemon restart or a dropped channel while the tab stays open.
@@ -691,8 +710,10 @@ export function Discovery() {
691
710
  // Group workspaces by machine: the stable hostId when the daemon advertises
692
711
  // one, else the hostname string (older daemons), else the peer stands alone.
693
712
  // Agents are machine-level (advertised by the host's root daemon) — collect
694
- // them per group, deduped by pid across peers.
695
- const hostGroups: { key: string; label: string; items: typeof filtered; agents: AgentInfo[] }[] = [];
713
+ // them per group, deduped by pid across peers, each remembering its room so
714
+ // a click can hand the agent-yes console the right token.
715
+ type RoomedAgent = AgentInfo & { room: string };
716
+ const hostGroups: { key: string; label: string; items: typeof filtered; agents: RoomedAgent[] }[] = [];
696
717
  for (const t of filtered) {
697
718
  const key = t.server.meta?.hostId ?? t.server.meta?.host ?? t.server.peerId;
698
719
  let group = hostGroups.find((g) => g.key === key);
@@ -702,7 +723,7 @@ export function Discovery() {
702
723
  }
703
724
  group.items.push(t);
704
725
  for (const a of t.server.meta?.agents ?? []) {
705
- if (!group.agents.some((x) => x.pid === a.pid)) group.agents.push(a);
726
+ if (!group.agents.some((x) => x.pid === a.pid)) group.agents.push({ ...a, room: t.room });
706
727
  }
707
728
  }
708
729
  const toggleTag = (t: string) =>
@@ -765,8 +786,15 @@ export function Discovery() {
765
786
  <header style={styles.header}>
766
787
  <span style={styles.brand}>codehost</span>
767
788
  <span style={styles.dim}>·</span>
768
- <span style={styles.dim}>{activeServer?.meta?.name ?? activePeerId?.slice(0, 8)}</span>
769
- {activeServer?.meta?.cwd && <span style={styles.cwd}>{activeServer.meta.cwd}</span>}
789
+ <span
790
+ style={styles.cwd}
791
+ title={`${activeServer?.meta?.name ?? ""} ${activeServer?.meta?.cwd ?? ""}`.trim()}
792
+ >
793
+ {shareLabel(sharePathRef.current) ??
794
+ activeServer?.meta?.cwd ??
795
+ activeServer?.meta?.name ??
796
+ activePeerId?.slice(0, 8)}
797
+ </span>
770
798
  <span style={{ flex: 1 }} />
771
799
  <button
772
800
  style={styles.shareBtn}
@@ -959,14 +987,20 @@ export function Discovery() {
959
987
  {g.agents.length > 0 && (
960
988
  <div style={styles.agentRow}>
961
989
  {g.agents.map((a) => (
962
- <span
990
+ <a
963
991
  key={a.pid}
964
992
  style={styles.agentChip}
965
- title={`${a.cwd}${a.title ? `\n${a.title}` : ""}`}
993
+ title={`${a.cwd}${a.title ? `\n${a.title}` : ""}\nopen in the agent-yes console`}
994
+ // Tail & send live in the agent-yes console — it joins
995
+ // this same room as a viewer (token rides the fragment,
996
+ // never sent to a server) and auto-selects the pid.
997
+ href={`https://agent-yes.com/?pid=${a.pid}#ch:${encodeURIComponent(a.room)}`}
998
+ target="_blank"
999
+ rel="noopener"
966
1000
  >
967
1001
  <span style={{ color: a.state === "active" ? "#4ec9b0" : "#777" }}>●</span> {a.tool}{" "}
968
1002
  {a.pid}
969
- </span>
1003
+ </a>
970
1004
  ))}
971
1005
  </div>
972
1006
  )}
@@ -1081,7 +1115,7 @@ const styles: Record<string, React.CSSProperties> = {
1081
1115
  agentRow: { display: "flex", flexWrap: "wrap", gap: 6, margin: "0 0 8px" },
1082
1116
  agentChip: {
1083
1117
  fontFamily: "monospace", fontSize: 11.5, padding: "2px 8px", borderRadius: 999,
1084
- border: "1px solid #3d3d3d", color: "#9aa4af",
1118
+ border: "1px solid #3d3d3d", color: "#9aa4af", textDecoration: "none", cursor: "pointer",
1085
1119
  },
1086
1120
  card: { display: "flex", alignItems: "center", gap: 12, background: "#252525", border: "1px solid #3d3d3d", borderRadius: 8, padding: "12px 14px" },
1087
1121
  cardMain: { flex: 1, minWidth: 0 },