bothouse-dsh 0.1.57 → 0.1.59

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.
@@ -29,13 +29,78 @@ exports.apply = function apply(ctx) {
29
29
  ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
30
30
  name: "sidebar.panellist", id: "collab-devices", order: 9, locale: NS, label: () => t("label"),
31
31
  }, Icon));
32
+ // #238: 「直到我断开」 must not become 「直到我离开这个面板」. The devices page publishes "1" on
33
+ // this key while its list holds such a link — the only kind pairing.mjs sweeps once no window
34
+ // beats (UNTIL_GRACE_MS) — and this window keeps it alive even after that page is unloaded. The
35
+ // The list is read from the server before every beat, so a revoked or disconnected link makes
36
+ // the window silent even when the devices page is closed. One window owns each beat: Web Locks
37
+ // uses ifAvailable; browsers without Web Locks use the same local-storage lease as the page.
32
38
  ctx.effect(() => {
33
- const beat = () => {
34
- globalThis.fetch?.(new URL("collab/api/remote/alive", document.baseURI), { method: "POST", credentials: "same-origin" }).catch(() => {});
39
+ const FLAG = NS + ": until-disconnect";
40
+ const LEASE = NS + ": heartbeat";
41
+ // 来源: dsh/collab/pairing.mjs ALIVE_EVERY_MS, the tick devices.html beats on as well.
42
+ const EVERY_MS = 3000;
43
+ const fetchImpl = globalThis.fetch;
44
+ const everyTick = globalThis.setInterval;
45
+ const stopTick = globalThis.clearInterval;
46
+ const base = document.baseURI;
47
+ const storage = globalThis.localStorage;
48
+ const read = () => { try { return storage?.getItem(FLAG) ?? null; } catch (error) { return null; } };
49
+ const write = (value) => { try { storage?.setItem(FLAG, value); } catch (error) {} };
50
+ const LEASE_KEY = LEASE + ": holder";
51
+ const owner = globalThis.crypto?.randomUUID?.() ?? String(Date.now()) + "-" + Math.random();
52
+ let holding = false;
53
+ const readLease = () => { try { return JSON.parse(storage?.getItem(LEASE_KEY) ?? "null"); } catch (error) { return null; } };
54
+ const writeLease = value => { try { storage?.setItem(LEASE_KEY, JSON.stringify(value)); } catch (error) {} };
55
+ const claim = () => {
56
+ const now = Date.now();
57
+ const current = readLease();
58
+ if (current && current.owner !== owner && current.expires > now) return false;
59
+ // 来源: two ALIVE_EVERY_MS ticks, so a closed tab's fallback lease is reclaimed promptly.
60
+ writeLease({ owner, expires: now + EVERY_MS * 2 });
61
+ holding = readLease()?.owner === owner;
62
+ return holding;
35
63
  };
36
- beat();
37
- const timer = setInterval(beat, 3000);
38
- return () => clearInterval(timer);
64
+ const release = () => { if (holding && readLease()?.owner === owner) { try { storage?.removeItem(LEASE_KEY); } catch (error) {} } };
65
+ const getLinks = async () => {
66
+ if (read() !== "1") return false;
67
+ try {
68
+ const res = await fetchImpl?.(new URL("collab/api/remote/links", base), { credentials: "same-origin" });
69
+ const body = await res?.json();
70
+ const active = Array.isArray(body?.links) && body.links.some(l => l.mode === "temp" && l.expires_at === null);
71
+ write(active ? "1" : "0");
72
+ return active;
73
+ } catch (error) { return true; }
74
+ };
75
+ const pulse = async () => {
76
+ if (!(await getLinks())) return;
77
+ await fetchImpl?.(new URL("collab/api/remote/alive", base), { method: "POST", credentials: "same-origin" }).catch(() => {});
78
+ };
79
+ let holderTimer;
80
+ const locks = globalThis.navigator?.locks;
81
+ if (locks?.request) {
82
+ const control = new AbortController();
83
+ const acquire = () => { if (read() !== "1") return Promise.resolve(); return locks.request(LEASE, { mode: "exclusive", ifAvailable: true }, lock => {
84
+ if (!lock) return;
85
+ holderTimer = everyTick(pulse, EVERY_MS);
86
+ void pulse();
87
+ return new Promise(resolve => control.signal.addEventListener("abort", resolve, { once: true }));
88
+ }).catch(() => {}); };
89
+ const retryTimer = everyTick(acquire, EVERY_MS);
90
+ void acquire();
91
+ return () => { control.abort(); stopTick(retryTimer); stopTick(holderTimer); };
92
+ }
93
+ const acquire = async () => {
94
+ if (read() !== "1" || (!holding && !claim())) return;
95
+ if (holderTimer === undefined) {
96
+ holderTimer = everyTick(pulse, EVERY_MS);
97
+ void pulse();
98
+ }
99
+ writeLease({ owner, expires: Date.now() + EVERY_MS * 2 });
100
+ };
101
+ const retryTimer = everyTick(acquire, EVERY_MS);
102
+ void acquire();
103
+ return () => { stopTick(retryTimer); stopTick(holderTimer); release(); };
39
104
  }, NS + ": remote link heartbeat");
40
105
  if (location.hash === "#collab-devices") ctx.layout.selectPanel("collab-devices");
41
106
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bothouse-dsh",
3
- "version": "0.1.57",
3
+ "version": "0.1.59",
4
4
  "description": "BotHouse channels for DeepSeek Harness: Discord-style rooms where DSH models and local CLIs (Claude Code, Codex, Gemini) are members",
5
5
  "type": "module",
6
6
  "main": "./collab/plugin.mjs",
@@ -51,20 +51,17 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@agentclientprotocol/sdk": "1.4.0",
54
- "@zed-industries/claude-code-acp": "^0.16.2",
55
54
  "bothouse-dsh-channels": "0.1.0",
56
55
  "bothouse-dsh-desk": "0.1.0",
57
56
  "bothouse-dsh-devices": "0.1.0",
58
57
  "bothouse-dsh-panel": "0.1.0",
59
- "ws": "8.22.1"
58
+ "ws": "8.22.1",
59
+ "zod": "^4.0.0"
60
60
  },
61
61
  "bundleDependencies": [
62
62
  "bothouse-dsh-channels",
63
63
  "bothouse-dsh-desk",
64
64
  "bothouse-dsh-panel",
65
65
  "bothouse-dsh-devices"
66
- ],
67
- "optionalDependencies": {
68
- "@agentclientprotocol/codex-acp": "^2.2.2"
69
- }
66
+ ]
70
67
  }