Skip to content

Add Kevin AI support chat to the studio footer - #6159

Open
AnkhHeart wants to merge 20 commits into
masterfrom
ankh/ai-chat-v3
Open

Add Kevin AI support chat to the studio footer#6159
AnkhHeart wants to merge 20 commits into
masterfrom
ankh/ai-chat-v3

Conversation

@AnkhHeart

Copy link
Copy Markdown
Contributor

Summary

  • Adds the Kevin AI support chat: a footer icon that opens a Streamlabs-API-backed support window (transcript, suggested prompts, inline markdown replies, tool approvals) replacing the chat tab that lived in the stream-avatar plugin.
  • Wires the chat onto the /v2 protocol, exposing OBS tools (scenes, sources, streaming, replay) and Desktop-side agent tools (mic filters, stream health, diagnostics, support tickets) for the agent to call.
  • Adds an interaction-quota meter and Ultra/Ultra+ upsell mirroring the Automations usage meter, including refusal-path fixes (stuck spinner, duplicate limit banner, silent re-refusal).
  • Restyles the chat surface to match the latest Figma: footer icon placement next to the notifications bell, new empty-state icon, updated suggested-prompt chips (distinct label vs. sent message), per-message agent avatar, a send-button icon, and bubble/color/spacing updates.

Test plan

  • yarn typecheck (already run clean during development)
  • yarn eslint on touched files (already run clean during development)
  • Manually open the studio footer, log in, and verify the Kevin icon sits directly left of the notifications bell
  • Send a message and confirm the transcript, agent avatar, and bubble styling render correctly in both day and night themes
  • Trigger a tool approval (via a voice request through the avatar plugin) and confirm it renders inline
  • Exhaust the interaction quota and confirm the upgrade modal appears instead of the chat silently failing

Copilot AI lite review requested due to automatic review settings September 4, 2026 16:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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 + AgentToolsService to 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.

Comment thread app/services/stream-avatar/kevin-support-service.ts
Comment thread app/components-react/agent/KevinSupport.tsx Outdated
Comment thread app/components-react/root/StudioFooter.m.less
Comment thread app/components-react/root/StudioFooter.tsx Outdated
Comment thread app/services/stream-avatar/kevin-support-service.ts
Comment thread app/services/stream-avatar/kevin-support-service.ts
Comment thread app/services/stream-avatar/v2/protocol.ts Outdated
@bundlemon

bundlemon Bot commented Sep 4, 2026

Copy link
Copy Markdown

BundleMon

Files updated (1)
Status Path Size Limits
renderer.(hash).js
10.61MB (+73.61KB +0.68%) -
Unchanged files (3)
Status Path Size Limits
vendors~renderer.(hash).js
4.67MB -
updater.js
115.29KB -
guest-api.js
40.23KB -

Total files change +73.61KB +0.47%

Final result: ✅

View report in BundleMon website ➡️


Current branch size history | Target branch size history

Copilot AI review requested due to automatic review settings September 4, 2026 17:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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 set type="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-icon style 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:error currently only surfaces an error message for auth failures. For other errors (e.g. protocol/internal), the UI clears pending but 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

Comment thread app/components-react/agent/KevinSupport.tsx Outdated
Copilot AI review requested due to automatic review settings September 4, 2026 19:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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 rateLimitRefusals is 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.error with a generic message. If the server already provided a specific error via v2: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

Comment thread app/services/stream-avatar/kevin-support-service.ts
@michelinewu michelinewu added the needs QA requires QA testing on the branch before merging label Sep 8, 2026
Copilot AI review requested due to automatic review settings September 9, 2026 17:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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

Comment thread app/services/stream-avatar/kevin-support-service.ts
Copilot AI review requested due to automatic review settings September 9, 2026 18:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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

Comment thread app/services/stream-avatar/v2/agent-tools.ts
Copilot AI review requested due to automatic review settings September 9, 2026 18:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 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
  • isFocused is only updated via focus/blur events; if the window is shown focused but Electron doesn't emit an initial focus (or the app is inactive on show), WindowsService.state[windowId].isFocused can remain undefined and 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 on show to avoid a persistent undefined focus state.
  • Files reviewed: 23/23 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings September 9, 2026 19:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 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.left with 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

Comment thread app/components-react/agent/KevinSupport.m.less Outdated
Comment thread app/components-react/agent/support-limits.tsx Outdated
Comment thread app/components-react/shared/icons/KevinChatIcon.tsx
Comment thread app/components-react/shared/icons/SendIcon.tsx
Comment thread app/services/windows.ts Outdated
Copilot AI review requested due to automatic review settings September 10, 2026 15:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 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

Copilot AI review requested due to automatic review settings September 10, 2026 16:59
Copilot AI review requested due to automatic review settings September 10, 2026 19:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 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

Copilot AI review requested due to automatic review settings September 11, 2026 15:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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 rateLimitRefusals after 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 activeRunIds specifically so a reconnecting client can resync live runs, but this handler only restores approvals. The disconnect handler has already set pending to 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.timeoutMs explicitly requires the client to abandon a call past the deadline, but this path awaits execute() 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 with invoke.timeoutMs and suppress or cancel the late result.
    app/services/stream-avatar/v2/agent-tools.ts:248
  • toggleStreaming() is asynchronous and can wait for the user's stop confirmation, but calling it through .actions uses 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-visible replacement, 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 expiresAt is a client-side dismissal deadline, but these cards remain rendered until a v2:approval.resolved event 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 when expiresAt is reached.
                {pendingApprovals.map(approval => (
                  <div
                    key={approval.approvalId}

app/components-react/agent/KevinSupport.tsx:150

  • This clickable span is 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 tier but only reruns when rateLimitRefusals changes. 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); include tier in the dependency list.
  useEffect(() => {
    if (rateLimitRefusals > 0) promptUpgrade(tier);
  }, [rateLimitRefusals]);

app/i18n/en-US/ai.json:36

  • Use the phrasal verb set up here; setup is 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 up here; setup is 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 for v2: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

Comment thread app/services/stream-avatar/kevin-support-service.ts
Comment thread app/services/stream-avatar/kevin-support-service.ts
Comment thread app/components-react/agent/KevinApprovalBubble.tsx
Comment thread app/components-react/shared/BrowserView.tsx
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.
Copilot AI review requested due to automatic review settings September 11, 2026 16:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 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

  • pending is set only inside sendMessage() after it awaits connection, while this handler neither awaits nor locally locks the action. During the initial connection, a second click or Enter still sees pending === false and 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 checkResize closure created before later visibility changes, so it can retain stale hideStyleBlockers/hidden values. 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.activeRunIds is provided specifically for reconnect resynchronization, but this handler only restores pendingApprovals. If a voice/plugin run is still active when Desktop reconnects, pending remains 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 openExternal promise is therefore unhandled and the agent is told the ticket page opened even when the OS/browser failed to launch it; await the call so execute() 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.less global/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

Comment thread app/services/stream-avatar/kevin-support-service.ts
Comment thread app/services/stream-avatar/kevin-support-service.ts
Comment thread app/services/stream-avatar/v2/agent-tools.ts
Comment thread app/components-react/agent/KevinSupport.m.less
Comment thread app/components-react/agent/KevinSupport.tsx
Comment thread app/components-react/agent/KevinSupport.tsx
Comment thread app/services/stream-avatar/kevin-support-service.ts
Comment thread app/services/stream-avatar/kevin-support-service.ts
Comment thread app/services/stream-avatar/v2/agent-tools.ts
Comment thread app/services/stream-avatar/v2/agent-tools.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs QA requires QA testing on the branch before merging

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants