Skip to content

[ryoku] gamebar: add the overlay and the Discord Voice widget - #771

Open
dodo986 wants to merge 33 commits into
Ryoku-dev:unstable-devfrom
dodo986:ryo-gamebar
Open

dodo986 wants to merge 33 commits into
Ryoku-dev:unstable-devfrom
dodo986:ryo-gamebar

Conversation

@dodo986

@dodo986 dodo986 commented Nov 9, 2026

Copy link
Copy Markdown
Member

What changed

Adds the Game Bar, like Windows, a passive overlay per output raised with Super+Shift+G
over 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.json at the built dist, and keeps a backup. The bridge runs
only 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 gamebar in shell.json.

Docs: new docs/gamebar.md, linked from docs/structure.md. CHANGELOG.md
carries the New and Fixed entries.

Area

ryoku

How it was tested

  • Super+Shift+G over a real fullscreen game on a
    1920x1080 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 vesktop cloned Vencord,
    installed with pnpm install --frozen-lockfile and built with pnpm build.
    The plugin is present in dist/renderer.js and dist/vencordDesktopMain.js,
    state.json points vencordDir at dist, and a backup
    state.json.pre-ryoku-voice exists. After restarting Vesktop, the Voice card
    showed the channel Genel, 1 person in voice, backend Vesktop and
    participant dodo985(me); ss -x showed an established connection to
    /run/user/1000/ryoku-discord-voice.sock.

  • luac -p on the Hyprland bind module, bash -n on the installer,

    python3 -m py_compile on the bridge, and bin/ryoku-dev-lint-qml on the
    shell and Hub roots. The layout, tasks and units JS tests pass. Commit hooks
    pass without --no-verify.

Checklist

  • One logical change, with a clear [area] scope: summary commit subject.
  • Matching CHANGELOG.md updated in the area I touched.
  • The git hooks pass locally; I did not use --no-verify.
  • Lua parses (luac -p), shell scripts pass bash -n, and QML passes
    qmllint where applicable.
  • No duplicated config, no dead code, no commented-out code, no em-dash.
  • Docs updated if behavior or layout changed.

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
@neur0map

neur0map commented Nov 9, 2026 •

Copy link
Copy Markdown
Member

🦊 mika's review

Incremental review: new commits since the last checkpoint.

Summary

  • Add a focused-output Game Bar with eight draggable, pinnable widgets.
  • Integrate Discord voice through native RPC or a Vencord companion.
  • Expose overlay settings in Hub and persist card layouts.
  • Fix Hub step controls to respect schema bounds.

Change diagram

flowchart 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"]
Loading

Estimated review effort: 5/5 · risk: high

📁 Walkthrough details

ryoku/shell/quickshell/shell/modules/gamebar/GameBar.qml

  • Adds the per-output overlay, widget strip, snapping, pinning and placement.
  • Coordinates capture suppression and restoration around screenshots and recordings.
    • Adds eight card slots and click-through pinned overlays.
    • Persists drag, resize and monitor placement through the store.

ryoku/shell/quickshell/shell/services/GameBarStore.qml

  • Persists open cards, pins, monitor, positions, sizes and resource tab.
  • Handles malformed state and defers writes until the file loads.
    • Adds gamebar.json state and card lifecycle operations.

ryoku/shell/quickshell/shell/modules/gamebar/GameBarCard.qml

  • Adds the shared draggable card frame with pin, close and resize controls.
    • Supports a compact presentation for pinned Voice.

ryoku/shell/quickshell/shell/modules/gamebar/cards/CaptureCard.qml

  • Connects screenshot targets and recording controls to existing shell services.
    • Delegates overlay hiding and restoration to the host.

ryoku/shell/quickshell/shell/modules/gamebar/cards/PerformanceCard.qml

  • Displays system utilization and compositor display mode without claiming game FPS.
    • Refreshes mode and sampler data while the card is visible.

ryoku/shell/quickshell/shell/modules/gamebar/cards/ResourcesCard.qml

  • Adds CPU, GPU and memory tabs with short utilization histories.
    • Pauses sampling when the overlay is hidden or idle.

ryoku/shell/quickshell/shell/modules/gamebar/cards/TasksCard.qml

  • Lists the user's busiest processes and offers two-step termination.
  • Rechecks process start time before signaling to reduce PID reuse mistakes.
    • Protects named desktop processes and gates polling on visibility.

ryoku/shell/quickshell/shell/modules/gamebar/cards/GalleryCard.qml

  • Shows recent screenshots and recordings from the existing capture folders.
    • Refreshes on folder model changes and decodes thumbnails at display size.

