Skip to content

Conversation

@stunningpixels
Copy link
Contributor

@stunningpixels stunningpixels commented Nov 29, 2025

Summary

This PR introduces a new WYSIWYG (What You See Is What You Get) editor powered by Lexical, replacing the previous plain text inputs for task creation, follow-ups, and inline editing throughout the app. It also implements a new "scratch" storage system for auto-saving draft content and adds support for queuing follow-up messages.

Key Changes

New WYSIWYG Editor (frontend/src/components/ui/wysiwyg.tsx)

  • Rich text editing with Lexical: bold, italic, strikethrough, blockquotes, lists, headings
  • Inline and multi-line code blocks with syntax highlighting via @lexical/code
  • File/tag picker triggered by @ mentions (similar to previous FileSearchTextArea)
  • Image support with thumbnails, preview dialogs, and markdown import/export
  • Keyboard shortcuts: CMD+Enter for submit, Shift+CMD+Enter for secondary action
  • Optional toolbar for formatting controls
  • Read-only mode with copy button for displaying rendered content

Scratch Storage System (replaces Drafts)

  • New scratch table and API for persisting editor content in real-time
  • WebSocket-based streaming for live updates (/api/scratch/:type/:id/stream)
  • Stores content as markdown for better serialization
  • Used for task follow-up drafts, task creation dialogs, and inline retry editors
  • Graceful error handling for malformed scratch data

Queued Follow-Up Messages

  • In-memory queue service for scheduling follow-up messages
  • Queue a message while an agent is working; it auto-sends when the current task completes
  • Cancel queued messages before they execute
  • Queue is cleared if task fails or is stopped (prevents unintended executions)

Image Handling Improvements

  • Images uploaded to follow-ups are immediately copied to the task worktree's .vibe-images/ folder
  • Relative paths (.vibe-images/...) stored in markdown instead of absolute paths
  • New endpoints: image metadata (/api/task-attempts/:id/images/:path/info) and proxy (/api/task-attempts/:id/images/:path)
  • Image preview dialog on click with full-size view

Cleanup & Refactoring

  • Removed old FileSearchTextArea component and related code
  • Removed old MarkdownRenderer in favor of WYSIWYG read-only mode
  • Deleted legacy drafts system (table, service, routes)
  • Consolidated markdown rendering into single component
  • Refactored useDefaultVariant hook for cleaner variant selection
  • Moved follow-up related hooks to dedicated directory

Files Changed

  • 100 files changed with +4,531 additions and deletions
  • Major changes span backend (Rust crates) and frontend (React/TypeScript)

Database Migrations

  • 20251120000001_refactor_to_scratch.sql - Creates new scratch table
  • 20251129155145_drop_drafts_table.sql - Removes deprecated drafts table

Test Plan

  • Create a new task using the task form dialog - verify rich text formatting works
  • Send follow-up messages with formatted text, code blocks, and images
  • Verify scratch auto-saves content and persists across page navigation
  • Test queuing a follow-up message while agent is running, verify it sends after completion
  • Cancel a queued message and verify it doesn't execute
  • Stop a running task and verify queued messages are cleared
  • Upload images in follow-ups, verify they appear with thumbnails and can be previewed
  • Test inline retry editor with the new WYSIWYG
  • Verify keyboard shortcuts (CMD+Enter to send, Shift+CMD+Enter for secondary action)
  • Test @-mention file picker in the editor

🤖 Generated with Claude Code

frontend/src/components/tasks/TaskFollowUpSection.tsx
frontend/src/components/ui/wysiwyg.tsx
frontend/src/components/ui/wysiwyg/image-chip-markdown.ts
frontend/src/components/ui/wysiwyg/image-chip-node.tsx
LexicalTypeaheadMenuPlugin
frontend/src/components/ui/wysiwyg.tsx
frontend/src/components/ui/file-search-textarea.tsx (old)
Instead of saving markdown, we should save JSON eg `editorState.toJSON();`.

This will enable us to properly serialize custom Elements in the future.

frontend/src/components/ui/wysiwyg.tsx
frontend/src/components/tasks/follow-up/FollowUpEditorCard.tsx
When searching for tags/files. Sometimes the dialog is cut off the bottom of the screen.

