Skip to content

feat: expandable pills and tool workflow styling from Ozwell widget - #294

Open
aditya-damerla128 wants to merge 13 commits into
mieweb:mainfrom
aditya-damerla128:feat/ai-pill-components
Open

feat: expandable pills and tool workflow styling from Ozwell widget#294
aditya-damerla128 wants to merge 13 commits into
mieweb:mainfrom
aditya-damerla128:feat/ai-pill-components

Conversation

@aditya-damerla128

@aditya-damerla128 aditya-damerla128 commented Jul 1, 2026

Copy link
Copy Markdown

Closes #290.

Brings the Ozwell chat widget's pill patterns into the UI package as themeable components.

What's added

  • PillSelect (src/components/PillSelect/) — the expandable option selector (the reasoning-mode capsule that expands into a segmented control). Collapsed pill → click → options → pick → collapses back. Any number of options, fully themeable via design tokens. Closes on click-outside / Escape.
pillselect-collapsed pillselect-expanded
  • ThinkingBlock (in AIMessage) — reasoning pill showing "Thinking" with a pulsing dot while streaming, then "Thought for Xs" once done. Click to expand/collapse the reasoning text.
thinking-active thinking-complete thinking-expanded
  • MCPToolCall — restyled as a status pill header + a capped-width detail box:
    • Friendly tool names with underscores stripped and tense shifted (create_patient → "Creating patient" → "Created patient").
    • Green (success) / red (error) color scheme kept the same as before.
    • Result summary and resource links always visible in the box; raw parameters toggle open/closed with a smooth animation.
  • CollapsiblePill (src/components/AI/CollapsiblePill.tsx) — shared primitive behind the thinking + tool pills.
tool-error tool-running tool-success

Checks

  • Typecheck, lint, prettier: clean
  • Tests: 15/15 pass
  • Storybook stories added/updated for all new states

All components use mieweb/ui design tokens (light + dark mode).

Copilot AI lite review requested due to automatic review settings July 1, 2026 21:07

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.

Pull request overview

This PR brings Ozwell-style “pill” UI patterns into @mieweb/ui by introducing a reusable collapsible pill primitive, a new expandable PillSelect component, and updating AI message/tool-call UI to use pill-based presentation consistent with the widget.

Changes:

  • Added PillSelect component (with Storybook stories) and exported it from the package.
  • Introduced CollapsiblePill primitive and exported it from the AI barrel.
  • Restyled MCPToolCallDisplay and replaced the AI “thinking” block UI with a pill-based ThinkingBlock.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/index.ts Exports the new PillSelect component from the package entrypoint.
src/components/PillSelect/PillSelect.tsx Adds the expandable pill selector component with click-outside/Escape collapse behavior.
src/components/PillSelect/PillSelect.stories.tsx Adds Storybook coverage for default/controlled/disabled/many-options states.
src/components/PillSelect/index.ts Adds the barrel export for PillSelect types + component.
src/components/AI/MCPToolCall.tsx Refactors tool-call UI into a status pill header + details box with animated parameter reveal.
src/components/AI/index.ts Exports the new CollapsiblePill primitive from the AI module.
src/components/AI/CollapsiblePill.tsx Introduces a shared collapsible pill primitive for thinking/tool displays.
src/components/AI/AIMessage.tsx Adds the new ThinkingBlock pill behavior and swaps out the old reasoning block UI.
src/components/AI/AIMessage.stories.tsx Updates stories to cover thinking pill states (active/complete/expanded).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/components/AI/MCPToolCall.tsx Outdated
Comment thread src/components/AI/CollapsiblePill.tsx
Comment thread src/components/PillSelect/PillSelect.tsx Outdated
Comment thread src/components/PillSelect/PillSelect.tsx Outdated
Comment thread src/components/AI/CollapsiblePill.tsx Outdated
Comment thread src/components/AI/MCPToolCall.tsx Outdated
Comment thread src/components/AI/AIMessage.tsx
@aditya-damerla128
aditya-damerla128 force-pushed the feat/ai-pill-components branch from 23736c9 to 45407e5 Compare July 2, 2026 19:41
Copilot AI review requested due to automatic review settings July 16, 2026 21:46
@aditya-damerla128
aditya-damerla128 force-pushed the feat/ai-pill-components branch from 45407e5 to be25404 Compare July 16, 2026 21:46

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.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Comment thread src/components/AI/CollapsiblePill.tsx
Comment thread src/components/AI/MCPToolCall.tsx Outdated
Comment thread src/components/AI/MCPToolCall.tsx
Comment thread src/components/AI/MCPToolCall.stories.tsx
Copilot AI review requested due to automatic review settings July 17, 2026 18:58

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.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.

Comment thread src/components/AI/CollapsiblePill.tsx
Comment thread src/tailwind-preset.ts
Comment thread src/components/PillSelect/PillSelect.tsx
Comment thread src/components/PillSelect/PillSelect.tsx Outdated
Comment thread src/components/PillSelect/PillSelect.tsx Outdated
Comment thread src/components/AI/MCPToolCall.tsx Outdated
Copilot AI review requested due to automatic review settings July 17, 2026 19:25

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.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Comment thread src/components/PillSelect/PillSelect.tsx Outdated
Copilot AI review requested due to automatic review settings July 20, 2026 15:15

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.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Comment thread src/components/PillSelect/PillSelect.tsx Outdated
Comment thread src/components/PillSelect/PillSelect.tsx Outdated
Comment thread src/components/AI/CollapsiblePill.tsx
Copilot AI review requested due to automatic review settings July 20, 2026 19:56

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.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

src/components/PillSelect/PillSelect.tsx:49

  • When the collapsed trigger is clicked, the focused <button> is unmounted and replaced by the expanded option buttons. Without moving focus, keyboard users can lose focus (often ending up on document.body) and have to tab around to find the options. Add a small effect to focus the selected (or first enabled) option when expanding.
  useClickOutside(ref, () => setExpanded(false), expanded);
  useEscapeKey(() => setExpanded(false), expanded);

Comment thread src/components/AI/MCPToolCall.tsx
Copilot AI review requested due to automatic review settings July 20, 2026 20:52

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.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Comment thread src/tailwind-preset.ts
Comment thread src/components/AI/AIMessage.tsx
Comment thread src/components/AI/AIMessage.tsx Outdated
Copilot AI review requested due to automatic review settings July 20, 2026 21:15
@aditya-damerla128
aditya-damerla128 force-pushed the feat/ai-pill-components branch from 4ec868f to d7f5718 Compare July 20, 2026 21:15

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.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

src/components/PillSelect/PillSelect.tsx:50

  • When the collapsed trigger button is activated, it’s immediately removed from the DOM and replaced with the expanded button list. This causes focus to be lost (especially for keyboard users), making the control hard to use without a mouse. Consider moving focus into the expanded options on open, and restoring focus to the collapsed trigger on close (without focusing anything on initial mount).
  const [expanded, setExpanded] = React.useState(false);
  const ref = React.useRef<HTMLDivElement>(null);

  const value = controlledValue !== undefined ? controlledValue : internalValue;
  const selectedOption = options.find((o) => o.value === value);

  useClickOutside(ref, () => setExpanded(false), expanded);
  useEscapeKey(() => setExpanded(false), expanded);

