Add Kevin AI support chat to the studio footer - #6159
Conversation
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 +73.61KB +0.47% 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
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.
🔵 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
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved moderate and critical issues remain in socket lifecycle, tool execution, approval expiry, and support-chat interactions.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (13)
Previously missed (4) — in code that hasn't changed since the last review.
app/components-react/agent/KevinSupport.tsx:215
- This effect treats the cumulative refusal count as an edge, but the service keeps
rateLimitRefusalsafter the one-off support window closes. After one quota refusal, closing and reopening Kevin runs this effect with a nonzero count and opens another upgrade modal without a new request, repeating on every remount. Track an acknowledged refusal in the service or otherwise only prompt when the count increases.
app/services/stream-avatar/kevin-support-service.ts:279 - The protocol provides
activeRunIdsspecifically so a reconnecting client can resync live runs, but this handler only restores approvals. The disconnect handler has already setpendingto false, so reconnecting during an active run enables Send and permits another request while the old run is still alive. Restore the pending state from the returned run list (or track the run IDs).
app/services/stream-avatar/kevin-support-service.ts:400 V2ToolInvokePayload.timeoutMsexplicitly requires the client to abandon a call past the deadline, but this path awaitsexecute()without any timeout or cancellation tracking. A slow diagnostics upload can outlive the server-side call and then emit a late result (and complete work) for a run that has already timed out or been cancelled. Race the execution withinvoke.timeoutMsand suppress or cancel the late result.
app/services/stream-avatar/v2/agent-tools.ts:248toggleStreaming()is asynchronous and can wait for the user's stop confirmation, but calling it through.actionsuses the fire-and-forget proxy even though this handler already runs in the worker. The handler therefore returns{ stopped: true }before the stream is stopped and hides cancellation or rejection. Await the worker-side service call so the tool result reflects the actual outcome.
app/components-react/agent/KevinSupport.m.less:95
- Removing the focus outline from these new prompt buttons leaves no
:focus-visiblereplacement, so keyboard users cannot tell which suggested prompt is selected. Preserve the reset for mouse focus but add a visible focus indicator for keyboard focus.
&:focus {
outline: none;
}
app/components-react/agent/KevinSupport.tsx:292
- The approval payload's
expiresAtis a client-side dismissal deadline, but these cards remain rendered until av2:approval.resolvedevent arrives and the buttons remain usable after expiry. During delayed delivery or a reconnect, users can be shown and submit an already-expired approval. Dismiss or disable each approval locally whenexpiresAtis reached.
{pendingApprovals.map(approval => (
<div
key={approval.approvalId}
app/components-react/agent/KevinSupport.tsx:150
- This clickable
spanis not keyboard-focusable or exposed as an actionable control, so keyboard-only users cannot reach the quota upgrade affordance. Use a button or add button semantics and keyboard handling for Enter/Space.
{atCap && !atTopTier && (
<span className={styles.upgradeLink} onClick={() => upgrade(p.tier, 'meter')}>
app/components-react/agent/KevinSupport.tsx:215
- This effect reads
tierbut only reruns whenrateLimitRefusalschanges. If the user's tier is refreshed after the first refusal, the modal can keep the Free copy/target (and its callback captures the old tier); includetierin the dependency list.
useEffect(() => {
if (rateLimitRefusals > 0) promptUpgrade(tier);
}, [rateLimitRefusals]);
app/i18n/en-US/ai.json:36
- Use the phrasal verb
set uphere;setupis a noun/adjective, so this newly added prompt is grammatically incorrect.
"How do I setup alerts & widgets?": "How do I setup alerts & widgets?",
app/i18n/en-US/ai.json:41
- Use the phrasal verb
set uphere;setupis a noun/adjective, so this newly added prompt is grammatically incorrect.
"How do I setup Streamlabs Sidekick?": "How do I setup Streamlabs Sidekick?",
app/services/stream-avatar/kevin-support-service.ts:246
- This disconnect path never recreates the socket with a fresh JWT. When the server disconnects an expired-token client, Socket.IO does not automatically reconnect, while transport reconnects reuse the expired token embedded in the original URL.
StreamAvatarApiService.getToken()has expiry-refresh logic, but it is only called before the initial socket, so a long-running Desktop session can silently lose its desktop device and approval surface.
socket.on('disconnect', (reason: string) => {
this.log('--', 'disconnect', { reason });
this.SET_CONNECTED(false);
this.SET_CONNECTING(false);
this.SET_PENDING(false);
app/services/stream-avatar/kevin-support-service.ts:218
- The protocol says clients should dismiss an approval at
expiresAt, but this listener only stores the request and waits forv2:approval.resolved. If the resolution packet is delayed or lost, the expired card remains in both the footer and chat and can still send a stale decision; schedule local expiry cleanup when adding the approval.
this.ADD_APPROVAL(p);
});
app/services/stream-avatar/kevin-support-service.ts:299
- When the initial handshake fails, this catch immediately disconnects the socket. That cancels Socket.IO's reconnect loop, and there is no later retry unless the user opens the chat or logs in again, so a transient startup/network failure leaves Desktop unattached and voice approvals can route to another device for the rest of the session. Keep the reconnect loop alive or schedule an explicit retry.
} catch (e: unknown) {
console.error('[KevinSupport] connect failed', e);
this.socket?.disconnect();
this.socket = null;
- Files reviewed: 28/28 changed files
- Comments generated: 4
- Review effort level: Lite
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
…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.
2340664 to
4b43e0f
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
Unresolved critical protocol, cancellation, reconnect, and tool-execution issues remain.
Review details
Suppressed comments (9)
app/components-react/agent/KevinApprovalBubble.tsx:81
- The anchor is measured only on
resize, but footer metrics/layout changes and horizontal footer scrolling can move the icon without a window resize. The fixed approval bubble and its BrowserView-occlusion check then remain at stale coordinates. Re-measure on footer scroll and relevant layout changes, for example with a scroll listener plus an appropriate observer.
// ponytail: re-measured on resize only. The icon also shifts when the
// performance metrics beside it change width, or if the footer is scrolled
// horizontally — watch those with a ResizeObserver if it ever looks off.
window.addEventListener('resize', measure);
return () => window.removeEventListener('resize', measure);
app/components-react/agent/KevinSupport.tsx:231
pendingis set only insidesendMessage()after it awaits connection, while this handler neither awaits nor locally locks the action. During the initial connection, a second click or Enter still seespending === falseand sends another request, producing duplicate user messages/runs. Add an in-flight guard or include the connection state in the send gate.
const send = useCallback(
(text: string) => {
const trimmed = text.trim();
if (!trimmed || pending) return;
KevinSupportService.actions.sendMessage(trimmed);
app/components-react/agent/KevinSupport.tsx:152
- This upgrade affordance is a clickable
span, so it has no keyboard focus or activation behavior and cannot be reached by keyboard-only users when the quota is exhausted. Use a real button or add equivalent button semantics and Enter/Space handling.
{atCap && !atTopTier && (
<span className={styles.upgradeLink} onClick={() => upgrade(p.tier, 'meter')}>
app/components-react/shared/BrowserView.tsx:166
- This interval reuses a
checkResizeclosure created before later visibility changes, so it can retain stalehideStyleBlockers/hiddenvalues. Toggling visibility can remove/show the native BrowserView while the registry keeps the old nonzero/zero rectangle, causing the approval bubble to be suppressed or drawn over a BrowserView. Recreate the interval for those inputs or publish the visibility change directly.
// The `p.hidden || hideStyleBlockers` branch above yields a zero rect, which
// the registry reads as "not covering" — that covers the removeBrowserView
// path below too, so it needs no hook of its own.
publishBrowserViewRect(viewKey.current, rect);
app/services/stream-avatar/kevin-support-service.ts:160
- The login check happens before an asynchronous token fetch. If the user logs out or switches accounts while
getToken()is pending, this continuation still builds a socket with the old JWT and assigns it to the service; a subsequent login can then reuse the previous account's connection. Invalidate the connect attempt and re-check the current session after the await before constructing the socket.
const token = await this.streamAvatarApiService.getToken();
const protocol = Utils.getAvatarEnvironment() === 'local' ? 'http://' : 'https://';
const url =
`${protocol}${this.hostsService.streamAvatarApi}${V2_NAMESPACE}` +
`?token=${token}&role=desktop&tv=${V2_TOOL_PROTOCOL_VERSION}`;
app/services/stream-avatar/kevin-support-service.ts:279
ready.activeRunIdsis provided specifically for reconnect resynchronization, but this handler only restorespendingApprovals. If a voice/plugin run is still active when Desktop reconnects,pendingremains false, so the chat hides its spinner and allows a new request even though the server still has an in-flight run. Restore the pending state from the active run IDs and keep it synchronized until those runs end.
// Replayed approvals: a prompt raised while we were reconnecting is
// still live server-side and must reappear here.
this.SET_APPROVALS(ready.pendingApprovals ?? []);
this.SET_CONNECTING(false);
this.SET_CONNECTED(true);
app/services/stream-avatar/kevin-support-service.ts:246
- For a server-initiated Socket.IO v2 disconnect (for example, an expired JWT), the client does not automatically reconnect. This handler only clears the connection and approvals, and no background path calls
connect()with a refreshed token, so Desktop stops receiving voice tool approvals until the chat is manually used again. Handle server disconnects by refreshing the token and reconnecting.
socket.on('disconnect', (reason: string) => {
this.log('--', 'disconnect', { reason });
this.SET_CONNECTED(false);
this.SET_CONNECTING(false);
this.SET_PENDING(false);
app/services/stream-avatar/v2/agent-tools.ts:387
- The external-open operation is asynchronous, but the handler returns success immediately. A rejected
openExternalpromise is therefore unhandled and the agent is told the ticket page opened even when the OS/browser failed to launch it; await the call soexecute()can return a failure.
support_open_ticket: async () => {
// @electron/remote, the way application-menu.ts opens external links.
require('@electron/remote').shell.openExternal(SUPPORT_TICKET_URL);
return { opened: true, url: SUPPORT_TICKET_URL };
app/themes.g.less:43
- This file is a
*.g.lessglobal/generated stylesheet, which the repository convention says not to edit directly; changes there can be overwritten by the style-generation pipeline. Add the variables in the authoritative source instead and regenerate this file as part of the build/update process.
--chat-bubble-user: @navy;
--chat-bubble-user-border: lighten(@dark-5, 4%);
- Files reviewed: 28/28 changed files
- Comments generated: 10
- Review effort level: Lite
Summary
Test plan
yarn typecheck(already run clean during development)yarn eslinton touched files (already run clean during development)