Repository navigation
Conversation
A passive overlay per output, raised with SUPER + SHIFT + G over a fullscreen game: a strip of widget buttons, draggable and resizable cards, edge and grid snapping with guides, and a settle curve. The cards are capture, performance, resources, tasks, media, voice, audio and gallery, and each reads only while it is on screen. Discord Voice reads the official client over native RPC, or Vesktop through a Vencord companion that publishes the call on a local socket. A pinned voice card shrinks to a content-sized overlay in the corner the card was placed, stays passive when the bar is closed, and stops the bridge when Voice is not shown. The Hub gains a Game Bar page for the master switch, placement, motion and the voice overlay, every key stored under gamebar in shell.json. The keybind is registered in every window manager. Note: New: the Game Bar overlay (SUPER + SHIFT + G) with widgets and a pinned Discord Voice overlay Note: Fixed: Hub step rows now keep to the schema range
🦊 mika's reviewIncremental review: new commits since the last checkpoint. Summary
Change diagramflowchart LR
Bind["Shortcut and shell IPC"] --> Bar["Game Bar overlay"]
Hub["Hub settings"] --> Config["Shell settings"]
Config --> Bar
Bar --> Store["Persisted layout"]
Bar --> Cards["Capture, system, media and audio cards"]
Bar --> Voice["Discord Voice service"]
Voice --> Bridge["Python bridge"]
Bridge --> RPC["Discord RPC"]
Bridge --> Companion["Vencord companion socket"]
Estimated review effort: 5/5 · risk: high 📁 Walkthrough details
|
| Severity | Category | Fix effort | File | Issue |
|---|---|---|---|---|
| 🟡 minor | correctness | low | ryoku/shell/quickshell/shell/modules/gamebar/discordVoice/companion/index.ts |
Bug — If the plugin stops and restarts while nextCommand() is pending, the old loop can consume and apply a command after the new loop starts. Check the generation again after the await so a stale loop cannot execute it. |
⚡ Quick fixes by effort
Low effort (apply from the diff):
ryoku/shell/quickshell/shell/modules/gamebar/discordVoice/companion/index.ts— Reject commands from stale loopsryoku/shell/quickshell/shell/modules/gamebar/discordVoice/companion/index.ts:117 — Bug — If the plugin stops and restarts whilenextCommand()is pending, the old loop can consume and apply a command after the new loop starts. Check the generation again after the await so a stale loop cannot execute it.
🤖 Prompt for AI agents (click, copy, paste into Claude/Codex)
Fix the following issues found by mika-agent on Ryoku-dev/ryoku#771 (branch ryo-gamebar):
- ryoku/shell/quickshell/shell/modules/gamebar/discordVoice/companion/index.ts:117-120 — Bug (minor severity, low effort): If the plugin stops and restarts while `nextCommand()` is pending, the old loop can consume and apply a command after the new loop starts. Check the generation again after the await so a stale loop cannot execute it.
suggested fix:
const line = await Native.nextCommand();
if (!running || generation !== commandGeneration)
return;
applyCommand(line);
For each: verify it is real in the current code, apply the smallest correct fix, and run the repo's tests. Skip anything already resolved and say why.
🛠 Deterministic checks run
- golangci-lint: not installed (skip)
- shellcheck: not installed (skip)
🚦 Merge recommendation: changes_required
🦊 Generated by Mika — the Ryoku org's self-hosted AI code reviewer. Machine findings: verify before merging.
Only the active output fills its cards, card loaders wait for the surface to be live, the exit timer follows the fade, and capture suppression follows the selection overlay instead of polling. Gallery gains a screenshots/recordings switch with play tiles and file names. Voice delegates bind the participant explicitly. The bridge ignores non-object companion messages, the companion clears queued lines on disconnect, the installer cannot swallow a failed build, and the tasks card reads cputimes as seconds. Note: Fixed: the Game Bar hides for captures, lists recordings, shows Voice participants and reports real CPU use
SelectedChannelStore may return no channel, and the companion build accepts the undefined case at runtime. Say so in the signature so tsc --noEmit passes.
neur0map
left a comment
There was a problem hiding this comment.
🔴 Changes requested — 0 critical and 1 major issue(s): see the inline comments and the walkthrough for fixes and an agent-ready prompt.
| implicitHeight: 32 * root.s | ||
| implicitWidth: 276 * root.s | ||
|
|
||
| onTaskChanged: confirming = false |
There was a problem hiding this comment.
🟡 minor · Possible Bug · fix: medium effort
Each process refresh replaces the row's task, resetting confirming every two seconds. A user who takes longer than the refresh interval to confirm an End action loses the confirmation; retain it while the PID remains the same.
| readonly property var shots: card.newest(shotFiles, 6, "shot") | ||
| readonly property var clips: card.newest(clipFiles, 6, "clip") | ||
| readonly property var items: card.mode === "clips" ? card.clips : card.shots |
There was a problem hiding this comment.
🟡 minor · Bug · fix: low effort
newest() reads model entries through get(), which does not establish a binding dependency on model changes. Depend on each model's count so the gallery refreshes when captures are added or removed.
Suggested change
| readonly property var shots: card.newest(shotFiles, 6, "shot") | |
| readonly property var clips: card.newest(clipFiles, 6, "clip") | |
| readonly property var items: card.mode === "clips" ? card.clips : card.shots | |
| readonly property var shots: { | |
| shotFiles.count; | |
| return card.newest(shotFiles, 6, "shot"); | |
| } | |
| readonly property var clips: { | |
| clipFiles.count; | |
| return card.newest(clipFiles, 6, "clip"); | |
| } | |
| readonly property var items: card.mode === "clips" ? card.clips : card.shots |
| protectedName: Tasks.isProtected(modelData.name) | ||
| onEndRequested: pid => { | ||
| killer.command = ["kill", "-TERM", String(pid)]; | ||
| killer.running = true; | ||
| settle.restart(); |
There was a problem hiding this comment.
🟡 minor · Possible bug · fix: low effort
ps lists processes owned by other users, and kill inherits the shell's privileges. Prevent ending another user's processes by limiting the displayed process list to the current user.
confidence: 60/100
Suggested change
| protectedName: Tasks.isProtected(modelData.name) | |
| onEndRequested: pid => { | |
| killer.command = ["kill", "-TERM", String(pid)]; | |
| killer.running = true; | |
| settle.restart(); | |
| onEndRequested: pid => { | |
| killer.command = ["kill", "-TERM", String(pid)]; | |
| killer.running = true; | |
| settle.restart(); | |
| } |
neur0map
left a comment
There was a problem hiding this comment.
🔍 mika reviewed this change; details in the walkthrough comment.
| Repeater { | ||
| model: card.streams.slice(0, 3) |
There was a problem hiding this comment.
🟡 minor · Possible Bug · fix: medium effort
The app list is limited to the first three streams with no scrolling or selection. When more than three applications are playing audio, users cannot adjust the remaining application volumes from the Game Bar. Make the list scrollable or otherwise expose all streams.
| def emit(kind: str, **fields: Any) -> None: | ||
| print(json.dumps({"type": kind, **fields}, separators=(",", ":")), flush=True) |
There was a problem hiding this comment.
🟡 minor · Bug · fix: low effort
Escape non-ASCII characters when emitting JSON. The bridge runs with a pipe on stdout, whose encoding can be ASCII under some locales; a Discord display name containing non-ASCII characters would otherwise terminate the process.
Suggested change
| def emit(kind: str, **fields: Any) -> None: | |
| print(json.dumps({"type": kind, **fields}, separators=(",", ":")), flush=True) | |
| def emit(kind: str, **fields: Any) -> None: | |
| print(json.dumps({"type": kind, **fields}, separators=(",", ":"), ensure_ascii=True), flush=True) |
Stop a pending capture restore before a new one and keep the bar hidden for the whole recording, clearing that hold when Recorder stops. Stop the exit timer when the surface comes back. Keep a task's End confirmation while its PID survives a refresh, and end the name at the CPU column. Make the audio app list scroll past three rows. Re-read the display mode when the monitor changes. Refresh the gallery when a folder count changes. Follow only the current user's processes and filter them from ps. Guard the companion's stale socket handlers, time out the RPC handshake, emit escaped JSON, and use a valid Discord avatar size. Note: Fixed: recordings keep the bar hidden, the audio app list scrolls, task rows keep their columns, and the process list stays yours
neur0map
left a comment
There was a problem hiding this comment.
🔴 Changes requested — 0 critical and 1 major issue(s): see the inline comments and the walkthrough for fixes and an agent-ready prompt.
Close the bar when a recording starts and reopen it when the capture ends. Opening the bar again during the recording releases the hold, so the Capture card's Stop button is reachable, and the fallback timer now keeps checking until the countdown ends instead of firing once. Bind the gallery to a revision counter driven by the folder models so a file replaced under the same count cannot leave stale paths on screen. Give the companion command loop a generation token, reject an unsupported --client in the installer, and let auto pick a single client. Keep a channel's name and guild when a refresh repeats only its id. Note: Fixed: the Game Bar keeps the recording reachable and the gallery refreshes in place
neur0map
left a comment
There was a problem hiding this comment.
🔍 mika reviewed this change; details in the walkthrough comment.
| FolderListModel { | ||
| id: shotFiles | ||
| folder: "file://" + card.shotsDir | ||
| showDirs: false | ||
| nameFilters: ["*.png", "*.jpg", "*.jpeg", "*.webp"] | ||
| } | ||
|
|
||
| FolderListModel { | ||
| id: clipFiles | ||
| folder: "file://" + card.clipsDir | ||
| showDirs: false |
There was a problem hiding this comment.
🟡 minor · Possible Bug · fix: low effort
Concatenating a filesystem path into a file:// URL leaves URL metacharacters unescaped. If the configured screenshots or recordings directory contains # or ?, the folder model reads the wrong URL and the gallery appears empty. Convert the paths to properly encoded local-file URLs.
| const current = snapshot(); | ||
| if (typeof command.mute === "boolean" && command.mute !== current.mute) | ||
| Audio.toggleSelfMute(); | ||
| if (typeof command.deaf === "boolean" && command.deaf !== current.deaf) | ||
| Audio.toggleSelfDeaf(); |
There was a problem hiding this comment.
🟡 minor · Bug · fix: low effort
Compare commands against self-mute and self-deafen rather than the combined server and self state. A server-muted user otherwise cannot toggle their own mute reliably, because toggling cannot remove server mute.
Suggested change
| const current = snapshot(); | |
| if (typeof command.mute === "boolean" && command.mute !== current.mute) | |
| Audio.toggleSelfMute(); | |
| if (typeof command.deaf === "boolean" && command.deaf !== current.deaf) | |
| Audio.toggleSelfDeaf(); | |
| const me = UserStore.getCurrentUser(); | |
| const own = me ? VoiceStateStore.getVoiceStateForUser(me.id) : null; | |
| if (typeof command.mute === "boolean" && command.mute !== Boolean(own?.selfMute)) | |
| Audio.toggleSelfMute(); | |
| if (typeof command.deaf === "boolean" && command.deaf !== Boolean(own?.selfDeaf)) | |
| Audio.toggleSelfDeaf(); |
neur0map
left a comment
There was a problem hiding this comment.
🔍 mika reviewed this change; details in the walkthrough comment.
| GameBarStore.resize(id, win.liveW / win.us, win.liveH / win.us); | ||
| win.resizeCard = ""; | ||
| const p = win.positionFor(id); | ||
| const clamped = win.clampPos(id, p.x, p.y); | ||
| if (clamped.x !== p.x || clamped.y !== p.y) | ||
| GameBarStore.place(id, clamped.x, clamped.y); |
There was a problem hiding this comment.
🟡 minor · Possible Bug · fix: low effort
Resizing a card near the right or bottom edge calls positionFor() after changing its size, but that function has already clamped the saved position. The subsequent comparison therefore never persists the adjusted position; shrinking the card again or reopening it can make it jump back to its old location. Compare the clamped position with the stored, unclamped position instead.
Persist the clamped position after a resize so a card near an edge does not spring back, and suppress the surface before closing it when a recording starts so pinned cards are not in the first frames. Build both gallery folder URLs with Util.fileUrl so a configured path with spaces or URL metacharacters still lists. Recheck companion_active after the native connect awaited, and unsubscribe the old channel before a switch or a leave so stale voice events cannot refill the participant list. Compare the companion's mute and deaf commands with the self state, cancel authorization timeouts when their RPC socket closes, and handle SIGTERM so shutdown unlinks the companion socket. Note: Fixed: resized cards stay put, the gallery accepts special-character folders, and voice leaves old channels cleanly
neur0map
left a comment
There was a problem hiding this comment.
🔍 mika reviewed this change; details in the walkthrough comment.
| Image { | ||
| anchors.fill: parent | ||
| source: thumb.modelData.kind === "shot" ? thumb.modelData.url : "" | ||
| visible: thumb.modelData.kind === "shot" | ||
| asynchronous: true | ||
| fillMode: Image.PreserveAspectCrop | ||
| } | ||
|
|
||
| GlyphIcon { | ||
| anchors.centerIn: parent | ||
| anchors.verticalCenterOffset: -5 * card.s | ||
| width: 24 * card.s | ||
| height: 24 * card.s | ||
| visible: thumb.modelData.kind === "clip" |
There was a problem hiding this comment.
🟡 minor · Possible Bug · fix: medium effort
Recording entries display only a play glyph and filename; the Image source is explicitly empty for clips. Users therefore cannot see the recordings themselves in the gallery as documented. Provide a video thumbnail or adjust the shipped feature claim.
neur0map
left a comment
There was a problem hiding this comment.
🔍 mika reviewed this change; details in the walkthrough comment.
| function newest(model, count, kind) { | ||
| const all = []; | ||
| for (let i = 0; i < model.count; i++) { | ||
| const url = model.get(i, "fileUrl"); | ||
| if (!url) | ||
| continue; | ||
| const stamp = model.get(i, "fileModified"); | ||
| const when = stamp ? (typeof stamp.getTime === "function" | ||
| ? stamp.getTime() : Date.parse(stamp) || 0) : 0; | ||
| all.push({ | ||
| url: "" + url, | ||
| path: "" + model.get(i, "filePath"), | ||
| name: "" + model.get(i, "fileName"), | ||
| kind: kind, | ||
| t: when | ||
| }); | ||
| } | ||
| all.sort((a, b) => b.t - a.t); | ||
| return all.slice(0, count); |
There was a problem hiding this comment.
🟡 minor · Performance · fix: medium effort
Each folder model change scans and sorts every matching capture, even though only six entries are displayed. A recordings folder with many files can stall the shell on each update, including updates while a recording is being written. Limit the model or maintain only the six newest entries.
neur0map
left a comment
There was a problem hiding this comment.
🔍 mika reviewed this change; details in the walkthrough comment.
| if (win.reopenAfterRecording) { | ||
| if (!GameBarStore.open) | ||
| GameBarStore.openBar(GameBarStore.monitor); | ||
| } else if (GameBarStore.open) { | ||
| GameBarStore.closeBar(); | ||
| } | ||
| win.reopenAfterRecording = false; | ||
| win.restoreAfter(900); |
There was a problem hiding this comment.
🟡 minor · Possible Bug · fix: medium effort
When pinned cards keep the overlay active, stopping a recording calls openBar() while suppressed is still true, then clears suppression 900 ms later. onActiveChanged can set live to true before suppression clears, but if exitTimer has already fired after the bar was closed and no pins exist, the restored bar depends on the subsequent wanted transition; add a recording lifecycle test covering both pinned and unpinned states.
confidence: 70/100
neur0map
left a comment
There was a problem hiding this comment.
🔍 mika reviewed this change; details in the walkthrough comment.
neur0map
left a comment
There was a problem hiding this comment.
🔴 Changes requested — 0 critical and 1 major issue(s): see the inline comments and the walkthrough for fixes and an agent-ready prompt.
| readonly property bool mine: GameBarStore.monitor.length > 0 | ||
| ? GameBarStore.monitor === win.mon : Wm.focusedOutput === win.mon |
There was a problem hiding this comment.
🟠 major · Possible Bug · fix: medium effort
Once a monitor name is saved, mine never falls back to the focused output if that monitor is disconnected. Pinned cards therefore disappear after unplugging the saved output, including after a shell restart, until the bar is opened on another monitor. Fall back when the saved output is no longer present.
| visible: card.voice.companionNeeded && !card.voice.installerBusy | ||
| s: card.s | ||
| compact: true | ||
| quiet: true | ||
| label: I18n.tr("Install voice companion") | ||
| onClicked: card.voice.installCompanion() |
There was a problem hiding this comment.
🟡 minor · Data Integrity · fix: medium effort
The new Voice card uses translation keys such as Install voice companion, but this PR does not add them to the source and English catalogs. These strings cannot be translated through the catalog as required, so add the new Game Bar strings to both catalogs.
| import "modules/notifications" | ||
| import "modules/capture" | ||
| import "modules/ask" | ||
| import "modules/gamebar" |
There was a problem hiding this comment.
🟡 minor · Bug · fix: low effort
openSurface receives the requested monitor in mon, but this branch discards it for gamebar. Pass it to GameBarStore.toggleBar so requests for a specific output do not open the bar on the focused output instead.
Suggested change
| import "modules/gamebar" | |
| if (id === "gamebar") { | |
| if (Config.gamebar.enabled) | |
| GameBarStore.toggleBar(mon); | |
| return; | |
| } | |
| if (id === "launcher" || id === "clipboard" || id === "settings") { | |
| root.toggleSurface(id); | |
| return; | |
| } |
| if (id === "launcher" || id === "clipboard" || id === "settings" || id === "gamebar") { | ||
| root.toggleSurface(id); | ||
| return; | ||
| } |
There was a problem hiding this comment.
💡 Suggestion (bug)
Honor the requested Game Bar monitor
Suggested change
| if (id === "launcher" || id === "clipboard" || id === "settings" || id === "gamebar") { | |
| root.toggleSurface(id); | |
| return; | |
| } | |
| if (id === "gamebar") { | |
| if (Config.gamebar.enabled) | |
| GameBarStore.toggleBar(mon); | |
| return; | |
| } | |
| if (id === "launcher" || id === "clipboard" || id === "settings") { | |
| root.toggleSurface(id); | |
| return; | |
| } |
To apply: click the sparkletip, or ask @mika-agent ask <question>.
| // ps rows for the Game Bar tasks card. The command is | ||
| // `ps -eo user=,pid=,comm=,cputimes=,etimes=,rss=`: user is the one leading | ||
| // token, comm may contain spaces, so the numeric columns are peeled off both | ||
| // ends and whatever remains is the name. cputimes is CPU time in seconds over |
There was a problem hiding this comment.
🟡 minor · Possible bug · fix: low effort
ps may truncate long usernames with a trailing +, so filtering by the full current username silently removes those processes. Request a sufficiently wide user column at the ps call site, or use numeric user IDs for filtering.
confidence: 60/100
Suggested change
| // ps rows for the Game Bar tasks card. The command is | |
| // `ps -eo user=,pid=,comm=,cputimes=,etimes=,rss=`: user is the one leading | |
| // token, comm may contain spaces, so the numeric columns are peeled off both | |
| // ends and whatever remains is the name. cputimes is CPU time in seconds over | |
| const cpuSecs = parseInt(parts[parts.length - 3], 10); | |
| const name = parts.slice(2, parts.length - 3).join(" "); | |
| if (!(pid > 0) || name.length === 0) | |
| continue; |
| const cpuSecs = parseInt(parts[parts.length - 3], 10); | ||
| const name = parts.slice(2, parts.length - 3).join(" "); | ||
| if (!(pid > 0) || name.length === 0) | ||
| continue; |
There was a problem hiding this comment.
💡 Suggestion (possible bug)
Preserve processes with long usernames
Suggested change
| const cpuSecs = parseInt(parts[parts.length - 3], 10); | |
| const name = parts.slice(2, parts.length - 3).join(" "); | |
| if (!(pid > 0) || name.length === 0) | |
| continue; | |
| const cpuSecs = parseInt(parts[parts.length - 3], 10); | |
| const name = parts.slice(2, parts.length - 3).join(" "); | |
| if (!(pid > 0) || name.length === 0) | |
| continue; |
To apply: click the sparkletip, or ask @mika-agent ask <question>.
neur0map
left a comment
There was a problem hiding this comment.
🔴 Changes requested — 0 critical and 1 major issue(s): see the inline comments and the walkthrough for fixes and an agent-ready prompt.
| Timer { | ||
| interval: 10000 | ||
| repeat: true | ||
| running: true | ||
| onTriggered: card.readMode(false) |
There was a problem hiding this comment.
🟡 minor · Performance · fix: medium effort
A pinned Performance card remains loaded while the bar is closed, so this unconditional timer continues spawning ryoku wm state every ten seconds even when the overlay is covered. Gate the polling on surface visibility and the applicable power tier.
| Audio.toggleSelfDeaf(); | ||
| } catch { | ||
| // A malformed command is not worth tearing the socket down for. | ||
| } |
There was a problem hiding this comment.
🟡 minor · Bug · fix: low effort
Discord automatically mutes when deafening and unmutes when undeafening. Sending both fields can therefore toggle mute twice or leave it different from the requested value; apply deaf first and re-read the self state before applying mute.
Suggested change
| Audio.toggleSelfDeaf(); | |
| } catch { | |
| // A malformed command is not worth tearing the socket down for. | |
| } | |
| if (typeof command.deaf === "boolean" && command.deaf !== Boolean(own?.selfDeaf)) | |
| Audio.toggleSelfDeaf(); | |
| const updated = me ? VoiceStateStore.getVoiceStateForUser(me.id) : null; | |
| if (typeof command.mute === "boolean" && command.mute !== Boolean(updated?.selfMute)) | |
| Audio.toggleSelfMute(); |
neur0map
left a comment
There was a problem hiding this comment.
🔍 mika reviewed this change; details in the walkthrough comment.
Co-authored-by: Carlos <90535780+neur0map@users.noreply.github.com>
neur0map
left a comment
There was a problem hiding this comment.
🔍 mika reviewed this change; details in the walkthrough comment.
Co-authored-by: Carlos <90535780+neur0map@users.noreply.github.com>
neur0map
left a comment
There was a problem hiding this comment.
🔴 Changes requested — 1 critical and 0 major issue(s): see the inline comments and the walkthrough for fixes and an agent-ready prompt.
| const ys = [ | ||
| const ys = [ |
There was a problem hiding this comment.
🔴 critical · Possible Bug · fix: low effort
The duplicated const ys = [ makes snapPosition invalid JavaScript. The Game Bar component cannot load, so the overlay is unavailable.
Suggested change
| const ys = [ | |
| const ys = [ | |
| const ys = [ |
|
|
||
|
|
||
| async def main() -> None: | ||
| bridge = VoiceBridge() | ||
| loop = asyncio.get_running_loop() | ||
| task = asyncio.current_task() | ||
| if task is not None: | ||
| loop.add_signal_handler(signal.SIGTERM, task.cancel) | ||
| await bridge.start_companion_server() |
There was a problem hiding this comment.
🟡 minor · Stability · fix: medium effort
A channel's roster is only a snapshot, while consume() handles speaking events but no participant join or leave events. Someone joining or leaving an existing call will not appear or disappear until the user changes channels or reconnects.
neur0map
left a comment
There was a problem hiding this comment.
🔍 mika reviewed this change; details in the walkthrough comment.
| readonly property string artist: card.player | ||
| ? Theme.joinArtists(card.player.trackArtists, card.player.trackArtist) : "" | ||
|
|
||
| component Transport: Item { |
There was a problem hiding this comment.
🟢 trivial · Maintainability · fix: medium effort
Transport is a second component declared inside MediaCard.qml, contrary to the one-component-per-file rule. Move it to its own component file.
neur0map
left a comment
There was a problem hiding this comment.
🔍 mika reviewed this change; details in the walkthrough comment.
neur0map
left a comment
There was a problem hiding this comment.
🔍 mika reviewed this change; details in the walkthrough comment.
| async def send_frame(self, payload: dict[str, Any], opcode: int = FRAME) -> None: | ||
| if not self.writer or self.writer.is_closing(): | ||
| return | ||
| body = json.dumps(payload).encode() | ||
| self.writer.write(struct.pack("<II", opcode, len(body)) + body) | ||
| await self.writer.drain() |
There was a problem hiding this comment.
🟡 minor · Stability · fix: low effort
The native RPC handshake performs blocking reads before a timeout is set. If an available Discord RPC socket accepts the connection but stops responding, the bridge cannot process Retry or stop commands until the process is killed. Apply a timeout before the handshake and handle timeout as a failed connection.
neur0map
left a comment
There was a problem hiding this comment.
🔍 mika reviewed this change; details in the walkthrough comment.
| readonly property bool polling: (!card.host || (card.host.live && !card.host.suppressed)) | ||
| && !Presence.dark && !Perf.saver |
There was a problem hiding this comment.
🟡 minor · Possible Bug · fix: low effort
In Power Saver, opening Tasks performs one initial scan but disables subsequent refreshes even while the card is visible. The displayed process list and CPU readings then remain stale; allow polling while the bar is open.
| raise ConnectionError("the RPC socket stopped reading") | ||
| else: | ||
| emit("auth_required") | ||
| return True |
There was a problem hiding this comment.
🟡 minor · Stability · fix: medium effort
A single recv(8) is not guaranteed to return the entire frame header on a stream socket. If Discord delivers the header in fragments, this treats a valid response as a disconnect and repeatedly restarts the connection. Read the header in a loop, as the payload is read below.
|
|
||
| install_client() { |
There was a problem hiding this comment.
🟡 minor · Possible Bug · fix: medium effort
ps -eo comm= reports executable names, not command-line arguments, so it will not find app.asar in an Electron command line. A running client whose executable is not named vesktop or equibop can therefore pass this guard, after which the installer modifies its state while it is running. Inspect process arguments as well as executable names before proceeding.
neur0map
left a comment
There was a problem hiding this comment.
🔍 mika reviewed this change; details in the walkthrough comment.
|
okay this is getting annoying im done for today |
Co-authored-by: Carlos <90535780+neur0map@users.noreply.github.com>
|
@dodo986 skill issue |
Note: Fixed: Game Bar cards keep updating in Power Saver while the bar is open.
Restore the protected-process guard and the delegate braces a previous edit lost, keep the verifier serialized, and hold a process start time as a real so it survives past 2038. Note: Fixed: the Game Bar Tasks card loads again and cannot offer to stop the desktop.
A connect request made while the bridge is down queued another connect that ready then sent again. Start the bridge instead, and clear the retry budget and stale mute state on stop. Note: Fixed: the Voice card no longer asks the Discord bridge to connect twice.
A cache file holding valid JSON that is not an object raised TypeError and stopped the bridge; read it as a cache miss so Voice asks for authorization again. Note: Fixed: a corrupted Discord voice token cache no longer stops the bridge.
neur0map
left a comment
There was a problem hiding this comment.
🔍 mika reviewed this change; details in the walkthrough comment.
| def token(self) -> str: | ||
| try: | ||
| os.chmod(self.token_file, 0o600) | ||
| return str(json.loads(self.token_file.read_text())["access_token"]) | ||
| except (OSError, KeyError, TypeError, ValueError): | ||
| return "" |
There was a problem hiding this comment.
💡 Suggestion (bug)
Reject non-string cached tokens
Suggested change
| def token(self) -> str: | |
| try: | |
| os.chmod(self.token_file, 0o600) | |
| return str(json.loads(self.token_file.read_text())["access_token"]) | |
| except (OSError, KeyError, TypeError, ValueError): | |
| return "" | |
| def token(self) -> str: | |
| try: | |
| os.chmod(self.token_file, 0o600) | |
| token = json.loads(self.token_file.read_text())["access_token"] | |
| return token if isinstance(token, str) else "" | |
| except (OSError, KeyError, TypeError, ValueError): | |
| return "" |
Reply here to discuss, say it's fixed, or explain why it's intended — no mention needed.
|
|
||
| repo="$(client_repo "$client")" | ||
| dir="$(client_dir "$client")" | ||
| state="$(client_state "$client")" |
There was a problem hiding this comment.
🟢 trivial · Maintainability · fix: low effort
This comment contains an en dash, which the repository forbids in code and comments. Replace it with ordinary punctuation.
Suggested change
| state="$(client_state "$client")" | |
| # Each srcDir comes from a fixed path; skip roots that are absent. |
Reply here to discuss, say it's fixed, or explain why it's intended — no mention needed.
Bound the album tile and each voice avatar to their display geometry, and refuse a cached voice token that is not a string. Note: Fixed: large album art and avatars no longer decode at full resolution.
Note: Fixed: gallery thumbnails no longer decode full screenshots at full resolution.
neur0map
left a comment
There was a problem hiding this comment.
🔍 mika reviewed this change; details in the walkthrough comment.
| const line = await Native.nextCommand(); | ||
| if (!running) | ||
| return; | ||
| applyCommand(line); |
There was a problem hiding this comment.
💡 Suggestion (bug)
Reject commands from stale loops
Suggested change
| const line = await Native.nextCommand(); | |
| if (!running) | |
| return; | |
| applyCommand(line); | |
| const line = await Native.nextCommand(); | |
| if (!running || generation !== commandGeneration) | |
| return; | |
| applyCommand(line); |
Reply here to discuss, say it's fixed, or explain why it's intended — no mention needed.
|
i fw ts |
i dont fw mika she's too perfectionist bro |

What changed
Adds the Game Bar, like Windows, a passive overlay per output raised with
Super+Shift+Gover the focused monitor. The strip carries eight widget buttons plus a
snapping toggle, clock and battery. The cards are capture, performance,
resources, tasks, media, voice, audio and gallery. Cards drag from their frame,
snap to screen edges, centre lines and each other with a guide grid, and keep
their position and size across restarts. A pinned card stays after the bar
closes and never takes pointer or keyboard focus; a pinned Voice card shrinks
to a content-sized panel in the corner its full card occupied.
Discord Voice reads the official client over native RPC, or Vesktop and Equibop
through a bundled Vencord companion that publishes the call over a local Unix
socket. The card offers to build and install that companion, points the
client's
state.jsonat the builtdist, and keeps a backup. The bridge runsonly while the widget is on screen.
Ryoku Hub gains a Game Bar page under Desktop for the master switch, dim, scale,
radius, fade, snapping and guides, settle spring, and the voice overlay. Every
key lives under
gamebarinshell.json.Docs: new
docs/gamebar.md, linked fromdocs/structure.md.CHANGELOG.mdcarries the New and Fixed entries.
Area
ryoku
How it was tested
Super+Shift+Gover a real fullscreen game on a1920x1080 output. All eight cards rendered and were working as they were expected; the bar closed cleanly.
Official Discord over native RPC: authorization, channel and participant
list, speaking state, mute and deafen.
Vesktop companion:
install_companion.sh --client vesktopcloned Vencord,installed with
pnpm install --frozen-lockfileand built withpnpm build.The plugin is present in
dist/renderer.jsanddist/vencordDesktopMain.js,state.jsonpointsvencordDiratdist, and a backupstate.json.pre-ryoku-voiceexists. After restarting Vesktop, the Voice cardshowed the channel
Genel,1 person in voice, backendVesktopandparticipant
dodo985(me);ss -xshowed an established connection to/run/user/1000/ryoku-discord-voice.sock.luac -pon the Hyprland bind module,bash -non the installer,python3 -m py_compileon the bridge, andbin/ryoku-dev-lint-qmlon theshell and Hub roots. The layout, tasks and units JS tests pass. Commit hooks
pass without
--no-verify.Checklist
[area] scope: summarycommit subject.CHANGELOG.mdupdated in the area I touched.--no-verify.luac -p), shell scripts passbash -n, and QML passesqmllintwhere applicable.