20 KiB
bDS — Remaining Feature Work
This document covers the features described in VISION.md that are not yet implemented. Each section is a self-contained plan that can be picked up independently.
1. Template Editor & Per-Entity Template Selection ✅ Done
Goal
Users can create, edit, and manage Liquid templates inside the application. Categories, tags, and individual posts can select which template to use for rendering. The bundled templates serve as defaults; user templates override them.
Current State
- Liquid templates are bundled in
src/main/engine/templates/(3 templates + partials + macros). PageRendererresolves templates from fixed directory roots.- No user-editable templates, no template CRUD, no per-entity template selection.
- The
ScriptEngine+ScriptsViewcombination already implements the exact pattern needed (file-based storage with YAML metadata, Monaco editor, CRUD, database index, file sync).
Implementation Plan
1.1 Database Schema
Add a templates table to schema.ts:
| Column | Type | Notes |
|---|---|---|
| id | text PK | UUID |
| projectId | text FK | References projects |
| slug | text | Unique per project |
| title | text | Display name |
| kind | text | 'post', 'list', 'not-found', 'partial' |
| filePath | text | Relative path within project templates/ dir |
| enabled | integer | 0/1 — disabled templates fall back to built-in |
| version | integer | Incremented on each save |
| createdAt | integer | Timestamp |
| updatedAt | integer | Timestamp |
Add template selection fields:
CategoryMetadata: add optionalpostTemplateSlugandlistTemplateSlugfields (stored inmeta/project.json).- Posts table: add optional
templateSlugcolumn for per-post overrides. - Tags table: add optional
postTemplateSlugcolumn for tag-level overrides.
1.2 Engine Class — TemplateEngine
Follow the ScriptEngine pattern exactly:
createTemplate(input)— write.liquidfile with YAML frontmatter + database entry.updateTemplate(id, updates)— update file + database, increment version.deleteTemplate(id)— remove file + database entry.getTemplate(id)/getAllTemplates()— read from database, load content from file.rebuildDatabaseFromFiles()— scantemplates/directory, rebuild database from file metadata.reconcileTemplatesFromGitChanges()— sync database after git operations.validateTemplate(content)— attempt Liquid parse, return errors.
Store templates as .liquid files in the project's templates/ directory with
YAML frontmatter:
---
id: <uuid>
projectId: <uuid>
slug: custom-post
title: Custom Post Layout
kind: post
enabled: true
version: 3
---
<main>
<article>{{ post.content | markdown }}</article>
</main>
1.3 Template Resolution in PageRenderer
Modify PageRenderer to resolve templates with priority:
- Post-specific template override (
posts.templateSlug) - Tag-level template override (first matching tag with a
postTemplateSlug) - Category-level template override (
CategoryMetadata.postTemplateSlug) - Built-in default template
Add the project's templates/ directory to resolvePageRendererTemplateRoots()
so Liquid's {% render %} can find user partials.
1.4 IPC Handlers
Register in handlers.ts:
templates:create,templates:update,templates:deletetemplates:get,templates:getAlltemplates:validate
Expose in preload.ts and update electronApi.ts types.
1.5 UI — TemplateEditorView
Mirror ScriptsView:
- Sidebar activity: add "Templates" icon to ActivityBar.
- Sidebar list: show all templates grouped by kind, with enabled/disabled state.
- Tab content: Monaco editor with
liquidorhtmllanguage mode. - Metadata fields: title, slug, kind dropdown, enabled toggle.
- Actions: save (Ctrl+S), validate syntax, delete.
- Footer: created/updated timestamps.
1.6 Template Assignment UI
In SettingsView, extend the category metadata section:
- Add "Post Template" and "List Template" dropdowns per category, populated from user templates of matching kind.
In the post editor metadata area:
- Add optional "Template Override" dropdown (only shows user templates of kind
post).
1.7 Starter Templates
On project creation, copy the bundled templates into the project's templates/
directory so users have a working starting point they can modify.
2. Post Translation System
Goal
Posts have a language attribute. The AI importing agent detects post language and can auto-translate posts. Posts link to their translations so the publishing pipeline can generate multilingual output.
Current State
- Posts have no
languagefield. - No translation relationship tracking.
- No language detection during import.
- No AI translation tools.
- The
excerptfield already exists and can serve as the summary field mentioned in the vision. analyzeMediaImage()inOpenCodeManageralready demonstrates the pattern for single-shot AI analysis with language parameters.- Project-level
mainLanguageexists inMetaEngine.
Implementation Plan
2.1 Database Schema
Extend the posts table:
| Column | Type | Notes |
|---|---|---|
| language | text | ISO code (en, de, etc.), defaults to project mainLanguage |
| translationOfId | text | FK to posts.id — the original post this is a translation of |
No separate junction table needed. A translated post is simply a post with
translationOfId pointing at its source. This keeps the model simple: each
post belongs to exactly one language and optionally references one original.
2.2 YAML Frontmatter
Extend postFileUtils.ts to read/write:
language: de
translationOf: <original-post-id>
On readPostFile(), parse these fields. On writePostFile(), include them
when present.
2.3 PostEngine Extensions
Add methods:
getTranslations(postId)— find all posts wheretranslationOfId === postId.getOriginal(postId)— if the post hastranslationOfId, return that post.createTranslation(originalPostId, targetLanguage, content)— create a new post linked to the original with the target language set.
Modify createPost() and updatePost() to accept and persist the language
and translationOfId fields.
2.4 AI Translation Tools in OpenCodeManager
Add three new methods following the analyzeMediaImage() pattern:
detectPostLanguage(postId)
- Read post content.
- Send to AI with prompt: "Detect the language of this text. Return a JSON
object with
language(ISO 639-1 code) andconfidence(0-1)." - Return
{ language: string, confidence: number }.
translatePost(postId, targetLanguage)
- Read full post content + title + excerpt.
- Send to AI with prompt: "Translate this blog post to {language}. Return JSON
with
title,content(markdown), andexcerpt." - Return translated fields without creating a post (caller decides).
generatePostSummary(postId)
- Read post content.
- Send to AI: "Write a 2-3 sentence summary of this blog post in
{post.language}. Return JSON with
excerpt." - Return
{ excerpt: string }.
Register these as IPC handlers: chat:detectPostLanguage,
chat:translatePost, chat:generatePostSummary.
2.5 Import Pipeline Integration
In ImportExecutionEngine, after a post is imported and published:
- Call
detectPostLanguage()to set thelanguagefield. - If the detected language differs from the project's
mainLanguage, queue a translation task viaTaskManager. - The translation task calls
translatePost(), creates a new post viacreateTranslation(), and publishes it.
This is optional and should be configurable per import definition (a checkbox
"Auto-detect language and translate" in ImportAnalysisView).
2.6 UI — Translation Panel
In the post editor metadata area, add a "Translations" section:
- Show current post language (dropdown to change).
- List existing translations with links (open in new tab).
- "Translate to..." button that opens a language picker, triggers AI translation, and creates the linked post.
- If the post is itself a translation, show "Original: {title}" link.
In the sidebar post list, optionally show a language badge per post.
2.7 Publishing Pipeline
In PageRenderer and BlogGenerationEngine:
- Add
hreflanglink tags to generated HTML when translations exist. - Optionally generate a language switcher partial that templates can include.
- Sitemap should include
xhtml:linkentries for alternate language versions.
3. MCP Server
Goal
Host an MCP (Model Context Protocol) server inside the application so external AI agents (Claude Code, Cursor, etc.) can connect and use bDS tools to query and manage blog content.
Current State
OpenCodeManageralready defines 16 data-access tools and 7 A2UI render tools with full implementations (getToolDefinitions(),executeTool()).PreviewServerprovides the architectural pattern for an in-process HTTP server with lifecycle management.- No MCP SDK dependency exists.
Implementation Plan
3.1 Dependencies
Add @modelcontextprotocol/sdk to package.json. This provides the standard
MCP server implementation with transport handling.
3.2 Engine Class — MCPServer
Follow the PreviewServer pattern:
src/main/engine/MCPServer.ts
- Constructor accepts dependency injection (engines via getters).
start(port)— create HTTP server implementing MCP protocol, or use stdio transport for local agent integration.stop()— clean shutdown.getToolDefinitions()— convert OpenCodeManager's Anthropic-format tool definitions to MCP schema format.executeTool(name, args)— delegate to OpenCodeManager'sexecuteTool().
3.3 Tool Mapping
Map the existing OpenCodeManager tools to MCP tools. The tool signatures are nearly identical between Anthropic tool_use format and MCP — both use JSON Schema for input definitions. The mapping is mechanical:
| OpenCodeManager Tool | MCP Tool Name |
|---|---|
| search_posts | search_posts |
| read_post | read_post |
| list_posts | list_posts |
| get_media | get_media |
| list_media | list_media |
| update_post_metadata | update_post_metadata |
| update_media_metadata | update_media_metadata |
| list_tags | list_tags |
| list_categories | list_categories |
| get_blog_stats | get_blog_stats |
| view_image | view_image |
| get_post_backlinks | get_post_backlinks |
| get_post_outlinks | get_post_outlinks |
| get_post_media | get_post_media |
| get_media_posts | get_media_posts |
Exclude A2UI render tools (they are UI-specific and not useful for external agents).
3.4 Transport
Support two transports:
- stdio — for local integration (agent runs
bds --mcpor connects via named pipe). This is the standard for MCP in coding agents. - HTTP/SSE — for network access, running alongside PreviewServer on a different port (e.g., 5174).
Start with stdio since that is what Claude Code and Cursor use.
3.5 Lifecycle Integration
In main.ts:
- Initialize
MCPServerininitialize(). - Start alongside
PreviewServerinapp.whenReady(). - Stop in
before-quithandler. - Respect active project context (tools operate on the active project).
3.6 Configuration
In SettingsView, add an "MCP Server" section:
- Enable/disable toggle.
- Port number (for HTTP transport).
- Show connection instructions (stdio command or URL).
3.7 Testing
- Unit tests for tool definition mapping (Anthropic → MCP format).
- Integration tests: start MCP server, send tool calls, verify responses.
- Follow existing engine test patterns with mocked dependencies.
4. AI Post Summary, Title & Slug Suggestions
Goal
The post editor has AI buttons that generate summaries (excerpts), improved titles, and better slugs — so the user can focus on writing content and let AI handle the metadata.
Current State
analyzeMediaImage()inOpenCodeManageralready implements the exact pattern: one-shot AI call, JSON response, language-aware.AISuggestionsModalalready provides the UI: loading state, field-by-field checkboxes, current vs. suggested comparison, apply/cancel.- The media editor has an "Analyze with AI" button in a quick-actions menu.
- The post editor metadata area has title, tags, author, slug, and categories fields but no AI buttons.
- The
excerptfield exists onPostDataand can serve as the summary. - Slug is read-only in the UI after first publish (auto-generated from title).
Implementation Plan
4.1 Backend — analyzePost() in OpenCodeManager
Add a new method following the analyzeMediaImage() pattern:
Input: postId: string, language: string
Process:
- Load post content, title, excerpt, and slug via
PostEngine. - Build a system prompt:
You are a blog editor assistant. Analyze the following blog post and suggest improvements. Return a JSON object with: - "title": a clear, engaging title for this post - "excerpt": a 2-3 sentence summary suitable for overview pages - "slug": a concise, SEO-friendly URL slug (lowercase, hyphens only) Respond in {language}. Return only the JSON object. - Send post content as user message to OpenCode Zen API.
- Parse JSON response.
- Return
{ success, title?, excerpt?, slug?, error? }.
Register IPC handler: chat:analyzePost.
4.2 Frontend — Post Editor AI Button
In the post editor metadata area (Editor.tsx, around line 720):
- Add a "Quick Actions" dropdown button (same pattern as media editor at line 1242).
- Menu item: "Suggest Title, Summary & Slug" with a robot icon.
- On click: call
window.electronAPI.chat.analyzePost(postId, projectLanguage). - Show
AISuggestionsModalwith the results.
4.3 Extend AISuggestionsModal
The modal currently supports title, alt, caption fields. Adapt it to
also support a post mode with title, excerpt, slug fields:
- Add a
modeprop ('media'|'post') or make field configuration dynamic. - For post mode, show title, excerpt, and slug fields.
- Slug field should show a warning that it only applies to unpublished posts.
Alternatively, keep the modal generic and pass field definitions as props:
interface SuggestionField {
key: string;
label: string; // i18n key
currentValue: string;
suggestedValue?: string;
warning?: string; // e.g., "slug is locked after first publish"
}
4.4 Applying Suggestions
On "Apply Selected":
- Title: update via existing
onTitleChangehandler. - Excerpt: update via
onExcerptChange(may need to add this handler if not present — excerpt editing may need a field in the metadata area). - Slug: only apply if post has never been published. Show a warning and disable
the checkbox if the post has
publishedAtset.
4.5 i18n
Add keys to all 5 locale files:
aiSuggestions.postTitle,aiSuggestions.excerptField,aiSuggestions.slugFieldaiSuggestions.analyzingPostaiSuggestions.slugLockedWarningpostEditor.quickActions,postEditor.analyzeWithAI
4.6 Excerpt Field in Editor
If the excerpt/summary is not currently editable in the post metadata area, add a multi-line text field for it between title and tags. This is needed both for manual editing and for applying AI suggestions.
5. Drag-and-Drop Image Insertion
Goal
Users can drag image files from the filesystem onto the editor to insert them. Dropped files are automatically imported into the media library and inserted as markdown images.
Current State
- Images are inserted only via
InsertModal(browse media library or enter URL). MediaEngine.importMedia(sourcePath)handles file import, thumbnail generation, and database indexing.imageResolverPluginalready converts relative media paths tobds-media://protocol URLs for editor display.LinkedMediaPanelhas working drag-drop for reordering (reference pattern).insertImageCommandfrom Milkdown inserts image nodes into the editor.
Implementation Plan
5.1 ProseMirror Drop Plugin
Create a new plugin in src/renderer/plugins/dropImagePlugin.ts following the
imageResolverPlugin pattern:
// Pseudo-structure
export const dropImagePlugin = $prose(() => {
return new Plugin({
props: {
handleDOMEvents: {
drop: (view, event) => {
// 1. Check for files in dataTransfer
// 2. Filter to image types
// 3. Get file paths (Electron exposes .path on File objects)
// 4. For each file: import via IPC, insert into editor
// 5. Return true to prevent default
},
dragover: (view, event) => {
// Show drop indicator if files are images
}
}
}
});
});
5.2 Drop Handler Flow
For each dropped file:
- Validate — check file extension against supported image types (jpg, png, gif, webp, svg, bmp).
- Import — call
window.electronAPI.media.import(file.path). This returnsMediaDatawith the media ID and file path. - Insert — use
insertImageCommandwith{ src: relativePath, alt: '' }whererelativePathis the media's storage path (e.g.,media/2025/01/uuid.jpg). - Link — call
window.electronAPI.postMedia.link(postId, mediaId)to track the relationship. - Resolve — the existing
imageResolverPluginwill automatically convert the relative path to abds-media://URL for display.
5.3 Visual Feedback
- On
dragoverwith image files: add a CSS class to the editor container showing a drop zone indicator (border highlight or overlay). - On
dragleave/drop: remove the indicator. - During import (for large files): show a small inline spinner or toast.
5.4 Integration into MilkdownEditor
In MilkdownEditor.tsx, register the new plugin alongside existing plugins:
import { dropImagePlugin } from '../../plugins/dropImagePlugin';
// In the editor setup, add to the plugin list
.use(dropImagePlugin)
Pass postId and the import callback to the plugin via the editor context or
a shared ref.
5.5 Paste Support (Optional Extension)
The same plugin can handle paste events with image files:
- Check
clipboardData.filesfor images. - Same import → insert → link flow as drop.
- This handles screenshots pasted from the clipboard.
5.6 Error Handling
- Non-image files: ignore silently (don't prevent default, let editor handle text drops normally).
- Import failure: show toast with error message, don't insert anything.
- Multiple files: process sequentially, insert at cursor position for first, then append after each previous insertion.
5.7 Testing
- Unit test the plugin's file validation logic.
- Integration test: mock
electronAPI.media.import, verify correct calls and editor state after drop. - Test edge cases: non-image files, failed imports, multiple simultaneous drops.