codehost 0.10.0 → 0.11.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.
@@ -0,0 +1 @@
1
+ {"sessionId":"f8e4e571-944e-43d7-bbfa-267a5251e41c","pid":87968,"procStart":"Mon Jun 8 09:46:02 2026","acquiredAt":1780974562147}
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [0.11.0](https://github.com/snomiao/codehost/compare/v0.10.0...v0.11.0) (2026-06-09)
2
+
3
+
4
+ ### Features
5
+
6
+ * **web:** update URL on Connect + Back returns to the list ([7258a16](https://github.com/snomiao/codehost/commit/7258a168a0bcc1d476aab8a7f3fcd81f35ccfcb5))
7
+
1
8
  # [0.10.0](https://github.com/snomiao/codehost/compare/v0.9.1...v0.10.0) (2026-06-09)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codehost",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -176,6 +176,11 @@ export function Discovery() {
176
176
  const activePeerRef = useRef<string | null>(null);
177
177
  const activeRoomRef = useRef<string | null>(null);
178
178
  const sendersRef = useRef<Map<string, (to: string, data: unknown) => void>>(new Map());
179
+ // Whether the live connection pushed a history entry (so Disconnect/Back can
180
+ // pop it), and whether we're currently *viewing* a workspace (so popstate only
181
+ // tears down from the iframe view, not during a mid-connect URL revert).
182
+ const pushedRef = useRef(false);
183
+ const viewingRef = useRef(false);
179
184
 
180
185
  // Deep-link resolution (/gh/<owner>/<repo>/... or /dev/<path>): parse once,
181
186
  // auto-connect when a matching server appears, remember the opened folder.
@@ -213,6 +218,13 @@ export function Discovery() {
213
218
  const folder = activeFolderRef.current;
214
219
  setTimeout(() => setIframeSrc(`/vs/${peerId}/${folderQuery(folder)}`), 400);
215
220
  });
221
+ // Back / Cmd+Left from a workspace returns to the list: the browser restores
222
+ // the previous URL and we drop the connection. Only when actually viewing —
223
+ // a mid-connect URL revert (failed dial) must leave the list state untouched.
224
+ const onPopState = () => {
225
+ if (viewingRef.current) teardownConn();
226
+ };
227
+ window.addEventListener("popstate", onPopState);
216
228
  // A valid token in the URL fragment (#t=<token>) joins the room and turns on
217
229
  // single-server auto-connect for it; consume it from the address bar after,
218
230
  // so the secret isn't left visible or re-applied on a manual reload.
@@ -242,6 +254,8 @@ export function Discovery() {
242
254
  }
243
255
  }
244
256
  }
257
+
258
+ return () => window.removeEventListener("popstate", onPopState);
245
259
  }, []);
246
260
 
247
261
  // Auto-connect once discovery turns up a match: a deep-link target across any
@@ -291,8 +305,19 @@ export function Discovery() {
291
305
  setActivePeerId(server.peerId);
292
306
  activePeerRef.current = server.peerId;
293
307
  activeRoomRef.current = room;
308
+ viewingRef.current = false;
294
309
  setConnState("connecting");
295
310
 
311
+ // Update the address bar the instant Connect is clicked (don't wait for the
312
+ // WebRTC handshake) and push a history entry, so Back / Cmd+Left returns to
313
+ // the list. Reverted if the connection fails.
314
+ const openFolder = folder ?? server.meta?.cwd;
315
+ const targetPath = shareablePathFor(server, openFolder);
316
+ const pushed = !!targetPath && targetPath !== window.location.pathname;
317
+ if (pushed) history.pushState(null, "", targetPath);
318
+ pushedRef.current = pushed;
319
+ sharePathRef.current = targetPath ?? window.location.pathname;
320
+
296
321
  // The broker decides whether this tab owns the connection. `establish` is
297
322
  // only invoked when we're the owner (or get promoted on failover); other
298
323
  // tabs reuse the owner's channel via a proxy, so they never open WebRTC.
@@ -326,29 +351,31 @@ export function Discovery() {
326
351
  await connBroker.connect(server.peerId, establish);
327
352
  setConnState("connected");
328
353
  // The daemon no longer sets a default folder (current VS Code serve-web
329
- // dropped that flag), so open the served workspace from here: an explicit
354
+ // dropped that flag), so open the served workspace from here: the
330
355
  // deep-link folder if we have one, else the server's reported cwd.
331
- const openFolder = folder ?? server.meta?.cwd;
332
356
  activeFolderRef.current = openFolder;
333
357
  setIframeSrc(`/vs/${server.peerId}/${folderQuery(openFolder)}`);
334
358
  setResolving(null);
335
359
  recordConnect(server, room, openFolder);
336
- updateAddressBar(server, openFolder);
360
+ viewingRef.current = true;
337
361
  } catch {
338
362
  setConnState("failed");
363
+ // Undo the optimistic history entry / URL change. We never started
364
+ // viewing, so the popstate handler leaves the "failed" card in place.
365
+ if (pushed) {
366
+ pushedRef.current = false;
367
+ history.back();
368
+ }
339
369
  }
340
370
  }
341
371
 
342
- // Reflect the live connection in the address bar as a clean, shareable deep
343
- // link (no token — Share adds that). If we arrived via a deep link, keep its
344
- // pathname; otherwise derive one from the server's repo identity or folder.
345
- function updateAddressBar(server: PeerInfo, folder?: string) {
346
- const path = deepLinkRef.current
372
+ // Shareable deep-link pathname for a server+folder, with no side effects (no
373
+ // token — Share adds that). Keeps an existing deep-link path as-is; otherwise
374
+ // derives /gh|/git|/dev from the server's repo identity or opened folder.
375
+ function shareablePathFor(server: PeerInfo, folder?: string): string | null {
376
+ return deepLinkRef.current
347
377
  ? window.location.pathname
348
378
  : shareableDeepLink({ repo: server.meta?.repo, branch: server.meta?.branch, folder });
349
- if (!path) return;
350
- sharePathRef.current = path;
351
- if (path !== window.location.pathname) history.replaceState(null, "", path);
352
379
  }
353
380
 
354
381
  async function shareLink() {
@@ -408,7 +435,10 @@ export function Discovery() {
408
435
  if (dl?.type === "repo") recordConnection(repoKey(dl.target), { ...base, folder });
409
436
  }
410
437
 
411
- function disconnect() {
438
+ // Tear down the active connection and return to the workspace list. Does NOT
439
+ // touch history — the caller (Disconnect → history.back, or a popstate from
440
+ // Cmd+Left) owns the URL.
441
+ function teardownConn() {
412
442
  rtcRef.current?.close();
413
443
  rtcRef.current = null;
414
444
  if (activePeerRef.current) connBroker.disconnect(activePeerRef.current);
@@ -418,6 +448,18 @@ export function Discovery() {
418
448
  activeRoomRef.current = null;
419
449
  setConnState("idle");
420
450
  sharePathRef.current = null;
451
+ pushedRef.current = false;
452
+ viewingRef.current = false;
453
+ }
454
+
455
+ function disconnect() {
456
+ // Mirror Cmd+Left: if connecting pushed a history entry, pop it — the
457
+ // browser restores the previous URL and our popstate handler tears down.
458
+ if (pushedRef.current) {
459
+ history.back();
460
+ return;
461
+ }
462
+ teardownConn();
421
463
  if (window.location.pathname !== "/") history.replaceState(null, "", "/");
422
464
  }
423
465