frontend/src/components/ui/wysiwyg.tsx
Currently used for follow ups, we should also use for task
frontend/src/components/tasks/follow-up/FollowUpEditorCard.tsx
frontend/src/components/dialogs/tasks/TaskFormDialog.tsx
frontend/src/components/ui/wysiwyg.tsx
We used to have a callback for:
- CMD+Enter
- Shift+CMD+Enter

In create task dialog:
- CMD+Enter = create and start
- Shift+CMD+Enter = create without start

In follow up:
- CMD+Enter = Follow up
- Shift+CMD+Enter = nothing

frontend/src/components/tasks/follow-up/FollowUpEditorCard.tsx
frontend/src/components/ui/wysiwyg.tsx
frontend/src/components/dialogs/tasks/TaskFormDialog.tsx

Ideally we can use the relevant Lexical plugin and callbacks, cleaning up the old `@/keyboard` hooks which no longer work.
LexicalTypeaheadMenuPlugin
frontend/src/components/ui/wysiwyg.tsx
frontend/src/components/ui/file-search-textarea.tsx (old)
Currently used for follow ups, we should also use for task
frontend/src/components/tasks/follow-up/FollowUpEditorCard.tsx
frontend/src/components/dialogs/tasks/TaskFormDialog.tsx
frontend/src/components/ui/wysiwyg.tsx
{
ID,
message_json: Value,
message_md: String
}

We'll also need some endpoints to CRUD them.

crates/db
crates/server
crates/server/src/routes/scratch.rs

It should be possible to listen for updates made to a single scratch
To consolidate the API stuff into frontend/src/lib/api.ts
Primary key should come from: ID and scratch type combination

The frontend will provide both.

