diff --git a/.cursor/plans/add_url_prop_to_button_41f497bd.plan.md b/.cursor/plans/add_url_prop_to_button_41f497bd.plan.md
deleted file mode 100644
index 950b10d2..00000000
--- a/.cursor/plans/add_url_prop_to_button_41f497bd.plan.md
+++ /dev/null
@@ -1,229 +0,0 @@
----
-name: Add LinkButton Component
-overview: Add a new LinkButton component with a separate LinkButtonElement AST type (type "link-button") for buttons that open URLs. All four platforms natively support this feature with distinct implementations.
-todos:
- - id: core-types
- content: Add LinkButtonElement interface and LinkButton() function in packages/chat/src/cards.ts
- status: completed
- - id: actions-element
- content: Update ActionsElement to accept both ButtonElement and LinkButtonElement
- status: completed
- - id: jsx-runtime
- content: Add LinkButton component and LinkButtonProps in packages/chat/src/jsx-runtime.ts
- status: completed
- - id: exports
- content: Export LinkButton and related types from packages/chat/src/index.ts
- status: completed
- - id: slack-adapter
- content: Handle link-button type in packages/adapter-slack/src/cards.ts
- status: completed
- - id: discord-adapter
- content: Handle link-button type in packages/adapter-discord/src/cards.ts using ButtonStyle.Link
- status: completed
- - id: teams-adapter
- content: Handle link-button type in packages/adapter-teams/src/cards.ts using Action.OpenUrl
- status: completed
- - id: gchat-adapter
- content: Handle link-button type in packages/adapter-gchat/src/cards.ts using openLink
- status: completed
- - id: core-tests
- content: Add LinkButton tests to packages/chat/src/cards.test.ts
- status: completed
- - id: jsx-tests
- content: Add LinkButton JSX tests to packages/chat/src/jsx-runtime.test.ts and jsx-runtime.test.tsx
- status: completed
- - id: slack-tests
- content: Add link button conversion tests to packages/adapter-slack/src/cards.test.ts
- status: completed
- - id: discord-tests
- content: Add link button conversion tests to packages/adapter-discord/src/cards.test.ts
- status: completed
- - id: teams-tests
- content: Add link button conversion tests to packages/adapter-teams/src/cards.test.ts
- status: completed
- - id: gchat-tests
- content: Add link button conversion tests to packages/adapter-gchat/src/cards.test.ts
- status: completed
- - id: readme
- content: Update README.md Rich Cards section to document LinkButton
- status: completed
-isProject: false
----
-
-# Add LinkButton Component
-
-## Rationale
-
-A separate `LinkButton` component (rather than polymorphic `Button`) because:
-
-- 3/4 platforms treat link buttons as fundamentally different (Discord, Teams,
-
-GChat)
-
-- Avoids nested discriminated unions
-- Cleaner pattern matching in adapters: `case "link-button":`
-- Easier test assertions: `expect(element.type).toBe("link-button")`
-
-## New Types
-
-Add to [packages/chat/src/cards.ts](packages/chat/src/cards.ts):
-
-```ts
-export interface LinkButtonElement {
- type: "link-button";
- url: string;
- label: string;
- style?: ButtonStyle;
-}
-
-export interface LinkButtonOptions {
- url: string;
- label: string;
- style?: ButtonStyle;
-}
-
-export function LinkButton(options: LinkButtonOptions): LinkButtonElement;
-```
-
-## Updated ActionsElement
-
-```ts
-export interface ActionsElement {
- type: "actions";
- children: (ButtonElement | LinkButtonElement)[];
-}
-```
-
-## JSX Usage
-
-```tsx
-import { Button, LinkButton, Actions, Card } from "chat";
-
-
-
-
- View Docs
-
-
-;
-```
-
-## Files to Modify
-
-### Core Package
-
-1. **[packages/chat/src/cards.ts](packages/chat/src/cards.ts)**
-
-- Add `LinkButtonElement` interface (after `ButtonElement`)
-- Add `LinkButtonOptions` interface (after `ButtonOptions`)
-- Add `LinkButton()` function (after `Button()`)
-- Update `ActionsElement.children` type to
-
-`(ButtonElement | LinkButtonElement)[]`
-
-- Update `AnyCardElement` union to include `LinkButtonElement`
-- Add `LinkButton` to debug name map
-
-1. **[packages/chat/src/jsx-runtime.ts](packages/chat/src/jsx-runtime.ts)**
-
-- Add `LinkButtonProps` interface
-- Add `LinkButton` to component type union
-- Add `isLinkButtonProps` type guard
-- Handle `LinkButton` in `toCardElement`
-- Export `LinkButton` and `LinkButtonProps`
-
-2. **[packages/chat/src/index.ts](packages/chat/src/index.ts)**
-
-- Export `LinkButton` function
-- Export `LinkButtonElement`, `LinkButtonOptions` types
-- Export `LinkButtonProps` from jsx-runtime
-
-### Adapters
-
-1. **[packages/adapter-slack/src/cards.ts](packages/adapter-slack/src/cards.ts)**
-
-- Import `LinkButtonElement` from chat
-- Update `convertActionsToBlock` to handle both button types
-- Add `convertLinkButtonToElement` that sets `url` property
-
-2. **[packages/adapter-discord/src/cards.ts](packages/adapter-discord/src/cards.ts)**
-
-- Import `LinkButtonElement` from chat
-- Update `convertActionsElement` to handle both button types
-- Add `convertLinkButtonElement` using `ButtonStyle.Link` (5) with `url` (no
-
-`custom_id`)
-
-1. **[packages/adapter-teams/src/cards.ts](packages/adapter-teams/src/cards.ts)**
-
-- Import `LinkButtonElement` from chat
-- Update `convertActionsToElements` to handle both button types
-- Add `convertLinkButtonToAction` using `Action.OpenUrl` with `url`
-
-2. **[packages/adapter-gchat/src/cards.ts](packages/adapter-gchat/src/cards.ts)**
-
-- Import `LinkButtonElement` from chat
-- Update `convertActionsToWidget` to handle both button types
-- Add `convertLinkButtonToGoogleButton` using `openLink: { url }` in onClick
-
-### Tests
-
-1. **[packages/chat/src/cards.test.ts](packages/chat/src/cards.test.ts)**
-
-- Add `describe("LinkButton")` block with tests for:
- - Creates a link button element
- - Creates a styled link button
-
-2. **[packages/chat/src/jsx-runtime.test.ts](packages/chat/src/jsx-runtime.test.ts)**
-
-- Add test for `jsx(LinkButton, { url, children })`
-
-3. **[packages/chat/src/jsx-runtime.test.tsx](packages/chat/src/jsx-runtime.test.tsx)**
-
-- Add test for `Label` in Actions
-
-4. **[packages/adapter-slack/src/cards.test.ts](packages/adapter-slack/src/cards.test.ts)**
-
-- Add test: "converts link buttons with url property"
-
-5. **[packages/adapter-discord/src/cards.test.ts](packages/adapter-discord/src/cards.test.ts)**
-
-- Add test: "converts link buttons using Link style"
-
-6. **[packages/adapter-teams/src/cards.test.ts](packages/adapter-teams/src/cards.test.ts)**
-
-- Add test: "converts link buttons to Action.OpenUrl"
-
-7. **[packages/adapter-gchat/src/cards.test.ts](packages/adapter-gchat/src/cards.test.ts)**
-
-- Add test: "converts link buttons with openLink"
-
-### Documentation
-
-1. **[README.md](README.md)**
-
-- Add `LinkButton` to import in "Rich Cards with Buttons" section
- - Add example showing LinkButton usage alongside Button
-
-## Platform-Specific Output
-
-| Platform | Link Button Output |
-
-|----------|-------------------|
-
-| Slack | `{ type: "button", url: "...", action_id: "link", text: {...} }` |
-
-| Discord | `{ type: 2, style: 5, url: "...", label: "..." }` (no custom_id) |
-
-| Teams | `{ type: "Action.OpenUrl", title: "...", url: "..." }` |
-
-| GChat | `{ text: "...", onClick: { openLink: { url: "..." } } }` |
-
-## Code Style Notes
-
-- Follow existing patterns: no gratuitous comments
-- JSDoc only on public exports (interfaces, functions)
-- Tests: clean describe/it blocks, no comments
-- Match existing formatting (single quotes in code, etc.)
diff --git a/.cursor/plans/ephemeral_messages_6f0d6906.plan.md b/.cursor/plans/ephemeral_messages_6f0d6906.plan.md
deleted file mode 100644
index e350b804..00000000
--- a/.cursor/plans/ephemeral_messages_6f0d6906.plan.md
+++ /dev/null
@@ -1,379 +0,0 @@
----
-name: Ephemeral Messages
-overview: Add ephemeral message support via thread.postEphemeral(userId, message) across all platform adapters. Slack and Google Chat use native ephemeral APIs, while Discord and Teams silently fallback to DMs for consistent cross-platform behavior.
-todos:
- - id: core-types
- content: Add EphemeralMessage type and postEphemeral to Adapter/Thread interfaces in types.ts
- status: completed
- - id: thread-impl
- content: Implement postEphemeral() in ThreadImpl with DM fallback logic
- status: completed
- - id: slack-adapter
- content: Implement postEphemeral using chat.postEphemeral API
- status: completed
- - id: slack-tests
- content: Add tests for Slack ephemeral messages
- status: completed
- - id: gchat-adapter
- content: Implement postEphemeral using privateMessageViewer field
- status: completed
- - id: gchat-tests
- content: Add tests for Google Chat ephemeral messages
- status: completed
- - id: discord-adapter
- content: Implement postEphemeral with DM fallback (no native support)
- status: completed
- - id: teams-adapter
- content: Implement postEphemeral with DM fallback (no native support)
- status: completed
- - id: exports
- content: Export EphemeralMessage and PostEphemeralOptions types from chat package index
- status: completed
- - id: docs
- content: Add compatibility table to README with platform behavior notes
- status: completed
-isProject: false
----
-
-# Ephemeral Messages Implementation
-
-## Platform Behavior Summary
-
-| Platform | Behavior | Notes |
-
-|----------|----------|-------|
-
-| Slack | Native ephemeral | Session-dependent, visible only to target user in
-
-channel |
-
-| Google Chat | Native private message | Uses `privateMessageViewer` field,
-
-persists |
-
-| Discord | Fallback to DM | No native ephemeral outside interactions |
-
-| Teams | Fallback to DM | No native ephemeral support |
-
-## Core Types
-
-Add to [packages/chat/src/types.ts](packages/chat/src/types.ts):
-
-```typescript
-/** Result of posting an ephemeral message */
-interface EphemeralMessage {
- /** Message ID (may be empty for some platforms) */
- id: string;
- /** Thread ID where message was sent (or DM thread if fallback) */
- threadId: string;
- /** Whether this used native ephemeral or fell back to DM */
- usedFallback: boolean;
- /** Platform-specific raw response */
- raw: unknown;
-}
-
-/** Options for postEphemeral */
-interface PostEphemeralOptions {
- /**
- * If true, falls back to sending a DM when native ephemeral is not supported.
- * If false, returns null when native ephemeral is not supported.
- */
- fallbackToDM: boolean;
-}
-```
-
-Add to `Adapter` interface:
-
-```typescript
-/**
- * Post an ephemeral message visible only to a specific user.
- * If not implemented, Thread will fallback to openDM + postMessage.
- */
-postEphemeral?(
- threadId: string,
- userId: string,
- message: AdapterPostableMessage,
-): Promise;
-```
-
-Add to `Thread` interface with comprehensive TSDoc:
-
-````typescript
-/**
- * Post an ephemeral message visible only to a specific user.
- *
- * **Platform Behavior:**
- * - **Slack**: Native ephemeral (session-dependent, disappears on reload)
- * - **Google Chat**: Native private message (persists, only target user sees it)
- * - **Discord**: No native support - requires fallbackToDM: true
- * - **Teams**: No native support - requires fallbackToDM: true
- *
- * @param user - User ID string or Author object (from message.author or event.user)
- * @param message - Message content (string, markdown, card, etc.)
- * @param options.fallbackToDM - Required. If true, falls back to DM when native
- * ephemeral is not supported. If false, returns null when unsupported.
- * @returns EphemeralMessage with `usedFallback: true` if DM was used, or null
- * if native ephemeral not supported and fallbackToDM is false
- *
- * @example
- * ```typescript
- * // Always send (DM fallback on Discord/Teams)
- * await thread.postEphemeral(user, 'Only you can see this!', { fallbackToDM: true })
- *
- * // Only send if native ephemeral supported (Slack/GChat)
- * const result = await thread.postEphemeral(user, 'Secret!', { fallbackToDM: false })
- * if (!result) {
- * // Platform doesn't support native ephemeral - handle accordingly
- * }
- * ```
- */
-postEphemeral(
- user: string | Author,
- message: string | PostableMessage | CardJSXElement,
- options: PostEphemeralOptions,
-): Promise;
-````
-
-## Implementation by Package
-
-### 1. packages/chat (Core)
-
-**thread.ts** - Implement with fallback logic:
-
-```typescript
-async postEphemeral(
- user: string | Author,
- message: string | PostableMessage | CardJSXElement,
- options: PostEphemeralOptions,
-): Promise {
- const { fallbackToDM } = options;
- const userId = typeof user === "string" ? user : user.userId;
-
- // Convert JSX to card if needed
- const postable = this.normalizeMessage(message);
-
- // Try native ephemeral if adapter supports it
- if (this.adapter.postEphemeral) {
- return this.adapter.postEphemeral(this.id, userId, postable);
- }
-
- // No native support - either fallback to DM or return null
- if (!fallbackToDM) {
- return null;
- }
-
- // Fallback: send via DM
- if (this.adapter.openDM) {
- const dmThreadId = await this.adapter.openDM(userId);
- const result = await this.adapter.postMessage(dmThreadId, postable);
- return {
- id: result.id,
- threadId: dmThreadId,
- usedFallback: true,
- raw: result.raw,
- };
- }
-
- // No DM support either - return null
- return null;
-}
-```
-
-### 2. packages/adapter-slack
-
-Uses Slack Web API `chat.postEphemeral`:
-
-```typescript
-async postEphemeral(
- threadId: string,
- userId: string,
- message: AdapterPostableMessage,
-): Promise {
- const { channel } = this.decodeThreadId(threadId);
-
- // Handle cards vs text
- const card = extractCard(message);
- if (card) {
- const blocks = cardToBlockKit(card);
- const result = await this.client.chat.postEphemeral({
- channel,
- user: userId,
- text: cardToFallbackText(card),
- blocks,
- });
- return { id: result.message_ts || "", threadId, usedFallback: false, raw: result };
- }
-
- const text = this.formatConverter.renderPostable(message);
- const result = await this.client.chat.postEphemeral({
- channel,
- user: userId,
- text,
- });
-
- return { id: result.message_ts || "", threadId, usedFallback: false, raw: result };
-}
-```
-
-Key details:
-
-- Requires `chat:write` scope (already configured)
-- User must be member of channel
-- Messages are session-dependent (don't persist across reloads)
-- Note: `thread_ts` is NOT supported for ephemeral messages
-
-### 3. packages/adapter-gchat
-
-Uses `privateMessageViewer` field:
-
-```typescript
-async postEphemeral(
- threadId: string,
- userId: string,
- message: AdapterPostableMessage,
-): Promise {
- const { spaceName, threadName } = this.decodeThreadId(threadId);
-
- const card = extractCard(message);
- const requestBody: chat_v1.Schema$Message = {
- privateMessageViewer: { name: userId }, // e.g. "users/123456"
- thread: threadName ? { name: threadName } : undefined,
- };
-
- if (card) {
- requestBody.cardsV2 = [cardToGoogleCard(card)];
- } else {
- requestBody.text = this.formatConverter.renderPostable(message);
- }
-
- const response = await this.chatApi.spaces.messages.create({
- parent: spaceName,
- messageReplyOption: threadName ? "REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD" : undefined,
- requestBody,
- });
-
- return {
- id: response.data.name || "",
- threadId,
- usedFallback: false,
- raw: response.data,
- };
-}
-```
-
-Key details:
-
-- userId format: `users/123456789`
-- Supports threading (unlike Slack)
-- Messages persist (unlike Slack's session-dependent behavior)
-
-### 4. packages/adapter-discord
-
-No native ephemeral outside interactions - relies on Thread fallback to DM:
-
-```typescript
-// No postEphemeral method - Thread.postEphemeral will use openDM fallback
-// openDM is already implemented
-```
-
-### 5. packages/adapter-teams
-
-No native ephemeral - relies on Thread fallback to DM:
-
-```typescript
-// No postEphemeral method - Thread.postEphemeral will use openDM fallback
-// openDM is already implemented
-```
-
-## Test Files
-
-- `packages/adapter-slack/src/index.test.ts` - Test native ephemeral
-- `packages/adapter-gchat/src/index.test.ts` - Test native ephemeral
-- `packages/chat/src/thread.test.ts` - Test postEphemeral with fallback logic
-
-## Documentation
-
-Add to README.md under a new "Ephemeral Messages" section:
-
-````markdown
-### Ephemeral Messages
-
-Send a message visible only to a specific user:
-
-```typescript
-await thread.postEphemeral(user, "Only you can see this!", {
- fallbackToDM: true,
-});
-```
-````
-
-The `fallbackToDM` option is required and controls behavior on platforms without
-native ephemeral support:
-
-- `fallbackToDM: true` - Send as DM if native ephemeral isn't supported
-- `fallbackToDM: false` - Return `null` if native ephemeral isn't supported
-
-#### Platform Behavior
-
-| Platform | Native Support | Behavior | Where it appears | Persistence |
-| ----------- | -------------- | -------------------- | ------------------------------------------- | ---------------------------------------- |
-| Slack | Yes | Ephemeral in channel | In the channel, only visible to target user | Session-only (disappears on page reload) |
-| Google Chat | Yes | Private message | In the space, only visible to target user | Persists until deleted |
-| Discord | No | DM (if enabled) | In a DM conversation with the bot | Persists in DM |
-| Teams | No | DM (if enabled) | In a DM conversation with the bot | Persists in DM |
-
-**Key differences:**
-
-- **Slack**: True ephemeral - message appears in the channel context but
- disappears when the user refreshes. Other users never see it.
-- **Google Chat**: Private message viewer - message appears in the space but
- only the target user can see it. It persists and can be deleted by the bot.
-- **Discord/Teams**: No native ephemeral support. With `fallbackToDM: true`,
- sends a DM instead. With `fallbackToDM: false`, returns `null`.
-
-#### Examples
-
-**Always deliver the message (DM fallback):**
-
-```typescript
-const result = await thread.postEphemeral(user, "Private notification", {
- fallbackToDM: true,
-});
-
-if (result.usedFallback) {
- // Was sent as DM on Discord/Teams
- console.log(`Sent as DM: ${result.threadId}`);
-}
-```
-
-**Only send if native ephemeral is supported:**
-
-```typescript
-const result = await thread.postEphemeral(user, "Contextual hint", {
- fallbackToDM: false,
-});
-
-if (!result) {
- // Platform doesn't support native ephemeral (Discord/Teams)
- // Message was not sent - handle accordingly or skip
-}
-```
-
-```
-
-## Files to Modify
-
-1. `packages/chat/src/types.ts` - Add EphemeralMessage, PostEphemeralOptions
-
-types and interface methods
-
-1. `packages/chat/src/thread.ts` - Implement postEphemeral with fallback logic
-2. `packages/chat/src/index.ts` - Export EphemeralMessage, PostEphemeralOptions
-
-types
-
-1. `packages/adapter-slack/src/index.ts` - Implement postEphemeral
-2. `packages/adapter-gchat/src/index.ts` - Implement postEphemeral
-3. `README.md` - Add ephemeral messages documentation section
-
-```