ryoku/shell/quickshell/shell/modules/gamebar/cards/VoiceCard.qml

  • Presents Discord participants, speaking state, authorization and voice controls.
    • Adds a compact pinned view and companion installation action.

ryoku/shell/quickshell/shell/services/DiscordVoice.qml

  • Manages the voice bridge process, participant model, retries and installer.
    • Starts the bridge when Voice is visible and releases it when hidden.

ryoku/shell/quickshell/shell/modules/gamebar/discordVoice/bridge.py

  • Bridges official Discord RPC or the local companion socket to the shell.
    • Handles authorization, cached tokens, voice state and mute commands.
    • Bounds RPC frames and handshake time and avoids unsafe stale-socket removal.

ryoku/shell/quickshell/shell/modules/gamebar/discordVoice/install_companion.sh

  • Builds the Vencord or Equicord companion and configures the selected client.
    • Requires the client to be closed and backs up its state file.

ryoku/shell/quickshell/shell/modules/gamebar/discordVoice/companion/index.ts

  • Publishes client voice state and handles mute and deafen commands.
    • Registers the companion's renderer-side IPC handlers.

ryoku/shell/quickshell/shell/modules/gamebar/discordVoice/companion/native.ts

  • Hosts the companion's runtime-directory Unix socket and forwards commands.
    • Restricts socket permissions and checks ownership before cleanup.

ryoku/shell/ipc/settings.go

  • Adds persisted gamebar settings, defaults and validation to the daemon.
    • Covers overlay appearance, snapping, motion and voice presentation.

ryoku/hub/quickshell/schema/GameBarPage.js

  • Defines Hub controls for Game Bar behavior, placement, motion and Voice.
    • Adds ranges and conditional visibility for overlay settings.

ryoku/hub/quickshell/SettingsSheet.qml

  • Fixes step controls to clamp against schema lo and hi values.
    • Replaces the unrelated from and to bounds.

ryoku/wm/binds.go

  • Adds shell.gamebar to the shared keybind catalogue.
    • Assigns Super+Shift+G as the default shortcut.

ryoku/shell/quickshell/shell/modules/gamebar/discordVoice/test_bridge.py

  • Adds unit tests for RPC framing, handshake failures and companion sockets.
    • Covers timeouts, stale sockets, token parsing and control failures.

docs/gamebar.md

  • Documents widgets, placement, settings, voice setup and troubleshooting.
    • Explains capture behavior, pinned overlays and companion installation.

Findings

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 loops
  • ryoku/shell/quickshell/shell/modules/gamebar/discordVoice/companion/index.ts:117 — 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.
🤖 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)

⚠️ Security concerns: A fake local Discord RPC socket can receive the cached OAuth token. Partial voice-settings events can also leave the displayed mute state incorrect.

🚦 Merge recommendation: changes_required


🦊 Generated by Mika — the Ryoku org's self-hosted AI code reviewer. Machine findings: verify before merging.

@dodo986

dodo986 commented Nov 9, 2026

Copy link
Copy Markdown
Member Author
image

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 neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Comment on lines +45 to +47
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

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