Scratch IDs should not be generated on the backend.
Use of hooks that reside in frontend/src/hooks/follow-up/* should be removed, except for frontend/src/hooks/follow-up/useFollowUpSend.ts

From: frontend/src/components/tasks/TaskFollowUpSection.tsx
The current task attempt ID should be used to save the content of the follow up box as scratch.

frontend/src/components/tasks/TaskFollowUpSection.tsx
frontend/src/hooks/useScratch.ts
crates/server/src/routes/scratch.rs
crates/db/src/models/scratch.rs

We are currently storing JSON + MD, however we should now store just MD and import/export the markdown into lexical.
Currently we have an old implementation of markdown rendering in frontend/src/components/ui/markdown-renderer.tsx

But we have recently introduced the new WYSIWYG editor frontend/src/components/ui/wysiwyg.tsx

wysiwyg takes JSON as input, not raw markdown.

Ideally we could just use a single component and have a read only mode, removing Markdown Renderer and its dependencies and custom styling.
Create a Lexical plugin for images, with markdown import/export support.

Visually, images should be displayed as a small thumbnail with the path truncated.

Export/import should support standard markdown image format.
Task attempt endpoint to get info, given the relative URL of an image.

We will also need an image that acts as a proxy to the file.

Info to return:
- Whether file exists
- Size of image
- Format
- File name
- Path
- URL to get image (the proxy URL)

The images are stored in the `.vibe-images` folder, relative to the task attempt container.

crates/server/src/routes/task_attempts.rs
Currently when we upload an image, it adds markdown with the full relative path of the image, eg:
/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban-dev/worktrees/2702-testing-images/.vibe-images/b01e6b02-dbd0-464c-aa9f-a42a89f6d67b.png

However, we should change this to be the path relative to the worktree eg .vibe-images/b01e6b02-dbd0-464c-aa9f-a42a89f6d67b.png
frontend/src/components/ui/wysiwyg/nodes/image-node.tsx

Check if the image comes from `./vibe-images/...`, if so:
Use the API endpoints to get and display metadata.
Use the image proxy to display the thumbnail image.

Do not render non `.vibe-images` images, instead just show the path and show a question icon as a thumbnail.
frontend/src/components/ui/wysiwyg.tsx
frontend/src/components/ui/wysiwyg/nodes/image-node.tsx
frontend/src/components/ui/wysiwyg.tsx
Currently when I type triple backticks it doesn't create a multi-line code block

frontend/src/components/ui/wysiwyg.tsx
I am only referring to the image upload for sending a follow up message.

Currently we:
- upload an image
- when a follow up is made, send file IDs
- copy the image into container based on those file IDs

We should tweak this so that:
- upload an image
- immediately the image is copied into container
- the image file location is added to the markdown of the follow up message (on the frontend)
- when user makes follow up, the image is already in the container

crates/server/src/routes/images.rs
crates/server/src/routes/task_attempts/images.rs
frontend/src/components/ui/wysiwyg.tsx
frontend/src/components/tasks/TaskFollowUpSection.tsx
crates/services/src/services/drafts.rs
crates/db/src/models/draft.rs
Remove:
- frontend/src/pages/TestScratch.tsx
- frontend/src/components/ScratchEditor.tsx
frontend/src/components/ui/wysiwyg.tsx

The placeholder can overlap the attachment icon
- New service (crates/services/src/services/...) that holds an in memory store
- When the final executor_action finishes, if another follow up prompt (scratch ID) is queued then we can automatically begin executing it (crates/local-deployment/src/container.rs after finalize)
- New endpoint required to modify the queue for a task attempt.
- Scratch should be wiped after the execution process is created
- Scratch can't be edited while queued
- Add button to TaskFollowUpSection to make current scratch queued, or cancel queued item
…c21)

- Type follow up
- Press send
- Expect follow up to be reset, but it is not

frontend/src/components/tasks/TaskFollowUpSection.tsx
i18next::translator: missingKey en-GB tasks followUp.queue Queue
frontend/src/components/ui/wysiwyg.tsx
frontend/src/components/tasks/TaskFollowUpSection.tsx
It takes 0.5-1s for the send button to go from no opacity to full opacity after I start typing

frontend/src/components/tasks/TaskFollowUpSection.tsx
frontend/src/components/tasks/TaskFollowUpSection.tsx

Dropdown should have settings-2
Say I have two messages to send:
- I send first
- I queue the second
- I now see "message queued" and the follow up editable text contains the second
- First finishes, second starts, no tasks are queued
- I still see "message queued" box but the follow up editable text gets wiped

frontend/src/components/tasks/TaskFollowUpSection.tsx
Attach button should be to the left of of the send button

frontend/src/components/ui/wysiwyg.tsx
frontend/src/components/tasks/TaskFollowUpSection.tsx
Props, and upstream logic:
- make placeholder optional:
- remove defaultValue: this seems redundant as value is always controlled, there may also be related cleanups for uncontrolled mode
- remove onFocusChange: toggling states is unnecessary here
- remove enableCopyButton: this is always enabled when the editor is disabled

frontend/src/components/ui/wysiwyg.tsx
If a task is stopped or fails, the next queued task runs, however this is not the desired behaviour. Instead the queued task should be removed from the queue
…428)

frontend/src/components/tasks/TaskFollowUpSection.tsx
I think we could change this so that it accepts a default variant and then returns what variant is currently selected, based on the user's preferences and if they select one from the dropdown
It seems to retry functionality was removed fromfrontend/src/components/NormalizedConversation/UserMessage.tsx
…b8e)

frontend/src/components/tasks/TaskFollowUpSection.tsx

If you write out a follow up and then hit send, if you then navigate away from the page quickly the scratch will still be present when you visit the page, when the expected behaviour is that the previous text would be cleared
Currently works for multi-line, can we get it working for multi-line

frontend/src/components/ui/wysiwyg.tsx
Replace with frontend/src/components/ui/wysiwyg.tsx

not frontend/src/components/ui/file-search-textarea.tsx
frontend/src/components/dialogs/tasks/TaskFormDialog.tsx

- Placeholder for WYSIWYG too small, just use default
- Make title same size as WYSIWYG H1
frontend/src/hooks/useVariant.ts

frontend/src/components/NormalizedConversation/RetryEditorInline.tsx

frontend/src/contexts/RetryUiContext.tsx

Removing all existing logic related to variant picking
Refactor the WYSIWYG implementation in thefrontend/src/components/NormalizedConversation/PendingApprovalEntry.tsx so the styles align with usage infrontend/src/components/tasks/TaskFollowUpSection.tsx
When I start typing, it's a really small font for some reason

frontend/src/components/tasks/TaskFollowUpSection.tsx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant