fix(share): terminal access for late joiners, auto-scroll, inventory scroll
release / release (push) Successful in 14s

- A guest who joined after the host enabled the terminal stayed read-only; the
  host now re-sends the terminal state on join (req_full), so the terminal works.
- The shared terminal follows the cursor to the bottom as output arrives (ls -la)
  instead of staying scrolled up.
- The Inventory page preserves scroll position on refresh (and skips re-rendering
  unchanged data), so it no longer jumps to the top while you're reading.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 20:22:25 +02:00
parent 2f6cab72c4
commit 00394c287c
6 changed files with 28 additions and 6 deletions
+9
View File
@@ -5,6 +5,15 @@ All notable changes to RigDoctor are recorded here. Format follows
(`MAJOR.MINOR.PATCH`, pre-1.0). `__version__` and `pyproject.toml` must match the git (`MAJOR.MINOR.PATCH`, pre-1.0). `__version__` and `pyproject.toml` must match the git
release tag (so the auto-updater, D18, can compare versions). release tag (so the auto-updater, D18, can compare versions).
## [0.7.1] - 2026-05-21
### Fixed
- Shared terminal: a guest who joined **after** the host enabled the terminal stayed read-only.
The host now re-sends the terminal state when a guest joins, so the terminal is available.
- Inventory page no longer jumps back to the top when it refreshes (e.g. when elevated data
arrives) — scroll position is preserved and unchanged data isn't re-rendered.
- Shared terminal now follows the cursor to the bottom as output arrives (e.g. `ls -la`),
instead of staying scrolled up.
## [0.7.0] - 2026-05-21 ## [0.7.0] - 2026-05-21
### Added ### Added
- **Shared terminal (M12, Tier 3)**: when the host enables it, the session shares a real **PTY** - **Shared terminal (M12, Tier 3)**: when the host enables it, the session shares a real **PTY**
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "rigdoctor" name = "rigdoctor"
version = "0.7.0" version = "0.7.1"
description = "Modular hardware monitoring & crash diagnostics for Linux gamers." description = "Modular hardware monitoring & crash diagnostics for Linux gamers."
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
+1 -1
View File
@@ -1,3 +1,3 @@
"""RigDoctor — modular hardware monitoring & crash diagnostics for Linux gamers.""" """RigDoctor — modular hardware monitoring & crash diagnostics for Linux gamers."""
__version__ = "0.7.0" __version__ = "0.7.1"
+7 -1
View File
@@ -81,7 +81,7 @@ class InventoryPage(QWidget):
header.addWidget(self._refresh_btn) header.addWidget(self._refresh_btn)
root.addLayout(header) root.addLayout(header)
scroll = QScrollArea() self._scroll = scroll = QScrollArea()
scroll.setWidgetResizable(True) scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.Shape.NoFrame) scroll.setFrameShape(QFrame.Shape.NoFrame)
scroll.setStyleSheet("background: transparent;") scroll.setStyleSheet("background: transparent;")
@@ -118,7 +118,11 @@ class InventoryPage(QWidget):
if sections is None: # collection failed — keep current if sections is None: # collection failed — keep current
self._status.setText("collection failed") self._status.setText("collection failed")
return return
if sections == self._sections: # unchanged — don't rebuild (would jump scroll)
self._status.setText("")
return
scroll_pos = self._scroll.verticalScrollBar().value()
self._sections = sections self._sections = sections
while self._list.count(): while self._list.count():
item = self._list.takeAt(0) item = self._list.takeAt(0)
@@ -129,6 +133,8 @@ class InventoryPage(QWidget):
self._list.addWidget(_section_card(section)) self._list.addWidget(_section_card(section))
self._list.addStretch(1) self._list.addStretch(1)
self._status.setText("") self._status.setText("")
# restore scroll after the layout settles so re-renders don't yank to the top
QTimer.singleShot(0, lambda: self._scroll.verticalScrollBar().setValue(scroll_pos))
def _copy(self) -> None: def _copy(self) -> None:
if self._sections: if self._sections:
+3
View File
@@ -147,7 +147,10 @@ class SharePage(QWidget):
return return
kind = data.get("type") # frames forwarded from a guest kind = data.get("type") # frames forwarded from a guest
if kind == "req_full": if kind == "req_full":
# A guest just joined — send a full frame AND the current terminal state, so a
# guest that joins *after* the host enabled the terminal still gets access.
self._host_ws.sendTextMessage(share.host_full_frame(self._sampler)) self._host_ws.sendTextMessage(share.host_full_frame(self._sampler))
self._send_terminal_state()
elif kind == "pty_in" and self._pty: elif kind == "pty_in" and self._pty:
self._pty.write(base64.b64decode(data["data"])) self._pty.write(base64.b64decode(data["data"]))
elif kind == "pty_resize" and self._pty: elif kind == "pty_resize" and self._pty:
+7 -3
View File
@@ -9,7 +9,7 @@ from __future__ import annotations
import pyte import pyte
from PySide6.QtCore import Qt, Signal from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QFontDatabase, QFontMetrics from PySide6.QtGui import QFontDatabase, QFontMetrics, QTextCursor
from PySide6.QtWidgets import QPlainTextEdit from PySide6.QtWidgets import QPlainTextEdit
@@ -39,9 +39,13 @@ class TerminalView(QPlainTextEdit):
self._render() self._render()
def _render(self) -> None: def _render(self) -> None:
bar = self.verticalScrollBar().value()
self.setPlainText("\n".join(self._screen.display)) self.setPlainText("\n".join(self._screen.display))
self.verticalScrollBar().setValue(bar) # Follow the terminal cursor so output (e.g. `ls -la`) stays in view.
cursor = self.textCursor()
cursor.movePosition(QTextCursor.MoveOperation.Start)
cursor.movePosition(QTextCursor.MoveOperation.Down, QTextCursor.MoveMode.MoveAnchor, self._screen.cursor.y)
self.setTextCursor(cursor)
self.ensureCursorVisible()
def resizeEvent(self, event): # noqa: N802 (Qt override) def resizeEvent(self, event): # noqa: N802 (Qt override)
super().resizeEvent(event) super().resizeEvent(event)