Add Kevin AI support chat to the studio footer - #6159
Conversation
Adds a Kevin chat-bubble icon to the studio footer that opens a "Streamlabs Desktop Support" window backed by the stream-avatar API. This replaces the chat tab that lived in the stream-avatar plugin. - KevinSupportService: worker-side StatefulService owning the Socket.IO connection and the transcript, so the conversation survives the window being closed and reopened. Desktop is on socket.io-client@2, which has no `auth` option, so the JWT goes in the query string; the server-side role then defaults to `desktop`, which keeps TEXT packets on the originating socket and away from the avatar browser sources. Waits for the server's `authenticated` event before emitting, since a text message sent ahead of `setGame` would run against the default agent. - KevinSupport: empty state, suggested prompts, transcript and composer. Replies are inline markdown only (links, emphasis, code spans), so a small splitter renders them rather than pulling in react-markdown. - Opened via createOneOffWindow rather than showWindow: there is only one shared `child` window, and support needs to sit alongside whatever the user is asking about. No API changes were needed — `setGame: STREAMLABS` selects the existing Support Bot persona.
The support chat moves to the /v2 namespace. socket.io-client 2.x cannot send a handshake auth object, so the JWT goes in the query string and the server accepts both. KevinSupportService connects in init() rather than on component mount. An approval can be triggered by a voice turn in the plugin while no chat window is open, and if Desktop is not attached at that moment the request is routed to the plugin instead — so the connection has to outlive the UI. surfaceApproval() opens or focuses the chat window when one arrives. AgentToolsService exposes eight OBS actions (scenes, sources, streaming, replay) that the agent can call. Desktop is preferred over the plugin for these because it owns the OBS state directly. Approval cards render inline in the transcript. The empty state previously keyed off message count alone, which hid a pending approval on a fresh chat.
handleText dropped every packet with kind === 'links'. That was written on the assumption the footer was a plugin concern, but the plugin's chat tab is gone and Desktop is now the only surface that renders text at all. The footer appends to the reply it belongs to, or opens its own bubble when the question was asked by voice and the spoken answer never came through here. renderText already linkifies markdown and opens URLs externally.
The contract is copied byte-for-byte from stream-avatar-api, which formats with different rules, so `yarn eslint --fix` rewrote it on every run. The sync guard now compares a normalized form instead of raw bytes, so each repo can format its copy however it likes. No semantic change.
The plugin gains audio_set_muted and change_filter. Desktop implements neither, and correctly so — both are executors: ["app"] and route to the plugin — but the contract is copied byte-for-byte across all three repos, so its copy moves too. Formatted by this repo's Prettier rather than the API's; the sync guard compares a normalized form, so the copies no longer have to agree on quotes or wrapping.
The Desktop half of the Sidekick support set. mic_enhance picks a filter chain from what the streamer says is wrong. general, background_noise, too_quiet and uneven_volume each get their own filters and values — a gate tuned for a quiet room does nothing in a noisy one, and compressing someone who is simply too quiet without makeup gain leaves them quieter still. too_quiet makes up level through the compressor's output_gain, since TSourceFilterType has no standalone gain filter, with a limiter so that gain cannot clip. echo applies nothing and explains why: no OBS filter removes room reverb, and the real fixes are headphones, mic placement and soft furnishings. Anything this service adds is named with a "Sidekick " prefix, and removal matches the prefix rather than the chosen preset — switching from too_quiet to background_noise must not leave the limiter stacked underneath, and asking twice must not end up with two gates fighting each other. The mic is resolved rather than asked for: Mic/Aux is the app's own default name, with a "mic" prefix match for anyone who renamed theirs. stream_health returns what PerformanceService already tracks plus the resolutions, so a question about dropped frames can be answered from the real numbers instead of guessed at. diagnostics_report uploads and returns the report code. support_open_ticket opens the same Zendesk form Settings > Get Support links to. Tool protocol version goes back to 1: v2 has not shipped, so there is no released client to be older than anything, and every client advertises this constant anyway.
Mirrors the Automations usage meter: a counter in the top right of the
support modal, and the same upgrade modal when the allowance runs out.
support-limits.tsx is the sibling of automations-limits.tsx and follows it
deliberately — same promptAction, same UltraIcon, same free -> Ultra ->
Ultra+ -> hard cap ladder. The refl is 'slobs-support-chat' rather than
'slobs-automations' so the two upsells stay separable in the conversion
funnel instead of blending into one number.
The meter does not wait for the server. Counts arrive on v2:rateLimit, but
only once a request has been handled, so gating the render on them left the
meter absent until after the first message — exactly when someone on the
free tier most wants to see their allowance. It renders 0/limit from the
tier and the server's figures replace that when they land, which is how the
Automations meter derives its numbers too.
Three faults in the refusal path came out of reviewing it against the
Automations flow:
The chat bricked at the limit. A refused request never starts a run, so
v2:run.ended never arrived and nothing cleared `pending`: the spinner span
forever and Send stayed disabled. Any v2:error now ends the turn.
The limit was announced twice. v2:error{rate_limit} also set the red banner,
so a raw "You have reached your usage limit." appeared alongside the modal.
Only one of the two is actionable, so quota is now the modal's alone; auth
still surfaces as a banner.
It prompted once, then went silent. The effect was keyed on `exceeded`,
which latches true for the rest of the period, so a second attempt was
refused with no feedback at all once the banner was gone. The service counts
refusals and the modal answers every one, as Automations does per blocked
action.
Also: supportTier now reads UserService.views.tier directly. That getter
already resolves the whole ladder, and wrapping it in an isPrime check
returned 'free' for anyone whose tier was set but whose isPrime was not.
Note the caveat automations-limits already carries: if the API only ever
returns 'free' and 'ultra', the Ultra+ branch is unreachable and an Ultra+
user is offered an upgrade they hold. The server's limiter assumes the
mirror image — tier === 'ultra' ? 1000 : 5000 — so that wants confirming.
…estyle bubbles, add agent avatar and send icon, update suggested prompts
There was a problem hiding this comment.
🟡 Changes recommended
It introduces user-facing and operational issues (production logging of sensitive chat/tool payloads, silent non-auth socket errors, and missing accessible labeling/focus) that should be corrected before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds a new “Kevin” AI support chat entry point in the Studio footer and introduces the Desktop-side worker service + /v2 protocol contract needed to connect to Streamlabs’ agent API, execute approved tools, and surface quota/upsell UI.
Changes:
- Adds a Studio footer icon that opens a dedicated “Kevin Support” one-off window.
- Introduces a worker-window
KevinSupportService+AgentToolsServiceto connect to/v2, handle approvals, and execute Desktop tools. - Adds new UI surface (chat window, icons, styling) and associated i18n + analytics/upsell wiring.
File summaries
| File | Description |
|---|---|
| app/services/windows.ts | Registers KevinSupport as a window component. |
| app/services/usage-statistics.ts | Adds analytics event + refl for SupportChat upsell tracking. |
| app/services/stream-avatar/v2/protocol.ts | Adds shared /v2 protocol contract definitions. |
| app/services/stream-avatar/v2/agent-tools.ts | Adds Desktop-executed tool handlers for the agent. |
| app/services/stream-avatar/kevin-support-service.ts | Adds worker-window Socket.IO client + approval/tool dispatch logic. |
| app/i18n/fallback.ts | Includes new stream-avatar agent i18n bundle in fallback. |
| app/i18n/en-US/stream-avatar-agent.json | Adds i18n strings for tool approval UI. |
| app/i18n/en-US/ai.json | Adds i18n strings for the Kevin Support chat UI and upsell. |
| app/components/shared/ReactComponentList.tsx | Exposes KevinSupport to the window system’s React component list. |
| app/components-react/shared/icons/SendIcon.tsx | Adds send icon for the composer button. |
| app/components-react/shared/icons/KevinChatIcon.tsx | Adds Kevin chat icon for footer/empty-state. |
| app/components-react/shared/icons/index.ts | Exports the new icon components. |
| app/components-react/root/StudioFooter.tsx | Adds footer entry point that opens the Kevin Support one-off window. |
| app/components-react/root/StudioFooter.m.less | Styles the new footer icon button. |
| app/components-react/index.ts | Registers KevinSupport in the React component registry. |
| app/components-react/agent/support-limits.tsx | Defines support interaction tiers + upgrade prompting. |
| app/components-react/agent/KevinSupport.tsx | Implements the chat/approval UI, usage meter, markdown-lite rendering. |
| app/components-react/agent/KevinSupport.m.less | Adds styling for the support chat window. |
| app/app-services.ts | Registers KevinSupportService and AgentToolsService in the service registry. |
Review details
- Files reviewed: 19/19 changed files
- Comments generated: 7
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
BundleMonFiles updated (1)
Unchanged files (3)
Total files change +70.6KB +0.45% Final result: ✅ View report in BundleMon website ➡️ |
There was a problem hiding this comment.
🟡 Changes recommended
It introduces a confirmed unsafe external-link handling path and a few user-facing accessibility/error-feedback gaps that should be addressed before merge.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (3)
app/components-react/root/StudioFooter.tsx:167
- This icon-only button has no accessible name; screen readers will announce it generically as “button”. Add an
aria-label(and settype="button"to avoid accidental form submission if this footer is ever rendered inside a form).
<Tooltip placement="top" title={$t('Streamlabs Desktop Support')}>
<button className={styles.kevinIcon} onClick={openKevinSupport}>
<KevinChatIcon />
</button>
app/components-react/root/StudioFooter.m.less:56
- The new
.kevin-iconstyle removes the focus outline without providing an alternative focus-visible indicator, which makes keyboard navigation difficult.
&:focus {
outline: none;
}
app/services/stream-avatar/kevin-support-service.ts:224
v2:errorcurrently only surfaces an error message forauthfailures. For other errors (e.g. protocol/internal), the UI clearspendingbut shows no feedback, which can look like the send was ignored.
// Quota is answered by the upgrade modal, not by a red line: showing
// both says the same thing twice, and only one of them is actionable.
if (p.code === 'rate_limit') return;
if (p.code === 'auth') this.SET_ERROR(p.message);
});
- Files reviewed: 20/20 changed files
- Comments generated: 1
- Review effort level: Lite
…support - Label the footer support button (aria-label + type="button") and restore a keyboard focus ring instead of suppressing the outline outright. - Render agent-supplied markdown links as real anchors, and only hand http(s) URLs to openExternal -- other schemes fall back to plain text. - Surface every non-rate-limit v2:error instead of only auth failures, so a protocol or internal error no longer fails silently. - Clear `connecting` on disconnect; a drop mid-handshake left it stuck on. - Log wire event names always, payload detail only in dev, to keep chat text and tool arguments out of production logs. - Reword the protocol header constraint that contradicted its own sync manifest.
There was a problem hiding this comment.
🟡 Changes recommended
The new support-chat connection and quota-upsell flows contain user-visible logic bugs (handshake error handling and repeated upsell prompting) that should be fixed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
app/components-react/agent/KevinSupport.tsx:210
- This effect will also run on initial mount. If
rateLimitRefusalsis already > 0 from a previous session (the service state survives closing the support window), opening the window will immediately re-show the upgrade modal even though the user didn’t just attempt an action. Track the last value and only prompt when the counter increments.
app/services/stream-avatar/kevin-support-service.ts:279
- The catch block always overwrites
state.errorwith a generic message. If the server already provided a specific error viav2:error(e.g. auth/protocol), that message is briefly set and then replaced by the generic one when the handshake promise rejects.
this.SET_CONNECTING(false);
this.SET_CONNECTED(false);
this.SET_PENDING(false);
this.SET_ERROR($t('Could not connect to Streamlabs Desktop Support. Please try again.'));
- Files reviewed: 20/20 changed files
- Comments generated: 1
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
The new support socket service can reuse cached auth across logout/account switch and can continue connecting after logout while awaiting a token, which is a correctness/security risk.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
app/services/stream-avatar/kevin-support-service.ts:153
- openSocket() can continue after a logout that happens while awaiting getToken(), creating a new authenticated socket even though the user is no longer logged in. Add a post-await login check and stop the connection attempt (also clear the connecting flag) to avoid reconnecting after logout.
- Files reviewed: 21/21 changed files
- Comments generated: 1
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
It introduces a server-driven tool dispatch path that needs a safer tool-name lookup and includes a new clickable span that isn’t keyboard-accessible.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
app/components-react/agent/KevinSupport.tsx:162
- The upgrade affordance is a clickable
<span>, which is not keyboard-accessible by default and won’t be announced as a button to assistive tech. Add button semantics (role/tabIndex + Enter/Space handling) or use a real<button>/<a>element.
- Files reviewed: 23/23 changed files
- Comments generated: 1
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
There’s a confirmed focus-state initialization bug that can misreport the support window’s focus, and a new clickable <span> upgrade CTA that is not keyboard-accessible.
Review details
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
app/components-react/agent/KevinSupport.tsx:163
- The upgrade call-to-action is a clickable
<span>, which is not keyboard-focusable and won’t be operable via Enter/Space for keyboard/screen-reader users. Make it accessible by adding button semantics (role/tabIndex + key handling) or switching to a real<button>element.
app/services/windows.ts:510 isFocusedis only updated via focus/blur events; if the window is shown focused but Electron doesn't emit an initialfocus(or the app is inactive on show),WindowsService.state[windowId].isFocusedcan remainundefinedand downstream UI (e.g. the footer approval bubble) may treat the chat as not focused even when it is. Initialize/sync focus state once and onshowto avoid a persistent undefined focus state.
- Files reviewed: 23/23 changed files
- Comments generated: 0 new
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
It introduces a new socket protocol plus Desktop-side tool execution/approval flows that can affect streaming behavior and warrants final human verification beyond automated review.
Review details
Suppressed comments (5)
Previously missed (4) — in code that hasn't changed since the last review.
app/components-react/agent/KevinApprovalBubble.tsx:58
- The bubble position is only re-measured on window resize, but the footer can scroll horizontally and nearby footer content can change width without a resize. That can leave the approval bubble pointing to the wrong place while it is visible.
app/components-react/agent/KevinApprovalBubble.tsx:70 - The bubble uses
left: anchor.leftwith a fixed 320px width, which can push the callout partially off-screen (especially since the icon lives near the right side of the footer). Clamp the computed left position to the viewport.
app/components-react/agent/KevinSupport.tsx:162 - The upgrade affordance is rendered as a clickable , which is not keyboard-focusable by default and is not announced as a button by assistive tech. Use a (or ) for an interactive control.
app/i18n/en-US/ai.json:35 - These strings use "setup" as a verb; grammatically it should be "set up". Since these are user-facing prompts, it’s worth correcting the displayed text (the JSON value) to avoid shipping the error.
This issue also appears on line 40 of the same file.
app/i18n/en-US/ai.json:40
- This prompt uses "setup" as a verb; grammatically it should be "set up". Adjust the displayed text (JSON value) accordingly.
"How do I setup Streamlabs Sidekick?": "How do I setup Streamlabs Sidekick?",
- Files reviewed: 23/23 changed files
- Comments generated: 0 new
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
There are confirmed UI behavior/accessibility issues (approval bubble can drift on footer scroll; upgrade control is not keyboard-accessible as implemented).
Review details
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
app/components-react/agent/KevinApprovalBubble.tsx:58
- KevinApprovalBubble only re-measures the anchor on window resize. Since the footer is horizontally scrollable (overflow-x: auto) and neighboring footer content can resize, the bubble can drift away from the Kevin icon while pending approvals are showing.
app/components-react/agent/KevinSupport.tsx:158 - The upgrade affordance is implemented as a clickable . This is not keyboard-focusable by default and won’t be announced as an interactive control by assistive tech.
- Files reviewed: 24/24 changed files
- Comments generated: 0 new
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
There are a few concrete UX/correctness issues (approval bubble positioning drift, keyboard accessibility for the upsell control, and a logout/connect race that can reintroduce stale errors) that should be addressed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
app/components-react/agent/KevinApprovalBubble.tsx:58
- The approval bubble only re-measures its anchor on window resize. Because the footer can scroll horizontally and adjacent content can change width, the fixed-position bubble can drift away from the icon while it’s visible. Consider also re-measuring on scroll (capture) while
showis true so the bubble stays aligned.
app/services/stream-avatar/kevin-support-service.ts:307 openSocket()can still be in-flight whenuserLogouttriggersRESET(). If that connect attempt later fails (timeout/connect_error), this catch block can repopulatestate.errorafter logout, leaving a stale “Could not connect…” message while logged out. Gate the generic error message onuserService.isLoggedInso logout remains clean.
- Files reviewed: 25/25 changed files
- Comments generated: 1
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
There are confirmed accessibility issues in the chat UI controls and a confirmed positioning bug in the footer approval bubble when the footer scrolls/resizes.
Review details
Suppressed comments (3)
Previously missed (2) — in code that hasn't changed since the last review.
app/components-react/agent/KevinApprovalBubble.tsx:58
- The bubble only re-measures the anchor on window resize, but the footer can scroll horizontally (and adjacent footer content can resize), which will move the icon without updating the bubble position. This can leave the approval prompt detached from the Kevin icon until a resize happens.
app/components-react/agent/KevinSupport.m.less:96 - The suggested-prompt buttons remove the focus outline but don’t add a focus-visible replacement, which makes keyboard navigation hard/impossible to see. Add a :focus-visible style (matching other buttons like the footer Kevin icon).
app/components-react/agent/KevinSupport.tsx:153
- The upgrade affordance is a clickable , which is not keyboard-focusable by default and won’t announce itself as a control to assistive tech. Use a (or an ) so it’s accessible via keyboard and screen readers.
{atCap && !atTopTier && (
<span className={styles.upgradeLink} onClick={() => upgrade(p.tier, 'meter')}>
<UltraIcon type="badge" />
<span className={styles.upgradeText}>
{p.tier === 'ultra'
- Files reviewed: 25/25 changed files
- Comments generated: 0 new
- Review effort level: Lite
Summary
Test plan
yarn typecheck(already run clean during development)yarn eslinton touched files (already run clean during development)