Composer Mode¶
Composer Mode transforms Tuitbot from a fully autonomous agent into a user-driven writing tool with on-demand AI intelligence. The same scoring engine, LLM integration, and safety guardrails power both modes — the difference is who decides when to post.
The composer UX is designed to be faster, more structurally powerful, and more accessible than any comparable tool. It gives you keyboard-first control over every aspect of content creation while providing real-time preview, server-backed autosave with revision history, and AI-assisted writing.
All writing flows through Draft Studio (/drafts), a three-zone workspace with a draft rail, full composer, and details/history panel. The home page, calendar, and Cmd+N shortcut all create server-backed drafts and redirect into Draft Studio.
Enabling Composer Mode¶
Dashboard Settings¶
Open the Settings page and select Composer from the Operating Mode dropdown.
config.toml¶
mode = "composer"
Environment variable¶
export TUITBOT_MODE=composer
The default mode is autopilot, which preserves the fully autonomous behavior.
What Changes¶
| Capability | Autopilot | Composer |
|---|---|---|
| Discovery loop | Active — finds and queues replies autonomously | Read-only — scores tweets for the Discovery Feed, never queues |
| Mentions loop | Active | Disabled |
| Target monitoring loop | Active | Disabled |
| Content posting loop | Active | Disabled |
| Thread publishing loop | Active | Disabled |
| Posting queue | Active | Active |
| Approval poster | Active | Active |
| Analytics snapshots | Active | Active |
| Token refresh | Active | Active |
| Approval mode | Configurable (approval_mode) |
Always on (implicit) |
| AI Assist | Available | Available |
| Drafts | Available | Available |
| Discovery Feed | Available | Available |
In Composer mode, you write and schedule content yourself. Tuitbot provides AI assistance on demand, surfaces interesting conversations through the Discovery Feed, and handles the mechanics of posting, scheduling, and analytics.
Thread Composer¶
The thread composer uses a card-based editor where each tweet in the thread is a visual card with its own textarea, character counter, and media slot. Both tweet and thread modes use a two-pane layout: editor on the left, X-accurate preview on the right (stacked vertically on mobile). See Preview Fidelity for details on the preview rendering.
Data Model¶
Each thread card is a ThreadBlock:
{
"id": "uuid-v4",
"text": "Tweet content",
"media_paths": ["path/to/image.jpg"],
"order": 0
}
Threads are stored as a ThreadBlocksPayload: { "version": 1, "blocks": [...] }. The server also accepts the legacy content string format (JSON-stringified text array) for backwards compatibility.
Validation¶
- Minimum 2 cards (single-card content should use tweet mode)
- Maximum 280 characters per card (weighted: URLs count as 23 characters, emoji as 2)
- No empty cards allowed
- Per-card media limits apply independently
Power Actions¶
Four structural operations give you fine-grained control over thread composition — all accessible via keyboard shortcuts, drag-and-drop, or the command palette:
| Action | Shortcut (Mac) | Description |
|---|---|---|
| Reorder | ⌥↑ / ⌥↓ or drag handle |
Move a card up or down in the thread order |
| Duplicate | ⌘D |
Copy the current card (text + media) as a new card below |
| Split | ⌘⇧S |
Split the current card into two cards at the cursor position |
| Merge | ⌘⇧M |
Combine the current card with the card below it |
Typefully offers only a single reorder action (drag-and-drop). Tuitbot provides 4 keyboard-accessible power actions that let you restructure threads without lifting your hands from the keyboard.
Per-Tweet Media¶
Each thread card has its own media slot supporting file picker and drag-and-drop attachment. Media constraints per card:
- Images: Up to 4 images, max 5 MB each (JPEG, PNG, WebP)
- GIF: 1 GIF, max 15 MB (exclusive — cannot combine with images or video)
- Video: 1 video, max 512 MB (MP4, exclusive — cannot combine with images or GIF)
Media follows its card on reorder, duplicate, and split operations.
Inspector Rail¶
The inspector is a collapsible right-side panel that houses all secondary composer controls — scheduling, voice context, and AI actions — so the main writing canvas stays clean.
Sections¶
| Section | Content |
|---|---|
| Schedule | TimePicker with preferred slots and custom time input. Shows "Posts immediately unless scheduled" hint when no time is selected. |
| Voice | Voice context panel (inline mode) showing brand voice, content style, pillars, and the quick cue input. |
| AI | Two action buttons: "AI Generate" (or "AI Improve" when content exists) and "From Notes". Hint: ⌘J to improve selected text. |
| From Notes | Expands when triggered from the AI section. Paste notes, generate content. Collapses independently. |
Toggle¶
- Keyboard:
⌘I(Mac) /Ctrl+I(Windows/Linux) - Header button: PanelRight icon in the compose modal header bar
- Command palette: "Toggle inspector" action
- Default state: Open on desktop, closed on mobile. Persisted in
localStorage(tuitbot:inspector:open).
Mobile Behavior¶
On screens narrower than 768px, the inspector renders as a bottom drawer overlay instead of an inline rail. The drawer: - Slides up with a pill-shaped drag handle - Has a semi-transparent backdrop (click to close) - Maxes out at 60vh height, scrollable - Closes on Escape (before focus mode in the escape cascade)
Layout¶
When the inspector is open on desktop, the compose modal widens from 640px to 900px. The main canvas and inspector rail sit side-by-side using flexbox. When closed, the modal returns to its standard 640px width.
Distraction-Free Mode¶
Toggle with ⌘⇧F (Mac) / Ctrl+Shift+F (Windows/Linux) or the focus mode button in the modal header.
Focus mode expands the compose modal to fill the entire viewport, hiding surrounding UI chrome. The editor, preview, and inspector rail are all preserved — all functionality, shortcuts, command palette, and AI assist remain fully accessible. The inspector can be independently toggled within focus mode.
Press Escape to exit focus mode (the modal stays open). This follows the escape cascade: pressing Escape repeatedly closes layers in order — command palette, from-notes panel, inspector drawer (mobile), focus mode, then the modal itself.
Command Palette¶
Press ⌘K (Mac) / Ctrl+K (Windows/Linux) to open the command palette.
The palette provides fuzzy search over 15 compose actions organized into 4 categories: Mode, Compose, AI, and Thread. Thread-specific actions are only visible when in thread mode. Each action that has a direct keyboard shortcut displays the hint inline.
Navigate with ↑ / ↓ arrow keys, execute with Enter, close with Escape.
For the full list of palette actions, see the Keyboard Shortcuts.
Keyboard Shortcuts¶
16 keyboard shortcuts cover all compose operations. Shortcuts are platform-aware (⌘ on Mac, Ctrl on Windows/Linux) and are active only while the Compose Modal is open.
Quick Reference¶
| Action | Mac | Win/Linux | When |
|---|---|---|---|
| Submit / Post | ⌘↩ |
Ctrl+Enter |
Always |
| Command palette | ⌘K |
Ctrl+K |
Always |
| Focus mode | ⌘⇧F |
Ctrl+Shift+F |
Always |
| Toggle inspector | ⌘I |
Ctrl+I |
Always |
| Toggle preview | ⌘⇧P |
Ctrl+Shift+P |
Always |
| AI improve | ⌘J |
Ctrl+J |
Always |
| Tweet mode | ⌘⇧N |
Ctrl+Shift+N |
Always |
| Thread mode | ⌘⇧T |
Ctrl+Shift+T |
Always |
| Close | Esc |
Esc |
Always |
| Insert separator | ⌘⇧↩ |
Ctrl+Shift+Enter |
Thread |
| Backspace merge | ⌫ at pos 0 |
Backspace at pos 0 |
Thread |
| Move card up/down | ⌥↑ / ⌥↓ |
Alt+↑/↓ |
Thread |
| Duplicate card | ⌘D |
Ctrl+D |
Thread |
| Split at cursor | ⌘⇧S |
Ctrl+Shift+S |
Thread |
| Merge with next | ⌘⇧M |
Ctrl+Shift+M |
Thread |
| Next / prev card | Tab / ⇧Tab |
Tab / Shift+Tab |
Thread |
Full reference with descriptions: Keyboard Shortcuts.
Typefully provides only Cmd+Enter for submission. Tuitbot provides 16 shortcuts covering every compose operation — you can create, restructure, and submit a thread without touching the mouse.
Auto-Save & Recovery¶
Draft Studio uses server-backed autosave with a 500 ms debounce. Every keystroke is persisted to the server, with revision history tracking all changes. The sync badge in the composer header shows the current save status (saved, saving, unsaved, offline, conflict).
- Conflict resolution: If two sessions edit the same draft, a conflict banner offers "Use mine" or "Reload server" options.
- Revision history: Every autosave creates a revision. View and restore past versions from the History panel (
Cmd+Shift+H). - Fallback:
localStoragerecovery is retained as a last-resort fallback for offline scenarios, using the storage keytuitbot:compose:draftwith a 7-day TTL.
Edge Cases¶
- Multiple browser tabs editing the same draft may trigger conflict resolution
- If the server is unreachable, sync status shows "offline" and retries on reconnection
localStoragefallback kicks in only when the server autosave path is unavailable
Voice Context¶
The Voice Context panel lives in the inspector rail's Voice section, giving you visibility into and control over the persona guiding AI generation. When the inspector is closed, voice settings are still applied — they persist in memory and affect all AI calls.
What it shows¶
- Brand voice — your configured voice personality (from Settings > Content Persona)
- Content style — your content style setting
- Content pillars — up to 3 topic pillars displayed as chips
- If no voice settings are configured, a hint links to Settings
When rendered in the inspector, the panel displays in inline mode (always expanded, no toggle). The standalone version collapses by default and remembers its state in localStorage (tuitbot:voice:expanded).
Quick Cue¶
The quick cue input lets you steer AI output with a tone directive (e.g., "more casual", "technical", "provocative"). The cue is threaded into assist calls:
- Improve (⌘J): Passed as the
contextparameter to/api/assist/improve - Tweet generation: Prepended to the topic string as
[Tone: <cue>] <topic> - Thread generation: Same prepend strategy
- From Notes: Prepended to the notes input
Cues are saved to a most-recently-used list (up to 5) in localStorage (tuitbot:voice:saved-cues). Click a saved cue to reuse it.
Data flow¶
The VoiceContextPanel reads settings from the config store ($lib/stores/settings). On modal open, if settings haven't been loaded yet, a fallback loadSettings() call fetches them. The quick cue value flows up to ComposeModal via oncuechange, which threads it into all AI assist calls.
Preview Fidelity¶
Both tweet and thread modes display a live preview inline below the editor. The preview can be toggled with ⌘⇧P (Mac) / Ctrl+Shift+P (Windows/Linux) or the eye icon in the header bar. On mobile (< 768px), the preview stacks vertically below the editor.
What the preview emulates¶
- Tweet card chrome — Avatar placeholder, handle, tweet numbering, and text rendering
- Thread connectors — Vertical line between cards showing thread continuity
- X-accurate media grids — 1, 2, 3, and 4 image arrangements that match X's layout patterns
- Video poster frame — First frame of video with centered play icon overlay
- Crop severity indicator — Small "cropped" badge when an image's shape significantly differs from the display slot
Media Grid Rules¶
| Image count | Grid layout | Slot aspect ratios |
|---|---|---|
| 1 | Full width | 16:9 landscape |
| 2 | Side by side | 4:5 portrait each |
| 3 | Left tall + right stacked | Left: 2:3, Right: 1:1 each |
| 4 | 2x2 grid | 1:1 square each |
All images use object-fit: cover to fill their slot, matching X's cropping behavior. Maximum 4 images displayed per tweet (X's limit).
Crop Indicator¶
When an image's intrinsic aspect ratio deviates significantly (> 30%) from its display slot, a subtle "cropped" badge appears in the bottom-right corner of the image. This helps you anticipate how your image will appear on X before posting.
Crop severity is calculated from the ratio between the image's natural dimensions and the slot's target aspect ratio. The indicator appears after the image loads (brief delay for local files, typically < 50ms).
Preview Fidelity Constants¶
The exact aspect ratios and crop math are defined in dashboard/src/lib/utils/mediaDimensions.ts. This utility provides:
X_SLOT_RATIOS— Display slot aspect ratios per media countcalculateCropSeverity()— Numeric crop severity (0 = no crop, 1 = extreme)CROP_SEVERITY_THRESHOLD— Threshold above which the crop badge appears (0.3)
Known Limitations¶
- No URL unfurling or link card preview
- No GIF animation toggle in preview
- No quote-tweet or poll preview
- No dark/light theme preview switching (follows the app theme)
- Crop indicator appears after image loads (brief delay)
- Preview shows a maximum of 4 images per tweet (matches X)
AI Assist¶
AI Assist provides on-demand content generation powered by your configured LLM. It uses the same persona, content frameworks, and topic knowledge as the autonomous loops — but only generates content when you ask.
Inline AI Improve (⌘J)¶
Select text in the tweet editor and press ⌘J to improve just the selection. If no text is selected, the entire tweet content is improved. In thread mode, the improvement targets the focused card.
Generate from Notes¶
Click the notes button in the modal footer or select "Generate from notes" from the command palette. Paste rough notes or bullet points, and AI generates a polished tweet or thread from them.
- Inline confirmation: If existing content is present, an inline banner asks "This will replace your current content" with Replace / Cancel buttons (no browser
confirm()dialog). - Loading shimmer: While generating, a shimmer animation overlays the textarea to indicate progress.
- Undo: After generation replaces content, an "Undo" button appears for 10 seconds. Clicking it restores the previous content.
AI Assist Button¶
The inspector rail's AI section contains an "AI Generate" / "AI Improve" button with context-aware behavior: - Tweet mode with content: Label shows "AI Improve" — runs AI Improve on the full text - Tweet mode without content: Label shows "AI Generate" — generates a new tweet on a general topic - Thread mode: Generates a full thread outline
The same action is available from the command palette as "AI Generate / Improve".
API Endpoints¶
| Method | Path | Description |
|---|---|---|
POST |
/api/assist/tweet |
Generate a tweet for a given topic |
POST |
/api/assist/reply |
Generate a reply to a specific tweet |
POST |
/api/assist/thread |
Generate a thread outline for a topic |
POST |
/api/assist/improve |
Improve or rephrase existing draft text |
GET |
/api/assist/topics |
Get suggested topics based on your profile and recent performance |
GET |
/api/assist/optimal-times |
Get recommended posting times based on historical engagement |
GET |
/api/assist/mode |
Get the current operating mode (autopilot or composer) |
Vault Context (Automatic)¶
When you use AI Assist — whether generating a tweet, generating a thread, or improving a draft — the backend automatically enriches the LLM prompt with context from your vault:
- Winning patterns: Your historically best-performing tweets (scored by engagement) provide examples of content that resonated with your audience.
- Relevant ideas: Notes ingested into the vault via content sources are surfaced as topic seeds when they match your configured product, competitor, or industry keywords.
This context is injected into the LLM system prompt alongside your existing voice settings and persona — it augments generation without replacing any user-supplied input.
How it works¶
- When an assist endpoint is called, the server loads your business profile keywords (
product_keywords,competitor_keywords,industry_topicsfromconfig.toml). - It queries the local SQLite database for winning ancestors (high-engagement tweets) and content seeds (vault notes matching your keywords).
- The resulting context block (capped at ~2000 characters) is injected into the system prompt before the generation task.
- The response shape is unchanged — no new fields, no new request parameters.
Affected endpoints¶
| Endpoint | Context injected |
|---|---|
POST /api/assist/tweet |
Vault context (automatic) |
POST /api/assist/thread |
Vault context (automatic) |
POST /api/assist/improve |
Vault context (automatic) + user-supplied tone cue (if provided) |
POST /api/assist/reply |
No vault context (uses different generation path) |
Fallback behavior¶
If vault data is unavailable — no keywords configured, empty database, config error, or a fresh account with no posting history — generation proceeds exactly as before. The feature is additive: its absence has zero impact on existing functionality.
No UI changes required¶
The vault context is resolved server-side and injected transparently. The existing quick-cue input, "From Notes" panel, and AI Generate/Improve actions in the composer all work through the same endpoints with unchanged request payloads. No frontend code was modified for this feature.
Compose Endpoint¶
The primary submission endpoint for tweets and threads:
| Method | Path | Description |
|---|---|---|
POST |
/api/content/compose |
Submit a tweet or thread for posting |
Request Body¶
{
"content_type": "tweet" | "thread",
"content": "string",
"blocks": [{"id": "uuid", "text": "...", "media_paths": [], "order": 0}],
"scheduled_for": "2026-03-01T14:30:00",
"media_paths": ["path/to/file.jpg"]
}
| Field | Required | Notes |
|---|---|---|
content_type |
Yes | "tweet" or "thread" |
content |
Yes | Tweet text, or JSON-stringified text array for threads (backwards compat) |
blocks |
No | Structured ThreadBlock[] for threads; takes precedence over content when present |
scheduled_for |
No | ISO 8601 datetime (without trailing Z); omit for immediate posting |
media_paths |
No | Server-side paths from /api/media/upload; for threads, per-card media is in blocks[].media_paths |
Media Upload¶
Upload media files before attaching them to tweets or thread cards:
| Method | Path | Description |
|---|---|---|
POST |
/api/media/upload |
Upload a media file (multipart form data) |
GET |
/api/media/file |
Serve an uploaded media file for preview |
Accepted types: JPEG, PNG, WebP, GIF, MP4. Size limits: images 5 MB, GIF 15 MB, video 512 MB.
Draft Studio¶
Draft Studio is the canonical writing workspace at /drafts. All compose entry points — home page, calendar, Cmd+N, sidebar — create server-backed drafts and open them in Draft Studio.
Workspace Layout¶
- Rail zone (left): Filterable, searchable list of drafts with tabs for Active, Scheduled, Posted, and Archive. Supports tag filtering and multiple sort orders.
- Composer zone (center): Full ComposeWorkspace with tweet/thread editing, preview, and AI assist.
- Details/History zone (right): Metadata editing (title, notes, tags, scheduling) and revision history with one-click restore.
Workflow¶
- Create a draft — from the home page quick-start, calendar time slot,
Cmd+N, or the rail's "+" button. - Edit the draft text, adjust metadata, or attach media. Thread drafts use the structured blocks format.
- Schedule the draft for a specific time from the details panel, or publish it immediately.
- Archive drafts you no longer need (soft-delete with restore capability).
API Endpoints¶
| Method | Path | Description |
|---|---|---|
POST |
/api/content/drafts |
Create a new draft |
GET |
/api/content/drafts |
List all drafts |
PATCH |
/api/content/drafts/{id} |
Update a draft |
DELETE |
/api/content/drafts/{id} |
Delete a draft |
POST |
/api/content/drafts/{id}/publish |
Publish a draft (queue for posting) |
POST |
/api/content/drafts/{id}/schedule |
Schedule a draft for future posting |
Discovery Feed¶
The Discovery Feed surfaces scored tweets from your configured keywords — the same tweets the autonomous discovery loop would find. In Composer mode, discovery runs in read-only mode: it scores and indexes tweets but never queues replies automatically.
Workflow¶
- Browse the feed — tweets are ranked by the 6-signal scoring engine.
- Compose a reply using AI Assist or write your own.
- Queue the reply for posting through the approval queue.
API Endpoints¶
| Method | Path | Description |
|---|---|---|
GET |
/api/discovery/feed |
Get scored tweets from recent discovery runs |
GET |
/api/discovery/keywords |
Get configured discovery keywords |
POST |
/api/discovery/{tweet_id}/compose-reply |
Compose a reply to a discovered tweet |
POST |
/api/discovery/{tweet_id}/queue-reply |
Queue a reply for posting |
MCP Tools¶
Four MCP tools are available for Composer mode workflows:
| Tool | Description | Key parameters |
|---|---|---|
get_mode |
Returns the current operating mode (autopilot or composer) |
None |
compose_tweet |
Generate a tweet using AI Assist | topic, format (optional) |
get_discovery_feed |
Retrieve scored tweets from the Discovery Feed | limit, min_score (optional) |
suggest_topics |
Get topic suggestions based on profile and performance data | count (optional) |
Switching Between Modes¶
You can switch between Autopilot and Composer at any time. Here is what happens to in-flight items:
- Approval queue: Items already in the queue are preserved and will be posted regardless of mode. Switching to Autopilot does not auto-approve pending items.
- Drafts: Drafts are mode-independent. They persist across mode switches and can be published in either mode.
- Scheduled content: Scheduled posts remain scheduled. The posting queue and approval poster run in both modes.
- Discovery data: Scored tweets from previous discovery runs remain available in the Discovery Feed. Switching to Autopilot resumes autonomous reply queuing.
Switching modes does not restart the runtime. The change takes effect on the next loop iteration (typically within one interval cycle).
Accessibility¶
The composer is built for full keyboard accessibility and meets WCAG AA standards.
- Full keyboard navigation: Every compose action is accessible without a mouse via 14 shortcuts and the command palette
- Focus trap: Tab cycles within the modal boundary and never escapes to the page behind it
- Focus return: Closing the modal returns focus to the element that triggered it (e.g., the Compose button)
- ARIA:
role="dialog",aria-modal="true",aria-live="polite"on character counters and error messages - Contrast: All text meets WCAG AA (4.5:1 minimum contrast ratio) in both light and dark themes
- Reduced motion:
prefers-reduced-motionmedia query disables all CSS transitions and animations globally - Mobile responsive: Full-viewport modal below 640px with 44px minimum touch targets, wrapped footer with full-width submit button, 16px textarea font size (prevents iOS Safari auto-zoom)
- Touch devices: Interactive elements expand to 44px targets on
pointer: coarsedevices; thread card actions are always visible onhover: nonedevices
Migration Notes¶
If you are upgrading from a pre-thread-composer version, here is what changed:
-
Thread editing is card-based. Each tweet in a thread is a visual card with its own textarea, character counter, and media slot. The old sequential textarea array is replaced.
-
Thread data uses structured blocks. Threads are stored as
{ "version": 1, "blocks": [...] }JSON. The server still accepts the legacycontentstring format for backwards compatibility — existing API integrations continue to work unchanged. -
Media can be attached per-tweet in threads. Previously, media was only available in tweet mode. Now each thread card has its own media slot.
-
Keyboard shortcuts are available. 14 shortcuts cover all compose operations. See the Keyboard Shortcuts.
-
Auto-save protects your work. Content is saved to
localStorageevery 500ms. If you close the modal without submitting, a recovery prompt appears next time. -
Command palette for power users. Press
⌘K/Ctrl+Kto search and execute any compose action without touching the mouse. -
API consumers: The
blocksfield in compose and draft endpoints is optional. Existing integrations using thecontentstring field continue to work unchanged. Whenblocksis present, it takes precedence for thread content.
Troubleshooting¶
Common Compose Errors¶
| Error | Cause | Solution |
|---|---|---|
| "Maximum 4 images allowed per tweet" | Attempting to attach a 5th image | Remove an image before adding another |
| "GIF/video cannot be combined with other media" | Attaching an image after a GIF or video | X API limitation: GIF and video attachments are exclusive |
| "Cannot add images when GIF/video is attached" | Attaching an image when a GIF/video exists | Remove the GIF/video first, then add images |
| "File exceeds maximum size" | Image > 5 MB, GIF > 15 MB, or video > 512 MB | Compress or resize the file before uploading |
| "Failed to upload media" | Server unreachable or disk full | Verify tuitbot-server is running; check available disk space |
Thread Validation Errors¶
| Error | Cause | Solution |
|---|---|---|
| Character count exceeds 280 | Tweet card text too long | Use Split (⌘⇧S) to break into two cards, or edit the text |
| Single-card thread | Only one card in thread mode | Add more cards, or switch to tweet mode for single-tweet content |
| Empty card | Card with no text content | Type content or delete the empty card |
| Submission returns 400 | Empty cards, single-card thread, or malformed blocks | Ensure at least 2 non-empty cards with unique IDs |
Autosave & Sync Issues¶
| Issue | Cause | Solution |
|---|---|---|
| Sync badge stuck on "saving" | Server unreachable or slow response | Check server connection; badge recovers automatically on reconnection |
| Conflict banner appears | Two sessions edited the same draft concurrently | Choose "Use mine" to keep local changes or "Reload server" to fetch latest |
| Revision not appearing in history | Very rapid edits within debounce window | Revisions are created per autosave cycle (500ms debounce); pause briefly |
| localStorage recovery prompt | Server was offline during editing session | Recover content, then verify it matches the server version |
Media in Threads¶
| Issue | Cause | Solution |
|---|---|---|
| Cannot attach media to thread card | Card media slot at per-card limit | Check per-card media limit (4 images or 1 GIF/video) |
| Media not visible in preview | Uploaded path not yet available | Media preview loads from localStorage blob URLs; refresh if stale |