Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 34 additions & 34 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/open-api-docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ openapi: 3.0.3
info:
title: The Agent's user-facing API
description: The user-facing parts of The Agent's API service (excluding system-level endpoints, chat completion, maintenance endpoints, etc.)
version: 5.16.1
version: 5.17.0
license:
name: MIT
url: https://opensource.org/licenses/MIT
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-05
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
## Context

The social card system renders tweet screenshots as SVG (via `card_template.py`) then rasterizes with `resvg_py`. Tweet text currently includes raw `t.co` URLs that are either self-referencing media links (redundant) or external URLs (meaningful but ugly). The Twitter API v2 provides an `entities` field with URL expansion metadata including OG title, description, images, and unwound URLs β€” but we don't request it today.

## Goals / Non-Goals

**Goals:**
- Strip self-referencing t.co URLs (media links) from rendered tweet text
- Render external URLs as rich link preview cards within the social card
- Support two visual modes: with OG image (blur overlay) and without (favicon-based)
- Keep the link preview renderer as a standalone, testable component
- Maintain visual consistency with existing card elements (corner radius, padding, fonts)

**Non-Goals:**
- Rendering video previews or animated content
- Caching OG metadata beyond what Twitter API provides
- Supporting non-Twitter social card sources
- Interactive elements (the output is a static PNG)

## Decisions

### 1. Link preview as SVG snippet generator

The link preview renderer produces SVG fragment strings (defs + content) that `card_template.py` composes into the full SVG. This mirrors how photos are already handled β€” no new rendering pipeline.

**Alternative**: Render link previews as separate images and embed as base64. Rejected because it adds complexity and a second render pass.

### 2. Blur via feGaussianBlur on clipped image duplicate

For the overlay panel, we duplicate the OG image region, clip it to the overlay bounds, apply `feGaussianBlur`, then overlay the semi-transparent rect and text. This is the standard SVG approach and `resvg` already supports `feGaussianBlur` (we use `feDropShadow` which wraps it).

**Alternative**: CSS `backdrop-filter` β€” not supported in SVG/resvg.

### 3. Color computation reuses existing theme infrastructure

The overlay contrast color is computed the same way as `_accent_color` / `_contrast_text` in `card_template.py` β€” extract dominant color from OG image, compute contrasting overlay fill and text color. For the no-image case, contrast against `theme.gradient_start`.

### 4. Favicon fetching with graceful fallback

Fetch `https://{domain}/favicon.ico` with a short timeout. If it fails or returns non-image content, fall back to the 🌐 emoji rendered via the emoji font. Favicon is embedded as base64 in the SVG.

**Alternative**: Parse HTML for `<link rel="icon">` tags. Rejected as too complex for marginal gain β€” most sites serve `/favicon.ico`.

### 5. URL shortening for displayed external links

External URLs are shortened using our existing URL shortener (same as the tweet card footer URL) with 365-day expiry. The shortened URL is displayed in the preview; the original expanded URL is what was shortened.

### 6. Data flow through TweetData

Add a new `TweetLinkPreview` dataclass to hold per-link metadata (title, description, OG image URL, expanded URL, domain). `TweetData` gets a new `link_previews: list[TweetLinkPreview]` field populated during `__parse_structured`. The orchestrator fetches OG images and favicons, then passes bytes to the renderer.

## Risks / Trade-offs

- **Favicon fetch latency** β†’ Short timeout (2s), parallel with OG image downloads via existing `PhotoDownloader`
- **Missing OG metadata** β†’ Graceful degradation: no title/description = skip preview entirely
- **Large OG images** β†’ We already handle image downloads for tweet photos; same size constraints apply
- **feGaussianBlur performance** β†’ Blur is applied to a small clipped region (~40% of a 3:2 box), not the full image. Negligible render cost.
- **Twitter API rate limits** β†’ No additional API calls needed; `entities` is part of the same tweet fetch response
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
## Why

Tweets containing external URLs currently display raw `t.co` shortened links in the social card text, which are ugly and meaningless to the viewer. Additionally, t.co links referencing the tweet's own media (photos) are redundant since we already render those as attached images. We need to strip self-referencing URLs and render external links as rich preview cards β€” similar to how Twitter/iMessage render link previews.

## What Changes

- Strip t.co URLs from tweet text when they reference the tweet's own media
- Request `entities` field from Twitter API to get URL expansion metadata
- Build a new link preview renderer that generates SVG snippets for external URLs
- Render link previews with OG image (blurred overlay panel) or without (favicon-based fallback)
- Integrate link preview boxes into the social card layout, positioned above post photos
- Fetch favicons from linked domains for display in the preview
- Shorten external URLs using our URL shortener before display

## Capabilities

### New Capabilities
- `link-preview-rendering`: Standalone SVG snippet generator for external URL preview boxes, supporting two layouts (with/without OG image), blur effects, favicon fetching, and color contrast computation
- `tweet-url-processing`: Logic to classify t.co URLs (media self-reference vs external), strip redundant ones from text, expand external URLs via entities metadata, and shorten them for display

### Modified Capabilities

## Impact

- `src/features/social_cards/card_template.py` β€” new section for link previews above photos
- `src/features/web_browsing/twitter_status_fetcher.py` β€” add `entities` to API params, strip/classify URLs, pass metadata downstream
- `src/features/social_cards/` β€” new module(s) for link preview rendering
- `src/features/social_cards/card_layout.py` β€” new layout constants for link preview dimensions
- `TweetData` dataclass β€” new field for external link metadata
- External dependency: favicon fetching from arbitrary domains (HTTP GET)
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
## ADDED Requirements

### Requirement: Link preview with OG image renders blur overlay panel

The system SHALL render a link preview box at full card inner width with a 3:2 aspect ratio OG image on top. The bottom ~40% SHALL contain an info overlay panel with a blurred copy of the image region underneath, covered by a semi-transparent rect (30% opacity) whose color contrasts to the OG image's average color. Text color SHALL contrast to the overlay color.

#### Scenario: External link with OG image and metadata
- **WHEN** a tweet contains an external URL with OG image, title, and description available from Twitter entities
- **THEN** the renderer produces an SVG snippet with the OG image cropped to 3:2, a feGaussianBlur filter on the bottom image region, a 30% opacity overlay rect with bottom corners rounded, and text content (title bold max 2 lines, description max 3 lines, small favicon + domain)

#### Scenario: OG image overlay corner radius
- **WHEN** the overlay panel is rendered on top of an OG image
- **THEN** only the bottom corners SHALL be rounded (matching photo corner radius), top corners SHALL be square since they overlap the image

### Requirement: Link preview without OG image renders favicon-based fallback

The system SHALL render a fully-rounded rect when no OG image is available. The rect SHALL be semi-transparent (30% opacity) with color contrasting to the social card's background gradient. A large favicon SHALL be placed on the left side, vertically centered against the text block. Text color SHALL contrast to the rect color.

#### Scenario: External link without OG image
- **WHEN** a tweet contains an external URL with title and description but no OG image
- **THEN** the renderer produces an SVG snippet with a fully-rounded semi-transparent rect, a large favicon on the left vertically centered, and text on the right (title bold max 2 lines, description max 3 lines, small favicon + domain on bottom line)

#### Scenario: Favicon unavailable
- **WHEN** the favicon cannot be fetched from the linked domain
- **THEN** the system SHALL use a 🌐 globe emoji as fallback, rendered via the emoji font

### Requirement: Domain display is shortened

The system SHALL display only the subdomain.domain.tld portion of the URL (e.g., "nasa.gov", "wikipedia.org"), not the full path.

#### Scenario: URL with path and query params
- **WHEN** the expanded URL is "https://www.nasa.gov/news-release/crew-13/?utm_source=twitter"
- **THEN** the displayed domain SHALL be "nasa.gov"

#### Scenario: URL with subdomain
- **WHEN** the expanded URL is "https://docs.python.org/3/library/re.html"
- **THEN** the displayed domain SHALL be "docs.python.org"

### Requirement: Text truncation with ellipsis

Title SHALL be bold, maximum 2 lines with ellipsis truncation. Description SHALL be regular weight, maximum 3 lines with ellipsis truncation.

#### Scenario: Title exceeds 2 lines
- **WHEN** the link title text would wrap beyond 2 lines at the rendered font size
- **THEN** the title SHALL be truncated at 2 lines with trailing ellipsis character

#### Scenario: Description exceeds 3 lines
- **WHEN** the link description text would wrap beyond 3 lines at the rendered font size
- **THEN** the description SHALL be truncated at 3 lines with trailing ellipsis character

### Requirement: Multiple link previews stack vertically

When multiple external links exist in a tweet, their preview boxes SHALL be stacked vertically with the same gap used between photos.

#### Scenario: Tweet with two external links
- **WHEN** a tweet text contains two external URLs with metadata
- **THEN** two link preview boxes SHALL be rendered stacked vertically with PHOTO_GAP spacing between them

### Requirement: Link previews are positioned above post photos

Link preview boxes SHALL appear in the card layout after the tweet body text and before any attached post photos.

#### Scenario: Tweet with external link and photos
- **WHEN** a tweet has both an external URL and attached photos
- **THEN** the link preview box SHALL render between the body text section and the photos section

### Requirement: Missing metadata omits preview

If no title and no description are available for an external URL, the link preview SHALL be omitted entirely.

#### Scenario: External URL with no OG metadata
- **WHEN** the Twitter API entities provide no title and no description for an external URL
- **THEN** no link preview box SHALL be rendered for that URL
Loading
Loading