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 +14 -0
- package/package.json +1 -1
- package/src/shared/signaling-client.ts +57 -1
- package/src/web/discovery.tsx +43 -9
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
|
@@ -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
|
-
|
|
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 {
|
package/src/web/discovery.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
|
769
|
-
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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 },
|