Comment on lines +86 to +90
protectedName: Tasks.isProtected(modelData.name)
onEndRequested: pid => {
killer.command = ["kill", "-TERM", String(pid)];
killer.running = true;
settle.restart();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

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 neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 mika reviewed this change; details in the walkthrough comment.

Comment on lines +70 to +71
Repeater {
model: card.streams.slice(0, 3)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Comment on lines +34 to +35
def emit(kind: str, **fields: Any) -> None:
print(json.dumps({"type": kind, **fields}, separators=(",", ":")), flush=True)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

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 neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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 neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 mika reviewed this change; details in the walkthrough comment.

Comment on lines +77 to +87
FolderListModel {
id: shotFiles
folder: "file://" + card.shotsDir
showDirs: false
nameFilters: ["*.png", "*.jpg", "*.jpeg", "*.webp"]
}

FolderListModel {
id: clipFiles
folder: "file://" + card.clipsDir
showDirs: false

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Comment on lines +95 to +99
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();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

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();

@dodo986
dodo986 requested a review from neur0map November 9, 2026 14:50
@neur0map neur0map added the mika:review Ask Mika to review this PR now (consumed after use) label Nov 9, 2026

@neur0map neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 mika reviewed this change; details in the walkthrough comment.

Comment on lines +344 to +349
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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

@neur0map neur0map removed the mika:review Ask Mika to review this PR now (consumed after use) label Nov 9, 2026
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 neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 mika reviewed this change; details in the walkthrough comment.

Comment on lines +144 to +157
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"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 mika reviewed this change; details in the walkthrough comment.

Comment on lines +26 to +44
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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 mika reviewed this change; details in the walkthrough comment.

Comment on lines +710 to +717
if (win.reopenAfterRecording) {
if (!GameBarStore.open)
GameBarStore.openBar(GameBarStore.monitor);
} else if (GameBarStore.open) {
GameBarStore.closeBar();
}
win.reopenAfterRecording = false;
win.restoreAfter(900);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 mika reviewed this change; details in the walkthrough comment.

@neur0map neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Changes requested — 0 critical and 1 major issue(s): see the inline comments and the walkthrough for fixes and an agent-ready prompt.

Comment on lines +37 to +38
readonly property bool mine: GameBarStore.monitor.length > 0
? GameBarStore.monitor === win.mon : Wm.focusedOutput === win.mon

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 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.

Comment on lines +274 to +279
visible: card.voice.companionNeeded && !card.voice.installerBusy
s: card.s
compact: true
quiet: true
label: I18n.tr("Install voice companion")
onClicked: card.voice.installCompanion()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

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;
}

Comment thread ryoku/shell/quickshell/shell/shell.qml Outdated
Comment on lines 906 to 909
if (id === "launcher" || id === "clipboard" || id === "settings" || id === "gamebar") {
root.toggleSurface(id);
return;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Suggestion (bug)

Honor the requested Game Bar monitor

Suggested change

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>.

Comment on lines +1 to +4
// 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

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;

Comment on lines +30 to +33
const cpuSecs = parseInt(parts[parts.length - 3], 10);
const name = parts.slice(2, parts.length - 3).join(" ");
if (!(pid > 0) || name.length === 0)
continue;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Suggestion (possible bug)

Preserve processes with long usernames

Suggested change

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 neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Changes requested — 0 critical and 1 major issue(s): see the inline comments and the walkthrough for fixes and an agent-ready prompt.

Comment on lines +54 to +58
Timer {
interval: 10000
repeat: true
running: true
onTriggered: card.readMode(false)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Comment on lines +102 to +105
Audio.toggleSelfDeaf();
} catch {
// A malformed command is not worth tearing the socket down for.
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

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 neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 mika reviewed this change; details in the walkthrough comment.

Co-authored-by: Carlos <90535780+neur0map@users.noreply.github.com>

@neur0map neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 mika reviewed this change; details in the walkthrough comment.

@dodo986
dodo986 requested a review from neur0map November 9, 2026 16:24
Co-authored-by: Carlos <90535780+neur0map@users.noreply.github.com>

@neur0map neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Changes requested — 1 critical and 0 major issue(s): see the inline comments and the walkthrough for fixes and an agent-ready prompt.

Comment on lines +201 to +202
const ys = [
const ys = [

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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

Suggested change
const ys = [
const ys = [
const ys = [

Comment on lines +530 to +538


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()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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 neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 mika reviewed this change; details in the walkthrough comment.

@neur0map neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 mika reviewed this change; details in the walkthrough comment.

Comment on lines +90 to +95
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()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 mika reviewed this change; details in the walkthrough comment.

Comment on lines +35 to +36
readonly property bool polling: (!card.host || (card.host.live && !card.host.suppressed))
&& !Presence.dark && !Perf.saver

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Comment on lines +178 to +181
raise ConnectionError("the RPC socket stopped reading")
else:
emit("auth_required")
return True

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Comment on lines +83 to +84

install_client() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 mika reviewed this change; details in the walkthrough comment.

@dodo986

dodo986 commented Nov 9, 2026

Copy link
Copy Markdown
Member Author

okay this is getting annoying im done for today

Co-authored-by: Carlos <90535780+neur0map@users.noreply.github.com>
@neur0map

neur0map commented Nov 9, 2026

Copy link
Copy Markdown
Member

@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 neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 mika reviewed this change; details in the walkthrough comment.

Comment on lines +69 to +74
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 ""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Suggestion (bug)

Reject non-string cached tokens

Suggested change

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")"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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

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 neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 mika reviewed this change; details in the walkthrough comment.

Comment on lines +117 to +120
const line = await Native.nextCommand();
if (!running)
return;
applyCommand(line);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Suggestion (bug)

Reject commands from stale loops

Suggested change

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.

@Sipper1236

Copy link
Copy Markdown
Member

i fw ts

@dodo986

dodo986 commented Nov 9, 2026

Copy link
Copy Markdown
Member Author

i fw ts

i dont fw mika she's too perfectionist bro

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants