The agent now implements HasTools and returns three tool instances
(GenerateImage, GenerateVideo, GenerateAudio) each wired with the
workspace + post + userId context.
Replaced the MEDIA GENERATION RULES block in the system prompt. Tools
replace the old [GENERATE_IMAGE:vertical] regex convention — the LLM
now invokes tools directly with typed parameters (prompt, orientation),
which eliminates the fragile text parsing in the controller.
Each tool implements Laravel\Ai\Contracts\Tool with description(),
handle(Request), and schema(JsonSchema). They share a common pattern:
- Constructor receives Workspace, optional Post, userId, and an optional
AttachmentCollector (resolves from container if not injected).
- handle() does the work (Image::of(), Audio::of(), or calls our custom
VideoGenerationService for Veo), persists media + usage log, pushes
the full attachment shape into the collector, and returns a short
text summary to the LLM.
- schema() exposes typed parameters with enum constraints and doc
strings so the LLM selects valid inputs.
Also fix the Ai::extend closure signature — MultipleInstanceManager
passes (app, config) not just (config) to custom creators.
The agent implements Laravel AI SDK's Agent + Conversational contracts:
- instructions() renders the existing Blade system prompt with workspace
brand context (name, description, website, tone, voice notes, locale)
- messages() reads directly from the AiMessage model scoped to the post,
so our existing conversation persistence stays the single source of
truth — no duplicate SDK-managed storage.
- Assistant messages with attachments are enriched inline with a summary
like [This assistant message attached: 2 image] so the model tracks
progress through carousel-style multi-image generations.
- provider() maps the trypost.ai.text_provider config to Lab::Gemini or
Lab::OpenAI, preserving the existing provider-switching behavior.
The stock provider only exposes 1:1, 2:3, 3:2 via defaultImageOptions()
match statement — any other ratio gets silently dropped to null.
ExtendedGeminiProvider adds 9:16, 16:9, 4:3, 3:4, 4:5, 5:4, 21:9 so
Image::of()->size('9:16') now works for Reels/Stories/TikTok/Shorts
and size('16:9') for X/LinkedIn/YouTube landscape content.
Gemini's native API supports all these ratios; the SDK restriction was
purely in the provider's match statement.
AiUsageLogTest and UsageController still passed string 'image'/'video'
arguments to monthlyCount() after the enum migration. Update them to
pass UsageType::Image and UsageType::Video.
- Block all billing/usage/subscribe routes in self-hosted mode (redirect to calendar)
- Hide billing_email field in account settings when self-hosted
- Migrate deprecated Facebook Page Insights metrics to new Media Views API (v25.0)
- Fix X analytics token refresh to use Basic Auth (matching XPublisher/ConnectionVerifier)
- Prevent duplicate social accounts by using updateOrCreate across all OAuth controllers
- Sidebar reorganized: Workspace group (connections, hashtags, labels,
API keys, settings) and Account group (settings, usage, billing)
- Account group only visible to owner and hidden in self-hosted mode
- Onboarding simplified: role -> account (connect socials) -> completed
-> redirect to /subscribe. Removed Subscription setup step.
- Subscribe page redesigned with 4 plan cards, monthly/yearly toggle,
trial info, and per-plan features list
- Billing page redesigned following Sendkit layout (sections with
sidebar labels)
- Processing page uses usePoll with immediate watch for subscription
activation
- Cancel URL redirects directly to /subscribe
- Account settings page with name and billing_email (syncs with Stripe)
- Usage page with ring meters for all plan limits
- Settings layout tabs only for user pages (profile, password,
notifications). Workspace/API keys/billing are standalone pages.
- GoogleAuthButton extracted as reusable component
- WorkspaceRole TypeScript enum for type-safe role checks in frontend
- Trial period changed to 7 days
- Fixed onboarding loop when user confirms email
- All 1101 tests passing
- Create Account model as Cashier Billable entity (stripe, plan, subscription)
- Account owns workspaces and has an owner_id (User)
- User belongs to one Account via account_id
- Workspace belongs to Account via account_id, no longer has billing fields
- Remove Brand model entirely (workspaces serve as grouping)
- Rename brand_limit to workspace_limit in plans
- Workspace roles simplified: admin/member/viewer (owner via Account)
- Invites now belong to Account with workspaces JSON array
- Pennant features scope changed from Workspace to Account
- EnsureSubscribed middleware checks Account subscription
- All controllers updated: BillingController, OnboardingController,
WorkspaceInviteController, SocialController, StripeEventListener
- Frontend: extract GoogleAuthButton component, create WorkspaceRole
enum for type-safe role checks, fix all views for new architecture
- All 1101 tests passing
- New card-based layout showing connected accounts with avatar, platform
badge, brand label, and connection date
- "Add Social" button opens dialog with platform grid for connecting
- Removed unique constraint on workspace_id+platform to allow multiple
accounts per platform
- Removed "already connected" checks from all 13 OAuth controllers
- Updated tests to verify multi-account connection works
Instead of separate alter-table migrations, edit the original
create_table migrations directly since we're resetting the database.
- Users table: removed stripe_id, pm_type, pm_last_four, trial_ends_at
- Workspaces table: added plan_id, stripe_id, pm_type, pm_last_four, trial_ends_at
- Subscriptions table: changed user_id FK to workspace_id FK
- Social accounts table: added brand_id FK
- Plans and brands tables ordered before their dependents
- Deleted 5 redundant alter-table migrations
The mock shorthand Mockery::mock([...]) was not compatible with the
Socialite facade mock. Replaced with explicit shouldReceive chains
matching each controller's actual Socialite method calls.
Also keeps Google OAuth routes always registered so Wayfinder can
generate TypeScript helpers regardless of GOOGLE_AUTH_ENABLED setting.
Integrates YouTube Analytics API v2 to display channel-level metrics
(views, minutes watched, avg view duration, avg view percentage,
subscribers gained/lost, likes) with date range support and caching.