Comment thread src/components/AI/CollapsiblePill.tsx
Copilot AI review requested due to automatic review settings July 20, 2026 21:24

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.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/components/PillSelect/PillSelect.tsx:73

  • When expanded flips from false to true, the collapsed trigger <button> unmounts and is replaced by the option buttons. This typically causes focus to be lost (focus was on a DOM node that no longer exists), which is a keyboard accessibility issue. Consider explicitly moving focus into the expanded option set when opening, and restoring focus to the collapsed trigger when closing (Escape/selection), similar to how Select returns focus to its trigger.
  return (
    <div ref={ref} className={cn('inline-flex', className)}>
      {expanded ? (
        <div
          role="group"
          aria-label={label ? `${label} options` : 'Options'}
          className="border-border bg-muted inline-flex items-center rounded-full border p-0.5"
        >

src/components/AI/MCPToolCall.tsx:673

  • defaultCollapsed is documented as status-based (completed tool calls start expanded), but isCompleted currently excludes cancelled, so cancelled tool calls will default to collapsed even though they are also terminal/completed states. This creates a mismatch between the prop documentation and runtime behavior.
  const hasParams = showParameters && toolCall.parameters.length > 0;
  const hasBoxContent = Boolean(toolCall.result || toolCall.error || hasParams);
  const isCompleted =
    toolCall.status === 'success' || toolCall.status === 'error';
  const effectiveDefaultCollapsed = defaultCollapsed ?? !isCompleted;

@garrity-miepub

Copy link
Copy Markdown
Collaborator

@aditya-damerla128 @aaronc-mie

Is there an example of where PillSelect will be used? It's exported but nothing in the repo consumes it yet, so it's hard to evaluate the UX in context.

My concern is content shift: expanding in-place grows the element from ~113px to ~481px wide (with the ManyOptions story), which reflows everything around it. In a chat composer or toolbar that jump would be pretty disruptive.

I'd like to see this take a dropdown approach instead: keep the pill as the trigger, but open the options in an anchored popover so the surrounding content stays put. The library already has floating primitives this could build on, so it should be a natural fit.

Copilot AI review requested due to automatic review settings August 6, 2026 12:26
@aditya-damerla128

Copy link
Copy Markdown
Author

Hey @garrity-miepub, i implemented your suggestion. lmk if this aligns closer to your understanding of it
image

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.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/components/PillSelect/PillSelect.tsx:82

  • left is clamped using menuWidth, which can make the computed left position negative when the trigger (or preferred menu width) is wider than the viewport. In that case window.innerWidth - menuWidth - viewportPadding becomes < 0, Math.min(...) picks the negative value, and the menu can render off-screen. Clamp using the actual rendered width (respecting maxWidth) and ensure the max-left bound is never smaller than viewportPadding.
    const menuWidth = Math.max(rect.width, 160);
    const maxWidth = Math.max(window.innerWidth - viewportPadding * 2, 0);
    const left = Math.min(
      Math.max(rect.left, viewportPadding),
      window.innerWidth - menuWidth - viewportPadding

Copilot AI review requested due to automatic review settings August 6, 2026 12:30
@aditya-damerla128
aditya-damerla128 force-pushed the feat/ai-pill-components branch from 8dffc3b to 7d4bd11 Compare August 6, 2026 12:30

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.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (3)

src/components/AI/CollapsiblePill.tsx:13

  • defaultOpen is documented as a “default” value, but the component synchronizes isOpen whenever defaultOpen changes. In this repo, defaultOpen typically means “initial uncontrolled state only” (see components/Collapsible, which separates open vs defaultOpen). At minimum, the prop should be documented as state-syncing to avoid surprising consumers.
  density?: 'standard' | 'condensed';
  defaultOpen?: boolean;
  pillClassName?: string;
  className?: string;
  /** Native tooltip shown on hover (e.g. "Show details") */
  title?: string;

src/components/PillSelect/PillSelect.tsx:51

  • Uncontrolled internalValue is only initialized from options[0] on the first render. If options is empty initially (common with async data) and later becomes non-empty, the component keeps value === '' (or a value no longer present in options), which makes the collapsed label ambiguous and can leave the selector in an invalid state. Consider syncing the uncontrolled value when options changes so it always points at an available option (without overriding a still-valid user selection).
  const [internalValue, setInternalValue] = React.useState(
    defaultValue ?? options[0]?.value ?? ''
  );
  const [expanded, setExpanded] = React.useState(false);
  const triggerRef = React.useRef<HTMLButtonElement>(null);
  const menuRef = React.useRef<HTMLDivElement>(null);
  const selectedOptionRef = React.useRef<HTMLButtonElement>(null);
  const firstEnabledOptionRef = React.useRef<HTMLButtonElement>(null);
  const listboxId = React.useId();
  const [menuStyle, setMenuStyle] = React.useState<React.CSSProperties>({});

  const value = controlledValue !== undefined ? controlledValue : internalValue;
  const selectedOption = options.find((o) => o.value === value);

src/components/PillSelect/PillSelect.test.tsx:63

  • This test asserts the option is disabled but never attempts to click it, so it doesn’t actually verify the “does not select disabled options” behavior (i.e. that clicks don’t call onValueChange / don’t change the trigger label).
    await user.click(screen.getByRole('button', { name: /size: small/i }));
    expect(screen.getByRole('option', { name: /medium/i })).toBeDisabled();
    expect(onValueChange).not.toHaveBeenCalled();
  });

@garrity-miepub garrity-miepub left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This update looks fantastic! Thank you for reworking it! I think this will make the component reusable in many different contexts.

@aditya-damerla128

Copy link
Copy Markdown
Author

@garrity-miepub perfect, so i will rebase it and it should be good to merge after that

aditya-damerla128 and others added 13 commits August 7, 2026 10:15
Addresses mieweb#290. Brings the Ozwell chat widget's pill patterns into the
UI package as themeable components:

- PillSelect: collapsible option selector (the reasoning-mode capsule ->
  segmented control). Any number of options, fully themeable via tokens.
- ThinkingBlock: reasoning pill that shows "Thinking" with a pulsing dot
  while streaming, then "Thought for Xs" when done. Click to expand/collapse.
- MCPToolCall: status pill header (green success / red error kept the same)
  with friendly tool names (underscores stripped, tense-shifted), result
  and resource links in a capped-width box, and toggleable raw params with
  a smooth expand/collapse animation.
- CollapsiblePill: shared primitive behind the thinking + tool pills.
- MCPToolCall: default shows only the status pill; clicking it reveals the
  whole box (result, links, raw params) in one click with a "Show details"
  tooltip. Input summary now rides in the pill while running
  ("Creating patient · John Smith"); success shows duration instead.
- Add `hidden` prop to turn the tool-call display off entirely (mirrors the
  Ozwell widget debug flag).
- tailwind-preset: safelist the AI pill arbitrary utilities, the violet
  thinking-pill classes, and the green/red status pill colors so Tailwind 3
  consumers get them.
- CollapsiblePill: don't mutate internal state when controlled; add optional
  title (tooltip).
- PillSelect: no trailing colon when empty; disable the pill when there are
  no options.
- Stories: add PillOnly and Hidden; refresh control docs.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 7, 2026 14:41
@aditya-damerla128
aditya-damerla128 force-pushed the feat/ai-pill-components branch from 7d4bd11 to 8f154a6 Compare August 7, 2026 14:41

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.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/components/PillSelect/PillSelect.tsx:80

  • Menu positioning can compute a negative left when menuWidth exceeds the available viewport width. left is clamped using menuWidth, but CSS later applies maxWidth, so the actual rendered width can be smaller than the value used for clamping, causing the popup to shift off-screen on narrow viewports.

Clamp using the effective width (min(menuWidth, maxWidth)) and also clamp minWidth so it never exceeds maxWidth.

    const maxWidth = Math.max(window.innerWidth - viewportPadding * 2, 0);
    const left = Math.min(
      Math.max(rect.left, viewportPadding),
      window.innerWidth - menuWidth - viewportPadding
    );

src/components/PillSelect/PillSelect.tsx:172

  • PillSelect exposes role="listbox"/role="option" semantics, but it doesn't implement expected listbox keyboard interactions (ArrowUp/ArrowDown/Home/End navigation). Without this, assistive tech users can get a listbox that only works via Tab, which is inconsistent with the ARIA listbox pattern.

Either remove the listbox roles and treat these as plain buttons, or add minimal arrow-key navigation that moves focus among enabled options.

          <div
            ref={menuRef}
            id={listboxId}
            role="listbox"
            aria-label={label ? `${label} options` : 'Options'}

@aditya-damerla128

Copy link
Copy Markdown
Author

@garrity-miepub i have rebased it as well, the only thing it requires is 2 approving reviewers, and i do not have access to add another reviewer, so if you can add someone to review we can move towards merging this in

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.

Adding expand pills and tool workflows from Ozwell Chat widget

3 participants