Skip to content

feat(sidekick): Gleap.openSidekick, an agent in a side panel next to the page - #193

Draft
boehlerlukas wants to merge 5 commits into
masterfrom
feat/sidekick
Draft

boehlerlukas wants to merge 5 commits into
masterfrom
feat/sidekick

Conversation

@boehlerlukas

@boehlerlukas boehlerlukas commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Sidekick mode: an agent in a side panel next to the page

Gleap.openSidekick() opens Kai Support or one of the workspace's customer agents in a full-height panel on the right edge (400 px), with the page's context. By default it pushes the page aside like Intercom's Fin side panel: the page stays visible and usable, no dim. Phones get a full-screen sheet. Agents can also reach out first with a message card above the launcher that opens the panel with their message in the thread. Plan: decision 13 of the Kai re-release (contract in .handover/kai-build/sidekick.md).

API

Gleap.openSidekick({ agentId, context, initialMessage, initialQuestion, mode: 'push' | 'overlay', newConversation });
Gleap.showSidekickMessage({ agentId, message, sender? }); // → boolean: the card above the launcher
Gleap.hideSidekickMessage();
Gleap.setSidekickContext({ title, step: { id, title, index, total }, data: { plan: 'Pro' } }); // a plain object counts as data
Gleap.closeSidekick();
Gleap.isSidekickOpen();
Gleap.setSidekickProactive(false); // no proactive cards or opens on this page
Gleap.on('sidekick-opened' | 'sidekick-closed' | 'sidekick-proactive' | 'sidekick-message-dismissed', cb);

All of it is additive: nothing changes for a page that never calls the sidekick and gets no sidekick trigger.

Layout

  • Push (default): a margin on <html> on the panel's side (never a transform), no backdrop, aria-modal="false", Escape inside a page field stays the page's. Fixed headers and footers shrink along: every position: fixed element spanning the viewport (left ≈ 0 and right ≈ the edge, or ≥ 90vw) gets inline left/width on the panel's side while pushed, slides with the page, and gets its previous inline values back exactly on close. A throttled MutationObserver handles fixed elements added (or turned fixed) while open; resizes re-measure. Gleap's own elements and the backdrop are skipped.
  • Overlay (dim layer that blocks the page, click closes): when asked (mode: 'overlay'), when the page would get narrower than 720 px, and by default on a page inside an iframe (mode: 'push' forces the push there). sidekick-opened says why (pushFallback: 'iframe').
  • Phones (≤ 600 px): the full-screen sheet, never a push; padded for the notch and home indicator, as tall as the visible viewport.
  • Closing gives the page back exactly: <html> inline styles, the fixed bars' inline styles, scroll position, focus, launcher. Other unread notifications hide while the panel is open and come back afterwards.

Message cards

  • A card in the existing notification stack (same chrome, z-index and position as the chat bubbles): agent avatar (or the bot's initial), up to 3 lines of the message, " · now", its own × button. Clicking it opens the sidekick with the message as the agent's first message; the agent continues from there.
  • × (or Escape on the card): gone, and the same card stays away for a day in every tab (gleap cache + BroadcastChannel removes it in open sibling tabs). A route change, the widget opening or another identity take it away without a dismissal. Not shown while the widget or the sidekick is open.
  • Built from DOM nodes (no markup from the message), the card is a real <button>, a polite live region announces it, reduced motion is respected. CSS goes through the SDK's existing injected stylesheet (CSP nonce); runtime styles only via CSSOM.
  • Default sender = the project's bot persona (operatorName / operatorAvatarImage), as the classic chat notification shows AI messages.

Proactive messages from the socket

The socket announces sidekick and the new sidekick-card. A sidekick message carries display: 'card' | 'open' (no display = open, the earlier protocol). Cards wait for nothing; opens keep the typing / hidden-tab waits. Both: at most one per page view, never over something else, quiet for a day after a dismissal, respect setSidekickProactive(false); cards also respect disabled in-app notifications. The server sends cards / opens only to sockets that announced them; every other client gets the classic in-app conversation instead (Server feat/redesign-rerelease).

Backwards compatible

startAgent and the AI bar are untouched; a Messenger build without the sidekick cap in its ping makes openSidekick (and a card click) fall back to startAgent.

Verified

  • npm test: 35 suites, 598 tests (GleapSidekickRules.test.js: envelope, push-risk decisions, card parsing, proactive guards). npm run build passes (build outputs not committed).
  • Browser, round 1 (Playwright, private stack): overlay and push open/close with the page restored exactly, Escape and backdrop close, SPA navigation updates the context, context reached Kai Support and a custom agent, proactive opens, phone sheet, light and dark. Screenshots: .handover/kai-build/shots/sidekick/.
  • Round 3 (10-27, built-in browser on the demo stack): push at 1440 px with the fixed header shrinking along and an exact restore (header + <html> inline style, scroll), a fixed bar added while open adjusted and restored, overlay at ~1000 px, a card on a 375/390 px phone opening the full-screen sheet with the message in the thread. Screenshots: .handover/kai-build/shots/sidekick/r3-*.

Not in this PR

No version bump or release. Needs GleapSDK/Messenger-App#268 deployed first (older Messenger builds fall back to startAgent) and the Server side (in feat/redesign-rerelease) for context, cards and proactive messages.

🤖 Generated with Claude Code

boehlerlukas and others added 5 commits October 25, 2026 04:53
…tive guards)

What the page tells the agent (origin + path only: query and fragment never
leave the page; title, onboarding step, the app's data, bounded like the
server's sanitiser) as one envelope with Kai Copilot (surface 'sidekick'),
overlay / push / sheet by viewport, and when a proactive open may show.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t to the page

The widget frame gets a 'sidekick' app mode: a full-height panel on the right
edge (400 px; a full-screen sheet on phones) over a dim layer that blocks the
page (overlay, default) or with the page pushed aside by a margin on <html>
(push). Closes by its x, Escape, a click on the dimmed page or closeSidekick();
the page comes back exactly (inline styles, scroll, focus, launcher). The page's
context follows SPA navigation. A messenger build without the sidekick falls
back to startAgent. API: openSidekick, setSidekickContext, closeSidekick,
isSidekickOpen, setSidekickProactive; events sidekick-opened/-closed/-proactive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The socket announces the 'sidekick' cap (the server sends proactive opens only
to SDKs that do) and hands 'sidekick' messages to the manager, which shows one
at most once per page view, never over something else, never while the person
types, and not again for a day after a dismissal. The page poll keeps the open
panel's context current.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- openSidekick pushes the page aside by default (no dim, page usable, aria-modal false,
  Escape in the page stays the page's). It overlays a dimmed page instead when asked
  (mode 'overlay'), when the page would get narrower than 720 px, or when a push could
  break it: a page in a frame, RTL, a margin on <html>, a 100vw layout or a body with its
  own width, a full-width fixed header/footer. A one-layout probe (margin applied, never
  painted, put back) decides; sidekick-opened says why (pushFallback). Phones keep the
  full-screen sheet, now clear of the notch and home indicator.
- Gleap.showSidekickMessage({ agentId, message, sender? }) / hideSidekickMessage(): a card
  in the notification stack (avatar, 3 lines, "<agent> · now", its own dismiss button).
  Clicking it opens the sidekick with the message as the agent's first message. Dismissed
  = gone for a day in every tab (BroadcastChannel + gleap cache); a route change, the
  widget opening or another identity take it away. Built with DOM nodes (no markup from
  the message), a real button, Escape dismisses, a polite live region announces it,
  reduced motion respected.
- Proactive socket messages carry display 'card' | 'open' (none = open, the old protocol);
  cards wait for nothing, respect setSidekickProactive(false) and disabled in-app
  notifications. The socket announces the new `sidekick-card` cap.
- Opening the sidekick no longer clears the other unread notifications; they hide while
  it is open and come back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hrink along

- Every position: fixed element that spans the viewport (left ≈ 0 and right ≈ the edge,
  or ≥ 90vw wide) gets inline left/width (!important) on the panel's side while pushed,
  slides with the page, and gets its previous inline values back exactly on close (an
  element without a style attribute gets none). A throttled MutationObserver adjusts fixed
  elements added (or turned fixed) while open; resizes re-measure. Gleap's own elements
  and the backdrop are skipped; transform-centered bars are left alone.
- The push probe and its fallbacks (fixed bar, 100vw, html margin, RTL) are gone. Overlay
  only when asked, when the page would get narrower than 720 px, and by default inside an
  iframe (mode 'push' forces it there). Phones keep the sheet. A right-to-left widget
  pushes from the left. <html>'s own margin is kept (ours adds to it).
- Pushed, the panel has a hairline edge instead of the overlay shadow.
- Sidekick card: in a collapsed notification stack only the stack's × shows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant