@blue-soda/dsh-remote 0.5.4 → 0.5.5

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.
@@ -1410,7 +1410,12 @@
1410
1410
  );
1411
1411
  }
1412
1412
  function RemoteWorkspaceAction(props) {
1413
- let { t } = props, [open, setOpen] = React.useState(!1), [status, setStatus] = React.useState(void 0), [devices, setDevices] = React.useState([]), [selectedHost, setSelectedHost] = React.useState(void 0), [connectingHost, setConnectingHost] = React.useState(void 0), [workspaces, setWorkspaces] = React.useState([]), [codexWorkspaces, setCodexWorkspaces] = React.useState([]), [codexNotice, setCodexNotice] = React.useState(void 0), [codexAvailable, setCodexAvailable] = React.useState(!1), codexProbeAvailable = React.useRef(!1), [codexOpen, setCodexOpen] = React.useState(!1), [workspaceBackend, setWorkspaceBackend] = React.useState("harness"), [codexWorkspaceId, setCodexWorkspaceId] = React.useState(void 0), [directory, setDirectory] = React.useState(void 0), [path, setPath] = React.useState(""), [addingWorkspace, setAddingWorkspace] = React.useState(!1), [showAllWorkspaces, setShowAllWorkspaces] = React.useState(!1), [showAllCodexWorkspaces, setShowAllCodexWorkspaces] = React.useState(!1), [devicesOpen, setDevicesOpen] = React.useState(!1), workspaceListId = "dsh-remote-workspace-list", codexWorkspaceHeadingId = "dsh-remote-codex-workspace-heading", codexWorkspaceListId = "dsh-remote-codex-workspace-list", [busy, setBusy] = React.useState(!1), [needsAuthorization, setNeedsAuthorization] = React.useState(!1), [authorizationResolved, setAuthorizationResolved] = React.useState(!1), [loginServerUrl, setLoginServerUrl] = React.useState(DEFAULT_REMOTE_SERVER_URL), [editingServerUrl, setEditingServerUrl] = React.useState(!1), [awaitingDeepSeek, setAwaitingDeepSeek] = React.useState(!1), [pendingAuthorizeUrl, setPendingAuthorizeUrl] = React.useState(void 0), [loginMethod] = React.useState(
1413
+ let { t } = props, [open, setOpen] = React.useState(!1);
1414
+ React.useEffect(() => {
1415
+ open && props.control("settings.get").catch(() => {
1416
+ });
1417
+ }, [open]);
1418
+ let [status, setStatus] = React.useState(void 0), [devices, setDevices] = React.useState([]), [selectedHost, setSelectedHost] = React.useState(void 0), [connectingHost, setConnectingHost] = React.useState(void 0), [workspaces, setWorkspaces] = React.useState([]), [codexWorkspaces, setCodexWorkspaces] = React.useState([]), [codexNotice, setCodexNotice] = React.useState(void 0), [codexAvailable, setCodexAvailable] = React.useState(!1), codexProbeAvailable = React.useRef(!1), [codexOpen, setCodexOpen] = React.useState(!1), [workspaceBackend, setWorkspaceBackend] = React.useState("harness"), [codexWorkspaceId, setCodexWorkspaceId] = React.useState(void 0), [directory, setDirectory] = React.useState(void 0), [path, setPath] = React.useState(""), [addingWorkspace, setAddingWorkspace] = React.useState(!1), [showAllWorkspaces, setShowAllWorkspaces] = React.useState(!1), [showAllCodexWorkspaces, setShowAllCodexWorkspaces] = React.useState(!1), [devicesOpen, setDevicesOpen] = React.useState(!1), workspaceListId = "dsh-remote-workspace-list", codexWorkspaceHeadingId = "dsh-remote-codex-workspace-heading", codexWorkspaceListId = "dsh-remote-codex-workspace-list", [busy, setBusy] = React.useState(!1), [needsAuthorization, setNeedsAuthorization] = React.useState(!1), authorizedOnce = React.useRef(!1), [authorizationResolved, setAuthorizationResolved] = React.useState(!1), [loginServerUrl, setLoginServerUrl] = React.useState(DEFAULT_REMOTE_SERVER_URL), [editingServerUrl, setEditingServerUrl] = React.useState(!1), [awaitingDeepSeek, setAwaitingDeepSeek] = React.useState(!1), [pendingAuthorizeUrl, setPendingAuthorizeUrl] = React.useState(void 0), [loginMethod] = React.useState(
1414
1419
  isEnabledQrProvider(props.preferredQrProvider) ? props.preferredQrProvider : defaultQrProvider
1415
1420
  ), [qrSession, setQrSession] = React.useState(void 0), [qrExpired, setQrExpired] = React.useState(!1), [progress, setProgress] = React.useState(void 0), progressRun = React.useRef(0), qrFlowRun = React.useRef(0), [notice, setNotice] = React.useState(void 0), [confirmingSignOut, setConfirmingSignOut] = React.useState(!1), [confirmingRevoke, setConfirmingRevoke] = React.useState(!1), [error, setError] = React.useState(void 0);
1416
1421
  React.useEffect(() => {
@@ -1476,7 +1481,7 @@
1476
1481
  polling || settled || (polling = !0, props.control("client.account.qr.poll", { qrId: qrSession.qrId }).then(async (result) => {
1477
1482
  if (!(!active || settled || run !== qrFlowRun.current))
1478
1483
  if (result.status === "complete") {
1479
- settled = !0, timer !== void 0 && window.clearInterval(timer), setBusy(!0), setError(void 0), setQrExpired(!1), setNeedsAuthorization(!1);
1484
+ settled = !0, timer !== void 0 && window.clearInterval(timer), setBusy(!0), setError(void 0), setQrExpired(!1), authorizedOnce.current = !0, setNeedsAuthorization(!1);
1480
1485
  try {
1481
1486
  let [nextDevices, nextStatus] = await Promise.all([
1482
1487
  props.control("devices"),
@@ -1575,25 +1580,25 @@
1575
1580
  try {
1576
1581
  let nextStatus = await props.control("status");
1577
1582
  if (setStatus(nextStatus), !nextStatus.available) {
1578
- setDevices([]), setNeedsAuthorization(!1), setAuthorizationResolved(!0), setSelectedHost(void 0), setWorkspaces([]), setCodexWorkspaces([]), setShowAllWorkspaces(!1), setShowAllCodexWorkspaces(!1), setWorkspaceBackend("harness"), setCodexWorkspaceId(void 0), setPath(""), setAddingWorkspace(!1), setDirectory(void 0);
1583
+ setDevices([]), authorizedOnce.current = !0, setNeedsAuthorization(!1), setAuthorizationResolved(!0), setSelectedHost(void 0), setWorkspaces([]), setCodexWorkspaces([]), setShowAllWorkspaces(!1), setShowAllCodexWorkspaces(!1), setWorkspaceBackend("harness"), setCodexWorkspaceId(void 0), setPath(""), setAddingWorkspace(!1), setDirectory(void 0);
1579
1584
  return;
1580
1585
  }
1581
1586
  let storedLocally;
1582
1587
  try {
1583
1588
  storedLocally = (await props.control("authorization.local"))?.stored === !0, setNeedsAuthorization(!storedLocally);
1584
1589
  } catch {
1585
- setNeedsAuthorization(!1);
1590
+ authorizedOnce.current = !0, setNeedsAuthorization(!1);
1586
1591
  } finally {
1587
1592
  setAuthorizationResolved(!0);
1588
1593
  }
1589
1594
  try {
1590
1595
  let nextDevices = await props.control("devices");
1591
- if (setDevices(nextDevices), storedLocally !== void 0 && setNeedsAuthorization(authorizationStillNeeded(storedLocally, nextDevices.length)), selectedHost !== void 0) {
1596
+ if (setDevices(nextDevices), !authorizedOnce.current && storedLocally !== void 0 && setNeedsAuthorization(authorizationStillNeeded(storedLocally, nextDevices.length)), selectedHost !== void 0) {
1592
1597
  let nextSelectedHost = nextDevices.find((device) => device.deviceId === selectedHost.deviceId);
1593
1598
  nextSelectedHost === void 0 ? (setSelectedHost(void 0), setWorkspaces([]), setCodexWorkspaces([]), setShowAllWorkspaces(!1), setShowAllCodexWorkspaces(!1), setWorkspaceBackend("harness"), setCodexWorkspaceId(void 0), setPath(""), setAddingWorkspace(!1), setDirectory(void 0)) : setSelectedHost(nextSelectedHost);
1594
1599
  }
1595
1600
  } catch {
1596
- setDevices([]), setNeedsAuthorization(!0), setSelectedHost(void 0), setWorkspaces([]), setCodexWorkspaces([]), setShowAllWorkspaces(!1), setShowAllCodexWorkspaces(!1), setWorkspaceBackend("harness"), setCodexWorkspaceId(void 0), setPath(""), setAddingWorkspace(!1), setDirectory(void 0);
1601
+ setDevices([]), authorizedOnce.current = !1, setNeedsAuthorization(!0), setSelectedHost(void 0), setWorkspaces([]), setCodexWorkspaces([]), setShowAllWorkspaces(!1), setShowAllCodexWorkspaces(!1), setWorkspaceBackend("harness"), setCodexWorkspaceId(void 0), setPath(""), setAddingWorkspace(!1), setDirectory(void 0);
1597
1602
  }
1598
1603
  selectedHost !== void 0 && (await props.control("remote.session.recover").then((next) => setStatus(next)).catch(() => {
1599
1604
  }), await Promise.all([loadHarnessWorkspaces(selectedHost), loadCodexWorkspaces(selectedHost)]));
@@ -1604,6 +1609,17 @@
1604
1609
  }
1605
1610
  }, show = async () => {
1606
1611
  setShowAllWorkspaces(!1), setShowAllCodexWorkspaces(!1), setOpen(!0), await refreshRemote();
1612
+ for (let attempt = 0; attempt < 10; attempt++) {
1613
+ let list = await props.control("devices").catch(() => {
1614
+ });
1615
+ if (list !== void 0) {
1616
+ setDevices(list), authorizedOnce.current = !0, setNeedsAuthorization(!1), setAuthorizationResolved(!0), await refreshRemote();
1617
+ return;
1618
+ }
1619
+ await new Promise((resolve) => {
1620
+ setTimeout(resolve, 1e3);
1621
+ });
1622
+ }
1607
1623
  };
1608
1624
  React.useEffect(() => {
1609
1625
  if (!(!open || selectedHost !== void 0))
@@ -1629,7 +1645,22 @@
1629
1645
  }, [open, selectedHost]);
1630
1646
  let chooseAnotherHost = () => {
1631
1647
  setSelectedHost(void 0), setWorkspaces([]), setCodexWorkspaces([]), setCodexNotice(void 0), setCodexAvailable(!1), codexProbeAvailable.current = !1, setShowAllWorkspaces(!1), setShowAllCodexWorkspaces(!1), setWorkspaceBackend("harness"), setCodexWorkspaceId(void 0), setDirectory(void 0), setPath(""), setAddingWorkspace(!1), setError(void 0);
1632
- }, requestDeepSeekSignIn = async () => {
1648
+ }, signingIn = busy || status?.authorizing === !0 || needsAuthorization && status?.hasDeepSeekSession === !0;
1649
+ React.useEffect(() => {
1650
+ if (!signingIn) return;
1651
+ let tries = 0, timer = window.setInterval(() => {
1652
+ tries += 1, (async () => {
1653
+ let next = await props.control("status").catch(() => {
1654
+ });
1655
+ next !== void 0 && setStatus(next);
1656
+ let list = await props.control("devices").catch(() => {
1657
+ });
1658
+ list !== void 0 && (setDevices(list), authorizedOnce.current = !0, setNeedsAuthorization(!1), setAuthorizationResolved(!0));
1659
+ })(), tries >= 5 && window.clearInterval(timer);
1660
+ }, 1e3);
1661
+ return () => window.clearInterval(timer);
1662
+ }, [signingIn]);
1663
+ let requestDeepSeekSignIn = async () => {
1633
1664
  let transport = globalThis.__DSH_TRANSPORT__, callbackOrigin = transport?.streamBaseUrl !== void 0 ? new URL(transport.streamBaseUrl).origin : window.location.origin, result = await props.control("settings.configure", {
1634
1665
  // No `role`: a device has one identity, and the control writes only the Server address plus this
1635
1666
  // sign-in. Sending a role here was the last thing that made one look like a runtime switch.
@@ -1667,7 +1698,7 @@
1667
1698
  if (state.pending) throw new Error(t("deepseekSignInTimeout"));
1668
1699
  } else
1669
1700
  tab?.close();
1670
- setDevices(await props.control("devices")), setStatus(await props.control("status")), setNeedsAuthorization(!1), setAuthorizationResolved(!0), setNotice(void 0);
1701
+ setDevices(await props.control("devices")), setStatus(await props.control("status")), authorizedOnce.current = !0, setNeedsAuthorization(!1), setAuthorizationResolved(!0), setNotice(void 0);
1671
1702
  } catch (reason) {
1672
1703
  setError(remoteErrorMessage(reason, t));
1673
1704
  } finally {
@@ -1692,7 +1723,7 @@
1692
1723
  }, revokeRemote = async () => {
1693
1724
  setBusy(!0), setError(void 0), setNotice(void 0);
1694
1725
  try {
1695
- await props.control("client.device.revoke"), setDevices([]), setNeedsAuthorization(!0), setStatus(await props.control("status"));
1726
+ await props.control("client.device.revoke"), setDevices([]), authorizedOnce.current = !1, setNeedsAuthorization(!0), setStatus(await props.control("status"));
1696
1727
  } catch (reason) {
1697
1728
  setError(remoteErrorMessage(reason, t));
1698
1729
  } finally {
@@ -1701,7 +1732,7 @@
1701
1732
  }, logoutRemote = async () => {
1702
1733
  setBusy(!0), setError(void 0), setNotice(void 0);
1703
1734
  try {
1704
- await props.control("settings.logout"), setDevices([]), setNeedsAuthorization(!0), setQrSession(void 0), setQrExpired(!1), setStatus(await props.control("status"));
1735
+ await props.control("settings.logout"), setDevices([]), authorizedOnce.current = !1, setNeedsAuthorization(!0), setQrSession(void 0), setQrExpired(!1), setStatus(await props.control("status"));
1705
1736
  } catch (reason) {
1706
1737
  setError(remoteErrorMessage(reason, t));
1707
1738
  } finally {
@@ -1909,11 +1940,11 @@
1909
1940
  onClick: () => setEditingServerUrl(!0)
1910
1941
  }, loginServerUrl)
1911
1942
  ),
1912
- React.createElement(
1913
- "button",
1914
- { type: "button", disabled: busy || loginServerUrl.trim() === "", onClick: () => void signInClient() },
1915
- t(busy ? "signingIn" : "deepseekSignIn")
1916
- ),
1943
+ React.createElement("button", {
1944
+ type: "button",
1945
+ disabled: busy || signingIn || loginServerUrl.trim() === "",
1946
+ onClick: () => void signInClient()
1947
+ }, t(signingIn ? "signingIn" : "deepseekSignIn")),
1917
1948
  awaitingDeepSeek ? React.createElement(
1918
1949
  "p",
1919
1950
  { className: "dshRemoteServiceAddress" },
package/dist/client.js CHANGED
@@ -1410,7 +1410,12 @@
1410
1410
  );
1411
1411
  }
1412
1412
  function RemoteWorkspaceAction(props) {
1413
- let { t } = props, [open, setOpen] = React.useState(!1), [status, setStatus] = React.useState(void 0), [devices, setDevices] = React.useState([]), [selectedHost, setSelectedHost] = React.useState(void 0), [connectingHost, setConnectingHost] = React.useState(void 0), [workspaces, setWorkspaces] = React.useState([]), [codexWorkspaces, setCodexWorkspaces] = React.useState([]), [codexNotice, setCodexNotice] = React.useState(void 0), [codexAvailable, setCodexAvailable] = React.useState(!1), codexProbeAvailable = React.useRef(!1), [codexOpen, setCodexOpen] = React.useState(!1), [workspaceBackend, setWorkspaceBackend] = React.useState("harness"), [codexWorkspaceId, setCodexWorkspaceId] = React.useState(void 0), [directory, setDirectory] = React.useState(void 0), [path, setPath] = React.useState(""), [addingWorkspace, setAddingWorkspace] = React.useState(!1), [showAllWorkspaces, setShowAllWorkspaces] = React.useState(!1), [showAllCodexWorkspaces, setShowAllCodexWorkspaces] = React.useState(!1), [devicesOpen, setDevicesOpen] = React.useState(!1), workspaceListId = "dsh-remote-workspace-list", codexWorkspaceHeadingId = "dsh-remote-codex-workspace-heading", codexWorkspaceListId = "dsh-remote-codex-workspace-list", [busy, setBusy] = React.useState(!1), [needsAuthorization, setNeedsAuthorization] = React.useState(!1), [authorizationResolved, setAuthorizationResolved] = React.useState(!1), [loginServerUrl, setLoginServerUrl] = React.useState(DEFAULT_REMOTE_SERVER_URL), [editingServerUrl, setEditingServerUrl] = React.useState(!1), [awaitingDeepSeek, setAwaitingDeepSeek] = React.useState(!1), [pendingAuthorizeUrl, setPendingAuthorizeUrl] = React.useState(void 0), [loginMethod] = React.useState(
1413
+ let { t } = props, [open, setOpen] = React.useState(!1);
1414
+ React.useEffect(() => {
1415
+ open && props.control("settings.get").catch(() => {
1416
+ });
1417
+ }, [open]);
1418
+ let [status, setStatus] = React.useState(void 0), [devices, setDevices] = React.useState([]), [selectedHost, setSelectedHost] = React.useState(void 0), [connectingHost, setConnectingHost] = React.useState(void 0), [workspaces, setWorkspaces] = React.useState([]), [codexWorkspaces, setCodexWorkspaces] = React.useState([]), [codexNotice, setCodexNotice] = React.useState(void 0), [codexAvailable, setCodexAvailable] = React.useState(!1), codexProbeAvailable = React.useRef(!1), [codexOpen, setCodexOpen] = React.useState(!1), [workspaceBackend, setWorkspaceBackend] = React.useState("harness"), [codexWorkspaceId, setCodexWorkspaceId] = React.useState(void 0), [directory, setDirectory] = React.useState(void 0), [path, setPath] = React.useState(""), [addingWorkspace, setAddingWorkspace] = React.useState(!1), [showAllWorkspaces, setShowAllWorkspaces] = React.useState(!1), [showAllCodexWorkspaces, setShowAllCodexWorkspaces] = React.useState(!1), [devicesOpen, setDevicesOpen] = React.useState(!1), workspaceListId = "dsh-remote-workspace-list", codexWorkspaceHeadingId = "dsh-remote-codex-workspace-heading", codexWorkspaceListId = "dsh-remote-codex-workspace-list", [busy, setBusy] = React.useState(!1), [needsAuthorization, setNeedsAuthorization] = React.useState(!1), authorizedOnce = React.useRef(!1), [authorizationResolved, setAuthorizationResolved] = React.useState(!1), [loginServerUrl, setLoginServerUrl] = React.useState(DEFAULT_REMOTE_SERVER_URL), [editingServerUrl, setEditingServerUrl] = React.useState(!1), [awaitingDeepSeek, setAwaitingDeepSeek] = React.useState(!1), [pendingAuthorizeUrl, setPendingAuthorizeUrl] = React.useState(void 0), [loginMethod] = React.useState(
1414
1419
  isEnabledQrProvider(props.preferredQrProvider) ? props.preferredQrProvider : defaultQrProvider
1415
1420
  ), [qrSession, setQrSession] = React.useState(void 0), [qrExpired, setQrExpired] = React.useState(!1), [progress, setProgress] = React.useState(void 0), progressRun = React.useRef(0), qrFlowRun = React.useRef(0), [notice, setNotice] = React.useState(void 0), [confirmingSignOut, setConfirmingSignOut] = React.useState(!1), [confirmingRevoke, setConfirmingRevoke] = React.useState(!1), [error, setError] = React.useState(void 0);
1416
1421
  React.useEffect(() => {
@@ -1476,7 +1481,7 @@
1476
1481
  polling || settled || (polling = !0, props.control("client.account.qr.poll", { qrId: qrSession.qrId }).then(async (result) => {
1477
1482
  if (!(!active || settled || run !== qrFlowRun.current))
1478
1483
  if (result.status === "complete") {
1479
- settled = !0, timer !== void 0 && window.clearInterval(timer), setBusy(!0), setError(void 0), setQrExpired(!1), setNeedsAuthorization(!1);
1484
+ settled = !0, timer !== void 0 && window.clearInterval(timer), setBusy(!0), setError(void 0), setQrExpired(!1), authorizedOnce.current = !0, setNeedsAuthorization(!1);
1480
1485
  try {
1481
1486
  let [nextDevices, nextStatus] = await Promise.all([
1482
1487
  props.control("devices"),
@@ -1575,25 +1580,25 @@
1575
1580
  try {
1576
1581
  let nextStatus = await props.control("status");
1577
1582
  if (setStatus(nextStatus), !nextStatus.available) {
1578
- setDevices([]), setNeedsAuthorization(!1), setAuthorizationResolved(!0), setSelectedHost(void 0), setWorkspaces([]), setCodexWorkspaces([]), setShowAllWorkspaces(!1), setShowAllCodexWorkspaces(!1), setWorkspaceBackend("harness"), setCodexWorkspaceId(void 0), setPath(""), setAddingWorkspace(!1), setDirectory(void 0);
1583
+ setDevices([]), authorizedOnce.current = !0, setNeedsAuthorization(!1), setAuthorizationResolved(!0), setSelectedHost(void 0), setWorkspaces([]), setCodexWorkspaces([]), setShowAllWorkspaces(!1), setShowAllCodexWorkspaces(!1), setWorkspaceBackend("harness"), setCodexWorkspaceId(void 0), setPath(""), setAddingWorkspace(!1), setDirectory(void 0);
1579
1584
  return;
1580
1585
  }
1581
1586
  let storedLocally;
1582
1587
  try {
1583
1588
  storedLocally = (await props.control("authorization.local"))?.stored === !0, setNeedsAuthorization(!storedLocally);
1584
1589
  } catch {
1585
- setNeedsAuthorization(!1);
1590
+ authorizedOnce.current = !0, setNeedsAuthorization(!1);
1586
1591
  } finally {
1587
1592
  setAuthorizationResolved(!0);
1588
1593
  }
1589
1594
  try {
1590
1595
  let nextDevices = await props.control("devices");
1591
- if (setDevices(nextDevices), storedLocally !== void 0 && setNeedsAuthorization(authorizationStillNeeded(storedLocally, nextDevices.length)), selectedHost !== void 0) {
1596
+ if (setDevices(nextDevices), !authorizedOnce.current && storedLocally !== void 0 && setNeedsAuthorization(authorizationStillNeeded(storedLocally, nextDevices.length)), selectedHost !== void 0) {
1592
1597
  let nextSelectedHost = nextDevices.find((device) => device.deviceId === selectedHost.deviceId);
1593
1598
  nextSelectedHost === void 0 ? (setSelectedHost(void 0), setWorkspaces([]), setCodexWorkspaces([]), setShowAllWorkspaces(!1), setShowAllCodexWorkspaces(!1), setWorkspaceBackend("harness"), setCodexWorkspaceId(void 0), setPath(""), setAddingWorkspace(!1), setDirectory(void 0)) : setSelectedHost(nextSelectedHost);
1594
1599
  }
1595
1600
  } catch {
1596
- setDevices([]), setNeedsAuthorization(!0), setSelectedHost(void 0), setWorkspaces([]), setCodexWorkspaces([]), setShowAllWorkspaces(!1), setShowAllCodexWorkspaces(!1), setWorkspaceBackend("harness"), setCodexWorkspaceId(void 0), setPath(""), setAddingWorkspace(!1), setDirectory(void 0);
1601
+ setDevices([]), authorizedOnce.current = !1, setNeedsAuthorization(!0), setSelectedHost(void 0), setWorkspaces([]), setCodexWorkspaces([]), setShowAllWorkspaces(!1), setShowAllCodexWorkspaces(!1), setWorkspaceBackend("harness"), setCodexWorkspaceId(void 0), setPath(""), setAddingWorkspace(!1), setDirectory(void 0);
1597
1602
  }
1598
1603
  selectedHost !== void 0 && (await props.control("remote.session.recover").then((next) => setStatus(next)).catch(() => {
1599
1604
  }), await Promise.all([loadHarnessWorkspaces(selectedHost), loadCodexWorkspaces(selectedHost)]));
@@ -1604,6 +1609,17 @@
1604
1609
  }
1605
1610
  }, show = async () => {
1606
1611
  setShowAllWorkspaces(!1), setShowAllCodexWorkspaces(!1), setOpen(!0), await refreshRemote();
1612
+ for (let attempt = 0; attempt < 10; attempt++) {
1613
+ let list = await props.control("devices").catch(() => {
1614
+ });
1615
+ if (list !== void 0) {
1616
+ setDevices(list), authorizedOnce.current = !0, setNeedsAuthorization(!1), setAuthorizationResolved(!0), await refreshRemote();
1617
+ return;
1618
+ }
1619
+ await new Promise((resolve) => {
1620
+ setTimeout(resolve, 1e3);
1621
+ });
1622
+ }
1607
1623
  };
1608
1624
  React.useEffect(() => {
1609
1625
  if (!(!open || selectedHost !== void 0))
@@ -1629,7 +1645,22 @@
1629
1645
  }, [open, selectedHost]);
1630
1646
  let chooseAnotherHost = () => {
1631
1647
  setSelectedHost(void 0), setWorkspaces([]), setCodexWorkspaces([]), setCodexNotice(void 0), setCodexAvailable(!1), codexProbeAvailable.current = !1, setShowAllWorkspaces(!1), setShowAllCodexWorkspaces(!1), setWorkspaceBackend("harness"), setCodexWorkspaceId(void 0), setDirectory(void 0), setPath(""), setAddingWorkspace(!1), setError(void 0);
1632
- }, requestDeepSeekSignIn = async () => {
1648
+ }, signingIn = busy || status?.authorizing === !0 || needsAuthorization && status?.hasDeepSeekSession === !0;
1649
+ React.useEffect(() => {
1650
+ if (!signingIn) return;
1651
+ let tries = 0, timer = window.setInterval(() => {
1652
+ tries += 1, (async () => {
1653
+ let next = await props.control("status").catch(() => {
1654
+ });
1655
+ next !== void 0 && setStatus(next);
1656
+ let list = await props.control("devices").catch(() => {
1657
+ });
1658
+ list !== void 0 && (setDevices(list), authorizedOnce.current = !0, setNeedsAuthorization(!1), setAuthorizationResolved(!0));
1659
+ })(), tries >= 5 && window.clearInterval(timer);
1660
+ }, 1e3);
1661
+ return () => window.clearInterval(timer);
1662
+ }, [signingIn]);
1663
+ let requestDeepSeekSignIn = async () => {
1633
1664
  let transport = globalThis.__DSH_TRANSPORT__, callbackOrigin = transport?.streamBaseUrl !== void 0 ? new URL(transport.streamBaseUrl).origin : window.location.origin, result = await props.control("settings.configure", {
1634
1665
  // No `role`: a device has one identity, and the control writes only the Server address plus this
1635
1666
  // sign-in. Sending a role here was the last thing that made one look like a runtime switch.
@@ -1667,7 +1698,7 @@
1667
1698
  if (state.pending) throw new Error(t("deepseekSignInTimeout"));
1668
1699
  } else
1669
1700
  tab?.close();
1670
- setDevices(await props.control("devices")), setStatus(await props.control("status")), setNeedsAuthorization(!1), setAuthorizationResolved(!0), setNotice(void 0);
1701
+ setDevices(await props.control("devices")), setStatus(await props.control("status")), authorizedOnce.current = !0, setNeedsAuthorization(!1), setAuthorizationResolved(!0), setNotice(void 0);
1671
1702
  } catch (reason) {
1672
1703
  setError(remoteErrorMessage(reason, t));
1673
1704
  } finally {
@@ -1692,7 +1723,7 @@
1692
1723
  }, revokeRemote = async () => {
1693
1724
  setBusy(!0), setError(void 0), setNotice(void 0);
1694
1725
  try {
1695
- await props.control("client.device.revoke"), setDevices([]), setNeedsAuthorization(!0), setStatus(await props.control("status"));
1726
+ await props.control("client.device.revoke"), setDevices([]), authorizedOnce.current = !1, setNeedsAuthorization(!0), setStatus(await props.control("status"));
1696
1727
  } catch (reason) {
1697
1728
  setError(remoteErrorMessage(reason, t));
1698
1729
  } finally {
@@ -1701,7 +1732,7 @@
1701
1732
  }, logoutRemote = async () => {
1702
1733
  setBusy(!0), setError(void 0), setNotice(void 0);
1703
1734
  try {
1704
- await props.control("settings.logout"), setDevices([]), setNeedsAuthorization(!0), setQrSession(void 0), setQrExpired(!1), setStatus(await props.control("status"));
1735
+ await props.control("settings.logout"), setDevices([]), authorizedOnce.current = !1, setNeedsAuthorization(!0), setQrSession(void 0), setQrExpired(!1), setStatus(await props.control("status"));
1705
1736
  } catch (reason) {
1706
1737
  setError(remoteErrorMessage(reason, t));
1707
1738
  } finally {
@@ -1909,11 +1940,11 @@
1909
1940
  onClick: () => setEditingServerUrl(!0)
1910
1941
  }, loginServerUrl)
1911
1942
  ),
1912
- React.createElement(
1913
- "button",
1914
- { type: "button", disabled: busy || loginServerUrl.trim() === "", onClick: () => void signInClient() },
1915
- t(busy ? "signingIn" : "deepseekSignIn")
1916
- ),
1943
+ React.createElement("button", {
1944
+ type: "button",
1945
+ disabled: busy || signingIn || loginServerUrl.trim() === "",
1946
+ onClick: () => void signInClient()
1947
+ }, t(signingIn ? "signingIn" : "deepseekSignIn")),
1917
1948
  awaitingDeepSeek ? React.createElement(
1918
1949
  "p",
1919
1950
  { className: "dshRemoteServiceAddress" },
package/dist/index.js CHANGED
@@ -13521,7 +13521,7 @@ function normalizeServerUrl(value) {
13521
13521
  }
13522
13522
 
13523
13523
  // src/version.ts
13524
- var PLUGIN_VERSION = "0.5.4";
13524
+ var PLUGIN_VERSION = "0.5.5";
13525
13525
 
13526
13526
  // src/server-api.ts
13527
13527
  var ENABLED_QR_PROVIDERS = ["github"];
@@ -14109,7 +14109,7 @@ var HostServerConnection = class {
14109
14109
  if (TERMINAL_AUTH_ERRORS.has(code)) {
14110
14110
  this.logger.warn(code === "CONNECTION_REPLACED" ? "Another instance is using this Host identity. Standing by: retrying in five minutes, and taking over as soon as it leaves." : code === "SERVER_CREDENTIALS_BUSY" ? "Credential refresh is locked. Stop other instances; after a crash, stop all instances before removing server-credentials.json.refresh-lock and authorizing again." : "Host authorization failed. Run /remote login or authorize this Host again in Remote settings.");
14111
14111
  }
14112
- if (code === "DEVICE_REVOKED") {
14112
+ if (code === "DEVICE_REVOKED" || code === "AUTH_INVALID") {
14113
14113
  try {
14114
14114
  await this.api.clearAuthorization();
14115
14115
  } catch (clearError) {
@@ -21920,10 +21920,16 @@ var ClientModeRuntime = class {
21920
21920
  registerControl(connection, webServer) {
21921
21921
  return registerControlRoute(connection, (endpoint, payload, signal) => this.handleControl(endpoint, payload, signal), webServer);
21922
21922
  }
21923
+ /** True while the plugin is exchanging DSH's DeepSeek credential for this device's Server credentials. */
21924
+ authorizingWithDeepSeek = false;
21925
+ /** Whether DSH held a DeepSeek session at the last reconciliation. */
21926
+ hasDeepSeekSession = false;
21923
21927
  status() {
21924
21928
  const targetStatus = this.gatewaySwitch.supportsCarrier() ? this.gatewaySwitch.status() : this.proxySwitch?.status() ?? this.gatewaySwitch.status();
21925
21929
  return {
21926
21930
  available: this.config.serverUrl !== void 0,
21931
+ authorizing: this.authorizingWithDeepSeek,
21932
+ hasDeepSeekSession: this.hasDeepSeekSession,
21927
21933
  identityReady: this.identity !== void 0,
21928
21934
  deviceId: this.identity?.deviceId,
21929
21935
  deviceName: this.identity?.name,
@@ -22086,6 +22092,11 @@ var ClientModeRuntime = class {
22086
22092
  reloadCredentials() {
22087
22093
  this.server.reloadCredentials();
22088
22094
  }
22095
+ /** Whether this device holds Server credentials, i.e. the panel shows it as signed in. */
22096
+ async hasServerAuthorization() {
22097
+ if (this.server.hasStoredAuthorization === void 0) return false;
22098
+ return await this.server.hasStoredAuthorization().catch(() => false);
22099
+ }
22089
22100
  async setHostAuthorization(enabled) {
22090
22101
  if (this.host === void 0) throw new ClientModeError("METHOD_NOT_ALLOWED", "This plugin is not running as a Host.");
22091
22102
  if (!enabled) {
@@ -25014,12 +25025,24 @@ var CodexRemoteDomain = class {
25014
25025
  idempotencyKey: call.params.idempotencyKey
25015
25026
  }));
25016
25027
  }
25028
+ if (isRecord12(call.params) && typeof call.params.threadId === "string") {
25029
+ const rowKey = call.params.threadId;
25030
+ const separator = rowKey.indexOf("~");
25031
+ if (separator > 0) call.params.threadId = rowKey.slice(0, separator);
25032
+ }
25017
25033
  if (call.method === "thread/list") {
25018
25034
  const result = sanitizeThreadList(await this.callUpstream(call.method, call.params));
25019
- return filterThreadListByWorkspaceAuthority(
25035
+ const authorized = filterThreadListByWorkspaceAuthority(
25020
25036
  result,
25021
25037
  await this.readWorkspaceAuthority(result.data)
25022
25038
  );
25039
+ return {
25040
+ ...authorized,
25041
+ data: authorized.data.map((row, index) => ({
25042
+ ...row,
25043
+ id: `${String(row.id)}~${String(row.createdAt ?? "")}~${index}`
25044
+ }))
25045
+ };
25023
25046
  }
25024
25047
  if (call.method === "thread/start") {
25025
25048
  const cwd = await this.requireCodexWorkspacePath(call.params.cwd);
@@ -26228,7 +26251,6 @@ var PluginControlRuntime = class {
26228
26251
  *
26229
26252
  * `undefined` means no sign-in is in flight, which is also the state after a completed authorization.
26230
26253
  */
26231
- deepSeekSignIn;
26232
26254
  register(connection, webServer) {
26233
26255
  const statusStream = new ControlStatusStream(() => this.streamStatus());
26234
26256
  return registerControlRoute(
@@ -26337,15 +26359,13 @@ var PluginControlRuntime = class {
26337
26359
  throw new ClientModeError("METHOD_NOT_ALLOWED", "DeepSeek account sign-in is unavailable in this profile.");
26338
26360
  }
26339
26361
  const session = await this.deepseekSession.read();
26340
- const counted = session !== void 0 && this.deepSeekSignIn !== void 0;
26341
- if (!counted) {
26362
+ if (session === void 0) {
26342
26363
  const started = await this.deepseekSession.startSignIn({
26343
26364
  callbackOrigin: typeof value.callbackOrigin === "string" ? value.callbackOrigin : "",
26344
26365
  loginSource: value.loginSource === "desktop" ? "desktop" : "web",
26345
26366
  locale: typeof value.locale === "string" ? value.locale : "en",
26346
26367
  timezoneOffsetSeconds: typeof value.timezoneOffsetSeconds === "number" ? value.timezoneOffsetSeconds : 0
26347
26368
  });
26348
- this.deepSeekSignIn = true;
26349
26369
  return {
26350
26370
  status: "deepseek-sign-in-required",
26351
26371
  ...started.authorizeUrl === void 0 ? {} : { authorizeUrl: started.authorizeUrl }
@@ -26358,7 +26378,6 @@ var PluginControlRuntime = class {
26358
26378
  this.verifiedDeepSeekToken = session.token;
26359
26379
  this.verifiedDeepSeekAuthorization = authorization;
26360
26380
  }
26361
- this.deepSeekSignIn = void 0;
26362
26381
  } else {
26363
26382
  if (typeof value.email !== "string" || typeof value.password !== "string") {
26364
26383
  throw new ClientModeError("INVALID_MESSAGE", "Email and password are required for account authorization.");
@@ -26371,7 +26390,8 @@ var PluginControlRuntime = class {
26371
26390
  if (this.host !== void 0 && !hostPaused) {
26372
26391
  try {
26373
26392
  this.host.reconnectHost?.();
26374
- } catch {
26393
+ } catch (error) {
26394
+ console.error(`[dsh-remote] reconcile failed ${JSON.stringify({ code: String(error?.code ?? "") })}`);
26375
26395
  }
26376
26396
  }
26377
26397
  await this.settings.replace(editableConfig(next));
@@ -26471,14 +26491,13 @@ var PluginControlRuntime = class {
26471
26491
  await this.settings.replace(editableConfig({ ...current, acp: { enabled: current.acp?.enabled ?? true, backends } }));
26472
26492
  return this.settingsView();
26473
26493
  }
26474
- async logout() {
26494
+ async logout(options = {}) {
26475
26495
  if (this.settings === void 0) {
26476
26496
  throw new ClientModeError("SETTINGS_UNAVAILABLE", "DSH user settings are unavailable in this profile.");
26477
26497
  }
26478
26498
  const config = resolveConfig(this.settings.get());
26479
26499
  this.verifiedDeepSeekToken = void 0;
26480
26500
  this.verifiedDeepSeekAuthorization = void 0;
26481
- this.deepSeekSignIn = void 0;
26482
26501
  if (config.serverUrl !== void 0) {
26483
26502
  await Promise.all([
26484
26503
  this.client?.clearClientAuthorization(),
@@ -26487,11 +26506,13 @@ var PluginControlRuntime = class {
26487
26506
  const directory = await ensureDeviceDirectory(this.identityDirectory, config.serverUrl);
26488
26507
  await new ServerCredentialStore(directory).clear();
26489
26508
  }
26509
+ if (this.client !== void 0) this.client.hasDeepSeekSession = false;
26490
26510
  let deepseekSignedOut = false;
26491
26511
  if (this.deepseekSession !== void 0) {
26492
26512
  try {
26493
- deepseekSignedOut = await this.deepseekSession.signOut();
26494
- } catch {
26513
+ deepseekSignedOut = options.releaseDeepSeek === false ? false : await this.deepseekSession.signOut();
26514
+ } catch (error) {
26515
+ console.error(`[dsh-remote] reconcile failed ${JSON.stringify({ code: String(error?.code ?? "") })}`);
26495
26516
  deepseekSignedOut = false;
26496
26517
  }
26497
26518
  }
@@ -26522,7 +26543,49 @@ var PluginControlRuntime = class {
26522
26543
  }
26523
26544
  return this.client === void 0 ? this.hostOnlyStatus() : await this.client.status();
26524
26545
  }
26546
+ /** Consecutive reconciliations without a DSH session: DSH refreshes its grant, so one empty read is not a sign-out. */
26547
+ /** Guards against re-entry through configure(), which reads the settings view back. */
26548
+ reconcilingWithDeepSeek = false;
26549
+ /**
26550
+ * Keep this device's Server credentials in step with DSH's DeepSeek session - that session is the credential, and
26551
+ * the Server verifies it before issuing this device its own tokens.
26552
+ *
26553
+ * Three cases: DSH signed in and this device not authorized -> authorize; DSH signed out and this device authorized
26554
+ * -> sign out; otherwise nothing. At most once per ten seconds, from the settings poll the panel already makes.
26555
+ */
26556
+ async reconcileWithDeepSeek() {
26557
+ if (this.deepseekSession === void 0 || this.reconcilingWithDeepSeek) return;
26558
+ this.reconcilingWithDeepSeek = true;
26559
+ try {
26560
+ const session = await this.deepseekSession.read();
26561
+ if (this.client !== void 0) this.client.hasDeepSeekSession = session !== void 0;
26562
+ const authorized = this.client === void 0 ? false : await this.client.hasServerAuthorization();
26563
+ console.error(`[dsh-remote] reconcile ${JSON.stringify({ hasSession: session !== void 0, authorized })}`);
26564
+ if (session === void 0 && authorized) {
26565
+ await this.logout({ releaseDeepSeek: false });
26566
+ return;
26567
+ }
26568
+ if (session === void 0 || authorized) return;
26569
+ if (this.client !== void 0) this.client.authorizingWithDeepSeek = true;
26570
+ void (async () => {
26571
+ try {
26572
+ await this.configure({ provider: "deepseek", serverUrl: this.config.serverUrl });
26573
+ } catch (error) {
26574
+ console.error(`[dsh-remote] reconcile failed ${JSON.stringify({ code: String(error?.code ?? "") })}`);
26575
+ } finally {
26576
+ if (this.client !== void 0) this.client.authorizingWithDeepSeek = false;
26577
+ }
26578
+ })();
26579
+ } finally {
26580
+ this.reconcilingWithDeepSeek = false;
26581
+ }
26582
+ }
26583
+ /**
26584
+ * Keeps the reconciliation alive without the panel: DSH can be signed in or out while the card is closed, and the
26585
+ * plugin has to follow. Only local reads - the exchange itself is throttled to one per ten seconds.
26586
+ */
26525
26587
  async settingsView() {
26588
+ await this.reconcileWithDeepSeek();
26526
26589
  const config = this.settings === void 0 ? editableConfig(this.config) : editableConfig(resolveConfig(this.settings.get()));
26527
26590
  const associations = await this.associations(config);
26528
26591
  const association = associations.host;