Repository: assistant-ui/tool-ui Branch: main Commit: e3f90d71c36e Files: 779 Total size: 4.2 MB Directory structure: gitextract_06635t45/ ├── .changeset/ │ └── config.json ├── .git-blame-ignore-revs ├── .githooks/ │ ├── pre-commit │ └── pre-push ├── .github/ │ ├── pull_request_template.md │ └── workflows/ │ ├── changeset.yaml │ ├── ci.yml │ └── npm-publish.yaml ├── .gitignore ├── .prettierignore ├── AGENTS.md ├── AGENT_CHANGELOG.md ├── CLAUDE.md ├── LICENSE.md ├── README.md ├── apps/ │ └── www/ │ ├── .oxfmtrc.jsonc │ ├── .oxlintrc.json │ ├── .prettierignore │ ├── app/ │ │ ├── api/ │ │ │ ├── builder/ │ │ │ │ ├── chat/ │ │ │ │ │ └── route.ts │ │ │ │ └── create-freestyle/ │ │ │ │ └── route.ts │ │ │ ├── chat/ │ │ │ │ └── route.ts │ │ │ ├── mcp-tools/ │ │ │ │ └── route.ts │ │ │ ├── playground/ │ │ │ │ └── chat/ │ │ │ │ └── route.ts │ │ │ └── weather-tuning/ │ │ │ ├── _lib/ │ │ │ │ └── tuned-presets-io.ts │ │ │ ├── apply/ │ │ │ │ └── route.ts │ │ │ └── recover/ │ │ │ └── route.ts │ │ ├── builder/ │ │ │ ├── layout.tsx │ │ │ ├── opengraph-image.tsx │ │ │ └── page.tsx │ │ ├── components/ │ │ │ ├── analytics/ │ │ │ │ └── posthog-init.client.tsx │ │ │ ├── assistant-ui/ │ │ │ │ ├── markdown-text.tsx │ │ │ │ ├── thread-list.tsx │ │ │ │ ├── tool-fallback.tsx │ │ │ │ └── tooltip-icon-button.tsx │ │ │ ├── builder/ │ │ │ │ ├── mcp-icon.tsx │ │ │ │ ├── theme-toggle.tsx │ │ │ │ ├── webview-actions.ts │ │ │ │ └── webview.tsx │ │ │ ├── home/ │ │ │ │ ├── chat-showcase.tsx │ │ │ │ ├── demo-chat.tsx │ │ │ │ ├── faux-chat-shell-animated.tsx │ │ │ │ ├── faux-chat-shell-mobile-animated.tsx │ │ │ │ ├── faux-chat-shell-mobile.tsx │ │ │ │ ├── faux-chat-shell.tsx │ │ │ │ ├── home-background.tsx │ │ │ │ ├── home-hero.tsx │ │ │ │ ├── home-hexnut-scene.tsx │ │ │ │ └── noise-texture.ts │ │ │ ├── layout/ │ │ │ │ ├── app-header.server.tsx │ │ │ │ ├── app-shell-animated.client.tsx │ │ │ │ ├── app-shell.tsx │ │ │ │ ├── header-active-link.client.tsx │ │ │ │ ├── mobile-nav-sheet-gate.client.tsx │ │ │ │ ├── mobile-nav-sheet.client.tsx │ │ │ │ ├── page-shell.tsx │ │ │ │ └── tracked-external-anchor.client.tsx │ │ │ ├── mdx/ │ │ │ │ ├── features.tsx │ │ │ │ └── mermaid.tsx │ │ │ ├── theme/ │ │ │ │ └── theme-provider.tsx │ │ │ └── visuals/ │ │ │ └── spinning-hexnut/ │ │ │ └── hexnut-scene.tsx │ │ ├── docs/ │ │ │ ├── _components/ │ │ │ │ ├── chat-context-preview.tsx │ │ │ │ ├── collaboration-diagram.tsx │ │ │ │ ├── component-docs-tabs.tsx │ │ │ │ ├── component-preview-shell.tsx │ │ │ │ ├── component-preview.tsx │ │ │ │ ├── component-previews/ │ │ │ │ │ └── social-post-preview.tsx │ │ │ │ ├── copy-markdown-button.tsx │ │ │ │ ├── docs-article.tsx │ │ │ │ ├── docs-bordered-shell.tsx │ │ │ │ ├── docs-content.tsx │ │ │ │ ├── docs-header.tsx │ │ │ │ ├── docs-nav.tsx │ │ │ │ ├── docs-pager.tsx │ │ │ │ ├── docs-pages.ts │ │ │ │ ├── docs-search-shortcut.ts │ │ │ │ ├── docs-search.client.tsx │ │ │ │ ├── docs-toc-context.tsx │ │ │ │ ├── docs-toc-wrapper.tsx │ │ │ │ ├── docs-toc.tsx │ │ │ │ ├── gallery-analytics.client.tsx │ │ │ │ ├── gallery-card-header.tsx │ │ │ │ ├── gallery-docs-link.tsx │ │ │ │ ├── header-preview-tabs.tsx │ │ │ │ ├── install-command-block.tsx │ │ │ │ ├── install-command-line.tsx │ │ │ │ ├── interactive-option-demo.tsx │ │ │ │ ├── mdx-to-markdown.ts │ │ │ │ ├── mock-thread.tsx │ │ │ │ ├── preset-example.tsx │ │ │ │ ├── preset-selector.tsx │ │ │ │ └── tracked-dynamic-codeblock.tsx │ │ │ ├── actions/ │ │ │ │ ├── actions-examples.tsx │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── advanced/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── agent-skills/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── approval-card/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── audio/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── changelog/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── chart/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── citation/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── code-block/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── code-diff/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── data-table/ │ │ │ │ ├── content.mdx │ │ │ │ ├── formatting-gallery.tsx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── tasks-demo.tsx │ │ │ ├── design-guidelines/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── gallery/ │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── geo-map/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── image/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── image-gallery/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── instagram-post/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── item-carousel/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── link-preview/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── linkedin-post/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── message-draft/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── option-list/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── order-summary/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── overview/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ ├── parameter-slider/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── plan/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── preferences-panel/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── progress-tracker/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── question-flow/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── quick-start/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── receipts/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── stats-display/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── terminal/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── video/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── weather-widget/ │ │ │ │ ├── content.mdx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ └── x-post/ │ │ │ ├── content.mdx │ │ │ ├── opengraph-image.tsx │ │ │ └── page.tsx │ │ ├── global-error.tsx │ │ ├── layout.tsx │ │ ├── not-found.tsx │ │ ├── opengraph-image.tsx │ │ ├── page.tsx │ │ ├── playground/ │ │ │ ├── chat-pane.tsx │ │ │ ├── chat-ui.tsx │ │ │ ├── opengraph-image.tsx │ │ │ ├── page.tsx │ │ │ ├── tool-inspector.tsx │ │ │ └── waymo-demo/ │ │ │ ├── opengraph-image.tsx │ │ │ └── page.tsx │ │ ├── sandbox/ │ │ │ ├── celestial-effect/ │ │ │ │ ├── celestial-canvas.tsx │ │ │ │ └── page.tsx │ │ │ ├── cloud-effect/ │ │ │ │ ├── cloud-canvas.tsx │ │ │ │ └── page.tsx │ │ │ ├── lightning-effect/ │ │ │ │ ├── lightning-canvas.tsx │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ ├── rain-effect/ │ │ │ │ ├── page.tsx │ │ │ │ └── rain-canvas.tsx │ │ │ ├── snow-effect/ │ │ │ │ ├── page.tsx │ │ │ │ └── snow-canvas.tsx │ │ │ ├── weather-compositor/ │ │ │ │ ├── celestial-canvas.tsx │ │ │ │ ├── interpolation.ts │ │ │ │ ├── page.tsx │ │ │ │ ├── presets.ts │ │ │ │ └── tuned-presets.json │ │ │ ├── weather-effects/ │ │ │ │ └── page.tsx │ │ │ ├── weather-tuning/ │ │ │ │ ├── components/ │ │ │ │ │ ├── checkpoint-dots.tsx │ │ │ │ │ ├── condition-sidebar.tsx │ │ │ │ │ ├── detail-editor.tsx │ │ │ │ │ ├── export-panel.tsx │ │ │ │ │ ├── glass-controls.tsx │ │ │ │ │ ├── parameter-definitions.ts │ │ │ │ │ ├── parameter-matrix-view.tsx │ │ │ │ │ ├── parameter-panel.tsx │ │ │ │ │ ├── parameter-row.tsx │ │ │ │ │ ├── time-dial.tsx │ │ │ │ │ ├── time-matrix-view.tsx │ │ │ │ │ ├── view-mode-toggle.tsx │ │ │ │ │ └── weather-data-overlay.tsx │ │ │ │ ├── hooks/ │ │ │ │ │ └── use-tuning-state.ts │ │ │ │ ├── lib/ │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── has-any-tuning-delta.ts │ │ │ │ │ ├── list-updated-params.ts │ │ │ │ │ ├── map-to-canvas-props.ts │ │ │ │ │ ├── recover-repo-overrides.ts │ │ │ │ │ ├── resolve-params.ts │ │ │ │ │ ├── studio-timestamp.ts │ │ │ │ │ ├── tool-ui-export.ts │ │ │ │ │ ├── tool-ui-import.ts │ │ │ │ │ └── workflow-state.ts │ │ │ │ ├── page.tsx │ │ │ │ └── types.ts │ │ │ ├── weather-widget/ │ │ │ │ └── page.tsx │ │ │ ├── weather-widget-production/ │ │ │ │ ├── page.tsx │ │ │ │ └── runtime-input.ts │ │ │ └── weather-widget-stress/ │ │ │ └── page.tsx │ │ ├── staging/ │ │ │ ├── _components/ │ │ │ │ ├── rounded-rect-overlay.tsx │ │ │ │ ├── staging-canvas.tsx │ │ │ │ ├── staging-showcase.tsx │ │ │ │ ├── staging-toolbar.tsx │ │ │ │ ├── use-keyboard-shortcuts.ts │ │ │ │ └── use-staging-state.ts │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ └── styles/ │ │ ├── builder-loader.css │ │ ├── custom-utilities.css │ │ ├── fumadocs-overrides.css │ │ ├── globals.css │ │ ├── leaflet-overrides.css │ │ ├── nav-sheet.css │ │ ├── prose.css │ │ ├── shadcn-theme.css │ │ ├── squircle.css │ │ └── theme-transition.css │ ├── components/ │ │ ├── assistant-ui/ │ │ │ ├── assistant-modal.tsx │ │ │ ├── attachment.tsx │ │ │ ├── markdown-text.tsx │ │ │ ├── reasoning.tsx │ │ │ ├── shiki-highlighter.tsx │ │ │ ├── thread-list.tsx │ │ │ ├── thread.tsx │ │ │ ├── threadlist-sidebar.tsx │ │ │ ├── tool-fallback.tsx │ │ │ └── tooltip-icon-button.tsx │ │ ├── tool-ui/ │ │ │ ├── approval-card/ │ │ │ │ ├── README.md │ │ │ │ ├── _adapter.tsx │ │ │ │ ├── approval-card.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── schema.ts │ │ │ ├── audio/ │ │ │ │ ├── README.md │ │ │ │ ├── _adapter.tsx │ │ │ │ ├── audio.tsx │ │ │ │ ├── context.tsx │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── chart/ │ │ │ │ ├── README.md │ │ │ │ ├── _adapter.tsx │ │ │ │ ├── chart.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── schema.ts │ │ │ ├── citation/ │ │ │ │ ├── README.md │ │ │ │ ├── _adapter.tsx │ │ │ │ ├── citation-list.tsx │ │ │ │ ├── citation.tsx │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── code-block/ │ │ │ │ ├── README.md │ │ │ │ ├── _adapter.tsx │ │ │ │ ├── code-block.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── schema.ts │ │ │ ├── code-diff/ │ │ │ │ ├── _adapter.tsx │ │ │ │ ├── code-diff.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── schema.ts │ │ │ ├── data-table/ │ │ │ │ ├── README.md │ │ │ │ ├── _adapter.tsx │ │ │ │ ├── data-table.tsx │ │ │ │ ├── formatters.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── schema.ts │ │ │ │ ├── types.ts │ │ │ │ └── utilities.ts │ │ │ ├── geo-map/ │ │ │ │ ├── README.md │ │ │ │ ├── _adapter.tsx │ │ │ │ ├── geo-map-engine.tsx │ │ │ │ ├── geo-map-icons.ts │ │ │ │ ├── geo-map-overlays.tsx │ │ │ │ ├── geo-map-theme.module.css │ │ │ │ ├── geo-map.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── schema.ts │ │ │ ├── image/ │ │ │ │ ├── README.md │ │ │ │ ├── _adapter.tsx │ │ │ │ ├── image.tsx │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── image-gallery/ │ │ │ │ ├── README.md │ │ │ │ ├── _adapter.tsx │ │ │ │ ├── context.tsx │ │ │ │ ├── gallery-grid.tsx │ │ │ │ ├── gallery-lightbox.tsx │ │ │ │ ├── image-gallery.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── schema.ts │ │ │ │ └── styles.css │ │ │ ├── index.ts │ │ │ ├── instagram-post/ │ │ │ │ ├── README.md │ │ │ │ ├── _adapter.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── instagram-post.tsx │ │ │ │ └── schema.ts │ │ │ ├── item-carousel/ │ │ │ │ ├── README.md │ │ │ │ ├── _adapter.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── item-card.tsx │ │ │ │ ├── item-carousel.tsx │ │ │ │ └── schema.ts │ │ │ ├── link-preview/ │ │ │ │ ├── README.md │ │ │ │ ├── _adapter.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── link-preview.tsx │ │ │ │ └── schema.ts │ │ │ ├── linkedin-post/ │ │ │ │ ├── README.md │ │ │ │ ├── _adapter.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── linkedin-post.tsx │ │ │ │ └── schema.ts │ │ │ ├── message-draft/ │ │ │ │ ├── README.md │ │ │ │ ├── _adapter.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── message-draft.tsx │ │ │ │ └── schema.ts │ │ │ ├── option-list/ │ │ │ │ ├── README.md │ │ │ │ ├── _adapter.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── option-list.tsx │ │ │ │ ├── schema.ts │ │ │ │ └── selection.ts │ │ │ ├── order-summary/ │ │ │ │ ├── README.md │ │ │ │ ├── _adapter.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── order-summary.tsx │ │ │ │ └── schema.ts │ │ │ ├── parameter-slider/ │ │ │ │ ├── README.md │ │ │ │ ├── _adapter.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── math.ts │ │ │ │ ├── parameter-slider.tsx │ │ │ │ └── schema.ts │ │ │ ├── plan/ │ │ │ │ ├── README.md │ │ │ │ ├── _adapter.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── plan.tsx │ │ │ │ ├── progress.ts │ │ │ │ └── schema.ts │ │ │ ├── preferences-panel/ │ │ │ │ ├── README.md │ │ │ │ ├── _adapter.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── preferences-panel.tsx │ │ │ │ ├── schema.ts │ │ │ │ └── signature.ts │ │ │ ├── progress-tracker/ │ │ │ │ ├── README.md │ │ │ │ ├── _adapter.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── progress-tracker.tsx │ │ │ │ └── schema.ts │ │ │ ├── question-flow/ │ │ │ │ ├── README.md │ │ │ │ ├── _adapter.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── question-flow.tsx │ │ │ │ └── schema.ts │ │ │ ├── shared/ │ │ │ │ ├── README.md │ │ │ │ ├── _adapter.tsx │ │ │ │ ├── action-buttons.tsx │ │ │ │ ├── actions-config.ts │ │ │ │ ├── contract.ts │ │ │ │ ├── decision-actions.tsx │ │ │ │ ├── embedded-actions.ts │ │ │ │ ├── index.ts │ │ │ │ ├── local-actions.tsx │ │ │ │ ├── media/ │ │ │ │ │ ├── aspect-ratio.ts │ │ │ │ │ ├── format-utils.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── overlay-gradient.ts │ │ │ │ │ ├── safe-navigation.ts │ │ │ │ │ └── sanitize-href.ts │ │ │ │ ├── parse.ts │ │ │ │ ├── pierre-dark-theme.js │ │ │ │ ├── pierre-light-theme.js │ │ │ │ ├── schema.ts │ │ │ │ ├── tool-ui-context.tsx │ │ │ │ ├── tool-ui.tsx │ │ │ │ ├── toolkit.tsx │ │ │ │ ├── use-action-buttons.tsx │ │ │ │ ├── use-controllable-state.ts │ │ │ │ ├── use-copy-to-clipboard.ts │ │ │ │ ├── use-signature-reset.ts │ │ │ │ └── utils.ts │ │ │ ├── stats-display/ │ │ │ │ ├── README.md │ │ │ │ ├── _adapter.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── schema.ts │ │ │ │ ├── sparkline.tsx │ │ │ │ └── stats-display.tsx │ │ │ ├── terminal/ │ │ │ │ ├── README.md │ │ │ │ ├── _adapter.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── schema.ts │ │ │ │ └── terminal.tsx │ │ │ ├── video/ │ │ │ │ ├── README.md │ │ │ │ ├── _adapter.tsx │ │ │ │ ├── context.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── schema.ts │ │ │ │ ├── video-helpers.ts │ │ │ │ └── video.tsx │ │ │ ├── weather-widget/ │ │ │ │ ├── generated/ │ │ │ │ │ └── weather-runtime-core.generated.ts │ │ │ │ ├── runtime.ts │ │ │ │ ├── schema-runtime.ts │ │ │ │ ├── weather-data-overlay.tsx │ │ │ │ └── weather-widget-container.tsx │ │ │ └── x-post/ │ │ │ ├── README.md │ │ │ ├── _adapter.tsx │ │ │ ├── index.ts │ │ │ ├── schema.ts │ │ │ └── x-post.tsx │ │ └── ui/ │ │ ├── accordion.tsx │ │ ├── alert.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── button-group.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── chart.tsx │ │ ├── code-block.tsx │ │ ├── collapsible.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input-group.tsx │ │ ├── input.tsx │ │ ├── item.tsx │ │ ├── label.tsx │ │ ├── logo.tsx │ │ ├── popover.tsx │ │ ├── radio-group.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ └── tooltip.tsx │ ├── components.json │ ├── css.d.ts │ ├── docs/ │ │ ├── changelog.md │ │ └── tests.md │ ├── eslint.config.ts │ ├── hooks/ │ │ ├── use-extract-headings.ts │ │ ├── use-headings-observer.ts │ │ ├── use-mobile.ts │ │ ├── use-preset-param.ts │ │ ├── use-reduced-motion.ts │ │ ├── use-responsive-preview.ts │ │ ├── use-tab-search-param.ts │ │ └── use-toc-keyboard-nav.ts │ ├── lib/ │ │ ├── analytics.ts │ │ ├── changelog/ │ │ │ ├── changelog.ts │ │ │ ├── git.ts │ │ │ └── inference.ts │ │ ├── demo-chat/ │ │ │ └── toolkit.tsx │ │ ├── docs/ │ │ │ ├── auto-link.tsx │ │ │ ├── component-ids.ts │ │ │ ├── component-registry.ts │ │ │ ├── gallery-component-docs.ts │ │ │ ├── gallery-layout.ts │ │ │ ├── gallery-usage-code.ts │ │ │ ├── install-snippet-analytics.ts │ │ │ └── preview-config.tsx │ │ ├── eslint/ │ │ │ └── tool-ui-action-model-plugin.ts │ │ ├── integrations/ │ │ │ ├── freestyle/ │ │ │ │ ├── create-chat.ts │ │ │ │ └── get-code.ts │ │ │ └── rate-limit/ │ │ │ └── upstash.ts │ │ ├── mocks/ │ │ │ ├── chat-showcase-data.ts │ │ │ ├── stocks.ts │ │ │ └── tasks.ts │ │ ├── og/ │ │ │ └── og-image.tsx │ │ ├── playground/ │ │ │ ├── constants.ts │ │ │ ├── index.ts │ │ │ ├── mocks.ts │ │ │ ├── prototypes/ │ │ │ │ ├── food-ordering/ │ │ │ │ │ ├── get-menu.ts │ │ │ │ │ ├── get-restaurant-details.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── place-order.ts │ │ │ │ │ ├── search-restaurants.ts │ │ │ │ │ └── shared.ts │ │ │ │ ├── index.ts │ │ │ │ ├── waymo/ │ │ │ │ │ ├── README.md │ │ │ │ │ ├── WaymoDemo.tsx │ │ │ │ │ ├── check-ride-prices.ts │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── BookingConfirmation.tsx │ │ │ │ │ │ ├── RideQuote.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── confirm-ride-booking.ts │ │ │ │ │ ├── confirm-user-payment.ts │ │ │ │ │ ├── get-profile-context.ts │ │ │ │ │ ├── get-user-destination.ts │ │ │ │ │ ├── get-user-location.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── precheck-prices.ts │ │ │ │ │ ├── request-payment-method.ts │ │ │ │ │ ├── schedule-ride.ts │ │ │ │ │ ├── search-places.ts │ │ │ │ │ ├── select-frequent-location-tool.tsx │ │ │ │ │ ├── shared.ts │ │ │ │ │ ├── show-ride-details.ts │ │ │ │ │ ├── show-ride-options.ts │ │ │ │ │ ├── system-message-v2.ts │ │ │ │ │ ├── toggle-gps.ts │ │ │ │ │ ├── tools.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── wip-tool-uis/ │ │ │ │ │ ├── FrequentLocationSelector.tsx │ │ │ │ │ ├── README.md │ │ │ │ │ ├── TOOL-UI-PATTERNS.md │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── ux-v3.md │ │ │ │ │ └── ux.md │ │ │ │ └── waymo-v2/ │ │ │ │ ├── DESIGN.md │ │ │ │ ├── components/ │ │ │ │ │ ├── DestinationPicker.tsx │ │ │ │ │ ├── PickupPicker.tsx │ │ │ │ │ ├── RideQuote.tsx │ │ │ │ │ ├── TripStatus.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── system-prompt.ts │ │ │ │ ├── tools/ │ │ │ │ │ ├── get-ride-quote.tsx │ │ │ │ │ ├── get-trip-status.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── select-destination.tsx │ │ │ │ │ └── select-pickup.tsx │ │ │ │ └── types.ts │ │ │ ├── registry.ts │ │ │ ├── runtime.ts │ │ │ ├── tool-uis.ts │ │ │ ├── types.ts │ │ │ └── weather-tuning/ │ │ │ ├── has-any-tuning-delta.test.ts │ │ │ ├── list-updated-params.test.ts │ │ │ ├── parameter-definitions-coverage.test.ts │ │ │ ├── rain-param-ranges.test.ts │ │ │ ├── recover-repo-overrides.test.ts │ │ │ ├── resolve-params.test.ts │ │ │ ├── snow-fall-speed-range.test.ts │ │ │ ├── studio-timestamp.test.ts │ │ │ ├── tool-ui-export.test.ts │ │ │ ├── tool-ui-import.test.ts │ │ │ └── workflow-state.test.ts │ │ ├── presets/ │ │ │ ├── approval-card.ts │ │ │ ├── audio.ts │ │ │ ├── chart.ts │ │ │ ├── citation.ts │ │ │ ├── code-block.ts │ │ │ ├── code-diff.ts │ │ │ ├── data-table.ts │ │ │ ├── geo-map.ts │ │ │ ├── image-gallery.ts │ │ │ ├── image.ts │ │ │ ├── instagram-post.ts │ │ │ ├── item-carousel.ts │ │ │ ├── link-preview.ts │ │ │ ├── linkedin-post.ts │ │ │ ├── message-draft.ts │ │ │ ├── option-list.ts │ │ │ ├── order-summary.ts │ │ │ ├── parameter-slider.ts │ │ │ ├── plan.ts │ │ │ ├── preferences-panel.ts │ │ │ ├── progress-tracker.ts │ │ │ ├── question-flow.ts │ │ │ ├── stats-display.ts │ │ │ ├── terminal.ts │ │ │ ├── types.ts │ │ │ ├── video.ts │ │ │ ├── weather-widget.ts │ │ │ └── x-post.ts │ │ ├── registry/ │ │ │ └── tool-ui-registry.ts │ │ ├── staging/ │ │ │ ├── configs/ │ │ │ │ ├── parameter-slider.tsx │ │ │ │ ├── plan.tsx │ │ │ │ ├── progress-tracker.tsx │ │ │ │ └── stats-display.tsx │ │ │ ├── staging-config.ts │ │ │ └── types.ts │ │ ├── system/ │ │ │ └── tool-builder-message.ts │ │ ├── tests/ │ │ │ ├── app/ │ │ │ │ └── leaflet-css-import.test.ts │ │ │ ├── docs/ │ │ │ │ ├── header-preview-tabs-hydration.test.ts │ │ │ │ └── tracked-dynamic-codeblock.test.ts │ │ │ ├── registry/ │ │ │ │ └── tool-ui-registry.test.ts │ │ │ ├── setup/ │ │ │ │ └── console-guard.ts │ │ │ └── tool-ui/ │ │ │ ├── docs/ │ │ │ │ ├── changelog-inference-parser.test.ts │ │ │ │ ├── changelog-inference-prompt.test.ts │ │ │ │ ├── docs-search-shortcut.test.ts │ │ │ │ ├── gallery-layout.test.ts │ │ │ │ └── install-snippet-analytics.test.ts │ │ │ ├── geo-map/ │ │ │ │ ├── geo-map-render.test.ts │ │ │ │ ├── schema.test.ts │ │ │ │ └── spatial.test.ts │ │ │ ├── scripts/ │ │ │ │ └── install-git-hooks.test.ts │ │ │ ├── shared/ │ │ │ │ └── safe-navigation.test.ts │ │ │ ├── social/ │ │ │ │ └── link-sanitization.test.ts │ │ │ ├── video/ │ │ │ │ └── media-events.test.ts │ │ │ └── weather-widget/ │ │ │ ├── canvas-resolver-parity.test.ts │ │ │ ├── glass-style-resolver.test.ts │ │ │ ├── parameter-mapper.test.ts │ │ │ ├── production-runtime-harness.test.ts │ │ │ ├── weather-data-overlay-observer.test.ts │ │ │ ├── weather-runtime-codegen.test.ts │ │ │ ├── weather-widget-layout.test.ts │ │ │ └── webgl-budget-guard.test.ts │ │ ├── ui/ │ │ │ └── cn.ts │ │ ├── utils.ts │ │ ├── weather-authoring/ │ │ │ ├── presets/ │ │ │ │ └── tuned-presets.json │ │ │ ├── runtime/ │ │ │ │ └── glass-panel-svg.tsx │ │ │ ├── shaders/ │ │ │ │ ├── celestial.frag.glsl │ │ │ │ ├── cloud.frag.glsl │ │ │ │ ├── composite.frag.glsl │ │ │ │ ├── fullscreen.vert.glsl │ │ │ │ ├── lightning.frag.glsl │ │ │ │ ├── rain.frag.glsl │ │ │ │ └── snow.frag.glsl │ │ │ ├── shared/ │ │ │ │ └── contract.ts │ │ │ └── weather-widget/ │ │ │ ├── README.md │ │ │ ├── _adapter.tsx │ │ │ ├── effects/ │ │ │ │ ├── canvas-resolver-base.ts │ │ │ │ ├── canvas-resolver-runtime.ts │ │ │ │ ├── canvas-resolver.ts │ │ │ │ ├── checkpoint-overrides.ts │ │ │ │ ├── custom-effect-props.ts │ │ │ │ ├── effect-compositor-custom-props.ts │ │ │ │ ├── effect-compositor-quality.ts │ │ │ │ ├── effect-compositor-runtime.tsx │ │ │ │ ├── effect-compositor.tsx │ │ │ │ ├── generated/ │ │ │ │ │ ├── glass-panel-svg.generated.tsx │ │ │ │ │ ├── tuned-presets.generated.ts │ │ │ │ │ └── weather-effect-shaders.generated.ts │ │ │ │ ├── glass-panel-svg.tsx │ │ │ │ ├── glass-style-resolver.ts │ │ │ │ ├── index.ts │ │ │ │ ├── parameter-mapper.generated.js │ │ │ │ ├── parameter-mapper.ts │ │ │ │ ├── tuned-presets.ts │ │ │ │ ├── tuning.ts │ │ │ │ ├── types.ts │ │ │ │ ├── use-glass-region.ts │ │ │ │ ├── use-glass-styles.ts │ │ │ │ ├── use-weather-effects-renderer.generated.js │ │ │ │ ├── use-weather-effects-renderer.ts │ │ │ │ ├── weather-compositor-types.ts │ │ │ │ ├── weather-effect-gl.ts │ │ │ │ ├── weather-effect-render-passes.generated.js │ │ │ │ ├── weather-effect-render-passes.ts │ │ │ │ ├── weather-effect-shaders.ts │ │ │ │ ├── weather-effects-canvas.tsx │ │ │ │ ├── weather-effects-defaults.ts │ │ │ │ ├── weather-effects-props.ts │ │ │ │ ├── weather-effects-types.ts │ │ │ │ └── weather-webgl-budget.ts │ │ │ ├── index.tsx │ │ │ ├── schema-runtime.ts │ │ │ ├── schema.ts │ │ │ ├── time.ts │ │ │ ├── weather-data-overlay.generated.ts │ │ │ ├── weather-data-overlay.tsx │ │ │ ├── weather-runtime-core.ts │ │ │ ├── weather-widget-container.tsx │ │ │ └── weather-widget.tsx │ │ └── weather-codegen/ │ │ └── compile-weather-runtime.ts │ ├── mdx-components.tsx │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── proxy.ts │ ├── public/ │ │ └── r/ │ │ ├── approval-card.json │ │ ├── audio.json │ │ ├── chart.json │ │ ├── citation.json │ │ ├── code-block.json │ │ ├── code-diff.json │ │ ├── data-table.json │ │ ├── geo-map.json │ │ ├── image-gallery.json │ │ ├── image.json │ │ ├── instagram-post.json │ │ ├── item-carousel.json │ │ ├── link-preview.json │ │ ├── linkedin-post.json │ │ ├── message-draft.json │ │ ├── option-list.json │ │ ├── order-summary.json │ │ ├── parameter-slider.json │ │ ├── plan.json │ │ ├── preferences-panel.json │ │ ├── progress-tracker.json │ │ ├── question-flow.json │ │ ├── registry.json │ │ ├── stats-display.json │ │ ├── terminal.json │ │ ├── video.json │ │ ├── weather-widget.json │ │ └── x-post.json │ ├── scripts/ │ │ ├── build-tool-ui-registry.ts │ │ ├── check-changelog.ts │ │ ├── compile-weather-runtime.ts │ │ ├── generate-changelog.ts │ │ ├── install-git-hooks.ts │ │ ├── new-tool-ui-component.ts │ │ ├── precommit-registry-sync.ts │ │ └── registry-check.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── package.json ├── packages/ │ └── agent/ │ ├── package.json │ ├── plugin/ │ │ ├── .claude-plugin/ │ │ │ └── plugin.json │ │ └── skills/ │ │ └── tool-ui/ │ │ └── SKILL.md │ ├── src/ │ │ └── cli.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── pnpm-workspace.yaml └── skills-lock.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .changeset/config.json ================================================ { "$schema": "https://unpkg.com/@changesets/config@3.0.1/schema.json", "access": "public", "privatePackages": { "version": false } } ================================================ FILE: .git-blame-ignore-revs ================================================ # oxfmt bulk reformat (Prettier → Oxfmt migration) e052c5666f770d1521b8e5e574d2fa69152f89dc ================================================ FILE: .githooks/pre-commit ================================================ #!/usr/bin/env sh set -eu pnpm hooks:pre-commit pnpm lint-staged ================================================ FILE: .githooks/pre-push ================================================ #!/usr/bin/env sh set -eu pnpm hooks:pre-push ================================================ FILE: .github/pull_request_template.md ================================================ ## Summary ## Verification - [ ] Ran `pnpm verify:ci` - [ ] Committed updated `public/r` artifacts when Tool UI or registry sources changed ================================================ FILE: .github/workflows/changeset.yaml ================================================ name: Changesets on: push: branches: - main permissions: contents: write pull-requests: write concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: CI: true jobs: version: name: Create Version PR runs-on: ubuntu-latest steps: - name: Checkout code repository uses: actions/checkout@v6 with: fetch-depth: 1 - name: Install pnpm uses: pnpm/action-setup@v4 - name: Setup node.js uses: actions/setup-node@v6 with: node-version: 24 cache: "pnpm" - name: Install dependencies run: pnpm install --frozen-lockfile - name: Create version PR id: changesets uses: changesets/action@v1 with: commit: "chore: update versions" title: "chore: update versions" version: pnpm ci:version env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Check if any packages need publishing id: check-publish if: steps.changesets.outputs.hasChangesets == 'false' run: | needs_publish="false" for pkg in packages/*/package.json; do [ -f "$pkg" ] || continue name=$(jq -r '.name' "$pkg") version=$(jq -r '.version' "$pkg") private=$(jq -r '.private // false' "$pkg") if [ "$private" = "true" ]; then echo "Skipping private package: $name" continue fi if npm view "${name}@${version}" version 2>/dev/null | grep -q "${version}"; then echo "✓ ${name}@${version} already published" else echo "✗ ${name}@${version} needs publishing" needs_publish="true" fi done echo "needsPublish=${needs_publish}" >> $GITHUB_OUTPUT - name: Trigger npm publish if: steps.changesets.outputs.hasChangesets == 'false' && steps.check-publish.outputs.needsPublish == 'true' uses: actions/github-script@v7 with: script: | await github.rest.repos.createDispatchEvent({ owner: context.repo.owner, repo: context.repo.repo, event_type: 'npm-publish' }); ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: pull_request: branches: [main] push: branches: [main] jobs: verify: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 cache: pnpm - name: Install dependencies run: pnpm install --frozen-lockfile - name: Format check run: pnpm lint:format - name: Lint - Oxlint run: pnpm lint:oxlint - name: Lint - ESLint custom rules run: pnpm lint:eslint - name: Typecheck run: pnpm typecheck - name: Test run: pnpm test - name: Check changelog structure run: pnpm changelog:check - name: Verify registry artifacts are up to date run: pnpm registry:check ================================================ FILE: .github/workflows/npm-publish.yaml ================================================ name: npm Publish on: repository_dispatch: types: [npm-publish] permissions: id-token: write contents: write env: CI: true jobs: publish: name: Publish to npm if: github.ref == 'refs/heads/main' environment: npm Publish runs-on: ubuntu-latest steps: - name: Checkout code repository uses: actions/checkout@v6 with: fetch-depth: 1 - name: Install pnpm uses: pnpm/action-setup@v4 - name: Setup node.js uses: actions/setup-node@v6 with: node-version: 24 registry-url: "https://registry.npmjs.org" - name: Install dependencies run: pnpm install --frozen-lockfile - name: Publish to npm run: pnpm ci:publish env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Create GitHub Releases env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | git fetch --tags for pkg in packages/*/package.json; do name=$(jq -r '.name' "$pkg") version=$(jq -r '.version' "$pkg") private=$(jq -r '.private // false' "$pkg") [[ "$private" == "true" ]] && continue tag="${name}@${version}" git rev-parse "$tag" >/dev/null 2>&1 || continue gh release view "$tag" >/dev/null 2>&1 && continue changelog_dir=$(dirname "$pkg") changelog="$changelog_dir/CHANGELOG.md" notes="" if [[ -f "$changelog" ]]; then notes=$(awk "/^## ${version//./\\.}\$/,/^## [0-9]/" "$changelog" | head -n -1 | tail -n +2) fi [[ -z "$notes" ]] && notes="Release ${tag}" echo "Creating release: $tag" gh release create "$tag" \ --title "$tag" \ --notes "$notes" \ --verify-tag || echo "Warning: Failed to create release for $tag" done ================================================ FILE: .gitignore ================================================ # Dependencies node_modules /.pnp .pnp.* # Testing coverage # Next.js .next/ out/ # Production build dist # Misc .DS_Store *.pem private # Debug npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* # Local env files .env*.local .env # Vercel .vercel # TypeScript *.tsbuildinfo next-env.d.ts # Turbo .turbo # Cursor .cursor # Claude .claude/ # VSCode .vscode/ .vscode.playwright-mcp/ .vscode/terminals.json .playwright-mcp ================================================ FILE: .prettierignore ================================================ # Generated artifacts (managed by build scripts) apps/www/public/r/ apps/www/components/tool-ui/weather-widget/generated/ apps/www/lib/weather-authoring/weather-widget/effects/generated/ apps/www/lib/weather-authoring/weather-widget/weather-data-overlay.generated.ts ================================================ FILE: AGENTS.md ================================================ # AGENTS.md ## Cursor Cloud specific instructions ### Project overview Tool UI is a Next.js 16 documentation/demo site and copy-paste component library for AI assistant interfaces. The app lives in `apps/www/` within a pnpm monorepo. There is no database, no Docker, and no required background services beyond the Node.js dev server. ### Running services - **Dev server**: `pnpm dev` (Turbopack, port 3000). The static docs, gallery, and component previews work without any API keys. - **Chat/Playground features** require `OPENAI_API_KEY` in `.env` (see `.env.example`). Without it the chat API routes return errors, but the rest of the site functions normally. ### Commands reference All standard commands are documented in `CLAUDE.md`. Key ones: | Task | Command | | ------------------------- | ---------------- | | Dev server | `pnpm dev` | | Lint + typecheck + format | `pnpm check` | | Tests (Vitest) | `pnpm test` | | Fix lint issues | `pnpm lint:fix` | | Typecheck only | `pnpm typecheck` | ### Non-obvious caveats - **Typecheck uses `tsgo`** (`@typescript/native-preview`), not standard `tsc`. The `pnpm typecheck` command runs `tsgo --noEmit`. - **Formatter is `oxfmt`**, not Prettier. Run `pnpm format` to format or `pnpm format:check` to verify. It handles Tailwind class sorting and import sorting. - **Linting is split**: `oxlint` handles standard rules; `eslint` is retained only for `no-restricted-syntax`, `no-restricted-imports`, custom `tool-ui/*` rules, and React Compiler hooks. `pnpm check` runs both in parallel. - **`pnpm install` triggers `prepare`** which sets up git hooks via `tsx apps/www/scripts/install-git-hooks.ts`. The hooks directory is `.githooks/`. - **Pre-existing lint warnings** (60 warnings, 0 errors): these are known oxlint a11y warnings in the codebase and are not regressions. - **Build uses `--experimental-build-mode=compile`**: `pnpm build` runs `next build --experimental-build-mode=compile`. ================================================ FILE: AGENT_CHANGELOG.md ================================================ # Agent Changelog > This file helps coding agents understand project evolution, key decisions, and deprecated patterns. > Updated: 2026-02-11 ## Current State Summary Tool UI is a maintainer-owned copy/paste component library (shadcn/ui model) for AI assistant interfaces with a registry-first install path (`https://tool-ui.com/r/.json`). Component APIs are flat, receipt semantics are unified on `choice`, and component directories remain the product surface (`schema`, `component`, `README`, exports). Registry adapters now use `@/lib/utils` for `cn` (no registry-shipped `lib/ui/cn.ts`), Tool UI component motion is constrained to Tailwind/tw-animate-compatible classes, and docs place Source/Install + GitHub source links ahead of feature marketing sections. ## Stale Information Detected | Location | States | Reality | Since | | -------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------- | | `README.md` (Shadcn Registry section) | Registry artifacts include `lib/ui/cn.ts` | Registry adapters use `@/lib/utils`; `lib/ui/cn.ts` is no longer part of generated install artifacts | 2026-02 | | `.claude/plans/plan-sequential-munching-canyon.md` | References `hooks/use-code-gen.ts` TypeScript generation | Tuning flow is apply/recover via repo routes; `use-code-gen.ts` removed | 2026-02 | | `.claude/plans/plan-sequential-munching-canyon.md` | Targets deletion of `app/sandbox/weather-compositor/` | `weather-tuning` still imports compositor presets/interpolation modules; compositor remains active | 2026-02 | | `.claude/docs/component-workflow.md` | New component contract requires `error-boundary.tsx` | Error-boundary layer was removed from component contracts; index exports no longer include local error boundaries | 2026-02 | | `.claude/agents/tool-ui-implementer.md` | Instructs creating `error-boundary.tsx` and using `createToolUiErrorBoundary` | Error-boundary wrappers were removed; component contract now centers on `_adapter`, schema, component, index, README | 2026-02 | | `.claude/agents/tool-ui-reviewer.md` | Blocks review if `error-boundary.tsx` is missing | `error-boundary.tsx` is no longer required in component directories | 2026-02 | | `.claude/compiled/component-creation.md` | Uses `../shared` barrel and error-boundary scaffolding in canonical template | Current standards forbid `../shared` barrel imports for core logic and remove per-component error boundary scaffolds | 2026-02 | | `docs/plans/2026-01-22-feat-wizard-step-component-plan.md` | Active implementation target is `WizardStep` at `components/tool-ui/wizard-step/*` | Implemented component is `QuestionFlow` at `components/tool-ui/question-flow/*` | 2026-01 | | `docs/plans/2026-01-23-feat-wizard-step-visual-polish-plan.md` | Polish targets `WizardStep` paths and naming | Final shipped component naming/path is `QuestionFlow` | 2026-01 | ## Timeline ### 2026-02-11 — Component Docs Prioritize Install + Source Visibility **What changed:** All component docs were normalized so `## Source and Install` appears above `## Key Features`, with a GitHub source link for each component (`components/tool-ui/`). Highlights: - Reordered sections across all component docs to surface install instructions first - Normalized mixed `## Features`/`## Key Features` headings to `## Key Features` - Added docs contract enforcement to prevent regressions in section order and source-link presence **Why:** Make integration steps and source discovery immediately visible for maintainers/copy-paste users. **Agent impact:** In component docs, place install/source sections before feature callouts. Include both: - `npx shadcn@latest add https://tool-ui.com/r/.json` - GitHub source link to `components/tool-ui/` **Files:** `app/docs/*/content.mdx`, `lib/tests/tool-ui/docs/registry-installation-contract.test.ts` --- ### 2026-02-11 — Tool UI Animation Portability Standardized for Registry Consumers **What changed:** Tool UI components were migrated off repo-private animation keyframes and now rely on Tailwind/tw-animate-compatible classes only. Highlights: - Replaced private animation names (e.g. `spring-bounce`, `check-draw`, `fade-blur-in`, `progress-pulse`) in shipped component code - Removed inline `@keyframes` from `stats-display/sparkline` - Added portability contract tests that fail on private keyframe tokens or inline keyframes in source and generated registry artifacts - Added registry dependency assertions for motion primitives (`accordion`/`collapsible`) **Why:** Prevent no-op/broken transitions in downstream shadcn apps that do not include this repo’s private CSS. **Agent impact:** For `components/tool-ui/**`, use Tailwind/tw-animate classes (`animate-in/out`, fade/zoom/slide, `animate-spin`, `animate-pulse`) and avoid custom keyframe names unless they are provided by stock shadcn/tw-animate setup. **Files:** `components/tool-ui/plan/plan.tsx`, `components/tool-ui/progress-tracker/progress-tracker.tsx`, `components/tool-ui/question-flow/question-flow.tsx`, `components/tool-ui/approval-card/approval-card.tsx`, `components/tool-ui/option-list/option-list.tsx`, `components/tool-ui/stats-display/sparkline.tsx`, `lib/tests/tool-ui/docs/animation-portability-contract.test.ts`, `lib/tests/registry/tool-ui-registry.test.ts` --- ### 2026-02-11 — Registry Adapter `cn` Dependency Migrated to shadcn `@/lib/utils` **What changed:** Registry component adapters were updated to import `cn` from `@/lib/utils`; generated artifacts no longer rely on/emit `lib/ui/cn.ts`. Highlights: - Adapter imports switched from `@/lib/ui/cn` to `@/lib/utils` - Registry artifact checks updated accordingly - Fresh install path validated against root-level shadcn command and hosted component JSON URLs **Why:** Align Tool UI install output with stock shadcn app structure and eliminate repo-specific `cn` scaffolding. **Agent impact:** For registry-consumed component adapters, expect: - `import { cn } from "@/lib/utils"` - no generated `lib/ui/cn.ts` dependency **Files:** `components/tool-ui/*/_adapter.tsx`, `lib/tests/registry/tool-ui-registry.test.ts`, `app/docs/quick-start/content.mdx`, `public/r/*.json` --- ### 2026-02-11 — Registry-First Install Path Finalized; ZIP/Manual Guidance Removed **What changed:** Docs were converted to a single registry-first install flow using full component URLs, and legacy ZIP/manual copy instructions were removed. **Why:** Reduce install ambiguity and support a single canonical setup path for consumers. **Agent impact:** Use only: - `npx shadcn@latest add https://tool-ui.com/r/.json` Do not propose ZIP/manual copy workflows. **Files:** `app/docs/*/content.mdx`, `app/docs/quick-start/content.mdx`, `lib/tests/tool-ui/docs/registry-installation-contract.test.ts` --- ### 2026-02-11 — Import Boundary Enforcement + Error Boundary Layer Removal **What changed:** Tool UI component contracts were tightened around portability boundaries and local adapter ownership. Highlights: - Removed per-component `error-boundary.tsx` files and related exports across component directories - Normalized component `_adapter.tsx` files to alias-based UI imports - Enforced adapter-only UI primitive imports via ESLint (`@/components/ui/*` and `@/lib/ui/cn` are restricted outside `_adapter.tsx`) - Hardened registry/ID contracts and tests (`data-table` row keys, `question-flow` ids, registry generation edge cases like OS metadata files) **Why:** Reduce copy/paste friction, prevent component-internal import drift, and keep portability constraints enforceable by lint/tests instead of convention. **Agent impact:** Do not add local `error-boundary.tsx` files for new components. In non-adapter component modules, import UI primitives and `cn` only from `./_adapter`; keep shared imports as direct leaf modules (`../shared/*`), not barrels. **Files:** `eslint.config.ts`, `components/tool-ui/*/_adapter.tsx`, `components/tool-ui/*/index.ts*`, `lib/registry/tool-ui-registry.ts`, `lib/tests/tool-ui/data-table/row-keys-contract.test.ts`, `lib/tests/tool-ui/question-flow/ids-contract.test.ts`, `lib/tests/registry/tool-ui-registry.test.ts` --- ### 2026-02-11 — Maintainer Workflow + State Contract Hardening **What changed:** Shifted docs and tooling to a maintainer-first workflow and added targeted regression contracts for high-risk UI state paths. Highlights: - Reframed onboarding/docs around maintainers (`README.md`, `CONTRIBUTING.md`, `app/docs/contributing/content.mdx`, `docs/playground.md`, `docs/tests.md`) - Added component-local README coverage and scaffold automation via `pnpm component:new` (`scripts/new-tool-ui-component.ts`) - Added `components/tool-ui/index.ts` aggregate export surface - Added contract tests for deterministic row keys, outcome sync transitions, step ids, and tab search param resolution - Closed component contract gaps and improved scaffold consistency **Why:** Reduce time-to-first-working-component, make component directory contracts explicit, and keep state/ID behavior stable as internals evolve. **Agent impact:** Use scaffold + maintainer docs as the default path for new components. When changing stateful behavior, expose deterministic helpers and add contract tests under `lib/tests/**`. **Files:** `README.md`, `CONTRIBUTING.md`, `app/docs/contributing/content.mdx`, `scripts/new-tool-ui-component.ts`, `components/tool-ui/index.ts`, `lib/tests/tool-ui/**/*` --- ### 2026-02-10 — Registry Pipeline Hardened for Per-Component Install **What changed:** Registry generation moved to component-directory discovery with per-item artifacts and minimal dependency closure (instead of relying on prefixed/manual lists and shared monolith assumptions). Highlights: - Component discovery from `components/tool-ui/*` (excluding `shared`) - Registry items unprefixed (`tool-ui-foo` → `foo`) - Shared artifact dependencies inlined per item where needed - Registry tests assert discovered items match component directories **Why:** Keep shadcn registry output aligned with the actual product surface and reduce drift from manually curated registry definitions. **Agent impact:** Treat `components/tool-ui` directories as source of truth for registry coverage. After adding/refactoring components, run `pnpm registry:build` and `pnpm registry:check`. **Files:** `lib/registry/tool-ui-registry.ts`, `lib/tests/registry/tool-ui-registry.test.ts`, `scripts/build-tool-ui-registry.ts`, `public/r/*.json` --- ### 2026-02-10 — CI 1.0 Quality Gates **What changed:** CI now enforces lint/typecheck/test/registry artifact consistency on push/PR to `main`. **Why:** Prevent state contract regressions and stale registry artifacts from landing. **Agent impact:** A local "done" state for maintainer work is: `pnpm lint:ci`, `pnpm typecheck`, `pnpm test`, `pnpm registry:check`. **Files:** `.github/workflows/ci.yml`, `package.json` --- ### 2026-02-10 — Weather Widget V3.1 Clean Break **What changed:** Weather widget migrated to a strict V3.1 payload contract and removed legacy compatibility layers. Key contract shifts: - Canonical parser: `safeParseWeatherWidgetPayload` - Payload shape is widget prop contract (+ UI-only props) - Deterministic time input is now `time` (`timeBucket` or `localTimeOfDay`) - Field names normalized (`current.conditionCode`, `units.temperature`, `forecast[].label`, etc.) **Why:** Keep provider normalization in the tool layer, simplify widget rendering inputs, and make day/night/effects deterministic. **Agent impact:** Emit only V3.1 payloads when rendering WeatherWidget. Do not use legacy serializable weather parser/types or provider-specific fields in widget render payloads. **Files:** `components/tool-ui/weather-widget/schema.ts`, `components/tool-ui/weather-widget/time.ts`, `components/tool-ui/weather-widget/weather-widget.tsx`, `lib/presets/weather-widget.ts` --- ### 2026-02-10 — Weather Tuning Workflow Simplified (Apply-Only) **What changed:** Removed client-side weather tuning codegen hook (`app/sandbox/weather-tuning/hooks/use-code-gen.ts`) and standardized on repo-backed apply/recover endpoints. **Why:** Reduce duplicate export logic and keep one source of truth (`components/tool-ui/weather-widget/effects/tuned-presets.ts`). **Agent impact:** In tuning flows, treat `Apply to repo` as the path to production. Use: - `POST /api/weather-tuning/apply` - `GET /api/weather-tuning/recover` Do not add/restore parallel clipboard/download codegen paths for production tuning updates. --- ### 2026-02-10 — Test Location Policy Enforced **What changed:** Weather + shared tests were moved under `lib/tests/**` to ensure they run under current Vitest include globs and are not copied with component folders. **Why:** Components are copy-paste product surface; test fixtures/infra should stay internal to this repo. **Agent impact:** Place new executable tests in: - `lib/tests/**` (preferred) - `lib/playground/**` (playground-specific) Tests under `components/tool-ui/**` are out-of-policy and may not run by default. **Files:** `vitest.config.ts`, `lib/tests/tool-ui/shared/*`, `lib/tests/tool-ui/weather-widget/*` --- ### 2026-01-30 — PostHog Analytics Added **What changed:** Added PostHog instrumentation with Vercel Analytics dual-tracking. **Why:** Track component usage, preset selection, and code copying to understand what components and presets are most valuable. **Files added:** - `instrumentation-client.ts` — Client-side PostHog initialization - `lib/posthog-server.ts` — Server-side PostHog SDK - `lib/analytics.ts` — Typed event tracking SDK **Files modified:** - `next.config.ts` — Added `/ph/*` proxy rewrites for PostHog - `preset-selector.tsx` — Tracks `component_preset_selected` - `component-preview-shell.tsx` — Tracks `component_code_copied`, now requires `componentId` prop **Agent impact:** When adding new trackable interactions, use `analytics.*` methods from `lib/analytics.ts`. The `ComponentPreviewShell` now requires a `componentId` prop. **Events tracked:** - `component_preset_selected` — User selects a preset - `component_code_copied` — User copies component code - `component_viewed` — User views a component (ready to instrument) - `search_no_results` — Search with no matches (ready to instrument) --- ### 2026-01-29 — Sandbox Middleware Added **What changed:** Added middleware to gate `/sandbox/*` routes behind a `?sandbox=true` query param in production. Development mode always allows access. **Why:** Keep experimental sandboxes (weather effects testing, etc.) accessible for development without exposing them in production. **Agent impact:** Sandbox pages work normally in dev. In production, add `?sandbox=true` to URL to access. --- ### 2026-01-29 — SVG Glass Panel Effect Added **What changed:** Added `GlassPanel` component and `useGlassStyles` hook for frosted glass refraction effects using SVG displacement maps via CSS `backdrop-filter`. **Why:** Provide realistic glass distortion effects for weather widget overlays without WebGL complexity. SVG approach composes naturally with DOM, handles transparency correctly, and doesn't require canvas management. **Technical approach:** - SVG `feDisplacementMap` filter encodes X/Y displacement via R/G color channels - Chromatic aberration displaces RGB channels by different amounts (R most, G middle, B least) - Filter embedded as data URI in `backdrop-filter` CSS property - Graceful degradation on unsupported browsers (no effect, content still visible) **Agent impact:** Use `useGlassStyles` hook or `GlassPanel` component for glass effects. Don't implement WebGL-based glass effects—the SVG approach is simpler and sufficient. ```tsx // Hook for applying to existing elements const glassStyles = useGlassStyles({ width: 300, height: 200, depth: 12, strength: 40, chromaticAberration: 8, }); // Component wrapper content ; ``` **Files:** `components/tool-ui/weather-widget/effects/glass-panel-svg.tsx` --- ### 2026-01-26 — AI SDK v5 → v6 Upgrade **What changed:** Upgraded AI SDK from v5 to v6, assistant-ui to v0.12 with new hook APIs. **Why:** Keep dependencies current with latest AI SDK patterns. **Agent impact:** When referencing AI SDK or assistant-ui patterns, use v6/v0.12 APIs. Don't reference deprecated v5 patterns. --- ### 2026-01-23 — Question Flow Component Added **What changed:** Added Question Flow component (renamed from "Wizard Step" during development). Includes variants: `inline` (default) and `upfront`. **Why:** Provide structured multi-step question UI for AI assistants. **Agent impact:** Use `QuestionFlow` for multi-step user input. Component was briefly in showcase, then removed—current best practice is to use `defaultValue` prop for pre-selected state. **Files:** `components/tool-ui/question-flow/` --- ### 2026-01-22 — Copy Humanization Pass **What changed:** Systematic rewrite of component docs and non-component docs to remove promotional language and improve voice. **Why:** Copy should feel like real, believable interactions (see `.claude/docs/copy-guide.md`). **Agent impact:** Follow copy-guide.md when writing example content. Avoid promotional language, generic placeholders, and tech demo patterns. --- ### 2026-01-19 — Unified Receipt Prop (`choice`) **What changed:** All receipt state props unified to `choice` across components. **Before:** `confirmed`, `decision` (inconsistent per component) **After:** `choice` (universal) **Why:** Consistent API for LLM serialization and code readability. **Agent impact:** Always use `choice` prop for receipt state. Never use `confirmed` or `decision`. ```tsx // Correct // Deprecated (don't use) ``` **Files:** Plan at `.claude/plans/2025-01-19-unified-receipt-prop.md` --- ### 2026-01-16 — MessageDraft Component Added **What changed:** Added MessageDraft component for email and Slack message previews. **Agent impact:** Use `MessageDraft` for email/Slack preview UIs, not custom implementations. **Files:** `components/tool-ui/message-draft/` --- ### 2026-01-14 — Gallery Removals (X Post, ParameterSlider) **What changed:** X Post and ParameterSlider removed from gallery (components still exist in codebase). **Why:** X Post scenario didn't pass believability test. ParameterSlider was experimental. **Agent impact:** Components exist but aren't prominently featured. ParameterSlider is still usable; X Post exists but has copy issues. --- ### 2026-01-06 — ImageGallery Migration to View Transitions API **What changed:** ImageGallery migrated from Framer Motion to native View Transitions API. **Why:** Reduce bundle size, use platform features. **Agent impact:** Don't add Framer Motion for ImageGallery animations. Use View Transitions API patterns. --- ## Deprecated Patterns | Don't | Do Instead | Since | | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ----------------- | | Use `confirmed` prop | Use `choice` prop | 2026-01-19 | | Use `decision` prop | Use `choice` prop | 2026-01-19 | | Add `maxWidth`/`padding` props | Let users customize via `className` | Project inception | | Use nested config objects | Use flat props | Project inception | | Add Framer Motion to ImageGallery | Use View Transitions API | 2026-01-06 | | Use AI SDK v5 patterns | Use AI SDK v6 patterns | 2026-01-26 | | Implement WebGL glass effects | Use `useGlassStyles` or `GlassPanel` from glass-panel-svg | 2026-01-29 | | Use `ComponentPreviewShell` without `componentId` | Always pass `componentId` prop for analytics | 2026-01-30 | | Use weather payload prop `visual` | Use weather payload prop `time` | 2026-02-10 | | Use legacy serializable weather parser/types | Use `WeatherWidgetPayloadSchema` + `safeParseWeatherWidgetPayload` | 2026-02-10 | | Put executable tests in `components/tool-ui/**` | Put tests in `lib/tests/**` (or `lib/playground/**`) | 2026-02-10 | | Use `app/sandbox/weather-tuning/hooks/use-code-gen.ts` export flow | Use apply/recover API routes and `tuned-presets.ts` | 2026-02-10 | | Curate registry component lists by hand | Discover from `components/tool-ui/*` and validate with registry tests | 2026-02-10 | | Import from `../shared` barrel in core interactive components | Import direct leaf modules from `../shared/*` | 2026-02-10 | | Import shadcn primitives (`@/components/ui/*`, `@/lib/ui/cn`) directly in non-adapter component files | Import those primitives from local `./_adapter` files | 2026-02-11 | | Require/export per-component `error-boundary.tsx` wrappers | Export component + schema contracts directly; rely on caller/app-level boundaries | 2026-02-11 | | Add new components without local READMEs and contract scaffold files | Use `pnpm component:new` and keep the full component directory contract | 2026-02-11 | | Depend on registry-shipped `lib/ui/cn.ts` in generated component installs | Use shadcn `@/lib/utils` (`cn`) in adapter output | 2026-02-11 | | Use private animation keyframes in `components/tool-ui/**` (e.g. `spring-bounce`, `check-draw`, `fade-blur-in`) | Use Tailwind/tw-animate-compatible classes only | 2026-02-11 | | Put `## Source and Install` below features in component docs | Put `## Source and Install` above `## Key Features` and include GitHub source link | 2026-02-11 | ## Trajectory Based on recent changes, the project is: - **Standardizing APIs** — Receipt props unified, flat prop patterns enforced - **Maintainer-first DX** — onboarding and docs tuned for direct maintenance in this repo - **Polishing copy** — Moving from capability demos to believable scenarios - **Keeping dependencies current** — AI SDK v6, assistant-ui v0.12 - **Reducing bundle** — View Transitions over Framer Motion where possible - **Adding specialized components** — MessageDraft, QuestionFlow, StatsDisplay for specific use cases - **Adding visual effects** — SVG-based glass refraction for weather widget, preferring CSS/SVG over WebGL - **Adding analytics** — PostHog + Vercel Analytics for usage tracking - **Hardening weather contracts** — V3.1 clean-break payloads with deterministic `time` input and apply-only tuning workflow - **Hardening delivery rails** — registry auto-discovery + CI gates to catch drift early - **Tightening portability boundaries** — adapter-only UI imports and removal of per-component error-boundary wrappers - **Registry-first distribution UX** — docs and tests now prioritize install/source visibility and hosted registry URLs - **Runtime portability over private styling** — shipped Tool UI components avoid repo-private keyframes and rely on tw-animate-compatible motion ================================================ FILE: CLAUDE.md ================================================ # Tool UI Copy/paste component library (shadcn/ui model) for AI assistant interfaces. Users copy component directories into projects and modify them. Source code is the product—readability over cleverness. ## Commands ```bash pnpm dev # Dev server (Turbopack) pnpm build # Production build pnpm check # Parallel: typecheck + oxlint + eslint + format check pnpm lint:fix # Fix lint errors (run before committing) pnpm typecheck # TypeScript checking (tsgo) pnpm test # Run tests (Vitest) ``` ## Tooling - **Formatter**: Oxfmt (config: `apps/www/.oxfmtrc.jsonc`) — Tailwind class sorting + import sorting - **Linter**: Oxlint (`apps/www/.oxlintrc.json`) handles standard rules; ESLint (`apps/www/eslint.config.ts`) retained only for `no-restricted-syntax`, `no-restricted-imports`, custom `tool-ui/*` rules, and React Compiler hooks - **Typecheck**: tsgo (`@typescript/native-preview`) — native TypeScript compiler - **Parallel checks**: `pnpm check` runs typecheck + all linters + format in parallel via `npm-run-all2` ## Stack - **Package manager**: pnpm (required) - **Dependencies**: Only shadcn/ui prerequisites (Tailwind, Radix, Lucide)—users shouldn't need new deps ## Architecture ### Component Structure Each component lives in `apps/www/components/tool-ui/{name}/`. Reference: `apps/www/components/tool-ui/approval-card/` Key files: - `index.tsx` — Barrel exports - `{name}.tsx` — Main component - `schema.ts` — Zod schema + SerializableX types - `_adapter.tsx` — shadcn re-exports The `shared/` directory contains utilities all components need. ### Documentation Site Interconnected registries: - Component metadata: `apps/www/lib/docs/component-registry.ts` - Presets (example data): `apps/www/lib/presets/{component}.ts` - Preview rendering: `apps/www/lib/docs/preview-config.tsx` - Doc pages: `apps/www/app/docs/{component}/content.mdx` - Gallery: `apps/www/app/docs/gallery/page.tsx` ## Key Patterns ### Component API - **Tailwind for layout**: No `maxWidth`/`padding` props—users customize via `className` - **Standard widths**: Cards use `min-w-80 max-w-md`, compact components use `max-w-sm` - **Flat props**: Avoid nested config objects - **Semantic action IDs**: Use `id: "confirm"` / `id: "cancel"` for local and decision actions - **Receipt state**: Use `choice` prop to render confirmed state (e.g., ``) ### Main Component Structure Reference: `apps/www/components/tool-ui/approval-card/approval-card.tsx:183` - Outer `
` with `data-slot`, `data-tool-ui-id`, `lang="en"`, `aria-busy` - Loading skeleton via `isLoading` prop - Optional sibling `ToolUI.LocalActions` / `ToolUI.DecisionActions` surfaces ## Discovery | What | Where | | ----------------------- | ------------------------------------------------ | | Tool UI components | `apps/www/components/tool-ui/` (scan barrels) | | Component docs metadata | `apps/www/lib/docs/component-registry.ts` | | Preset configurations | `apps/www/lib/presets/*.ts` | | Types & validation | Colocated `schema.ts` files | | assistant-ui reference | `private/reference-docs/assistant-ui/` | | Design system specs | `private/design-system/` | ## Task Guides - Adding/modifying components: `.claude/docs/component-workflow.md` - Writing doc pages: `.claude/docs/mdx-authoring.md` - Copy style for examples: `.claude/docs/copy-guide.md` - Writing changelog entries: `apps/www/docs/changelog.md` ================================================ FILE: LICENSE.md ================================================ MIT License Copyright (c) 2025 AgentbaseAI Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ Tool UI Copy/paste React components for rendering tool calls in AI chat interfaces. Built by [assistant-ui](https://github.com/assistant-ui). **[Docs](https://tool-ui.com/docs/overview)** · **[Gallery](https://tool-ui.com/docs/gallery)** · **[Quick Start](https://tool-ui.com/docs/quick-start)** When a model calls a tool, most apps dump raw JSON into the conversation. These components turn tool payloads into interactive UI like approvals, forms, tables, charts, and media cards so users can understand and act without leaving the chat. ## Featured Components
Option List
Let users select from multiple choices

Option List component
Question Flow
Multi-step guided questions with branching

Question Flow component
## Why Tool UI? - **Copy/paste, not install** — shadcn/ui model. Components live in your codebase. No dependency lock-in. - **Schema-validated** — Every component has a Zod schema. Parse tool output, render when valid, fail safely when not. - **Interactive with receipts** — Components aren't just displays. Users make choices that flow back to the assistant. Choices persist as receipts. - **Built on shadcn/ui** — Radix primitives, Tailwind styling, your theme. No new design system to learn. ## Components - **Progress**: Plan, Progress Tracker - **Input**: Option List, Parameter Slider, Preferences Panel, Question Flow - **Display**: Citation, Geo Map, Item Carousel, Link Preview, Stats Display, Terminal, Weather Widget - **Artifacts**: Chart, Code Block, Code Diff, Data Table, Instagram Post, LinkedIn Post, Message Draft, X Post - **Confirmation**: Approval Card, Order Summary - **Media**: Audio, Image, Image Gallery, Video Each component includes a Zod schema for payload validation and presets for realistic example data. Browse them all in the [Gallery](https://tool-ui.com/docs/gallery). Tool UI component gallery ## License MIT License. See [LICENSE](LICENSE.md) for details. ================================================ FILE: apps/www/.oxfmtrc.jsonc ================================================ { "$schema": "https://raw.githubusercontent.com/nicolo-ribaudo/oxfmt/main/npm/oxfmt/schema.json", // Match existing Prettier settings "semi": true, "singleQuote": false, "trailingComma": "all", "tabWidth": 2, "printWidth": 80, // Tailwind class sorting (v4 entry point) "tailwindcss": { "stylesheet": "./app/styles/globals.css", "functions": ["clsx", "cn"], }, // Import sorting (bonus — Prettier didn't do this) "importSort": {}, } ================================================ FILE: apps/www/.oxlintrc.json ================================================ { "$schema": "./node_modules/oxlint/configuration_schema.json", "plugins": [ "typescript", "react", "unicorn", "oxc", "import", "jsx-a11y", "nextjs", "vitest" ], "categories": { "correctness": "error" }, "rules": { "@typescript-eslint/no-explicit-any": "error", "no-unused-vars": [ "error", { "ignoreRestSiblings": true, "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" } ], // jsx-a11y: match Next.js defaults (warn, not error) for rules // that fire on component library patterns "jsx-a11y/heading-has-content": "warn", "jsx-a11y/anchor-has-content": "warn", "jsx-a11y/media-has-caption": "warn", "jsx-a11y/no-autofocus": "warn", "jsx-a11y/no-static-element-interactions": "warn", "jsx-a11y/click-events-have-key-events": "warn", "jsx-a11y/prefer-tag-over-role": "warn", "jsx-a11y/iframe-has-title": "warn", "jsx-a11y/aria-role": "warn", // tool-ui components use for portability (not Next.js specific) "nextjs/no-img-element": "off", // jest rules leak through vitest plugin — disable "jest/valid-expect": "off", // false positives: \$ before { in template literals, control chars in sanitizer "no-useless-escape": "warn", "no-control-regex": "warn" }, "env": { "browser": true, "builtin": true, "es2024": true, "node": true }, "ignorePatterns": [ "**/dist/**", "**/node_modules/**", "**/.next/**", "**/out/**", "**/next-env.d.ts", "components/tool-ui/weather-widget/generated/**" ], "settings": { "next": { "rootDir": ["."] }, "react": { "version": "19" } } } ================================================ FILE: apps/www/.prettierignore ================================================ # Generated artifacts (managed by build scripts) .next/ public/r/ components/tool-ui/weather-widget/generated/ lib/weather-authoring/weather-widget/effects/generated/ lib/weather-authoring/weather-widget/weather-data-overlay.generated.ts ================================================ FILE: apps/www/app/api/builder/chat/route.ts ================================================ import { openai } from "@ai-sdk/openai"; import { anthropic } from "@ai-sdk/anthropic"; import { streamText, convertToModelMessages, stepCountIs } from "ai"; import { experimental_createMCPClient as createMCPClient } from "@ai-sdk/mcp"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { checkRateLimit } from "@/lib/integrations/rate-limit/upstash"; import { requestDevServer } from "@/lib/integrations/freestyle/create-chat"; import { SYSTEM_MESSAGE } from "@/lib/system/tool-builder-message"; export const maxDuration = 300; export async function POST(req: Request) { try { if (!process.env.OPENAI_API_KEY && !process.env.ANTHROPIC_API_KEY) { return new Response( JSON.stringify({ error: "API key is not configured. Please set OPENAI_API_KEY or ANTHROPIC_API_KEY in your environment variables.", }), { status: 500, headers: { "Content-Type": "application/json" }, }, ); } const ip = req.headers.get("x-forwarded-for")?.split(",")[0] || req.headers.get("x-real-ip") || "anonymous"; const rateLimitResult = await checkRateLimit(ip); if (!rateLimitResult.success) { return new Response( JSON.stringify({ error: "Rate limit exceeded. Please try again later.", reset: rateLimitResult.reset, }), { status: 429, headers: { "Content-Type": "application/json", "X-RateLimit-Limit": rateLimitResult.limit.toString(), "X-RateLimit-Remaining": rateLimitResult.remaining.toString(), "X-RateLimit-Reset": rateLimitResult.reset.toString(), }, }, ); } const { messages } = await req.json(); const repoId = req.headers.get("Repo-Id"); if (!messages || !Array.isArray(messages)) { return new Response( JSON.stringify({ error: "Invalid request: messages array required" }), { status: 400, headers: { "Content-Type": "application/json" }, }, ); } const modelMessages = await convertToModelMessages(messages); // Choose model based on what's available const model = process.env.ANTHROPIC_API_KEY ? anthropic("claude-sonnet-4-5-20250929") : openai("gpt-5-nano"); // If we have a repoId and Freestyle is configured, use Freestyle tools let tools = {}; if (repoId && process.env.FREESTYLE_API_KEY) { try { const { mcpEphemeralUrl } = await requestDevServer({ repoId }); if (mcpEphemeralUrl) { const devServerMcp = await createMCPClient({ transport: new StreamableHTTPClientTransport( new URL(mcpEphemeralUrl), ), }); tools = await devServerMcp.tools(); } } catch (error) { console.error("Error setting up Freestyle MCP:", error); // Continue without Freestyle tools if there's an error } } const result = streamText({ model, messages: modelMessages, system: SYSTEM_MESSAGE, tools, stopWhen: stepCountIs(100), temperature: 0.7, }); result.consumeStream(); return result.toUIMessageStreamResponse(); } catch (error) { console.error("Error in builder chat API route:", error); return new Response( JSON.stringify({ error: "An error occurred while processing your request.", }), { status: 500, headers: { "Content-Type": "application/json" }, }, ); } } ================================================ FILE: apps/www/app/api/builder/create-freestyle/route.ts ================================================ import { createChat } from "@/lib/integrations/freestyle/create-chat"; export async function POST() { try { if (!process.env.FREESTYLE_API_KEY) { return new Response( JSON.stringify({ error: "Freestyle API key is not configured. Please set FREESTYLE_API_KEY in your environment variables.", }), { status: 500, headers: { "Content-Type": "application/json" }, }, ); } const { repoId, ephemeralUrl, mcpEphemeralUrl } = await createChat(); return new Response( JSON.stringify({ repoId, ephemeralUrl, mcpEphemeralUrl, }), { status: 200, headers: { "Content-Type": "application/json" }, }, ); } catch (error) { console.error("Error creating Freestyle project:", error); return new Response( JSON.stringify({ error: "Failed to create Freestyle project.", }), { status: 500, headers: { "Content-Type": "application/json" }, }, ); } } ================================================ FILE: apps/www/app/api/chat/route.ts ================================================ import { openai } from "@ai-sdk/openai"; import { streamText, convertToModelMessages, tool } from "ai"; import { z } from "zod"; import { checkRateLimit } from "@/lib/integrations/rate-limit/upstash"; import { getMockTasks } from "@/lib/mocks/tasks"; import { STATS_DISPLAY_DATA } from "@/lib/mocks/chat-showcase-data"; export const runtime = "edge"; const DEMO_SYSTEM_PROMPT = `You are a helpful assistant that showcases the power of Tool UI—a copy-paste component library for AI assistant interfaces. Your role is to demonstrate how AI assistants can render rich, interactive UIs in chat. Use the available tools proactively and enthusiastically when they fit the user's request. ## Tools and when to use them - **show_plan**: Present a step-by-step plan for multi-step tasks. Use when the user asks for a plan, workflow, checklist, deployment steps, research approach, or "how do I" questions that break down into phases. - **get_tasks**: Retrieve and display support tickets or task lists. Use when the user asks about tickets, tasks, support queue, what's open, or wants to see a table of work items. - **show_stats**: Display key metrics and KPIs. Use when the user asks about performance, revenue, dashboards, quarterly numbers, or business metrics. - **show_terminal**: Show command-line output (test results, logs, build output). Use when the user asks to run tests, show terminal output, execute a command, or see build/log output. ## Guidelines - Be concise and friendly. Add a short preamble before or after tool output—don't repeat what the UI already shows. - NEVER describe or assume data without calling the tool. Always invoke the tool first—the tool returns real data. - When the user asks to see support tickets, tasks, or a queue → ALWAYS call get_tasks. - When the user asks for a plan, deployment steps, or a checklist → ALWAYS call show_plan. - When the user asks about Q4, metrics, revenue, or dashboards → ALWAYS call show_stats. - When the user asks to run tests, execute a command, or show terminal output → ALWAYS call show_terminal. - When using show_plan, create relevant plans (e.g. "Deploy to production", "Research competitors"). - When using get_tasks, the tool returns a support ticket queue. Present it with a brief intro. - When get_tasks returns an empty list (no tickets), you MUST add a friendly message such as: "It looks like there are currently no support tickets in the system. If you need assistance with anything else, feel free to ask!" - When using show_stats, the tool returns Q4 metrics. Present it with a brief intro. - When using show_terminal, create realistic output for commands like "pnpm test auth", "npm run build". - Keep responses brief—let the Tool UI components do the visual heavy lifting.`; export async function POST(req: Request) { try { if (!process.env.OPENAI_API_KEY) { return new Response( JSON.stringify({ error: "OpenAI API key is not configured. Please set OPENAI_API_KEY in your environment variables.", }), { status: 500, headers: { "Content-Type": "application/json" }, }, ); } const ip = req.headers.get("x-forwarded-for")?.split(",")[0] || req.headers.get("x-real-ip") || "anonymous"; const rateLimitResult = await checkRateLimit(ip); if (!rateLimitResult.success) { return new Response( JSON.stringify({ error: "Rate limit exceeded. Please try again later.", reset: rateLimitResult.reset, }), { status: 429, headers: { "Content-Type": "application/json", "X-RateLimit-Limit": rateLimitResult.limit.toString(), "X-RateLimit-Remaining": rateLimitResult.remaining.toString(), "X-RateLimit-Reset": rateLimitResult.reset.toString(), }, }, ); } const { messages } = await req.json(); if (!messages || !Array.isArray(messages)) { return new Response( JSON.stringify({ error: "Invalid request: messages array required" }), { status: 400, headers: { "Content-Type": "application/json" }, }, ); } const modelMessages = await convertToModelMessages(messages); const result = streamText({ model: openai("gpt-4o-mini"), messages: modelMessages, system: DEMO_SYSTEM_PROMPT, tools: { show_plan: tool({ description: "Present a step-by-step plan for the user to follow. Use for workflows, checklists, deployment steps, research plans, or any multi-phase task.", inputSchema: z.object({ title: z.string().describe("Short title for the plan"), description: z.string().optional().describe("Context or goal"), todos: z .array( z.object({ id: z.string(), label: z.string(), status: z .enum(["pending", "in_progress", "completed", "cancelled"]) .default("pending"), description: z.string().optional(), }), ) .min(1) .describe("Steps in order"), }), execute: async ({ title, description, todos }) => { return { id: `plan-${Date.now()}`, title, description: description ?? title, todos: todos.map((t) => ({ ...t, status: t.status ?? "pending", })), }; }, }), get_tasks: tool({ description: "Retrieve the support ticket queue and display it in a sortable table. Use when the user asks about tickets, tasks, support queue, or open work.", inputSchema: z.object({ assignee: z .string() .optional() .describe('Filter by assignee name (e.g. "Chen", "Patel")'), }), execute: async ({ assignee }) => { const tasks = await getMockTasks({ assignee }); const rank = { high: 1, medium: 2, low: 3 } as const; const sorted = [...tasks].sort((a, b) => { const byUrgency = rank[a.priority] - rank[b.priority]; if (byUrgency !== 0) return byUrgency; return ( new Date(b.created).getTime() - new Date(a.created).getTime() ); }); const columns = [ { key: "priority", label: "Urgency", format: { kind: "status" as const, statusMap: { high: { tone: "danger" as const, label: "High" }, medium: { tone: "warning" as const, label: "Medium" }, low: { tone: "neutral" as const, label: "Low" }, }, }, }, { key: "issue", label: "Issue", truncate: true, priority: "primary" as const, }, { key: "customer", label: "Customer", priority: "primary" as const, }, { key: "status", label: "Status", format: { kind: "badge" as const, colorMap: { open: "info", "in-progress": "warning", waiting: "neutral", done: "success", }, }, }, { key: "assignee", label: "Owner" }, { key: "created", label: "Created", format: { kind: "date" as const, dateFormat: "relative" as const, }, hideOnMobile: true, }, ]; const data = sorted.map((t) => ({ id: t.id, issue: t.issue, customer: t.customer, priority: t.priority, status: t.status, assignee: t.assignee, created: t.created, urgencyOrder: t.priority === "high" ? 1 : t.priority === "medium" ? 2 : 3, })); return { id: `tasks-${Date.now()}`, columns, data, rowIdKey: "id", defaultSort: { by: "urgencyOrder", direction: "asc" as const }, }; }, }), show_stats: tool({ description: "Display key metrics and KPIs in a visual stats grid. Use when the user asks about performance, revenue, dashboards, quarterly numbers, or business metrics.", inputSchema: z.object({}), execute: async () => { return { id: `stats-${Date.now()}`, ...STATS_DISPLAY_DATA, }; }, }), show_terminal: tool({ description: "Show command-line output (test results, logs, build output). Use when the user asks to run tests, execute a command, show terminal output, or see build/log results.", inputSchema: z.object({ command: z.string().describe("The command that was run"), stdout: z.string().describe("The terminal output"), exitCode: z.number().describe("Exit code (0 = success)"), durationMs: z.number().optional().describe("Execution time in ms"), }), execute: async ({ command, stdout, exitCode, durationMs }) => { return { id: `terminal-${Date.now()}`, command, stdout, exitCode, durationMs, }; }, }), }, temperature: 0.7, }); return result.toUIMessageStreamResponse(); } catch (error) { console.error("Error in chat API route:", error); return new Response( JSON.stringify({ error: "An error occurred while processing your request.", }), { status: 500, headers: { "Content-Type": "application/json" }, }, ); } } ================================================ FILE: apps/www/app/api/mcp-tools/route.ts ================================================ import { experimental_createMCPClient as createMCPClient } from "@ai-sdk/mcp"; export const runtime = "edge"; interface MCPTool { name: string; description?: string; inputSchema: { type: string; properties?: Record; required?: string[]; }; } export async function POST(req: Request) { try { const { serverUrl, transportType = "http" } = await req.json(); if (!serverUrl || typeof serverUrl !== "string") { return new Response( JSON.stringify({ error: "Invalid request: serverUrl is required" }), { status: 400, headers: { "Content-Type": "application/json" }, }, ); } // Validate URL format try { new URL(serverUrl); } catch { return new Response( JSON.stringify({ error: "Invalid URL format", }), { status: 400, headers: { "Content-Type": "application/json" }, }, ); } // Use Vercel AI SDK's MCP client console.log( `[MCP] Creating MCP client for ${serverUrl} with transport: ${transportType}`, ); try { const mcpClient = await createMCPClient({ transport: { type: transportType as "http" | "sse", url: serverUrl, }, }); console.log(`[MCP] Client created, fetching tools...`); const tools = await mcpClient.tools(); console.log(`[MCP] Received ${Object.keys(tools).length} tools`); // Convert AI SDK tool format to our format const mcpTools: MCPTool[] = Object.entries(tools).map(([name, tool]) => ({ name, description: tool.description, inputSchema: tool.inputSchema as unknown as { type: string; properties?: Record; required?: string[]; }, })); // Close the client await mcpClient.close(); return new Response( JSON.stringify({ tools: mcpTools, serverUrl, count: mcpTools.length, }), { status: 200, headers: { "Content-Type": "application/json" }, }, ); } catch (error) { console.error(`[MCP] Error with AI SDK client:`, error); return new Response( JSON.stringify({ error: `Could not connect to MCP server: ${error instanceof Error ? error.message : "Unknown error"}`, }), { status: 500, headers: { "Content-Type": "application/json" }, }, ); } } catch (error) { console.error("Error fetching MCP tools:", error); return new Response( JSON.stringify({ error: "An error occurred while fetching MCP tools", details: error instanceof Error ? error.message : "Unknown error", }), { status: 500, headers: { "Content-Type": "application/json" }, }, ); } } ================================================ FILE: apps/www/app/api/playground/chat/route.ts ================================================ import type { NextRequest } from "next/server"; import type { UIMessage } from "ai"; import { findPrototype, streamPrototypeResponse } from "@/lib/playground"; import { PROTOTYPE_SLUG_HEADER } from "@/lib/playground/constants"; const isUiMessageArray = (value: unknown): value is UIMessage[] => Array.isArray(value); const extractSlug = (request: NextRequest): string | null => { const headerSlug = request.headers.get(PROTOTYPE_SLUG_HEADER)?.trim(); if (headerSlug) { return headerSlug; } const url = new URL(request.url); const querySlug = url.searchParams.get("slug")?.trim(); if (querySlug) { return querySlug; } return null; }; export async function POST(request: NextRequest) { const slug = extractSlug(request); if (!slug) { return new Response(JSON.stringify({ error: "Missing prototype slug." }), { status: 400, headers: { "Content-Type": "application/json" }, }); } const prototype = findPrototype(slug); if (!prototype) { return new Response( JSON.stringify({ error: `Prototype "${slug}" not found.` }), { status: 404, headers: { "Content-Type": "application/json" }, }, ); } let body: unknown; try { body = await request.json(); } catch { return new Response(JSON.stringify({ error: "Invalid JSON body." }), { status: 400, headers: { "Content-Type": "application/json" }, }); } const messages = typeof body === "object" && body !== null && "messages" in body ? (body as Record).messages : undefined; if (!isUiMessageArray(messages)) { return new Response( JSON.stringify({ error: "Request body must include a messages array." }), { status: 400, headers: { "Content-Type": "application/json" }, }, ); } if (process.env.NODE_ENV !== "production") { try { const lastAssistantWithTools = [...messages] .reverse() .find( (message) => message.role === "assistant" && Array.isArray(message.parts) && message.parts.some( (part) => typeof part?.type === "string" && part.type.startsWith("tool-"), ), ); if (lastAssistantWithTools) { const toolParts = lastAssistantWithTools.parts?.filter( (part) => typeof part?.type === "string" && part.type.startsWith("tool-"), ); console.debug( "[playground] forwarding tool parts:", JSON.stringify(toolParts, null, 2), ); } } catch (error) { console.warn("[playground] failed to log tool parts", error); } } // Extract frontend tools from request body const clientTools: unknown = typeof body === "object" && body !== null && "tools" in body ? (body as Record).tools : undefined; const result = await streamPrototypeResponse( prototype, messages, clientTools, ); return result.toUIMessageStreamResponse(); } ================================================ FILE: apps/www/app/api/weather-tuning/_lib/tuned-presets-io.ts ================================================ import { readFile } from "fs/promises"; import path from "path"; import type { WeatherEffectsTunedPresets } from "../../../../lib/weather-authoring/weather-widget/effects/tuning"; export const TOOL_UI_TUNED_PRESETS_PATH = path.join( process.cwd(), "lib/weather-authoring/presets/tuned-presets.json", ); export async function readToolUiTunedPresetsFromDisk(): Promise { const source = await readFile(TOOL_UI_TUNED_PRESETS_PATH, "utf8"); return JSON.parse(source) as WeatherEffectsTunedPresets; } ================================================ FILE: apps/www/app/api/weather-tuning/apply/route.ts ================================================ import { writeFile } from "fs/promises"; import type { WeatherConditionCode } from "../../../../lib/weather-authoring/weather-widget/schema"; import type { CheckpointOverrides } from "../../../sandbox/weather-compositor/presets"; import { buildCanonicalToolUiPresetsForEditedConditions, replaceEditedConditions, } from "../../../sandbox/weather-tuning/lib/tool-ui-export"; import type { WeatherEffectsTunedPresets } from "../../../../lib/weather-authoring/weather-widget/effects/tuning"; import { canonicalizeWeatherPresetData, writeWeatherRuntimeArtifacts, } from "../../../../lib/weather-codegen/compile-weather-runtime"; import { readToolUiTunedPresetsFromDisk, TOOL_UI_TUNED_PRESETS_PATH, } from "../_lib/tuned-presets-io"; import { mapToolUiPresetsToCompositor } from "../../../sandbox/weather-tuning/lib/tool-ui-import"; export const runtime = "nodejs"; export async function POST(request: Request) { if (process.env.NODE_ENV === "production") { return new Response("Disabled in production.", { status: 403 }); } type ApplyPayload = { checkpointOverrides?: Partial< Record >; signedOff?: WeatherConditionCode[]; }; let payload: ApplyPayload | null = null; try { payload = (await request.json()) as ApplyPayload; } catch { return new Response("Invalid JSON payload.", { status: 400 }); } if ( !payload?.checkpointOverrides || typeof payload.checkpointOverrides !== "object" ) { return new Response("Missing 'checkpointOverrides' field.", { status: 400, }); } if (Object.keys(payload.checkpointOverrides).length === 0) { return new Response("No tuning changes to apply.", { status: 400 }); } let base: WeatherEffectsTunedPresets; try { base = await readToolUiTunedPresetsFromDisk(); } catch (error) { console.warn( "Failed to read current tuned presets; falling back to empty.", error, ); base = {}; } const repoCheckpointOverrides = mapToolUiPresetsToCompositor(base); const canonicalEditedConditions = buildCanonicalToolUiPresetsForEditedConditions( payload.checkpointOverrides, repoCheckpointOverrides, ); if (Object.keys(canonicalEditedConditions).length === 0) { return new Response("No tuning changes to apply.", { status: 400 }); } const merged = replaceEditedConditions(base, canonicalEditedConditions); const canonicalMerged = canonicalizeWeatherPresetData( merged, ) as WeatherEffectsTunedPresets; const content = `${JSON.stringify(canonicalMerged, null, 2)}\n`; await writeFile(TOOL_UI_TUNED_PRESETS_PATH, content, "utf8"); const generated = await writeWeatherRuntimeArtifacts(process.cwd()); const updatedArtifacts = [ "lib/weather-authoring/presets/tuned-presets.json", ...generated.written, ]; return Response.json({ ok: true, path: "lib/weather-authoring/presets/tuned-presets.json", updatedArtifacts, checkpointOverrides: mapToolUiPresetsToCompositor(canonicalMerged), }); } ================================================ FILE: apps/www/app/api/weather-tuning/recover/route.ts ================================================ import { mapToolUiPresetsToCompositor } from "../../../sandbox/weather-tuning/lib/tool-ui-import"; import { readToolUiTunedPresetsFromDisk } from "../_lib/tuned-presets-io"; export const runtime = "nodejs"; export async function GET() { if (process.env.NODE_ENV === "production") { return new Response("Disabled in production.", { status: 403 }); } try { const presets = await readToolUiTunedPresetsFromDisk(); const checkpointOverrides = mapToolUiPresetsToCompositor(presets); return Response.json({ ok: true, checkpointOverrides }); } catch (error) { console.error("Failed to recover tuning from repo presets.", error); return new Response("Failed to recover tuning from repo presets.", { status: 500, }); } } ================================================ FILE: apps/www/app/builder/layout.tsx ================================================ import type { ReactNode } from "react"; import ContentLayout from "@/app/components/layout/page-shell"; import { HeaderFrame } from "@/app/components/layout/app-shell"; import { ThemeToggle } from "@/app/components/builder/theme-toggle"; export default function BuilderLayout({ children }: { children: ReactNode }) { return ( }> {children} ); } ================================================ FILE: apps/www/app/builder/opengraph-image.tsx ================================================ import { generateOgImage, size as ogSize, contentType as ogContentType, } from "@/lib/og/og-image"; export const runtime = "nodejs"; export const alt = "Tool UI - Builder"; export const size = ogSize; export const contentType = ogContentType; export default async function Image() { return generateOgImage("Builder", "Construct components visually"); } ================================================ FILE: apps/www/app/builder/page.tsx ================================================ "use client"; import { AssistantRuntimeProvider, ThreadPrimitive, ComposerPrimitive, MessagePrimitive, useAui, useAuiState, makeAssistantToolUI, ActionBarPrimitive, BranchPickerPrimitive, ErrorPrimitive, makeAssistantTool, } from "@assistant-ui/react"; import { useChatRuntime, AssistantChatTransport, } from "@assistant-ui/react-ai-sdk"; import { ThreadList } from "@/app/components/assistant-ui/thread-list"; import { MarkdownText } from "@/app/components/assistant-ui/markdown-text"; import WebView from "@/app/components/builder/webview"; import { ArrowUpIcon, Square, Loader2, Eye, Code, Copy, Check, PencilIcon, RefreshCw, FileEdit, FileText, CopyIcon, CheckIcon, RefreshCwIcon, ChevronLeftIcon, ChevronRightIcon, ConstructionIcon, } from "lucide-react"; import { MCPIcon } from "@/app/components/builder/mcp-icon"; import { Button } from "@/components/ui/button"; import { CodeBlock, CodeBlockCode } from "@/components/ui/code-block"; import { getComponentCode } from "@/lib/integrations/freestyle/get-code"; import { Card, CardHeader, CardTitle, CardDescription, CardContent, } from "@/components/ui/card"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { InputGroup, InputGroupInput, InputGroupAddon, } from "@/components/ui/input-group"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { useState, useEffect, type FC, createContext, useContext } from "react"; import { ToolFallback } from "@/app/components/assistant-ui/tool-fallback"; import { TooltipIconButton } from "@/app/components/assistant-ui/tooltip-icon-button"; import React from "react"; import { cn } from "@/lib/ui/cn"; // Context for refreshing the preview pane const PreviewRefreshContext = createContext<(() => void) | null>(null); const usePreviewRefresh = () => { const refresh = useContext(PreviewRefreshContext); return refresh; }; // Module-level holder for the refresh function (accessible from streamCall) let globalRefreshPreview: (() => void) | null = null; const PreviewRefreshSetter: FC = () => { const refresh = usePreviewRefresh(); useEffect(() => { globalRefreshPreview = refresh; return () => { globalRefreshPreview = null; }; }, [refresh]); return null; }; const UserMessage: FC = () => { return (
); }; const AssistantMessage: FC = () => { return (
); }; const MessageError: FC = () => { return ( ); }; const UserActionBar: FC = () => { return ( ); }; const AssistantActionBar: FC = () => { return ( ); }; const BranchPicker: FC = ({ className, ...rest }) => { return ( / ); }; const EditComposer: FC = () => { return (
); }; interface MCPTool { name: string; description?: string; inputSchema: { type: string; properties?: Record; required?: string[]; }; } const MCPModal: FC<{ open: boolean; onOpenChange: (open: boolean) => void; }> = ({ open, onOpenChange }) => { const aui = useAui(); const [mcpUrl, setMcpUrl] = useState(""); const [transportType, setTransportType] = useState<"http" | "sse">("http"); const [tools, setTools] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // Auto-detect transport type based on URL useEffect(() => { if (mcpUrl.toLowerCase().endsWith("/sse")) { setTransportType("sse"); } else { setTransportType("http"); } }, [mcpUrl]); const loadTools = async () => { if (!mcpUrl.trim()) return; setLoading(true); setError(null); try { const response = await fetch("/api/mcp-tools", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ serverUrl: mcpUrl, transportType, }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || "Failed to fetch tools"); } setTools(data.tools || []); } catch (err) { setError(err instanceof Error ? err.message : "Failed to fetch tools"); setTools([]); } finally { setLoading(false); } }; const handleGenerateUI = (tool: MCPTool) => { // Create a formatted prompt with tool information const prompt = `Please create a Tool UI component for the following MCP tool: **Tool Name:** ${tool.name} **Description:** ${tool.description || "No description provided"} **Full Schema:** \`\`\`json ${JSON.stringify(tool.inputSchema, null, 2)} \`\`\``; // Send the message to the current thread aui.thread().append({ role: "user", content: [{ type: "text", text: prompt }], }); // Close the modal onOpenChange(false); }; return ( Import MCP Tool
setMcpUrl(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { loadTools(); } }} />
{error && (
{error}
)}
{tools.length === 0 ? (
{loading ? "Loading tools..." : "No tools loaded"}
) : (
{tools.map((tool) => (
{tool.name}
{tool.description && (
{tool.description}
)}
))}
)}
); }; const Composer: FC = () => { const [mcpModalOpen, setMcpModalOpen] = useState(false); const isNewThread = useAuiState( ({ threadListItem }) => threadListItem.status === "new", ); return ( <>
{isNewThread && ( )}
This builder is under heavy construction and may not always work as expected.
); }; const Thread: FC = () => { return (
); }; type ViewMode = "rendered" | "code"; export default function BuilderPage() { const [repoId, setRepoId] = useState(null); const repoIdRef = useState({ current: repoId })[0]; const [_appId, setAppId] = useState(null); const [webviewWidth, setWebviewWidth] = useState(50); // percentage const [viewMode, setViewMode] = useState("rendered"); const [codeContent, setCodeContent] = useState(null); const [isCodeLoading, setIsCodeLoading] = useState(false); const [copied, setCopied] = useState(false); const [refreshKey, setRefreshKey] = useState(0); const timestampRef = useState(() => Date.now())[0]; // Keep ref in sync with state repoIdRef.current = repoId; // Load code when switching to code view useEffect(() => { if (viewMode === "code" && repoId && !codeContent) { setIsCodeLoading(true); getComponentCode(repoId, "components/demo-tool-ui.tsx") .then((content) => { setCodeContent(content); setIsCodeLoading(false); }) .catch((err) => { console.error("Failed to load code:", err); setIsCodeLoading(false); }); } }, [viewMode, repoId, codeContent]); const runtime = useChatRuntime({ transport: new AssistantChatTransport({ api: "/api/builder/chat", headers: async () => { // Auto-create Freestyle project on first message if not already created if ( !repoIdRef.current && process.env.NEXT_PUBLIC_FREESTYLE_ENABLED !== "false" ) { try { const response = await fetch("/api/builder/create-freestyle", { method: "POST", }); if (response.ok) { const data = await response.json(); setRepoId(data.repoId); repoIdRef.current = data.repoId; // Use a unique ID for this app instance setAppId(data.repoId + "-" + timestampRef); } } catch (error) { console.error("Failed to create Freestyle project:", error); } } return { "Repo-Id": repoIdRef.current || "", }; }, }), }); const handleCopy = async () => { if (!codeContent) return; try { await navigator.clipboard.writeText(codeContent); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch (err) { console.error("Failed to copy:", err); } }; const handleMouseDown = (e: React.MouseEvent) => { e.preventDefault(); const startX = e.clientX; const startWidth = webviewWidth; const containerWidth = window.innerWidth - 240; // Subtract sidebar width const handleMouseMove = (e: MouseEvent) => { const diff = startX - e.clientX; const percentageChange = (diff / containerWidth) * 100; const newWidth = Math.min( Math.max(startWidth + percentageChange, 20), 80, ); setWebviewWidth(newWidth); }; const handleMouseUp = () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); }; document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); }; const handleRefreshPreview = () => { setRefreshKey((prev) => prev + 1); }; return (
{/* Thread List Sidebar - hidden on mobile */}
{/* Main Thread Area */}
{/* Preview/Code Panel */} {repoId && ( <> {/* Resize Handle - hidden on mobile */}
{/* Preview Panel */}
{/* Header with view toggle and copy/refresh button */}
{viewMode === "rendered" ? ( ) : ( )}
{viewMode === "rendered" ? ( ) : (
{isCodeLoading ? (

Loading code...

) : codeContent ? ( ) : (

Failed to load code

)}
)}
)}
); } const EditFileToolUI = makeAssistantTool< { path?: string; edits?: Array<{ oldText?: string; newText?: string; }>; }, {} >({ type: "backend", toolName: "edit_file", streamCall: async (reader) => { await reader.response.get(); // Refresh the preview pane after edit completes if (globalRefreshPreview) { globalRefreshPreview(); } }, render: ({ args }) => { console.log("EditFileToolUI", args); const path = args?.path; const edits = args?.edits; if (!path && (!edits || edits.length === 0)) { return null; } return (
Editing File
{path && ( {path} )}
{edits && edits.length > 0 && (
{edits.map( (edit, index) => (edit.oldText || edit.newText) && ( {edit.oldText && ( <> {edit.oldText.split("\n").length > 5 && (
+{edit.oldText.split("\n").length - 5} more
)} )} {edit.newText && ( <> {edit.newText.split("\n").length > 5 && (
+{edit.newText.split("\n").length - 5} more
)} )}
), )}
)}
); }, }); const WriteFileToolUI = makeAssistantTool< { path?: string; content?: string; }, {} >({ type: "backend", toolName: "write_file", streamCall: async (reader) => { await reader.response.get(); // Refresh the preview pane after write completes if (globalRefreshPreview) { globalRefreshPreview(); } }, render: ({ args }) => { const path = args?.path; if (!path) { return null; } return (
Writing File
{path}
); }, }); const ReadFileToolUI = makeAssistantToolUI< { path?: string; }, {} >({ toolName: "read_file", render: ({ args }) => { const path = args?.path; if (!path) { return null; } return (
Reading File
{path}
); }, }); ================================================ FILE: apps/www/app/components/analytics/posthog-init.client.tsx ================================================ "use client"; import { useEffect } from "react"; const apiKey = process.env["NEXT_PUBLIC_POSTHOG_API_KEY"]; const isDev = process.env.NODE_ENV === "development"; let didInit = false; let initPromise: Promise | null = null; export function PostHogInit() { useEffect(() => { if (didInit || initPromise || !apiKey) { return; } initPromise = (async () => { const { default: posthog } = await import("posthog-js"); if (didInit) { return; } posthog.init(apiKey, { api_host: "/ph", ui_host: "https://us.posthog.com", defaults: "2025-11-30", capture_exceptions: true, advanced_disable_flags: true, // Skip feature flags API call loaded: (instance) => { // Tag all events with environment for filtering. instance.register({ environment: isDev ? "development" : "production", app: "tool-ui", }); }, }); didInit = true; })() .catch((error) => { if (isDev) { console.error("[PostHog] failed to initialize", error); } }) .finally(() => { initPromise = null; }); }, []); return null; } ================================================ FILE: apps/www/app/components/assistant-ui/markdown-text.tsx ================================================ "use client"; import "@assistant-ui/react-markdown/styles/dot.css"; import { type CodeHeaderProps, MarkdownTextPrimitive, unstable_memoizeMarkdownComponents as memoizeMarkdownComponents, useIsMarkdownCodeBlock, } from "@assistant-ui/react-markdown"; import remarkGfm from "remark-gfm"; import { type FC, memo, useState } from "react"; import { CheckIcon, CopyIcon } from "lucide-react"; import { TooltipIconButton } from "@/app/components/assistant-ui/tooltip-icon-button"; import { cn } from "@/lib/ui/cn"; const MarkdownTextImpl = () => { return ( ); }; export const MarkdownText = memo(MarkdownTextImpl); const CodeHeader: FC = ({ language, code }) => { const { isCopied, copyToClipboard } = useCopyToClipboard(); const onCopy = () => { if (!code || isCopied) return; copyToClipboard(code); }; return (
{language} {!isCopied && } {isCopied && }
); }; const useCopyToClipboard = ({ copiedDuration = 3000, }: { copiedDuration?: number; } = {}) => { const [isCopied, setIsCopied] = useState(false); const copyToClipboard = (value: string) => { if (!value) return; navigator.clipboard.writeText(value).then(() => { setIsCopied(true); setTimeout(() => setIsCopied(false), copiedDuration); }); }; return { isCopied, copyToClipboard }; }; const defaultComponents = memoizeMarkdownComponents({ h1: ({ className, ...props }) => (

), h2: ({ className, ...props }) => (

), h3: ({ className, ...props }) => (

), h4: ({ className, ...props }) => (

), h5: ({ className, ...props }) => (

), h6: ({ className, ...props }) => (
), p: ({ className, ...props }) => (

), a: ({ className, ...props }) => ( ), blockquote: ({ className, ...props }) => (

), ul: ({ className, ...props }) => (
    li]:mt-2", className, )} {...props} /> ), ol: ({ className, ...props }) => (
      li]:mt-2", className)} {...props} /> ), hr: ({ className, ...props }) => (
      ), table: ({ className, ...props }) => ( ), th: ({ className, ...props }) => ( td:first-child]:rounded-bl-lg [&:last-child>td:last-child]:rounded-br-lg", className, )} {...props} /> ), sup: ({ className, ...props }) => ( a]:text-xs [&>a]:no-underline", className)} {...props} /> ), pre: ({ className, ...props }) => (
        ),
        code: function Code({ className, ...props }) {
          const isCodeBlock = useIsMarkdownCodeBlock();
          return (
            
          );
        },
        CodeHeader,
      });
      
      
      ================================================
      FILE: apps/www/app/components/assistant-ui/thread-list.tsx
      ================================================
      import { useCallback, type FC } from "react";
      import {
        ThreadListItemPrimitive,
        ThreadListPrimitive,
        useAuiState,
      } from "@assistant-ui/react";
      import { ArchiveIcon, HistoryIcon, PlusIcon } from "lucide-react";
      
      import { Button } from "@/components/ui/button";
      import { TooltipIconButton } from "@/app/components/assistant-ui/tooltip-icon-button";
      import { Skeleton } from "@/components/ui/skeleton";
      
      export type ThreadListProps = {
        allowedThreadIds?: Set;
        newLabel?: string;
        emptyState?: React.ReactNode;
        onReplay?: (threadId: string) => void;
      };
      
      export const ThreadList: FC = ({
        allowedThreadIds,
        newLabel = "New Tool UI",
        emptyState,
        onReplay,
      }) => {
        return (
          
            
            
          
        );
      };
      
      const ThreadListNew: FC<{ label: string }> = ({ label }) => {
        return (
          
            
          
        );
      };
      
      type ThreadListItemsProps = {
        allowedThreadIds?: Set;
        emptyState?: React.ReactNode;
        onReplay?: (threadId: string) => void;
      };
      
      const ThreadListItems: FC = ({
        allowedThreadIds,
        emptyState,
        onReplay,
      }) => {
        const threadsState = useAuiState(useCallback((state) => state.threads, []));
        const isLoading = threadsState.isLoading;
        const threadIds = threadsState.threadIds;
      
        if (isLoading) {
          return ;
        }
      
        const indexMap = threadIds
          .map((id, index) => ({ id, index }))
          .filter(({ id }) => !allowedThreadIds || allowedThreadIds.has(id));
      
        if (indexMap.length === 0) {
          return emptyState ? <>{emptyState} : null;
        }
      
        return indexMap.map(({ id, index }) => (
           ,
            }}
          />
        ));
      };
      
      const ThreadListSkeleton: FC = () => {
        return (
          <>
            {Array.from({ length: 5 }, (_, i) => (
              
      ))} ); }; const ThreadListItem: FC<{ onReplay?: (threadId: string) => void }> = ({ onReplay, }) => { return ( ); }; const ThreadListItemTitle: FC = () => { return ( ); }; const ThreadListItemActions: FC<{ onReplay?: (threadId: string) => void }> = ({ onReplay, }) => { const threadId = useAuiState(({ threadListItem }) => threadListItem.id); return (
      {onReplay && ( { event.stopPropagation(); onReplay(threadId); }} > )}
      ); }; ================================================ FILE: apps/www/app/components/assistant-ui/tool-fallback.tsx ================================================ "use client"; import type { ToolCallMessagePartComponent } from "@assistant-ui/react"; import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; import { useState } from "react"; import { Button } from "@/components/ui/button"; export const ToolFallback: ToolCallMessagePartComponent = ({ toolName, argsText, result, }) => { const [isCollapsed, setIsCollapsed] = useState(true); return (

      Used tool: {toolName}

      {!isCollapsed && (
                    {argsText}
                  
      {result !== undefined && (

      Result:

                      {typeof result === "string"
                        ? result
                        : JSON.stringify(result, null, 2)}
                    
      )}
      )}
      ); }; ================================================ FILE: apps/www/app/components/assistant-ui/tooltip-icon-button.tsx ================================================ "use client"; import { ComponentPropsWithRef, forwardRef } from "react"; import { Slottable } from "@radix-ui/react-slot"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/ui/cn"; export type TooltipIconButtonProps = ComponentPropsWithRef & { tooltip: string; side?: "top" | "bottom" | "left" | "right"; }; export const TooltipIconButton = forwardRef< HTMLButtonElement, TooltipIconButtonProps >(({ children, tooltip, side = "bottom", className, ...rest }, ref) => { return ( {tooltip} ); }); TooltipIconButton.displayName = "TooltipIconButton"; ================================================ FILE: apps/www/app/components/builder/mcp-icon.tsx ================================================ import * as React from "react"; export function MCPIcon({ className, ...props }: React.SVGProps) { return ( ); } ================================================ FILE: apps/www/app/components/builder/theme-toggle.tsx ================================================ "use client"; import * as React from "react"; import { Moon, Sun } from "lucide-react"; import { useTheme } from "next-themes"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/ui/cn"; export function ThemeToggle() { const { setTheme, resolvedTheme } = useTheme(); const [mounted, setMounted] = React.useState(false); function changeTheme(newTheme: string) { if (!document.startViewTransition) { setTheme(newTheme); return; } document.documentElement.dataset.themeTransition = ""; const transition = document.startViewTransition(() => { setTheme(newTheme); }); transition.finished.then(() => { delete document.documentElement.dataset.themeTransition; }); } React.useEffect(() => { setMounted(true); }, []); const isDark = mounted && resolvedTheme === "dark"; const toggleTheme = () => { changeTheme(isDark ? "light" : "dark"); }; return ( ); } ================================================ FILE: apps/www/app/components/builder/webview-actions.ts ================================================ "use server"; import { requestDevServer as requestDevServerInner } from "@/lib/integrations/freestyle/create-chat"; export async function requestDevServer({ repoId }: { repoId: string }) { return await requestDevServerInner({ repoId }); } ================================================ FILE: apps/www/app/components/builder/webview.tsx ================================================ "use client"; import { requestDevServer as requestDevServerInner } from "./webview-actions"; import "@/app/styles/builder-loader.css"; import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useState, } from "react"; export interface WebViewHandle { refresh: () => void; } type DevServerResponse = Awaited>; export default forwardRef< WebViewHandle, { repo: string; } >(function WebView(props, ref) { const [devServer, setDevServer] = useState(null); const [requesting, setRequesting] = useState(false); const [iframeLoading, setIframeLoading] = useState(true); const [error, setError] = useState(null); const devServerUrl = useMemo( () => devServer?.ephemeralUrl ?? null, [devServer], ); const requestDevServer = useCallback(async () => { setRequesting(true); setIframeLoading(true); setError(null); try { const response = await requestDevServerInner({ repoId: props.repo }); setDevServer(response); } catch (err) { const message = err instanceof Error ? err.message : String(err); setError(message); setDevServer(null); setIframeLoading(false); } finally { setRequesting(false); } }, [props.repo]); useImperativeHandle(ref, () => ({ refresh: () => { void requestDevServer(); }, })); useEffect(() => { if (!props.repo) { setDevServer(null); setError("Repo ID is missing."); return; } void requestDevServer(); }, [props.repo, requestDevServer]); return (
      {(requesting || iframeLoading) && !devServer?.devCommandRunning && (
      {iframeLoading ? "JavaScript Loading" : "Starting VM"}
      )} {error ? (
      {error}
      ) : devServerUrl ? (
      ), td: ({ className, ...props }) => ( ), tr: ({ className, ...props }) => (