Dench workspace: Tiptap markdown editor, subagent sessions, and error surfacing
── Tiptap Markdown Editor ──
- Add full Tiptap-based WYSIWYG markdown editor (markdown-editor.tsx, 709 LOC)
with bubble menu, auto-save (debounced), image drag-and-drop/paste upload,
table editing, task list checkboxes, and frontmatter preservation on save.
- Add slash command system (slash-command.tsx, 607 LOC) with "/" trigger for
block insertion (headings, lists, tables, code blocks, images, reports) and
"@" trigger for file/document mention with fuzzy search across the workspace
tree.
- Add ReportBlockNode (report-block-node.tsx) — custom Tiptap node that renders
embedded report-json blocks as interactive ReportCard widgets inline in the
editor, with expand/collapse and edit-JSON support.
- Add workspace asset serving API (api/workspace/assets/[...path]/route.ts) to
serve images from the workspace with proper MIME types.
- Add workspace file upload orkspace/upload/route.ts) for multipart
image uploads (10 MB limit, image types only), saving to assets/ directory.
- Add ~500 lines of Tiptap editor CSS to globals.css (editor layout, task lists,
images, tables, slash command dropdown, bubble menu toolbar, code blocks, etc.).
- Add 14 @tiptap/* dependencies to apps/web/package.json (react, starter-kit,
markdown, image, link, table, task-list, suggestion, placeholder, etc.).
── Document View: Edit/Read Mode Toggle ──
- document-view.tsx: Add edit/read mode toggle; defaults to edit mode when a
filePath is available. Lazy-loads MarkdownEditor to keep initial bundle light.
- workspace/page.tsx: Pass activePath, tree, onSave, onNavigate, and
onRefreshTree through to DocumentView for full editor integration with
workspace navigation and tree refresh after saves.
── Subagent Session Isolation ──
- agent-runner.ts: Add RunAgentOptions with optional sessionId; when set, spawns
the agent with --session-key agent:main:subagent:<id> ant so
file-scoped sidebar chats run in isolated sessions independent of the main
agent.
- route.ts (chat API): Accept sessionId from request body and forward it to
runAgent. Resolve workspace file path prefixes (resolveAgentWorkspacePrefix)
so tree-relative paths become agent-cwd-relative.
- chat-panel.tsx: Create per-instance DefaultChatTransport that injects sessionId
via body function and a ref (avoids stale closures). On file change, auto-load
the most recent session and its messages. Refresh session tab list after
streaming ends. Stop ongoing stream when switching sessions.
- register.agent.ts: Add --session-key <key> and --lane <lane> CLI flags.
- agent-via-gateway.ts: Wire sessionKey into session resolution and validation
for both interactive and --stream-json code paths.
- workspace.ts: Add resolveAgentWorkspacePrefix() to map workspace-root-relative
paths to repo-root-relative paths for the agent process.
── Error Surfacing ──
- agent-runner.ts: Add onAgentError callback extraction helpers
(parseAgentErrorMessage, parseErrorBody, parseErrorFromStderr) to surface
API-level errors (402 payment, rate limits, etc.) to the UI. Captures stderr
for fallback error detection on non-zero exit.
- route.ts: Wire onAgentError into the SSE stream as [error]-prefixed text
parts. Improve onError and onClose handlers with clearer error messages and
exit code reporting.
- chat-message.tsx: Detect [error]-prefixed text segments and render them as
styled error banners with alert icon instead of plain text.
- chat-panel.tsx: Restyle the transport-level error bar with themed colors and
an alert icon consistent with in-message error styling.
2026-02-11 20:54:30 -08:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { useEditor, EditorContent } from "@tiptap/react";
|
|
|
|
|
import { BubbleMenu } from "@tiptap/react/menus";
|
|
|
|
|
import StarterKit from "@tiptap/starter-kit";
|
|
|
|
|
import { Markdown } from "@tiptap/markdown";
|
|
|
|
|
import Image from "@tiptap/extension-image";
|
|
|
|
|
import Link from "@tiptap/extension-link";
|
|
|
|
|
import { Table } from "@tiptap/extension-table";
|
|
|
|
|
import TableRow from "@tiptap/extension-table-row";
|
|
|
|
|
import TableCell from "@tiptap/extension-table-cell";
|
|
|
|
|
import TableHeader from "@tiptap/extension-table-header";
|
|
|
|
|
import TaskList from "@tiptap/extension-task-list";
|
|
|
|
|
import TaskItem from "@tiptap/extension-task-item";
|
|
|
|
|
import Placeholder from "@tiptap/extension-placeholder";
|
|
|
|
|
import { useState, useCallback, useEffect, useRef, useMemo } from "react";
|
|
|
|
|
|
|
|
|
|
import { ReportBlockNode, preprocessReportBlocks, postprocessReportBlocks } from "./report-block-node";
|
Dench workspace: unified @ mention search, entry detail modal, and workspace link system
New libraries:
- workspace-links.ts: builders, parsers, and type guards for workspace URLs
(/workspace?path=... for files/objects, /workspace?entry=objName:id for entries).
Replaces ad-hoc relative-path links with real navigable URLs.
- search-index.ts: useSearchIndex hook that fetches a unified search index from
the API and provides Fuse.js-powered fuzzy search across files, objects, and
database entries. Exposes a stable ref-based search function safe for capture
in tiptap extensions.
New API routes:
- GET /api/workspace/search-index: builds a flat search index from the filesystem
tree (knowledge/, reports/, top-level files) and all DuckDB object entries with
display-field labels and preview fields.
- GET /api/workspace/objects/[name]/entries/[id]: fetches a single entry with
all field values, resolved relation labels, reverse relations (incoming links
from other objects), and the effective display field.
New component:
- EntryDetailModal: slide-over modal for viewing an individual entry's fields,
enum badges, user avatars, clickable relation chips (navigate to related
entries), reverse relation sections, and timestamps. Supports Escape to close
and backdrop click dismiss.
Slash command refactor (slash-command.tsx):
- New createWorkspaceMention(searchFn) replaces the old file-only @ mention with
unified search across files, objects, and entries.
- searchItemToSlashItem() converts search index items into tiptap suggestion
items with proper icons, badges (object name pill for entries), and link
insertion commands using real workspace URLs.
- Legacy createFileMention(tree) now delegates to createWorkspaceMention with a
simple substring-match fallback for when no search index is available.
Editor integration (markdown-editor.tsx, document-view.tsx):
- MarkdownEditor accepts optional searchFn prop; when provided, uses
createWorkspaceMention instead of the legacy createFileMention.
- Link click interception now uses the shared isWorkspaceLink() helper and
registers handlers in capture phase for reliable interception.
- DocumentView forwards searchFn to editor and adds a delegated click handler
in read mode to intercept workspace links and navigate via onNavigate.
Object table (object-table.tsx):
- Added onEntryClick prop; table rows are now clickable with cursor-pointer
styling, firing the callback with the entry ID.
Workspace page (page.tsx):
- Integrates useSearchIndex hook and passes search function down to editor.
- Entry detail modal state with URL synchronization (?entry=objName:id param).
- New resolveNode() with fallback strategies: exact match, knowledge/ prefix
toggle, and last-segment object name matching.
- Unified handleEditorNavigate() dispatches /workspace?entry=... to the modal
and /workspace?path=... to file/object navigation.
- URL bar syncs with activePath via router.replace (no full page reloads).
- Top-level container click safety net catches any workspace link clicks that
bubble up unhandled.
Styles (globals.css):
- Added .slash-cmd-item-badge for object-name pills in the @ mention popup.
2026-02-11 21:41:23 -08:00
|
|
|
import { createSlashCommand, createWorkspaceMention, createFileMention, type TreeNode, type MentionSearchFn } from "./slash-command";
|
|
|
|
|
import { isWorkspaceLink } from "@/lib/workspace-links";
|
Dench workspace: Tiptap markdown editor, subagent sessions, and error surfacing
── Tiptap Markdown Editor ──
- Add full Tiptap-based WYSIWYG markdown editor (markdown-editor.tsx, 709 LOC)
with bubble menu, auto-save (debounced), image drag-and-drop/paste upload,
table editing, task list checkboxes, and frontmatter preservation on save.
- Add slash command system (slash-command.tsx, 607 LOC) with "/" trigger for
block insertion (headings, lists, tables, code blocks, images, reports) and
"@" trigger for file/document mention with fuzzy search across the workspace
tree.
- Add ReportBlockNode (report-block-node.tsx) — custom Tiptap node that renders
embedded report-json blocks as interactive ReportCard widgets inline in the
editor, with expand/collapse and edit-JSON support.
- Add workspace asset serving API (api/workspace/assets/[...path]/route.ts) to
serve images from the workspace with proper MIME types.
- Add workspace file upload orkspace/upload/route.ts) for multipart
image uploads (10 MB limit, image types only), saving to assets/ directory.
- Add ~500 lines of Tiptap editor CSS to globals.css (editor layout, task lists,
images, tables, slash command dropdown, bubble menu toolbar, code blocks, etc.).
- Add 14 @tiptap/* dependencies to apps/web/package.json (react, starter-kit,
markdown, image, link, table, task-list, suggestion, placeholder, etc.).
── Document View: Edit/Read Mode Toggle ──
- document-view.tsx: Add edit/read mode toggle; defaults to edit mode when a
filePath is available. Lazy-loads MarkdownEditor to keep initial bundle light.
- workspace/page.tsx: Pass activePath, tree, onSave, onNavigate, and
onRefreshTree through to DocumentView for full editor integration with
workspace navigation and tree refresh after saves.
── Subagent Session Isolation ──
- agent-runner.ts: Add RunAgentOptions with optional sessionId; when set, spawns
the agent with --session-key agent:main:subagent:<id> ant so
file-scoped sidebar chats run in isolated sessions independent of the main
agent.
- route.ts (chat API): Accept sessionId from request body and forward it to
runAgent. Resolve workspace file path prefixes (resolveAgentWorkspacePrefix)
so tree-relative paths become agent-cwd-relative.
- chat-panel.tsx: Create per-instance DefaultChatTransport that injects sessionId
via body function and a ref (avoids stale closures). On file change, auto-load
the most recent session and its messages. Refresh session tab list after
streaming ends. Stop ongoing stream when switching sessions.
- register.agent.ts: Add --session-key <key> and --lane <lane> CLI flags.
- agent-via-gateway.ts: Wire sessionKey into session resolution and validation
for both interactive and --stream-json code paths.
- workspace.ts: Add resolveAgentWorkspacePrefix() to map workspace-root-relative
paths to repo-root-relative paths for the agent process.
── Error Surfacing ──
- agent-runner.ts: Add onAgentError callback extraction helpers
(parseAgentErrorMessage, parseErrorBody, parseErrorFromStderr) to surface
API-level errors (402 payment, rate limits, etc.) to the UI. Captures stderr
for fallback error detection on non-zero exit.
- route.ts: Wire onAgentError into the SSE stream as [error]-prefixed text
parts. Improve onError and onClose handlers with clearer error messages and
exit code reporting.
- chat-message.tsx: Detect [error]-prefixed text segments and render them as
styled error banners with alert icon instead of plain text.
- chat-panel.tsx: Restyle the transport-level error bar with themed colors and
an alert icon consistent with in-message error styling.
2026-02-11 20:54:30 -08:00
|
|
|
|
|
|
|
|
// --- Types ---
|
|
|
|
|
|
|
|
|
|
export type MarkdownEditorProps = {
|
|
|
|
|
/** The markdown body (frontmatter already stripped by parent). */
|
|
|
|
|
content: string;
|
|
|
|
|
/** Original raw file content including frontmatter, used to preserve it on save. */
|
|
|
|
|
rawContent?: string;
|
|
|
|
|
filePath: string;
|
|
|
|
|
tree: TreeNode[];
|
|
|
|
|
onSave?: () => void;
|
|
|
|
|
onNavigate?: (path: string) => void;
|
Dench workspace: unified @ mention search, entry detail modal, and workspace link system
New libraries:
- workspace-links.ts: builders, parsers, and type guards for workspace URLs
(/workspace?path=... for files/objects, /workspace?entry=objName:id for entries).
Replaces ad-hoc relative-path links with real navigable URLs.
- search-index.ts: useSearchIndex hook that fetches a unified search index from
the API and provides Fuse.js-powered fuzzy search across files, objects, and
database entries. Exposes a stable ref-based search function safe for capture
in tiptap extensions.
New API routes:
- GET /api/workspace/search-index: builds a flat search index from the filesystem
tree (knowledge/, reports/, top-level files) and all DuckDB object entries with
display-field labels and preview fields.
- GET /api/workspace/objects/[name]/entries/[id]: fetches a single entry with
all field values, resolved relation labels, reverse relations (incoming links
from other objects), and the effective display field.
New component:
- EntryDetailModal: slide-over modal for viewing an individual entry's fields,
enum badges, user avatars, clickable relation chips (navigate to related
entries), reverse relation sections, and timestamps. Supports Escape to close
and backdrop click dismiss.
Slash command refactor (slash-command.tsx):
- New createWorkspaceMention(searchFn) replaces the old file-only @ mention with
unified search across files, objects, and entries.
- searchItemToSlashItem() converts search index items into tiptap suggestion
items with proper icons, badges (object name pill for entries), and link
insertion commands using real workspace URLs.
- Legacy createFileMention(tree) now delegates to createWorkspaceMention with a
simple substring-match fallback for when no search index is available.
Editor integration (markdown-editor.tsx, document-view.tsx):
- MarkdownEditor accepts optional searchFn prop; when provided, uses
createWorkspaceMention instead of the legacy createFileMention.
- Link click interception now uses the shared isWorkspaceLink() helper and
registers handlers in capture phase for reliable interception.
- DocumentView forwards searchFn to editor and adds a delegated click handler
in read mode to intercept workspace links and navigate via onNavigate.
Object table (object-table.tsx):
- Added onEntryClick prop; table rows are now clickable with cursor-pointer
styling, firing the callback with the entry ID.
Workspace page (page.tsx):
- Integrates useSearchIndex hook and passes search function down to editor.
- Entry detail modal state with URL synchronization (?entry=objName:id param).
- New resolveNode() with fallback strategies: exact match, knowledge/ prefix
toggle, and last-segment object name matching.
- Unified handleEditorNavigate() dispatches /workspace?entry=... to the modal
and /workspace?path=... to file/object navigation.
- URL bar syncs with activePath via router.replace (no full page reloads).
- Top-level container click safety net catches any workspace link clicks that
bubble up unhandled.
Styles (globals.css):
- Added .slash-cmd-item-badge for object-name pills in the @ mention popup.
2026-02-11 21:41:23 -08:00
|
|
|
/** Optional search function from useSearchIndex for fuzzy @ mention search. */
|
|
|
|
|
searchFn?: MentionSearchFn;
|
Dench workspace: Tiptap markdown editor, subagent sessions, and error surfacing
── Tiptap Markdown Editor ──
- Add full Tiptap-based WYSIWYG markdown editor (markdown-editor.tsx, 709 LOC)
with bubble menu, auto-save (debounced), image drag-and-drop/paste upload,
table editing, task list checkboxes, and frontmatter preservation on save.
- Add slash command system (slash-command.tsx, 607 LOC) with "/" trigger for
block insertion (headings, lists, tables, code blocks, images, reports) and
"@" trigger for file/document mention with fuzzy search across the workspace
tree.
- Add ReportBlockNode (report-block-node.tsx) — custom Tiptap node that renders
embedded report-json blocks as interactive ReportCard widgets inline in the
editor, with expand/collapse and edit-JSON support.
- Add workspace asset serving API (api/workspace/assets/[...path]/route.ts) to
serve images from the workspace with proper MIME types.
- Add workspace file upload orkspace/upload/route.ts) for multipart
image uploads (10 MB limit, image types only), saving to assets/ directory.
- Add ~500 lines of Tiptap editor CSS to globals.css (editor layout, task lists,
images, tables, slash command dropdown, bubble menu toolbar, code blocks, etc.).
- Add 14 @tiptap/* dependencies to apps/web/package.json (react, starter-kit,
markdown, image, link, table, task-list, suggestion, placeholder, etc.).
── Document View: Edit/Read Mode Toggle ──
- document-view.tsx: Add edit/read mode toggle; defaults to edit mode when a
filePath is available. Lazy-loads MarkdownEditor to keep initial bundle light.
- workspace/page.tsx: Pass activePath, tree, onSave, onNavigate, and
onRefreshTree through to DocumentView for full editor integration with
workspace navigation and tree refresh after saves.
── Subagent Session Isolation ──
- agent-runner.ts: Add RunAgentOptions with optional sessionId; when set, spawns
the agent with --session-key agent:main:subagent:<id> ant so
file-scoped sidebar chats run in isolated sessions independent of the main
agent.
- route.ts (chat API): Accept sessionId from request body and forward it to
runAgent. Resolve workspace file path prefixes (resolveAgentWorkspacePrefix)
so tree-relative paths become agent-cwd-relative.
- chat-panel.tsx: Create per-instance DefaultChatTransport that injects sessionId
via body function and a ref (avoids stale closures). On file change, auto-load
the most recent session and its messages. Refresh session tab list after
streaming ends. Stop ongoing stream when switching sessions.
- register.agent.ts: Add --session-key <key> and --lane <lane> CLI flags.
- agent-via-gateway.ts: Wire sessionKey into session resolution and validation
for both interactive and --stream-json code paths.
- workspace.ts: Add resolveAgentWorkspacePrefix() to map workspace-root-relative
paths to repo-root-relative paths for the agent process.
── Error Surfacing ──
- agent-runner.ts: Add onAgentError callback extraction helpers
(parseAgentErrorMessage, parseErrorBody, parseErrorFromStderr) to surface
API-level errors (402 payment, rate limits, etc.) to the UI. Captures stderr
for fallback error detection on non-zero exit.
- route.ts: Wire onAgentError into the SSE stream as [error]-prefixed text
parts. Improve onError and onClose handlers with clearer error messages and
exit code reporting.
- chat-message.tsx: Detect [error]-prefixed text segments and render them as
styled error banners with alert icon instead of plain text.
- chat-panel.tsx: Restyle the transport-level error bar with themed colors and
an alert icon consistent with in-message error styling.
2026-02-11 20:54:30 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// --- Main component ---
|
|
|
|
|
|
|
|
|
|
/** Extract YAML frontmatter (if any) from raw file content. */
|
|
|
|
|
function extractFrontmatter(raw: string): string {
|
|
|
|
|
const match = raw.match(/^(---\s*\n[\s\S]*?\n---\s*\n)/);
|
|
|
|
|
return match ? match[1] : "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function MarkdownEditor({
|
|
|
|
|
content,
|
|
|
|
|
rawContent,
|
|
|
|
|
filePath,
|
|
|
|
|
tree,
|
|
|
|
|
onSave,
|
|
|
|
|
onNavigate,
|
Dench workspace: unified @ mention search, entry detail modal, and workspace link system
New libraries:
- workspace-links.ts: builders, parsers, and type guards for workspace URLs
(/workspace?path=... for files/objects, /workspace?entry=objName:id for entries).
Replaces ad-hoc relative-path links with real navigable URLs.
- search-index.ts: useSearchIndex hook that fetches a unified search index from
the API and provides Fuse.js-powered fuzzy search across files, objects, and
database entries. Exposes a stable ref-based search function safe for capture
in tiptap extensions.
New API routes:
- GET /api/workspace/search-index: builds a flat search index from the filesystem
tree (knowledge/, reports/, top-level files) and all DuckDB object entries with
display-field labels and preview fields.
- GET /api/workspace/objects/[name]/entries/[id]: fetches a single entry with
all field values, resolved relation labels, reverse relations (incoming links
from other objects), and the effective display field.
New component:
- EntryDetailModal: slide-over modal for viewing an individual entry's fields,
enum badges, user avatars, clickable relation chips (navigate to related
entries), reverse relation sections, and timestamps. Supports Escape to close
and backdrop click dismiss.
Slash command refactor (slash-command.tsx):
- New createWorkspaceMention(searchFn) replaces the old file-only @ mention with
unified search across files, objects, and entries.
- searchItemToSlashItem() converts search index items into tiptap suggestion
items with proper icons, badges (object name pill for entries), and link
insertion commands using real workspace URLs.
- Legacy createFileMention(tree) now delegates to createWorkspaceMention with a
simple substring-match fallback for when no search index is available.
Editor integration (markdown-editor.tsx, document-view.tsx):
- MarkdownEditor accepts optional searchFn prop; when provided, uses
createWorkspaceMention instead of the legacy createFileMention.
- Link click interception now uses the shared isWorkspaceLink() helper and
registers handlers in capture phase for reliable interception.
- DocumentView forwards searchFn to editor and adds a delegated click handler
in read mode to intercept workspace links and navigate via onNavigate.
Object table (object-table.tsx):
- Added onEntryClick prop; table rows are now clickable with cursor-pointer
styling, firing the callback with the entry ID.
Workspace page (page.tsx):
- Integrates useSearchIndex hook and passes search function down to editor.
- Entry detail modal state with URL synchronization (?entry=objName:id param).
- New resolveNode() with fallback strategies: exact match, knowledge/ prefix
toggle, and last-segment object name matching.
- Unified handleEditorNavigate() dispatches /workspace?entry=... to the modal
and /workspace?path=... to file/object navigation.
- URL bar syncs with activePath via router.replace (no full page reloads).
- Top-level container click safety net catches any workspace link clicks that
bubble up unhandled.
Styles (globals.css):
- Added .slash-cmd-item-badge for object-name pills in the @ mention popup.
2026-02-11 21:41:23 -08:00
|
|
|
searchFn,
|
Dench workspace: Tiptap markdown editor, subagent sessions, and error surfacing
── Tiptap Markdown Editor ──
- Add full Tiptap-based WYSIWYG markdown editor (markdown-editor.tsx, 709 LOC)
with bubble menu, auto-save (debounced), image drag-and-drop/paste upload,
table editing, task list checkboxes, and frontmatter preservation on save.
- Add slash command system (slash-command.tsx, 607 LOC) with "/" trigger for
block insertion (headings, lists, tables, code blocks, images, reports) and
"@" trigger for file/document mention with fuzzy search across the workspace
tree.
- Add ReportBlockNode (report-block-node.tsx) — custom Tiptap node that renders
embedded report-json blocks as interactive ReportCard widgets inline in the
editor, with expand/collapse and edit-JSON support.
- Add workspace asset serving API (api/workspace/assets/[...path]/route.ts) to
serve images from the workspace with proper MIME types.
- Add workspace file upload orkspace/upload/route.ts) for multipart
image uploads (10 MB limit, image types only), saving to assets/ directory.
- Add ~500 lines of Tiptap editor CSS to globals.css (editor layout, task lists,
images, tables, slash command dropdown, bubble menu toolbar, code blocks, etc.).
- Add 14 @tiptap/* dependencies to apps/web/package.json (react, starter-kit,
markdown, image, link, table, task-list, suggestion, placeholder, etc.).
── Document View: Edit/Read Mode Toggle ──
- document-view.tsx: Add edit/read mode toggle; defaults to edit mode when a
filePath is available. Lazy-loads MarkdownEditor to keep initial bundle light.
- workspace/page.tsx: Pass activePath, tree, onSave, onNavigate, and
onRefreshTree through to DocumentView for full editor integration with
workspace navigation and tree refresh after saves.
── Subagent Session Isolation ──
- agent-runner.ts: Add RunAgentOptions with optional sessionId; when set, spawns
the agent with --session-key agent:main:subagent:<id> ant so
file-scoped sidebar chats run in isolated sessions independent of the main
agent.
- route.ts (chat API): Accept sessionId from request body and forward it to
runAgent. Resolve workspace file path prefixes (resolveAgentWorkspacePrefix)
so tree-relative paths become agent-cwd-relative.
- chat-panel.tsx: Create per-instance DefaultChatTransport that injects sessionId
via body function and a ref (avoids stale closures). On file change, auto-load
the most recent session and its messages. Refresh session tab list after
streaming ends. Stop ongoing stream when switching sessions.
- register.agent.ts: Add --session-key <key> and --lane <lane> CLI flags.
- agent-via-gateway.ts: Wire sessionKey into session resolution and validation
for both interactive and --stream-json code paths.
- workspace.ts: Add resolveAgentWorkspacePrefix() to map workspace-root-relative
paths to repo-root-relative paths for the agent process.
── Error Surfacing ──
- agent-runner.ts: Add onAgentError callback extraction helpers
(parseAgentErrorMessage, parseErrorBody, parseErrorFromStderr) to surface
API-level errors (402 payment, rate limits, etc.) to the UI. Captures stderr
for fallback error detection on non-zero exit.
- route.ts: Wire onAgentError into the SSE stream as [error]-prefixed text
parts. Improve onError and onClose handlers with clearer error messages and
exit code reporting.
- chat-message.tsx: Detect [error]-prefixed text segments and render them as
styled error banners with alert icon instead of plain text.
- chat-panel.tsx: Restyle the transport-level error bar with themed colors and
an alert icon consistent with in-message error styling.
2026-02-11 20:54:30 -08:00
|
|
|
}: MarkdownEditorProps) {
|
|
|
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
|
const [saveStatus, setSaveStatus] = useState<"idle" | "saved" | "error">("idle");
|
|
|
|
|
const [isDirty, setIsDirty] = useState(false);
|
|
|
|
|
// Tracks the `content` prop so we can detect external updates (parent re-fetch).
|
|
|
|
|
// Only updated when the prop itself changes -- never on save.
|
|
|
|
|
const lastPropContentRef = useRef(content);
|
|
|
|
|
const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
|
// Preserve frontmatter so save can prepend it back
|
|
|
|
|
const frontmatterRef = useRef(extractFrontmatter(rawContent ?? ""));
|
|
|
|
|
|
Dench workspace: unified @ mention search, entry detail modal, and workspace link system
New libraries:
- workspace-links.ts: builders, parsers, and type guards for workspace URLs
(/workspace?path=... for files/objects, /workspace?entry=objName:id for entries).
Replaces ad-hoc relative-path links with real navigable URLs.
- search-index.ts: useSearchIndex hook that fetches a unified search index from
the API and provides Fuse.js-powered fuzzy search across files, objects, and
database entries. Exposes a stable ref-based search function safe for capture
in tiptap extensions.
New API routes:
- GET /api/workspace/search-index: builds a flat search index from the filesystem
tree (knowledge/, reports/, top-level files) and all DuckDB object entries with
display-field labels and preview fields.
- GET /api/workspace/objects/[name]/entries/[id]: fetches a single entry with
all field values, resolved relation labels, reverse relations (incoming links
from other objects), and the effective display field.
New component:
- EntryDetailModal: slide-over modal for viewing an individual entry's fields,
enum badges, user avatars, clickable relation chips (navigate to related
entries), reverse relation sections, and timestamps. Supports Escape to close
and backdrop click dismiss.
Slash command refactor (slash-command.tsx):
- New createWorkspaceMention(searchFn) replaces the old file-only @ mention with
unified search across files, objects, and entries.
- searchItemToSlashItem() converts search index items into tiptap suggestion
items with proper icons, badges (object name pill for entries), and link
insertion commands using real workspace URLs.
- Legacy createFileMention(tree) now delegates to createWorkspaceMention with a
simple substring-match fallback for when no search index is available.
Editor integration (markdown-editor.tsx, document-view.tsx):
- MarkdownEditor accepts optional searchFn prop; when provided, uses
createWorkspaceMention instead of the legacy createFileMention.
- Link click interception now uses the shared isWorkspaceLink() helper and
registers handlers in capture phase for reliable interception.
- DocumentView forwards searchFn to editor and adds a delegated click handler
in read mode to intercept workspace links and navigate via onNavigate.
Object table (object-table.tsx):
- Added onEntryClick prop; table rows are now clickable with cursor-pointer
styling, firing the callback with the entry ID.
Workspace page (page.tsx):
- Integrates useSearchIndex hook and passes search function down to editor.
- Entry detail modal state with URL synchronization (?entry=objName:id param).
- New resolveNode() with fallback strategies: exact match, knowledge/ prefix
toggle, and last-segment object name matching.
- Unified handleEditorNavigate() dispatches /workspace?entry=... to the modal
and /workspace?path=... to file/object navigation.
- URL bar syncs with activePath via router.replace (no full page reloads).
- Top-level container click safety net catches any workspace link clicks that
bubble up unhandled.
Styles (globals.css):
- Added .slash-cmd-item-badge for object-name pills in the @ mention popup.
2026-02-11 21:41:23 -08:00
|
|
|
// "/" for block commands, "@" for workspace search (files + entries)
|
Dench workspace: Tiptap markdown editor, subagent sessions, and error surfacing
── Tiptap Markdown Editor ──
- Add full Tiptap-based WYSIWYG markdown editor (markdown-editor.tsx, 709 LOC)
with bubble menu, auto-save (debounced), image drag-and-drop/paste upload,
table editing, task list checkboxes, and frontmatter preservation on save.
- Add slash command system (slash-command.tsx, 607 LOC) with "/" trigger for
block insertion (headings, lists, tables, code blocks, images, reports) and
"@" trigger for file/document mention with fuzzy search across the workspace
tree.
- Add ReportBlockNode (report-block-node.tsx) — custom Tiptap node that renders
embedded report-json blocks as interactive ReportCard widgets inline in the
editor, with expand/collapse and edit-JSON support.
- Add workspace asset serving API (api/workspace/assets/[...path]/route.ts) to
serve images from the workspace with proper MIME types.
- Add workspace file upload orkspace/upload/route.ts) for multipart
image uploads (10 MB limit, image types only), saving to assets/ directory.
- Add ~500 lines of Tiptap editor CSS to globals.css (editor layout, task lists,
images, tables, slash command dropdown, bubble menu toolbar, code blocks, etc.).
- Add 14 @tiptap/* dependencies to apps/web/package.json (react, starter-kit,
markdown, image, link, table, task-list, suggestion, placeholder, etc.).
── Document View: Edit/Read Mode Toggle ──
- document-view.tsx: Add edit/read mode toggle; defaults to edit mode when a
filePath is available. Lazy-loads MarkdownEditor to keep initial bundle light.
- workspace/page.tsx: Pass activePath, tree, onSave, onNavigate, and
onRefreshTree through to DocumentView for full editor integration with
workspace navigation and tree refresh after saves.
── Subagent Session Isolation ──
- agent-runner.ts: Add RunAgentOptions with optional sessionId; when set, spawns
the agent with --session-key agent:main:subagent:<id> ant so
file-scoped sidebar chats run in isolated sessions independent of the main
agent.
- route.ts (chat API): Accept sessionId from request body and forward it to
runAgent. Resolve workspace file path prefixes (resolveAgentWorkspacePrefix)
so tree-relative paths become agent-cwd-relative.
- chat-panel.tsx: Create per-instance DefaultChatTransport that injects sessionId
via body function and a ref (avoids stale closures). On file change, auto-load
the most recent session and its messages. Refresh session tab list after
streaming ends. Stop ongoing stream when switching sessions.
- register.agent.ts: Add --session-key <key> and --lane <lane> CLI flags.
- agent-via-gateway.ts: Wire sessionKey into session resolution and validation
for both interactive and --stream-json code paths.
- workspace.ts: Add resolveAgentWorkspacePrefix() to map workspace-root-relative
paths to repo-root-relative paths for the agent process.
── Error Surfacing ──
- agent-runner.ts: Add onAgentError callback extraction helpers
(parseAgentErrorMessage, parseErrorBody, parseErrorFromStderr) to surface
API-level errors (402 payment, rate limits, etc.) to the UI. Captures stderr
for fallback error detection on non-zero exit.
- route.ts: Wire onAgentError into the SSE stream as [error]-prefixed text
parts. Improve onError and onClose handlers with clearer error messages and
exit code reporting.
- chat-message.tsx: Detect [error]-prefixed text segments and render them as
styled error banners with alert icon instead of plain text.
- chat-panel.tsx: Restyle the transport-level error bar with themed colors and
an alert icon consistent with in-message error styling.
2026-02-11 20:54:30 -08:00
|
|
|
const slashCommand = useMemo(() => createSlashCommand(), []);
|
Dench workspace: unified @ mention search, entry detail modal, and workspace link system
New libraries:
- workspace-links.ts: builders, parsers, and type guards for workspace URLs
(/workspace?path=... for files/objects, /workspace?entry=objName:id for entries).
Replaces ad-hoc relative-path links with real navigable URLs.
- search-index.ts: useSearchIndex hook that fetches a unified search index from
the API and provides Fuse.js-powered fuzzy search across files, objects, and
database entries. Exposes a stable ref-based search function safe for capture
in tiptap extensions.
New API routes:
- GET /api/workspace/search-index: builds a flat search index from the filesystem
tree (knowledge/, reports/, top-level files) and all DuckDB object entries with
display-field labels and preview fields.
- GET /api/workspace/objects/[name]/entries/[id]: fetches a single entry with
all field values, resolved relation labels, reverse relations (incoming links
from other objects), and the effective display field.
New component:
- EntryDetailModal: slide-over modal for viewing an individual entry's fields,
enum badges, user avatars, clickable relation chips (navigate to related
entries), reverse relation sections, and timestamps. Supports Escape to close
and backdrop click dismiss.
Slash command refactor (slash-command.tsx):
- New createWorkspaceMention(searchFn) replaces the old file-only @ mention with
unified search across files, objects, and entries.
- searchItemToSlashItem() converts search index items into tiptap suggestion
items with proper icons, badges (object name pill for entries), and link
insertion commands using real workspace URLs.
- Legacy createFileMention(tree) now delegates to createWorkspaceMention with a
simple substring-match fallback for when no search index is available.
Editor integration (markdown-editor.tsx, document-view.tsx):
- MarkdownEditor accepts optional searchFn prop; when provided, uses
createWorkspaceMention instead of the legacy createFileMention.
- Link click interception now uses the shared isWorkspaceLink() helper and
registers handlers in capture phase for reliable interception.
- DocumentView forwards searchFn to editor and adds a delegated click handler
in read mode to intercept workspace links and navigate via onNavigate.
Object table (object-table.tsx):
- Added onEntryClick prop; table rows are now clickable with cursor-pointer
styling, firing the callback with the entry ID.
Workspace page (page.tsx):
- Integrates useSearchIndex hook and passes search function down to editor.
- Entry detail modal state with URL synchronization (?entry=objName:id param).
- New resolveNode() with fallback strategies: exact match, knowledge/ prefix
toggle, and last-segment object name matching.
- Unified handleEditorNavigate() dispatches /workspace?entry=... to the modal
and /workspace?path=... to file/object navigation.
- URL bar syncs with activePath via router.replace (no full page reloads).
- Top-level container click safety net catches any workspace link clicks that
bubble up unhandled.
Styles (globals.css):
- Added .slash-cmd-item-badge for object-name pills in the @ mention popup.
2026-02-11 21:41:23 -08:00
|
|
|
const fileMention = useMemo(
|
|
|
|
|
() => searchFn ? createWorkspaceMention(searchFn) : createFileMention(tree),
|
|
|
|
|
// searchFn from useSearchIndex is a stable ref-based function, so this
|
|
|
|
|
// only re-runs on initial mount or if tree changes as fallback.
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
[searchFn, tree],
|
|
|
|
|
);
|
Dench workspace: Tiptap markdown editor, subagent sessions, and error surfacing
── Tiptap Markdown Editor ──
- Add full Tiptap-based WYSIWYG markdown editor (markdown-editor.tsx, 709 LOC)
with bubble menu, auto-save (debounced), image drag-and-drop/paste upload,
table editing, task list checkboxes, and frontmatter preservation on save.
- Add slash command system (slash-command.tsx, 607 LOC) with "/" trigger for
block insertion (headings, lists, tables, code blocks, images, reports) and
"@" trigger for file/document mention with fuzzy search across the workspace
tree.
- Add ReportBlockNode (report-block-node.tsx) — custom Tiptap node that renders
embedded report-json blocks as interactive ReportCard widgets inline in the
editor, with expand/collapse and edit-JSON support.
- Add workspace asset serving API (api/workspace/assets/[...path]/route.ts) to
serve images from the workspace with proper MIME types.
- Add workspace file upload orkspace/upload/route.ts) for multipart
image uploads (10 MB limit, image types only), saving to assets/ directory.
- Add ~500 lines of Tiptap editor CSS to globals.css (editor layout, task lists,
images, tables, slash command dropdown, bubble menu toolbar, code blocks, etc.).
- Add 14 @tiptap/* dependencies to apps/web/package.json (react, starter-kit,
markdown, image, link, table, task-list, suggestion, placeholder, etc.).
── Document View: Edit/Read Mode Toggle ──
- document-view.tsx: Add edit/read mode toggle; defaults to edit mode when a
filePath is available. Lazy-loads MarkdownEditor to keep initial bundle light.
- workspace/page.tsx: Pass activePath, tree, onSave, onNavigate, and
onRefreshTree through to DocumentView for full editor integration with
workspace navigation and tree refresh after saves.
── Subagent Session Isolation ──
- agent-runner.ts: Add RunAgentOptions with optional sessionId; when set, spawns
the agent with --session-key agent:main:subagent:<id> ant so
file-scoped sidebar chats run in isolated sessions independent of the main
agent.
- route.ts (chat API): Accept sessionId from request body and forward it to
runAgent. Resolve workspace file path prefixes (resolveAgentWorkspacePrefix)
so tree-relative paths become agent-cwd-relative.
- chat-panel.tsx: Create per-instance DefaultChatTransport that injects sessionId
via body function and a ref (avoids stale closures). On file change, auto-load
the most recent session and its messages. Refresh session tab list after
streaming ends. Stop ongoing stream when switching sessions.
- register.agent.ts: Add --session-key <key> and --lane <lane> CLI flags.
- agent-via-gateway.ts: Wire sessionKey into session resolution and validation
for both interactive and --stream-json code paths.
- workspace.ts: Add resolveAgentWorkspacePrefix() to map workspace-root-relative
paths to repo-root-relative paths for the agent process.
── Error Surfacing ──
- agent-runner.ts: Add onAgentError callback extraction helpers
(parseAgentErrorMessage, parseErrorBody, parseErrorFromStderr) to surface
API-level errors (402 payment, rate limits, etc.) to the UI. Captures stderr
for fallback error detection on non-zero exit.
- route.ts: Wire onAgentError into the SSE stream as [error]-prefixed text
parts. Improve onError and onClose handlers with clearer error messages and
exit code reporting.
- chat-message.tsx: Detect [error]-prefixed text segments and render them as
styled error banners with alert icon instead of plain text.
- chat-panel.tsx: Restyle the transport-level error bar with themed colors and
an alert icon consistent with in-message error styling.
2026-02-11 20:54:30 -08:00
|
|
|
|
|
|
|
|
const editor = useEditor({
|
|
|
|
|
extensions: [
|
|
|
|
|
StarterKit.configure({
|
|
|
|
|
codeBlock: {
|
|
|
|
|
HTMLAttributes: { class: "code-block" },
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
Markdown.configure({
|
|
|
|
|
markedOptions: { gfm: true },
|
|
|
|
|
}),
|
|
|
|
|
Image.configure({
|
|
|
|
|
inline: false,
|
|
|
|
|
allowBase64: true,
|
|
|
|
|
HTMLAttributes: { class: "editor-image" },
|
|
|
|
|
}),
|
|
|
|
|
Link.configure({
|
|
|
|
|
openOnClick: false,
|
Dench workspace: unified @ mention search, entry detail modal, and workspace link system
New libraries:
- workspace-links.ts: builders, parsers, and type guards for workspace URLs
(/workspace?path=... for files/objects, /workspace?entry=objName:id for entries).
Replaces ad-hoc relative-path links with real navigable URLs.
- search-index.ts: useSearchIndex hook that fetches a unified search index from
the API and provides Fuse.js-powered fuzzy search across files, objects, and
database entries. Exposes a stable ref-based search function safe for capture
in tiptap extensions.
New API routes:
- GET /api/workspace/search-index: builds a flat search index from the filesystem
tree (knowledge/, reports/, top-level files) and all DuckDB object entries with
display-field labels and preview fields.
- GET /api/workspace/objects/[name]/entries/[id]: fetches a single entry with
all field values, resolved relation labels, reverse relations (incoming links
from other objects), and the effective display field.
New component:
- EntryDetailModal: slide-over modal for viewing an individual entry's fields,
enum badges, user avatars, clickable relation chips (navigate to related
entries), reverse relation sections, and timestamps. Supports Escape to close
and backdrop click dismiss.
Slash command refactor (slash-command.tsx):
- New createWorkspaceMention(searchFn) replaces the old file-only @ mention with
unified search across files, objects, and entries.
- searchItemToSlashItem() converts search index items into tiptap suggestion
items with proper icons, badges (object name pill for entries), and link
insertion commands using real workspace URLs.
- Legacy createFileMention(tree) now delegates to createWorkspaceMention with a
simple substring-match fallback for when no search index is available.
Editor integration (markdown-editor.tsx, document-view.tsx):
- MarkdownEditor accepts optional searchFn prop; when provided, uses
createWorkspaceMention instead of the legacy createFileMention.
- Link click interception now uses the shared isWorkspaceLink() helper and
registers handlers in capture phase for reliable interception.
- DocumentView forwards searchFn to editor and adds a delegated click handler
in read mode to intercept workspace links and navigate via onNavigate.
Object table (object-table.tsx):
- Added onEntryClick prop; table rows are now clickable with cursor-pointer
styling, firing the callback with the entry ID.
Workspace page (page.tsx):
- Integrates useSearchIndex hook and passes search function down to editor.
- Entry detail modal state with URL synchronization (?entry=objName:id param).
- New resolveNode() with fallback strategies: exact match, knowledge/ prefix
toggle, and last-segment object name matching.
- Unified handleEditorNavigate() dispatches /workspace?entry=... to the modal
and /workspace?path=... to file/object navigation.
- URL bar syncs with activePath via router.replace (no full page reloads).
- Top-level container click safety net catches any workspace link clicks that
bubble up unhandled.
Styles (globals.css):
- Added .slash-cmd-item-badge for object-name pills in the @ mention popup.
2026-02-11 21:41:23 -08:00
|
|
|
autolink: true,
|
|
|
|
|
HTMLAttributes: {
|
|
|
|
|
class: "editor-link",
|
|
|
|
|
// Prevent browser from following workspace links as real URLs
|
|
|
|
|
rel: "noopener",
|
|
|
|
|
},
|
Dench workspace: Tiptap markdown editor, subagent sessions, and error surfacing
── Tiptap Markdown Editor ──
- Add full Tiptap-based WYSIWYG markdown editor (markdown-editor.tsx, 709 LOC)
with bubble menu, auto-save (debounced), image drag-and-drop/paste upload,
table editing, task list checkboxes, and frontmatter preservation on save.
- Add slash command system (slash-command.tsx, 607 LOC) with "/" trigger for
block insertion (headings, lists, tables, code blocks, images, reports) and
"@" trigger for file/document mention with fuzzy search across the workspace
tree.
- Add ReportBlockNode (report-block-node.tsx) — custom Tiptap node that renders
embedded report-json blocks as interactive ReportCard widgets inline in the
editor, with expand/collapse and edit-JSON support.
- Add workspace asset serving API (api/workspace/assets/[...path]/route.ts) to
serve images from the workspace with proper MIME types.
- Add workspace file upload orkspace/upload/route.ts) for multipart
image uploads (10 MB limit, image types only), saving to assets/ directory.
- Add ~500 lines of Tiptap editor CSS to globals.css (editor layout, task lists,
images, tables, slash command dropdown, bubble menu toolbar, code blocks, etc.).
- Add 14 @tiptap/* dependencies to apps/web/package.json (react, starter-kit,
markdown, image, link, table, task-list, suggestion, placeholder, etc.).
── Document View: Edit/Read Mode Toggle ──
- document-view.tsx: Add edit/read mode toggle; defaults to edit mode when a
filePath is available. Lazy-loads MarkdownEditor to keep initial bundle light.
- workspace/page.tsx: Pass activePath, tree, onSave, onNavigate, and
onRefreshTree through to DocumentView for full editor integration with
workspace navigation and tree refresh after saves.
── Subagent Session Isolation ──
- agent-runner.ts: Add RunAgentOptions with optional sessionId; when set, spawns
the agent with --session-key agent:main:subagent:<id> ant so
file-scoped sidebar chats run in isolated sessions independent of the main
agent.
- route.ts (chat API): Accept sessionId from request body and forward it to
runAgent. Resolve workspace file path prefixes (resolveAgentWorkspacePrefix)
so tree-relative paths become agent-cwd-relative.
- chat-panel.tsx: Create per-instance DefaultChatTransport that injects sessionId
via body function and a ref (avoids stale closures). On file change, auto-load
the most recent session and its messages. Refresh session tab list after
streaming ends. Stop ongoing stream when switching sessions.
- register.agent.ts: Add --session-key <key> and --lane <lane> CLI flags.
- agent-via-gateway.ts: Wire sessionKey into session resolution and validation
for both interactive and --stream-json code paths.
- workspace.ts: Add resolveAgentWorkspacePrefix() to map workspace-root-relative
paths to repo-root-relative paths for the agent process.
── Error Surfacing ──
- agent-runner.ts: Add onAgentError callback extraction helpers
(parseAgentErrorMessage, parseErrorBody, parseErrorFromStderr) to surface
API-level errors (402 payment, rate limits, etc.) to the UI. Captures stderr
for fallback error detection on non-zero exit.
- route.ts: Wire onAgentError into the SSE stream as [error]-prefixed text
parts. Improve onError and onClose handlers with clearer error messages and
exit code reporting.
- chat-message.tsx: Detect [error]-prefixed text segments and render them as
styled error banners with alert icon instead of plain text.
- chat-panel.tsx: Restyle the transport-level error bar with themed colors and
an alert icon consistent with in-message error styling.
2026-02-11 20:54:30 -08:00
|
|
|
}),
|
|
|
|
|
Table.configure({ resizable: false }),
|
|
|
|
|
TableRow,
|
|
|
|
|
TableCell,
|
|
|
|
|
TableHeader,
|
|
|
|
|
TaskList,
|
|
|
|
|
TaskItem.configure({ nested: true }),
|
|
|
|
|
Placeholder.configure({
|
|
|
|
|
placeholder: "Start writing, or type / for commands...",
|
|
|
|
|
}),
|
|
|
|
|
ReportBlockNode,
|
|
|
|
|
slashCommand,
|
|
|
|
|
fileMention,
|
|
|
|
|
],
|
|
|
|
|
// Parse initial content as markdown (not HTML -- the default)
|
|
|
|
|
content: preprocessReportBlocks(content),
|
|
|
|
|
contentType: "markdown",
|
|
|
|
|
immediatelyRender: false,
|
|
|
|
|
onUpdate: () => {
|
|
|
|
|
setIsDirty(true);
|
|
|
|
|
setSaveStatus("idle");
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// --- Image upload helper ---
|
|
|
|
|
const uploadImage = useCallback(
|
|
|
|
|
async (file: File): Promise<string | null> => {
|
|
|
|
|
const form = new FormData();
|
|
|
|
|
form.append("file", file);
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch("/api/workspace/upload", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
body: form,
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) {return null;}
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
// Return a URL the browser can fetch to display the image
|
|
|
|
|
return `/api/workspace/assets/${(data.path as string).replace(/^assets\//, "")}`;
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/** Upload one or more image Files and insert them at the current cursor. */
|
|
|
|
|
const insertUploadedImages = useCallback(
|
|
|
|
|
async (files: File[]) => {
|
|
|
|
|
if (!editor) {return;}
|
|
|
|
|
for (const file of files) {
|
|
|
|
|
const url = await uploadImage(file);
|
|
|
|
|
if (url) {
|
|
|
|
|
editor.chain().focus().setImage({ src: url, alt: file.name }).run();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[editor, uploadImage],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// --- Drop & paste handlers for images ---
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!editor) {return;}
|
|
|
|
|
|
|
|
|
|
const editorElement = editor.view.dom;
|
|
|
|
|
|
|
|
|
|
// Prevent the browser default (open file in tab) and upload instead
|
|
|
|
|
const handleDrop = (event: DragEvent) => {
|
|
|
|
|
if (!event.dataTransfer?.files?.length) {return;}
|
|
|
|
|
|
|
|
|
|
const imageFiles = Array.from(event.dataTransfer.files).filter((f) =>
|
|
|
|
|
f.type.startsWith("image/"),
|
|
|
|
|
);
|
|
|
|
|
if (imageFiles.length === 0) {return;}
|
|
|
|
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
event.stopPropagation();
|
2026-02-16 00:30:13 -08:00
|
|
|
void insertUploadedImages(imageFiles);
|
Dench workspace: Tiptap markdown editor, subagent sessions, and error surfacing
── Tiptap Markdown Editor ──
- Add full Tiptap-based WYSIWYG markdown editor (markdown-editor.tsx, 709 LOC)
with bubble menu, auto-save (debounced), image drag-and-drop/paste upload,
table editing, task list checkboxes, and frontmatter preservation on save.
- Add slash command system (slash-command.tsx, 607 LOC) with "/" trigger for
block insertion (headings, lists, tables, code blocks, images, reports) and
"@" trigger for file/document mention with fuzzy search across the workspace
tree.
- Add ReportBlockNode (report-block-node.tsx) — custom Tiptap node that renders
embedded report-json blocks as interactive ReportCard widgets inline in the
editor, with expand/collapse and edit-JSON support.
- Add workspace asset serving API (api/workspace/assets/[...path]/route.ts) to
serve images from the workspace with proper MIME types.
- Add workspace file upload orkspace/upload/route.ts) for multipart
image uploads (10 MB limit, image types only), saving to assets/ directory.
- Add ~500 lines of Tiptap editor CSS to globals.css (editor layout, task lists,
images, tables, slash command dropdown, bubble menu toolbar, code blocks, etc.).
- Add 14 @tiptap/* dependencies to apps/web/package.json (react, starter-kit,
markdown, image, link, table, task-list, suggestion, placeholder, etc.).
── Document View: Edit/Read Mode Toggle ──
- document-view.tsx: Add edit/read mode toggle; defaults to edit mode when a
filePath is available. Lazy-loads MarkdownEditor to keep initial bundle light.
- workspace/page.tsx: Pass activePath, tree, onSave, onNavigate, and
onRefreshTree through to DocumentView for full editor integration with
workspace navigation and tree refresh after saves.
── Subagent Session Isolation ──
- agent-runner.ts: Add RunAgentOptions with optional sessionId; when set, spawns
the agent with --session-key agent:main:subagent:<id> ant so
file-scoped sidebar chats run in isolated sessions independent of the main
agent.
- route.ts (chat API): Accept sessionId from request body and forward it to
runAgent. Resolve workspace file path prefixes (resolveAgentWorkspacePrefix)
so tree-relative paths become agent-cwd-relative.
- chat-panel.tsx: Create per-instance DefaultChatTransport that injects sessionId
via body function and a ref (avoids stale closures). On file change, auto-load
the most recent session and its messages. Refresh session tab list after
streaming ends. Stop ongoing stream when switching sessions.
- register.agent.ts: Add --session-key <key> and --lane <lane> CLI flags.
- agent-via-gateway.ts: Wire sessionKey into session resolution and validation
for both interactive and --stream-json code paths.
- workspace.ts: Add resolveAgentWorkspacePrefix() to map workspace-root-relative
paths to repo-root-relative paths for the agent process.
── Error Surfacing ──
- agent-runner.ts: Add onAgentError callback extraction helpers
(parseAgentErrorMessage, parseErrorBody, parseErrorFromStderr) to surface
API-level errors (402 payment, rate limits, etc.) to the UI. Captures stderr
for fallback error detection on non-zero exit.
- route.ts: Wire onAgentError into the SSE stream as [error]-prefixed text
parts. Improve onError and onClose handlers with clearer error messages and
exit code reporting.
- chat-message.tsx: Detect [error]-prefixed text segments and render them as
styled error banners with alert icon instead of plain text.
- chat-panel.tsx: Restyle the transport-level error bar with themed colors and
an alert icon consistent with in-message error styling.
2026-02-11 20:54:30 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Also prevent dragover so the browser doesn't hijack the drop
|
|
|
|
|
const handleDragOver = (event: DragEvent) => {
|
|
|
|
|
if (event.dataTransfer?.types?.includes("Files")) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handlePaste = (event: ClipboardEvent) => {
|
|
|
|
|
if (!event.clipboardData) {return;}
|
|
|
|
|
|
|
|
|
|
// 1. Handle pasted image files (e.g. screenshots)
|
|
|
|
|
const imageFiles = Array.from(event.clipboardData.files).filter((f) =>
|
|
|
|
|
f.type.startsWith("image/"),
|
|
|
|
|
);
|
|
|
|
|
if (imageFiles.length > 0) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
event.stopPropagation();
|
2026-02-16 00:30:13 -08:00
|
|
|
void insertUploadedImages(imageFiles);
|
Dench workspace: Tiptap markdown editor, subagent sessions, and error surfacing
── Tiptap Markdown Editor ──
- Add full Tiptap-based WYSIWYG markdown editor (markdown-editor.tsx, 709 LOC)
with bubble menu, auto-save (debounced), image drag-and-drop/paste upload,
table editing, task list checkboxes, and frontmatter preservation on save.
- Add slash command system (slash-command.tsx, 607 LOC) with "/" trigger for
block insertion (headings, lists, tables, code blocks, images, reports) and
"@" trigger for file/document mention with fuzzy search across the workspace
tree.
- Add ReportBlockNode (report-block-node.tsx) — custom Tiptap node that renders
embedded report-json blocks as interactive ReportCard widgets inline in the
editor, with expand/collapse and edit-JSON support.
- Add workspace asset serving API (api/workspace/assets/[...path]/route.ts) to
serve images from the workspace with proper MIME types.
- Add workspace file upload orkspace/upload/route.ts) for multipart
image uploads (10 MB limit, image types only), saving to assets/ directory.
- Add ~500 lines of Tiptap editor CSS to globals.css (editor layout, task lists,
images, tables, slash command dropdown, bubble menu toolbar, code blocks, etc.).
- Add 14 @tiptap/* dependencies to apps/web/package.json (react, starter-kit,
markdown, image, link, table, task-list, suggestion, placeholder, etc.).
── Document View: Edit/Read Mode Toggle ──
- document-view.tsx: Add edit/read mode toggle; defaults to edit mode when a
filePath is available. Lazy-loads MarkdownEditor to keep initial bundle light.
- workspace/page.tsx: Pass activePath, tree, onSave, onNavigate, and
onRefreshTree through to DocumentView for full editor integration with
workspace navigation and tree refresh after saves.
── Subagent Session Isolation ──
- agent-runner.ts: Add RunAgentOptions with optional sessionId; when set, spawns
the agent with --session-key agent:main:subagent:<id> ant so
file-scoped sidebar chats run in isolated sessions independent of the main
agent.
- route.ts (chat API): Accept sessionId from request body and forward it to
runAgent. Resolve workspace file path prefixes (resolveAgentWorkspacePrefix)
so tree-relative paths become agent-cwd-relative.
- chat-panel.tsx: Create per-instance DefaultChatTransport that injects sessionId
via body function and a ref (avoids stale closures). On file change, auto-load
the most recent session and its messages. Refresh session tab list after
streaming ends. Stop ongoing stream when switching sessions.
- register.agent.ts: Add --session-key <key> and --lane <lane> CLI flags.
- agent-via-gateway.ts: Wire sessionKey into session resolution and validation
for both interactive and --stream-json code paths.
- workspace.ts: Add resolveAgentWorkspacePrefix() to map workspace-root-relative
paths to repo-root-relative paths for the agent process.
── Error Surfacing ──
- agent-runner.ts: Add onAgentError callback extraction helpers
(parseAgentErrorMessage, parseErrorBody, parseErrorFromStderr) to surface
API-level errors (402 payment, rate limits, etc.) to the UI. Captures stderr
for fallback error detection on non-zero exit.
- route.ts: Wire onAgentError into the SSE stream as [error]-prefixed text
parts. Improve onError and onClose handlers with clearer error messages and
exit code reporting.
- chat-message.tsx: Detect [error]-prefixed text segments and render them as
styled error banners with alert icon instead of plain text.
- chat-panel.tsx: Restyle the transport-level error bar with themed colors and
an alert icon consistent with in-message error styling.
2026-02-11 20:54:30 -08:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Handle pasted text that looks like a local image path or file:// URL
|
|
|
|
|
const text = event.clipboardData.getData("text/plain");
|
|
|
|
|
if (!text) {return;}
|
|
|
|
|
|
|
|
|
|
const isLocalPath =
|
|
|
|
|
text.startsWith("file://") ||
|
|
|
|
|
/^(\/|~\/|[A-Z]:\\).*\.(png|jpe?g|gif|webp|svg|bmp|ico)$/i.test(text.trim());
|
|
|
|
|
|
|
|
|
|
if (isLocalPath) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
// Insert as an image node directly -- the browser can't fetch file:// but
|
|
|
|
|
// the user likely has the file accessible on their machine. We insert the
|
|
|
|
|
// cleaned path; the asset serving route won't help here but at least the
|
|
|
|
|
// markdown  will be correct.
|
|
|
|
|
const cleanPath = text.trim().replace(/^file:\/\//, "");
|
|
|
|
|
editor?.chain().focus().setImage({ src: cleanPath }).run();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
editorElement.addEventListener("drop", handleDrop);
|
|
|
|
|
editorElement.addEventListener("dragover", handleDragOver);
|
|
|
|
|
editorElement.addEventListener("paste", handlePaste);
|
|
|
|
|
return () => {
|
|
|
|
|
editorElement.removeEventListener("drop", handleDrop);
|
|
|
|
|
editorElement.removeEventListener("dragover", handleDragOver);
|
|
|
|
|
editorElement.removeEventListener("paste", handlePaste);
|
|
|
|
|
};
|
|
|
|
|
}, [editor, insertUploadedImages]);
|
|
|
|
|
|
Dench workspace: unified @ mention search, entry detail modal, and workspace link system
New libraries:
- workspace-links.ts: builders, parsers, and type guards for workspace URLs
(/workspace?path=... for files/objects, /workspace?entry=objName:id for entries).
Replaces ad-hoc relative-path links with real navigable URLs.
- search-index.ts: useSearchIndex hook that fetches a unified search index from
the API and provides Fuse.js-powered fuzzy search across files, objects, and
database entries. Exposes a stable ref-based search function safe for capture
in tiptap extensions.
New API routes:
- GET /api/workspace/search-index: builds a flat search index from the filesystem
tree (knowledge/, reports/, top-level files) and all DuckDB object entries with
display-field labels and preview fields.
- GET /api/workspace/objects/[name]/entries/[id]: fetches a single entry with
all field values, resolved relation labels, reverse relations (incoming links
from other objects), and the effective display field.
New component:
- EntryDetailModal: slide-over modal for viewing an individual entry's fields,
enum badges, user avatars, clickable relation chips (navigate to related
entries), reverse relation sections, and timestamps. Supports Escape to close
and backdrop click dismiss.
Slash command refactor (slash-command.tsx):
- New createWorkspaceMention(searchFn) replaces the old file-only @ mention with
unified search across files, objects, and entries.
- searchItemToSlashItem() converts search index items into tiptap suggestion
items with proper icons, badges (object name pill for entries), and link
insertion commands using real workspace URLs.
- Legacy createFileMention(tree) now delegates to createWorkspaceMention with a
simple substring-match fallback for when no search index is available.
Editor integration (markdown-editor.tsx, document-view.tsx):
- MarkdownEditor accepts optional searchFn prop; when provided, uses
createWorkspaceMention instead of the legacy createFileMention.
- Link click interception now uses the shared isWorkspaceLink() helper and
registers handlers in capture phase for reliable interception.
- DocumentView forwards searchFn to editor and adds a delegated click handler
in read mode to intercept workspace links and navigate via onNavigate.
Object table (object-table.tsx):
- Added onEntryClick prop; table rows are now clickable with cursor-pointer
styling, firing the callback with the entry ID.
Workspace page (page.tsx):
- Integrates useSearchIndex hook and passes search function down to editor.
- Entry detail modal state with URL synchronization (?entry=objName:id param).
- New resolveNode() with fallback strategies: exact match, knowledge/ prefix
toggle, and last-segment object name matching.
- Unified handleEditorNavigate() dispatches /workspace?entry=... to the modal
and /workspace?path=... to file/object navigation.
- URL bar syncs with activePath via router.replace (no full page reloads).
- Top-level container click safety net catches any workspace link clicks that
bubble up unhandled.
Styles (globals.css):
- Added .slash-cmd-item-badge for object-name pills in the @ mention popup.
2026-02-11 21:41:23 -08:00
|
|
|
// Handle link clicks for workspace navigation.
|
|
|
|
|
// Links are real URLs like /workspace?path=... so clicking them navigates
|
|
|
|
|
// within the same tab. We intercept to avoid a full page reload.
|
Dench workspace: Tiptap markdown editor, subagent sessions, and error surfacing
── Tiptap Markdown Editor ──
- Add full Tiptap-based WYSIWYG markdown editor (markdown-editor.tsx, 709 LOC)
with bubble menu, auto-save (debounced), image drag-and-drop/paste upload,
table editing, task list checkboxes, and frontmatter preservation on save.
- Add slash command system (slash-command.tsx, 607 LOC) with "/" trigger for
block insertion (headings, lists, tables, code blocks, images, reports) and
"@" trigger for file/document mention with fuzzy search across the workspace
tree.
- Add ReportBlockNode (report-block-node.tsx) — custom Tiptap node that renders
embedded report-json blocks as interactive ReportCard widgets inline in the
editor, with expand/collapse and edit-JSON support.
- Add workspace asset serving API (api/workspace/assets/[...path]/route.ts) to
serve images from the workspace with proper MIME types.
- Add workspace file upload orkspace/upload/route.ts) for multipart
image uploads (10 MB limit, image types only), saving to assets/ directory.
- Add ~500 lines of Tiptap editor CSS to globals.css (editor layout, task lists,
images, tables, slash command dropdown, bubble menu toolbar, code blocks, etc.).
- Add 14 @tiptap/* dependencies to apps/web/package.json (react, starter-kit,
markdown, image, link, table, task-list, suggestion, placeholder, etc.).
── Document View: Edit/Read Mode Toggle ──
- document-view.tsx: Add edit/read mode toggle; defaults to edit mode when a
filePath is available. Lazy-loads MarkdownEditor to keep initial bundle light.
- workspace/page.tsx: Pass activePath, tree, onSave, onNavigate, and
onRefreshTree through to DocumentView for full editor integration with
workspace navigation and tree refresh after saves.
── Subagent Session Isolation ──
- agent-runner.ts: Add RunAgentOptions with optional sessionId; when set, spawns
the agent with --session-key agent:main:subagent:<id> ant so
file-scoped sidebar chats run in isolated sessions independent of the main
agent.
- route.ts (chat API): Accept sessionId from request body and forward it to
runAgent. Resolve workspace file path prefixes (resolveAgentWorkspacePrefix)
so tree-relative paths become agent-cwd-relative.
- chat-panel.tsx: Create per-instance DefaultChatTransport that injects sessionId
via body function and a ref (avoids stale closures). On file change, auto-load
the most recent session and its messages. Refresh session tab list after
streaming ends. Stop ongoing stream when switching sessions.
- register.agent.ts: Add --session-key <key> and --lane <lane> CLI flags.
- agent-via-gateway.ts: Wire sessionKey into session resolution and validation
for both interactive and --stream-json code paths.
- workspace.ts: Add resolveAgentWorkspacePrefix() to map workspace-root-relative
paths to repo-root-relative paths for the agent process.
── Error Surfacing ──
- agent-runner.ts: Add onAgentError callback extraction helpers
(parseAgentErrorMessage, parseErrorBody, parseErrorFromStderr) to surface
API-level errors (402 payment, rate limits, etc.) to the UI. Captures stderr
for fallback error detection on non-zero exit.
- route.ts: Wire onAgentError into the SSE stream as [error]-prefixed text
parts. Improve onError and onClose handlers with clearer error messages and
exit code reporting.
- chat-message.tsx: Detect [error]-prefixed text segments and render them as
styled error banners with alert icon instead of plain text.
- chat-panel.tsx: Restyle the transport-level error bar with themed colors and
an alert icon consistent with in-message error styling.
2026-02-11 20:54:30 -08:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (!editor || !onNavigate) {return;}
|
|
|
|
|
|
|
|
|
|
const handleClick = (event: MouseEvent) => {
|
|
|
|
|
const target = event.target as HTMLElement;
|
|
|
|
|
const link = target.closest("a");
|
|
|
|
|
if (!link) {return;}
|
|
|
|
|
|
|
|
|
|
const href = link.getAttribute("href");
|
|
|
|
|
if (!href) {return;}
|
|
|
|
|
|
Dench workspace: unified @ mention search, entry detail modal, and workspace link system
New libraries:
- workspace-links.ts: builders, parsers, and type guards for workspace URLs
(/workspace?path=... for files/objects, /workspace?entry=objName:id for entries).
Replaces ad-hoc relative-path links with real navigable URLs.
- search-index.ts: useSearchIndex hook that fetches a unified search index from
the API and provides Fuse.js-powered fuzzy search across files, objects, and
database entries. Exposes a stable ref-based search function safe for capture
in tiptap extensions.
New API routes:
- GET /api/workspace/search-index: builds a flat search index from the filesystem
tree (knowledge/, reports/, top-level files) and all DuckDB object entries with
display-field labels and preview fields.
- GET /api/workspace/objects/[name]/entries/[id]: fetches a single entry with
all field values, resolved relation labels, reverse relations (incoming links
from other objects), and the effective display field.
New component:
- EntryDetailModal: slide-over modal for viewing an individual entry's fields,
enum badges, user avatars, clickable relation chips (navigate to related
entries), reverse relation sections, and timestamps. Supports Escape to close
and backdrop click dismiss.
Slash command refactor (slash-command.tsx):
- New createWorkspaceMention(searchFn) replaces the old file-only @ mention with
unified search across files, objects, and entries.
- searchItemToSlashItem() converts search index items into tiptap suggestion
items with proper icons, badges (object name pill for entries), and link
insertion commands using real workspace URLs.
- Legacy createFileMention(tree) now delegates to createWorkspaceMention with a
simple substring-match fallback for when no search index is available.
Editor integration (markdown-editor.tsx, document-view.tsx):
- MarkdownEditor accepts optional searchFn prop; when provided, uses
createWorkspaceMention instead of the legacy createFileMention.
- Link click interception now uses the shared isWorkspaceLink() helper and
registers handlers in capture phase for reliable interception.
- DocumentView forwards searchFn to editor and adds a delegated click handler
in read mode to intercept workspace links and navigate via onNavigate.
Object table (object-table.tsx):
- Added onEntryClick prop; table rows are now clickable with cursor-pointer
styling, firing the callback with the entry ID.
Workspace page (page.tsx):
- Integrates useSearchIndex hook and passes search function down to editor.
- Entry detail modal state with URL synchronization (?entry=objName:id param).
- New resolveNode() with fallback strategies: exact match, knowledge/ prefix
toggle, and last-segment object name matching.
- Unified handleEditorNavigate() dispatches /workspace?entry=... to the modal
and /workspace?path=... to file/object navigation.
- URL bar syncs with activePath via router.replace (no full page reloads).
- Top-level container click safety net catches any workspace link clicks that
bubble up unhandled.
Styles (globals.css):
- Added .slash-cmd-item-badge for object-name pills in the @ mention popup.
2026-02-11 21:41:23 -08:00
|
|
|
// Intercept /workspace?... links to handle via client-side state
|
|
|
|
|
if (isWorkspaceLink(href)) {
|
Dench workspace: Tiptap markdown editor, subagent sessions, and error surfacing
── Tiptap Markdown Editor ──
- Add full Tiptap-based WYSIWYG markdown editor (markdown-editor.tsx, 709 LOC)
with bubble menu, auto-save (debounced), image drag-and-drop/paste upload,
table editing, task list checkboxes, and frontmatter preservation on save.
- Add slash command system (slash-command.tsx, 607 LOC) with "/" trigger for
block insertion (headings, lists, tables, code blocks, images, reports) and
"@" trigger for file/document mention with fuzzy search across the workspace
tree.
- Add ReportBlockNode (report-block-node.tsx) — custom Tiptap node that renders
embedded report-json blocks as interactive ReportCard widgets inline in the
editor, with expand/collapse and edit-JSON support.
- Add workspace asset serving API (api/workspace/assets/[...path]/route.ts) to
serve images from the workspace with proper MIME types.
- Add workspace file upload orkspace/upload/route.ts) for multipart
image uploads (10 MB limit, image types only), saving to assets/ directory.
- Add ~500 lines of Tiptap editor CSS to globals.css (editor layout, task lists,
images, tables, slash command dropdown, bubble menu toolbar, code blocks, etc.).
- Add 14 @tiptap/* dependencies to apps/web/package.json (react, starter-kit,
markdown, image, link, table, task-list, suggestion, placeholder, etc.).
── Document View: Edit/Read Mode Toggle ──
- document-view.tsx: Add edit/read mode toggle; defaults to edit mode when a
filePath is available. Lazy-loads MarkdownEditor to keep initial bundle light.
- workspace/page.tsx: Pass activePath, tree, onSave, onNavigate, and
onRefreshTree through to DocumentView for full editor integration with
workspace navigation and tree refresh after saves.
── Subagent Session Isolation ──
- agent-runner.ts: Add RunAgentOptions with optional sessionId; when set, spawns
the agent with --session-key agent:main:subagent:<id> ant so
file-scoped sidebar chats run in isolated sessions independent of the main
agent.
- route.ts (chat API): Accept sessionId from request body and forward it to
runAgent. Resolve workspace file path prefixes (resolveAgentWorkspacePrefix)
so tree-relative paths become agent-cwd-relative.
- chat-panel.tsx: Create per-instance DefaultChatTransport that injects sessionId
via body function and a ref (avoids stale closures). On file change, auto-load
the most recent session and its messages. Refresh session tab list after
streaming ends. Stop ongoing stream when switching sessions.
- register.agent.ts: Add --session-key <key> and --lane <lane> CLI flags.
- agent-via-gateway.ts: Wire sessionKey into session resolution and validation
for both interactive and --stream-json code paths.
- workspace.ts: Add resolveAgentWorkspacePrefix() to map workspace-root-relative
paths to repo-root-relative paths for the agent process.
── Error Surfacing ──
- agent-runner.ts: Add onAgentError callback extraction helpers
(parseAgentErrorMessage, parseErrorBody, parseErrorFromStderr) to surface
API-level errors (402 payment, rate limits, etc.) to the UI. Captures stderr
for fallback error detection on non-zero exit.
- route.ts: Wire onAgentError into the SSE stream as [error]-prefixed text
parts. Improve onError and onClose handlers with clearer error messages and
exit code reporting.
- chat-message.tsx: Detect [error]-prefixed text segments and render them as
styled error banners with alert icon instead of plain text.
- chat-panel.tsx: Restyle the transport-level error bar with themed colors and
an alert icon consistent with in-message error styling.
2026-02-11 20:54:30 -08:00
|
|
|
event.preventDefault();
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
onNavigate(href);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const editorElement = editor.view.dom;
|
Dench workspace: unified @ mention search, entry detail modal, and workspace link system
New libraries:
- workspace-links.ts: builders, parsers, and type guards for workspace URLs
(/workspace?path=... for files/objects, /workspace?entry=objName:id for entries).
Replaces ad-hoc relative-path links with real navigable URLs.
- search-index.ts: useSearchIndex hook that fetches a unified search index from
the API and provides Fuse.js-powered fuzzy search across files, objects, and
database entries. Exposes a stable ref-based search function safe for capture
in tiptap extensions.
New API routes:
- GET /api/workspace/search-index: builds a flat search index from the filesystem
tree (knowledge/, reports/, top-level files) and all DuckDB object entries with
display-field labels and preview fields.
- GET /api/workspace/objects/[name]/entries/[id]: fetches a single entry with
all field values, resolved relation labels, reverse relations (incoming links
from other objects), and the effective display field.
New component:
- EntryDetailModal: slide-over modal for viewing an individual entry's fields,
enum badges, user avatars, clickable relation chips (navigate to related
entries), reverse relation sections, and timestamps. Supports Escape to close
and backdrop click dismiss.
Slash command refactor (slash-command.tsx):
- New createWorkspaceMention(searchFn) replaces the old file-only @ mention with
unified search across files, objects, and entries.
- searchItemToSlashItem() converts search index items into tiptap suggestion
items with proper icons, badges (object name pill for entries), and link
insertion commands using real workspace URLs.
- Legacy createFileMention(tree) now delegates to createWorkspaceMention with a
simple substring-match fallback for when no search index is available.
Editor integration (markdown-editor.tsx, document-view.tsx):
- MarkdownEditor accepts optional searchFn prop; when provided, uses
createWorkspaceMention instead of the legacy createFileMention.
- Link click interception now uses the shared isWorkspaceLink() helper and
registers handlers in capture phase for reliable interception.
- DocumentView forwards searchFn to editor and adds a delegated click handler
in read mode to intercept workspace links and navigate via onNavigate.
Object table (object-table.tsx):
- Added onEntryClick prop; table rows are now clickable with cursor-pointer
styling, firing the callback with the entry ID.
Workspace page (page.tsx):
- Integrates useSearchIndex hook and passes search function down to editor.
- Entry detail modal state with URL synchronization (?entry=objName:id param).
- New resolveNode() with fallback strategies: exact match, knowledge/ prefix
toggle, and last-segment object name matching.
- Unified handleEditorNavigate() dispatches /workspace?entry=... to the modal
and /workspace?path=... to file/object navigation.
- URL bar syncs with activePath via router.replace (no full page reloads).
- Top-level container click safety net catches any workspace link clicks that
bubble up unhandled.
Styles (globals.css):
- Added .slash-cmd-item-badge for object-name pills in the @ mention popup.
2026-02-11 21:41:23 -08:00
|
|
|
editorElement.addEventListener("click", handleClick, true);
|
|
|
|
|
return () => editorElement.removeEventListener("click", handleClick, true);
|
Dench workspace: Tiptap markdown editor, subagent sessions, and error surfacing
── Tiptap Markdown Editor ──
- Add full Tiptap-based WYSIWYG markdown editor (markdown-editor.tsx, 709 LOC)
with bubble menu, auto-save (debounced), image drag-and-drop/paste upload,
table editing, task list checkboxes, and frontmatter preservation on save.
- Add slash command system (slash-command.tsx, 607 LOC) with "/" trigger for
block insertion (headings, lists, tables, code blocks, images, reports) and
"@" trigger for file/document mention with fuzzy search across the workspace
tree.
- Add ReportBlockNode (report-block-node.tsx) — custom Tiptap node that renders
embedded report-json blocks as interactive ReportCard widgets inline in the
editor, with expand/collapse and edit-JSON support.
- Add workspace asset serving API (api/workspace/assets/[...path]/route.ts) to
serve images from the workspace with proper MIME types.
- Add workspace file upload orkspace/upload/route.ts) for multipart
image uploads (10 MB limit, image types only), saving to assets/ directory.
- Add ~500 lines of Tiptap editor CSS to globals.css (editor layout, task lists,
images, tables, slash command dropdown, bubble menu toolbar, code blocks, etc.).
- Add 14 @tiptap/* dependencies to apps/web/package.json (react, starter-kit,
markdown, image, link, table, task-list, suggestion, placeholder, etc.).
── Document View: Edit/Read Mode Toggle ──
- document-view.tsx: Add edit/read mode toggle; defaults to edit mode when a
filePath is available. Lazy-loads MarkdownEditor to keep initial bundle light.
- workspace/page.tsx: Pass activePath, tree, onSave, onNavigate, and
onRefreshTree through to DocumentView for full editor integration with
workspace navigation and tree refresh after saves.
── Subagent Session Isolation ──
- agent-runner.ts: Add RunAgentOptions with optional sessionId; when set, spawns
the agent with --session-key agent:main:subagent:<id> ant so
file-scoped sidebar chats run in isolated sessions independent of the main
agent.
- route.ts (chat API): Accept sessionId from request body and forward it to
runAgent. Resolve workspace file path prefixes (resolveAgentWorkspacePrefix)
so tree-relative paths become agent-cwd-relative.
- chat-panel.tsx: Create per-instance DefaultChatTransport that injects sessionId
via body function and a ref (avoids stale closures). On file change, auto-load
the most recent session and its messages. Refresh session tab list after
streaming ends. Stop ongoing stream when switching sessions.
- register.agent.ts: Add --session-key <key> and --lane <lane> CLI flags.
- agent-via-gateway.ts: Wire sessionKey into session resolution and validation
for both interactive and --stream-json code paths.
- workspace.ts: Add resolveAgentWorkspacePrefix() to map workspace-root-relative
paths to repo-root-relative paths for the agent process.
── Error Surfacing ──
- agent-runner.ts: Add onAgentError callback extraction helpers
(parseAgentErrorMessage, parseErrorBody, parseErrorFromStderr) to surface
API-level errors (402 payment, rate limits, etc.) to the UI. Captures stderr
for fallback error detection on non-zero exit.
- route.ts: Wire onAgentError into the SSE stream as [error]-prefixed text
parts. Improve onError and onClose handlers with clearer error messages and
exit code reporting.
- chat-message.tsx: Detect [error]-prefixed text segments and render them as
styled error banners with alert icon instead of plain text.
- chat-panel.tsx: Restyle the transport-level error bar with themed colors and
an alert icon consistent with in-message error styling.
2026-02-11 20:54:30 -08:00
|
|
|
}, [editor, onNavigate]);
|
|
|
|
|
|
|
|
|
|
// Save handler
|
|
|
|
|
const handleSave = useCallback(async () => {
|
|
|
|
|
if (!editor || saving) {return;}
|
|
|
|
|
|
|
|
|
|
setSaving(true);
|
|
|
|
|
setSaveStatus("idle");
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Serialize editor content back to markdown
|
|
|
|
|
// The Markdown extension adds getMarkdown() to the editor instance
|
|
|
|
|
const editorAny = editor as unknown as { getMarkdown?: () => string };
|
|
|
|
|
let markdown: string;
|
|
|
|
|
|
|
|
|
|
if (typeof editorAny.getMarkdown === "function") {
|
|
|
|
|
markdown = editorAny.getMarkdown();
|
|
|
|
|
} else {
|
|
|
|
|
// Fallback: use HTML output
|
|
|
|
|
markdown = editor.getHTML();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Convert report block HTML back to ```report-json``` fenced blocks
|
|
|
|
|
const bodyContent = postprocessReportBlocks(markdown);
|
|
|
|
|
// Prepend preserved frontmatter so it isn't lost on save
|
|
|
|
|
const finalContent = frontmatterRef.current + bodyContent;
|
|
|
|
|
|
2026-02-15 18:18:15 -08:00
|
|
|
// Virtual paths (~skills/*) use the virtual-file API
|
Dench workspace: virtual folders (Skills, Memories, Chats), landing page, and unified workspace layout
Add virtual folder system that surfaces Skills, Memories, and Chat sessions
in the workspace sidebar alongside real dench files. Rearchitect the home
page into a landing hub and move the ChatPanel into the workspace as the
default view.
New API route — virtual-file:
- apps/web/app/api/workspace/virtual-file/route.ts: new GET/POST API that
resolves virtual paths (~skills/*, ~memories/*) to absolute filesystem
paths in ~/.openclaw/skills/ d ~/.openclaw/workspace/. Includes path
traversal protection and directory allowlisting.
Tree API — virtual folder builders:
- apps/web/app/api/workspace/tree/route.ts: add `virtual` field to TreeNode
type. Add ildSkillsVirtualFolder() scanning ~/.openclaw/skills/ and
~/.openclaw/workspace/skills/ with SKILL.md frontmatter parsing (name +
emoji). Add buildMemoriesVirtualFolder() scanning MEMORY.md and daily
logs from ~/.openclaw/workspace/memory/. Virtual folders are appended
after real workspace entries and are also returned when no dench root
exists.
File manager tree — virtual node awareness:
- apps/web/app/components/workspace/file-manager-tree.tsx: add isVirtualNode()
helper and RESERVED_FOLDER_NAMES set (Chats, Skills, Memories). Virtual
nodes show a lock badge, disable drag-and-drop, block rename/delete, and
reject reserved names during create/rename. Add ChatBubbleIcon for ~chats/
paths.
Markdown editor — virtual path routing:
- apps/web/app/components/workspace/markdown-editor.tsx: save to
/api/workspace/virtual-file for paths starting with ~ instead of the
regular /api/workspace/file endpoint.
Home page redesign:
- apps/web/app/page.tsx: replace the chat-first layout (SidebarhatPanel)
with a branded landing page showing the OpenClaw Dench heading, tagline,
and a single "Open Workspace" CTA linking to /workspace.
Workspace page — unified layout with integrated chat:
- apps/web/app/workspace/page.tsx: ChatPanel is now the default main view
when no file is selected. Add session fetching from /api/web-sessions and
build an enhanced tree with a virtual "Chats" folder listing all sessions.
Clicking ~chats/<id> loads the session; clicking ~chats starts a new one.
Add isVirtualPath()/fileApiUrl() helpers for virtual file reads. Add a
"Back to chat" button in the top bar alongside the chat sidebar toggle.
Sidebar + empty-state cosmetic updates:
- apps/web/app/components/workspace/workspace-sidebar.tsx: rename BackIcon
to HomeIcon (house SVG), change label from "Back to Chat" to "Home".
- apps/web/app/components/workspace/empty-state.tsx: update link text from
"Back to Chat" to "Back to Home".
2026-02-11 22:09:59 -08:00
|
|
|
const saveEndpoint = filePath.startsWith("~")
|
|
|
|
|
? "/api/workspace/virtual-file"
|
|
|
|
|
: "/api/workspace/file";
|
|
|
|
|
const res = await fetch(saveEndpoint, {
|
Dench workspace: Tiptap markdown editor, subagent sessions, and error surfacing
── Tiptap Markdown Editor ──
- Add full Tiptap-based WYSIWYG markdown editor (markdown-editor.tsx, 709 LOC)
with bubble menu, auto-save (debounced), image drag-and-drop/paste upload,
table editing, task list checkboxes, and frontmatter preservation on save.
- Add slash command system (slash-command.tsx, 607 LOC) with "/" trigger for
block insertion (headings, lists, tables, code blocks, images, reports) and
"@" trigger for file/document mention with fuzzy search across the workspace
tree.
- Add ReportBlockNode (report-block-node.tsx) — custom Tiptap node that renders
embedded report-json blocks as interactive ReportCard widgets inline in the
editor, with expand/collapse and edit-JSON support.
- Add workspace asset serving API (api/workspace/assets/[...path]/route.ts) to
serve images from the workspace with proper MIME types.
- Add workspace file upload orkspace/upload/route.ts) for multipart
image uploads (10 MB limit, image types only), saving to assets/ directory.
- Add ~500 lines of Tiptap editor CSS to globals.css (editor layout, task lists,
images, tables, slash command dropdown, bubble menu toolbar, code blocks, etc.).
- Add 14 @tiptap/* dependencies to apps/web/package.json (react, starter-kit,
markdown, image, link, table, task-list, suggestion, placeholder, etc.).
── Document View: Edit/Read Mode Toggle ──
- document-view.tsx: Add edit/read mode toggle; defaults to edit mode when a
filePath is available. Lazy-loads MarkdownEditor to keep initial bundle light.
- workspace/page.tsx: Pass activePath, tree, onSave, onNavigate, and
onRefreshTree through to DocumentView for full editor integration with
workspace navigation and tree refresh after saves.
── Subagent Session Isolation ──
- agent-runner.ts: Add RunAgentOptions with optional sessionId; when set, spawns
the agent with --session-key agent:main:subagent:<id> ant so
file-scoped sidebar chats run in isolated sessions independent of the main
agent.
- route.ts (chat API): Accept sessionId from request body and forward it to
runAgent. Resolve workspace file path prefixes (resolveAgentWorkspacePrefix)
so tree-relative paths become agent-cwd-relative.
- chat-panel.tsx: Create per-instance DefaultChatTransport that injects sessionId
via body function and a ref (avoids stale closures). On file change, auto-load
the most recent session and its messages. Refresh session tab list after
streaming ends. Stop ongoing stream when switching sessions.
- register.agent.ts: Add --session-key <key> and --lane <lane> CLI flags.
- agent-via-gateway.ts: Wire sessionKey into session resolution and validation
for both interactive and --stream-json code paths.
- workspace.ts: Add resolveAgentWorkspacePrefix() to map workspace-root-relative
paths to repo-root-relative paths for the agent process.
── Error Surfacing ──
- agent-runner.ts: Add onAgentError callback extraction helpers
(parseAgentErrorMessage, parseErrorBody, parseErrorFromStderr) to surface
API-level errors (402 payment, rate limits, etc.) to the UI. Captures stderr
for fallback error detection on non-zero exit.
- route.ts: Wire onAgentError into the SSE stream as [error]-prefixed text
parts. Improve onError and onClose handlers with clearer error messages and
exit code reporting.
- chat-message.tsx: Detect [error]-prefixed text segments and render them as
styled error banners with alert icon instead of plain text.
- chat-panel.tsx: Restyle the transport-level error bar with themed colors and
an alert icon consistent with in-message error styling.
2026-02-11 20:54:30 -08:00
|
|
|
method: "POST",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
body: JSON.stringify({ path: filePath, content: finalContent }),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
setSaveStatus("saved");
|
|
|
|
|
setIsDirty(false);
|
|
|
|
|
// Sync the prop tracker to the body we just saved so the external-update
|
|
|
|
|
// effect doesn't see a mismatch and reset the editor.
|
|
|
|
|
lastPropContentRef.current = content;
|
|
|
|
|
onSave?.();
|
|
|
|
|
|
|
|
|
|
// Clear "saved" indicator after 2s
|
|
|
|
|
if (saveTimerRef.current) {clearTimeout(saveTimerRef.current);}
|
|
|
|
|
saveTimerRef.current = setTimeout(() => setSaveStatus("idle"), 2000);
|
|
|
|
|
} else {
|
|
|
|
|
setSaveStatus("error");
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
setSaveStatus("error");
|
|
|
|
|
} finally {
|
|
|
|
|
setSaving(false);
|
|
|
|
|
}
|
|
|
|
|
}, [editor, filePath, saving, onSave]);
|
|
|
|
|
|
|
|
|
|
// Keyboard shortcut: Cmd/Ctrl+S
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
|
|
|
if ((e.metaKey || e.ctrlKey) && e.key === "s") {
|
|
|
|
|
e.preventDefault();
|
2026-02-16 00:30:13 -08:00
|
|
|
void handleSave();
|
Dench workspace: Tiptap markdown editor, subagent sessions, and error surfacing
── Tiptap Markdown Editor ──
- Add full Tiptap-based WYSIWYG markdown editor (markdown-editor.tsx, 709 LOC)
with bubble menu, auto-save (debounced), image drag-and-drop/paste upload,
table editing, task list checkboxes, and frontmatter preservation on save.
- Add slash command system (slash-command.tsx, 607 LOC) with "/" trigger for
block insertion (headings, lists, tables, code blocks, images, reports) and
"@" trigger for file/document mention with fuzzy search across the workspace
tree.
- Add ReportBlockNode (report-block-node.tsx) — custom Tiptap node that renders
embedded report-json blocks as interactive ReportCard widgets inline in the
editor, with expand/collapse and edit-JSON support.
- Add workspace asset serving API (api/workspace/assets/[...path]/route.ts) to
serve images from the workspace with proper MIME types.
- Add workspace file upload orkspace/upload/route.ts) for multipart
image uploads (10 MB limit, image types only), saving to assets/ directory.
- Add ~500 lines of Tiptap editor CSS to globals.css (editor layout, task lists,
images, tables, slash command dropdown, bubble menu toolbar, code blocks, etc.).
- Add 14 @tiptap/* dependencies to apps/web/package.json (react, starter-kit,
markdown, image, link, table, task-list, suggestion, placeholder, etc.).
── Document View: Edit/Read Mode Toggle ──
- document-view.tsx: Add edit/read mode toggle; defaults to edit mode when a
filePath is available. Lazy-loads MarkdownEditor to keep initial bundle light.
- workspace/page.tsx: Pass activePath, tree, onSave, onNavigate, and
onRefreshTree through to DocumentView for full editor integration with
workspace navigation and tree refresh after saves.
── Subagent Session Isolation ──
- agent-runner.ts: Add RunAgentOptions with optional sessionId; when set, spawns
the agent with --session-key agent:main:subagent:<id> ant so
file-scoped sidebar chats run in isolated sessions independent of the main
agent.
- route.ts (chat API): Accept sessionId from request body and forward it to
runAgent. Resolve workspace file path prefixes (resolveAgentWorkspacePrefix)
so tree-relative paths become agent-cwd-relative.
- chat-panel.tsx: Create per-instance DefaultChatTransport that injects sessionId
via body function and a ref (avoids stale closures). On file change, auto-load
the most recent session and its messages. Refresh session tab list after
streaming ends. Stop ongoing stream when switching sessions.
- register.agent.ts: Add --session-key <key> and --lane <lane> CLI flags.
- agent-via-gateway.ts: Wire sessionKey into session resolution and validation
for both interactive and --stream-json code paths.
- workspace.ts: Add resolveAgentWorkspacePrefix() to map workspace-root-relative
paths to repo-root-relative paths for the agent process.
── Error Surfacing ──
- agent-runner.ts: Add onAgentError callback extraction helpers
(parseAgentErrorMessage, parseErrorBody, parseErrorFromStderr) to surface
API-level errors (402 payment, rate limits, etc.) to the UI. Captures stderr
for fallback error detection on non-zero exit.
- route.ts: Wire onAgentError into the SSE stream as [error]-prefixed text
parts. Improve onError and onClose handlers with clearer error messages and
exit code reporting.
- chat-message.tsx: Detect [error]-prefixed text segments and render them as
styled error banners with alert icon instead of plain text.
- chat-panel.tsx: Restyle the transport-level error bar with themed colors and
an alert icon consistent with in-message error styling.
2026-02-11 20:54:30 -08:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
document.addEventListener("keydown", handleKeyDown);
|
|
|
|
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
|
|
|
}, [handleSave]);
|
|
|
|
|
|
|
|
|
|
// Update content when file changes externally (parent re-fetched the file)
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!editor || isDirty) {return;}
|
|
|
|
|
if (content !== lastPropContentRef.current) {
|
|
|
|
|
lastPropContentRef.current = content;
|
|
|
|
|
// Also update frontmatter in case the raw content changed
|
|
|
|
|
frontmatterRef.current = extractFrontmatter(rawContent ?? "");
|
|
|
|
|
const processed = preprocessReportBlocks(content);
|
|
|
|
|
editor.commands.setContent(processed, { contentType: "markdown" });
|
|
|
|
|
setIsDirty(false);
|
|
|
|
|
}
|
|
|
|
|
}, [content, rawContent, editor, isDirty]);
|
|
|
|
|
|
|
|
|
|
if (!editor) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="animate-pulse space-y-3 py-4 px-6">
|
|
|
|
|
<div className="h-4 rounded" style={{ background: "var(--color-surface)", width: "80%" }} />
|
|
|
|
|
<div className="h-4 rounded" style={{ background: "var(--color-surface)", width: "60%" }} />
|
|
|
|
|
<div className="h-4 rounded" style={{ background: "var(--color-surface)", width: "70%" }} />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="markdown-editor-container">
|
2026-02-12 09:04:34 -08:00
|
|
|
{/* Sticky top bar: save status + save button */}
|
Dench workspace: Tiptap markdown editor, subagent sessions, and error surfacing
── Tiptap Markdown Editor ──
- Add full Tiptap-based WYSIWYG markdown editor (markdown-editor.tsx, 709 LOC)
with bubble menu, auto-save (debounced), image drag-and-drop/paste upload,
table editing, task list checkboxes, and frontmatter preservation on save.
- Add slash command system (slash-command.tsx, 607 LOC) with "/" trigger for
block insertion (headings, lists, tables, code blocks, images, reports) and
"@" trigger for file/document mention with fuzzy search across the workspace
tree.
- Add ReportBlockNode (report-block-node.tsx) — custom Tiptap node that renders
embedded report-json blocks as interactive ReportCard widgets inline in the
editor, with expand/collapse and edit-JSON support.
- Add workspace asset serving API (api/workspace/assets/[...path]/route.ts) to
serve images from the workspace with proper MIME types.
- Add workspace file upload orkspace/upload/route.ts) for multipart
image uploads (10 MB limit, image types only), saving to assets/ directory.
- Add ~500 lines of Tiptap editor CSS to globals.css (editor layout, task lists,
images, tables, slash command dropdown, bubble menu toolbar, code blocks, etc.).
- Add 14 @tiptap/* dependencies to apps/web/package.json (react, starter-kit,
markdown, image, link, table, task-list, suggestion, placeholder, etc.).
── Document View: Edit/Read Mode Toggle ──
- document-view.tsx: Add edit/read mode toggle; defaults to edit mode when a
filePath is available. Lazy-loads MarkdownEditor to keep initial bundle light.
- workspace/page.tsx: Pass activePath, tree, onSave, onNavigate, and
onRefreshTree through to DocumentView for full editor integration with
workspace navigation and tree refresh after saves.
── Subagent Session Isolation ──
- agent-runner.ts: Add RunAgentOptions with optional sessionId; when set, spawns
the agent with --session-key agent:main:subagent:<id> ant so
file-scoped sidebar chats run in isolated sessions independent of the main
agent.
- route.ts (chat API): Accept sessionId from request body and forward it to
runAgent. Resolve workspace file path prefixes (resolveAgentWorkspacePrefix)
so tree-relative paths become agent-cwd-relative.
- chat-panel.tsx: Create per-instance DefaultChatTransport that injects sessionId
via body function and a ref (avoids stale closures). On file change, auto-load
the most recent session and its messages. Refresh session tab list after
streaming ends. Stop ongoing stream when switching sessions.
- register.agent.ts: Add --session-key <key> and --lane <lane> CLI flags.
- agent-via-gateway.ts: Wire sessionKey into session resolution and validation
for both interactive and --stream-json code paths.
- workspace.ts: Add resolveAgentWorkspacePrefix() to map workspace-root-relative
paths to repo-root-relative paths for the agent process.
── Error Surfacing ──
- agent-runner.ts: Add onAgentError callback extraction helpers
(parseAgentErrorMessage, parseErrorBody, parseErrorFromStderr) to surface
API-level errors (402 payment, rate limits, etc.) to the UI. Captures stderr
for fallback error detection on non-zero exit.
- route.ts: Wire onAgentError into the SSE stream as [error]-prefixed text
parts. Improve onError and onClose handlers with clearer error messages and
exit code reporting.
- chat-message.tsx: Detect [error]-prefixed text segments and render them as
styled error banners with alert icon instead of plain text.
- chat-panel.tsx: Restyle the transport-level error bar with themed colors and
an alert icon consistent with in-message error styling.
2026-02-11 20:54:30 -08:00
|
|
|
<div className="editor-top-bar">
|
|
|
|
|
<div className="editor-top-bar-left">
|
|
|
|
|
{isDirty && (
|
|
|
|
|
<span className="editor-save-indicator editor-save-unsaved">
|
|
|
|
|
Unsaved changes
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
{saveStatus === "saved" && !isDirty && (
|
|
|
|
|
<span className="editor-save-indicator editor-save-saved">
|
|
|
|
|
Saved
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
{saveStatus === "error" && (
|
|
|
|
|
<span className="editor-save-indicator editor-save-error">
|
|
|
|
|
Save failed
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="editor-top-bar-right">
|
|
|
|
|
<span className="editor-save-hint">
|
|
|
|
|
{typeof navigator !== "undefined" && navigator.platform?.includes("Mac") ? "\u2318" : "Ctrl"}+S
|
|
|
|
|
</span>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
2026-02-16 00:30:13 -08:00
|
|
|
onClick={() => void handleSave()}
|
Dench workspace: Tiptap markdown editor, subagent sessions, and error surfacing
── Tiptap Markdown Editor ──
- Add full Tiptap-based WYSIWYG markdown editor (markdown-editor.tsx, 709 LOC)
with bubble menu, auto-save (debounced), image drag-and-drop/paste upload,
table editing, task list checkboxes, and frontmatter preservation on save.
- Add slash command system (slash-command.tsx, 607 LOC) with "/" trigger for
block insertion (headings, lists, tables, code blocks, images, reports) and
"@" trigger for file/document mention with fuzzy search across the workspace
tree.
- Add ReportBlockNode (report-block-node.tsx) — custom Tiptap node that renders
embedded report-json blocks as interactive ReportCard widgets inline in the
editor, with expand/collapse and edit-JSON support.
- Add workspace asset serving API (api/workspace/assets/[...path]/route.ts) to
serve images from the workspace with proper MIME types.
- Add workspace file upload orkspace/upload/route.ts) for multipart
image uploads (10 MB limit, image types only), saving to assets/ directory.
- Add ~500 lines of Tiptap editor CSS to globals.css (editor layout, task lists,
images, tables, slash command dropdown, bubble menu toolbar, code blocks, etc.).
- Add 14 @tiptap/* dependencies to apps/web/package.json (react, starter-kit,
markdown, image, link, table, task-list, suggestion, placeholder, etc.).
── Document View: Edit/Read Mode Toggle ──
- document-view.tsx: Add edit/read mode toggle; defaults to edit mode when a
filePath is available. Lazy-loads MarkdownEditor to keep initial bundle light.
- workspace/page.tsx: Pass activePath, tree, onSave, onNavigate, and
onRefreshTree through to DocumentView for full editor integration with
workspace navigation and tree refresh after saves.
── Subagent Session Isolation ──
- agent-runner.ts: Add RunAgentOptions with optional sessionId; when set, spawns
the agent with --session-key agent:main:subagent:<id> ant so
file-scoped sidebar chats run in isolated sessions independent of the main
agent.
- route.ts (chat API): Accept sessionId from request body and forward it to
runAgent. Resolve workspace file path prefixes (resolveAgentWorkspacePrefix)
so tree-relative paths become agent-cwd-relative.
- chat-panel.tsx: Create per-instance DefaultChatTransport that injects sessionId
via body function and a ref (avoids stale closures). On file change, auto-load
the most recent session and its messages. Refresh session tab list after
streaming ends. Stop ongoing stream when switching sessions.
- register.agent.ts: Add --session-key <key> and --lane <lane> CLI flags.
- agent-via-gateway.ts: Wire sessionKey into session resolution and validation
for both interactive and --stream-json code paths.
- workspace.ts: Add resolveAgentWorkspacePrefix() to map workspace-root-relative
paths to repo-root-relative paths for the agent process.
── Error Surfacing ──
- agent-runner.ts: Add onAgentError callback extraction helpers
(parseAgentErrorMessage, parseErrorBody, parseErrorFromStderr) to surface
API-level errors (402 payment, rate limits, etc.) to the UI. Captures stderr
for fallback error detection on non-zero exit.
- route.ts: Wire onAgentError into the SSE stream as [error]-prefixed text
parts. Improve onError and onClose handlers with clearer error messages and
exit code reporting.
- chat-message.tsx: Detect [error]-prefixed text segments and render them as
styled error banners with alert icon instead of plain text.
- chat-panel.tsx: Restyle the transport-level error bar with themed colors and
an alert icon consistent with in-message error styling.
2026-02-11 20:54:30 -08:00
|
|
|
disabled={saving || !isDirty}
|
|
|
|
|
className="editor-save-button"
|
|
|
|
|
>
|
|
|
|
|
{saving ? "Saving..." : "Save"}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Toolbar */}
|
|
|
|
|
<EditorToolbar editor={editor} onUploadImages={insertUploadedImages} />
|
|
|
|
|
|
|
|
|
|
{/* Bubble menu for text selection */}
|
2026-02-12 00:29:39 -08:00
|
|
|
<BubbleMenu editor={editor}>
|
Dench workspace: Tiptap markdown editor, subagent sessions, and error surfacing
── Tiptap Markdown Editor ──
- Add full Tiptap-based WYSIWYG markdown editor (markdown-editor.tsx, 709 LOC)
with bubble menu, auto-save (debounced), image drag-and-drop/paste upload,
table editing, task list checkboxes, and frontmatter preservation on save.
- Add slash command system (slash-command.tsx, 607 LOC) with "/" trigger for
block insertion (headings, lists, tables, code blocks, images, reports) and
"@" trigger for file/document mention with fuzzy search across the workspace
tree.
- Add ReportBlockNode (report-block-node.tsx) — custom Tiptap node that renders
embedded report-json blocks as interactive ReportCard widgets inline in the
editor, with expand/collapse and edit-JSON support.
- Add workspace asset serving API (api/workspace/assets/[...path]/route.ts) to
serve images from the workspace with proper MIME types.
- Add workspace file upload orkspace/upload/route.ts) for multipart
image uploads (10 MB limit, image types only), saving to assets/ directory.
- Add ~500 lines of Tiptap editor CSS to globals.css (editor layout, task lists,
images, tables, slash command dropdown, bubble menu toolbar, code blocks, etc.).
- Add 14 @tiptap/* dependencies to apps/web/package.json (react, starter-kit,
markdown, image, link, table, task-list, suggestion, placeholder, etc.).
── Document View: Edit/Read Mode Toggle ──
- document-view.tsx: Add edit/read mode toggle; defaults to edit mode when a
filePath is available. Lazy-loads MarkdownEditor to keep initial bundle light.
- workspace/page.tsx: Pass activePath, tree, onSave, onNavigate, and
onRefreshTree through to DocumentView for full editor integration with
workspace navigation and tree refresh after saves.
── Subagent Session Isolation ──
- agent-runner.ts: Add RunAgentOptions with optional sessionId; when set, spawns
the agent with --session-key agent:main:subagent:<id> ant so
file-scoped sidebar chats run in isolated sessions independent of the main
agent.
- route.ts (chat API): Accept sessionId from request body and forward it to
runAgent. Resolve workspace file path prefixes (resolveAgentWorkspacePrefix)
so tree-relative paths become agent-cwd-relative.
- chat-panel.tsx: Create per-instance DefaultChatTransport that injects sessionId
via body function and a ref (avoids stale closures). On file change, auto-load
the most recent session and its messages. Refresh session tab list after
streaming ends. Stop ongoing stream when switching sessions.
- register.agent.ts: Add --session-key <key> and --lane <lane> CLI flags.
- agent-via-gateway.ts: Wire sessionKey into session resolution and validation
for both interactive and --stream-json code paths.
- workspace.ts: Add resolveAgentWorkspacePrefix() to map workspace-root-relative
paths to repo-root-relative paths for the agent process.
── Error Surfacing ──
- agent-runner.ts: Add onAgentError callback extraction helpers
(parseAgentErrorMessage, parseErrorBody, parseErrorFromStderr) to surface
API-level errors (402 payment, rate limits, etc.) to the UI. Captures stderr
for fallback error detection on non-zero exit.
- route.ts: Wire onAgentError into the SSE stream as [error]-prefixed text
parts. Improve onError and onClose handlers with clearer error messages and
exit code reporting.
- chat-message.tsx: Detect [error]-prefixed text segments and render them as
styled error banners with alert icon instead of plain text.
- chat-panel.tsx: Restyle the transport-level error bar with themed colors and
an alert icon consistent with in-message error styling.
2026-02-11 20:54:30 -08:00
|
|
|
<div className="bubble-menu">
|
|
|
|
|
<BubbleButton
|
|
|
|
|
active={editor.isActive("bold")}
|
|
|
|
|
onClick={() => editor.chain().focus().toggleBold().run()}
|
|
|
|
|
title="Bold"
|
|
|
|
|
>
|
|
|
|
|
<strong>B</strong>
|
|
|
|
|
</BubbleButton>
|
|
|
|
|
<BubbleButton
|
|
|
|
|
active={editor.isActive("italic")}
|
|
|
|
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
|
|
|
|
title="Italic"
|
|
|
|
|
>
|
|
|
|
|
<em>I</em>
|
|
|
|
|
</BubbleButton>
|
|
|
|
|
<BubbleButton
|
|
|
|
|
active={editor.isActive("strike")}
|
|
|
|
|
onClick={() => editor.chain().focus().toggleStrike().run()}
|
|
|
|
|
title="Strikethrough"
|
|
|
|
|
>
|
|
|
|
|
<s>S</s>
|
|
|
|
|
</BubbleButton>
|
|
|
|
|
<BubbleButton
|
|
|
|
|
active={editor.isActive("code")}
|
|
|
|
|
onClick={() => editor.chain().focus().toggleCode().run()}
|
|
|
|
|
title="Inline code"
|
|
|
|
|
>
|
|
|
|
|
{"<>"}
|
|
|
|
|
</BubbleButton>
|
|
|
|
|
<BubbleButton
|
|
|
|
|
active={editor.isActive("link")}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
if (editor.isActive("link")) {
|
|
|
|
|
editor.chain().focus().unsetLink().run();
|
|
|
|
|
} else {
|
|
|
|
|
const url = window.prompt("URL:");
|
|
|
|
|
if (url) {
|
|
|
|
|
editor.chain().focus().setLink({ href: url }).run();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
title="Link"
|
|
|
|
|
>
|
|
|
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
|
|
|
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
|
|
|
|
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
|
|
|
|
</svg>
|
|
|
|
|
</BubbleButton>
|
|
|
|
|
</div>
|
|
|
|
|
</BubbleMenu>
|
|
|
|
|
|
|
|
|
|
{/* Editor content */}
|
|
|
|
|
<div className="editor-content-area workspace-prose">
|
|
|
|
|
<EditorContent editor={editor} />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Toolbar ---
|
|
|
|
|
|
|
|
|
|
function EditorToolbar({
|
|
|
|
|
editor,
|
|
|
|
|
onUploadImages,
|
|
|
|
|
}: {
|
|
|
|
|
editor: ReturnType<typeof useEditor>;
|
|
|
|
|
onUploadImages?: (files: File[]) => void;
|
|
|
|
|
}) {
|
|
|
|
|
const imageInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
|
|
|
|
|
if (!editor) {return null;}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="editor-toolbar">
|
|
|
|
|
{/* Headings */}
|
|
|
|
|
<ToolbarGroup>
|
|
|
|
|
<ToolbarButton
|
|
|
|
|
active={editor.isActive("heading", { level: 1 })}
|
|
|
|
|
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
|
|
|
|
title="Heading 1"
|
|
|
|
|
>
|
|
|
|
|
H1
|
|
|
|
|
</ToolbarButton>
|
|
|
|
|
<ToolbarButton
|
|
|
|
|
active={editor.isActive("heading", { level: 2 })}
|
|
|
|
|
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
|
|
|
|
title="Heading 2"
|
|
|
|
|
>
|
|
|
|
|
H2
|
|
|
|
|
</ToolbarButton>
|
|
|
|
|
<ToolbarButton
|
|
|
|
|
active={editor.isActive("heading", { level: 3 })}
|
|
|
|
|
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
|
|
|
|
|
title="Heading 3"
|
|
|
|
|
>
|
|
|
|
|
H3
|
|
|
|
|
</ToolbarButton>
|
|
|
|
|
</ToolbarGroup>
|
|
|
|
|
|
|
|
|
|
<ToolbarDivider />
|
|
|
|
|
|
|
|
|
|
{/* Inline formatting */}
|
|
|
|
|
<ToolbarGroup>
|
|
|
|
|
<ToolbarButton
|
|
|
|
|
active={editor.isActive("bold")}
|
|
|
|
|
onClick={() => editor.chain().focus().toggleBold().run()}
|
|
|
|
|
title="Bold"
|
|
|
|
|
>
|
|
|
|
|
<strong>B</strong>
|
|
|
|
|
</ToolbarButton>
|
|
|
|
|
<ToolbarButton
|
|
|
|
|
active={editor.isActive("italic")}
|
|
|
|
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
|
|
|
|
title="Italic"
|
|
|
|
|
>
|
|
|
|
|
<em>I</em>
|
|
|
|
|
</ToolbarButton>
|
|
|
|
|
<ToolbarButton
|
|
|
|
|
active={editor.isActive("strike")}
|
|
|
|
|
onClick={() => editor.chain().focus().toggleStrike().run()}
|
|
|
|
|
title="Strikethrough"
|
|
|
|
|
>
|
|
|
|
|
<s>S</s>
|
|
|
|
|
</ToolbarButton>
|
|
|
|
|
<ToolbarButton
|
|
|
|
|
active={editor.isActive("code")}
|
|
|
|
|
onClick={() => editor.chain().focus().toggleCode().run()}
|
|
|
|
|
title="Inline code"
|
|
|
|
|
>
|
|
|
|
|
{"<>"}
|
|
|
|
|
</ToolbarButton>
|
|
|
|
|
</ToolbarGroup>
|
|
|
|
|
|
|
|
|
|
<ToolbarDivider />
|
|
|
|
|
|
|
|
|
|
{/* Block elements */}
|
|
|
|
|
<ToolbarGroup>
|
|
|
|
|
<ToolbarButton
|
|
|
|
|
active={editor.isActive("bulletList")}
|
|
|
|
|
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
|
|
|
|
title="Bullet list"
|
|
|
|
|
>
|
|
|
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
|
|
|
<line x1="8" x2="21" y1="6" y2="6" /><line x1="8" x2="21" y1="12" y2="12" /><line x1="8" x2="21" y1="18" y2="18" />
|
|
|
|
|
<line x1="3" x2="3.01" y1="6" y2="6" /><line x1="3" x2="3.01" y1="12" y2="12" /><line x1="3" x2="3.01" y1="18" y2="18" />
|
|
|
|
|
</svg>
|
|
|
|
|
</ToolbarButton>
|
|
|
|
|
<ToolbarButton
|
|
|
|
|
active={editor.isActive("orderedList")}
|
|
|
|
|
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
|
|
|
|
title="Ordered list"
|
|
|
|
|
>
|
|
|
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
|
|
|
<line x1="10" x2="21" y1="6" y2="6" /><line x1="10" x2="21" y1="12" y2="12" /><line x1="10" x2="21" y1="18" y2="18" />
|
|
|
|
|
<path d="M4 6h1v4" /><path d="M4 10h2" /><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1" />
|
|
|
|
|
</svg>
|
|
|
|
|
</ToolbarButton>
|
|
|
|
|
<ToolbarButton
|
|
|
|
|
active={editor.isActive("taskList")}
|
|
|
|
|
onClick={() => editor.chain().focus().toggleTaskList().run()}
|
|
|
|
|
title="Task list"
|
|
|
|
|
>
|
|
|
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
|
|
|
<rect x="3" y="5" width="6" height="6" rx="1" /><path d="m3 17 2 2 4-4" /><line x1="13" x2="21" y1="6" y2="6" /><line x1="13" x2="21" y1="18" y2="18" />
|
|
|
|
|
</svg>
|
|
|
|
|
</ToolbarButton>
|
|
|
|
|
<ToolbarButton
|
|
|
|
|
active={editor.isActive("blockquote")}
|
|
|
|
|
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
|
|
|
|
title="Blockquote"
|
|
|
|
|
>
|
|
|
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
|
|
|
<path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V21z" />
|
|
|
|
|
<path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3z" />
|
|
|
|
|
</svg>
|
|
|
|
|
</ToolbarButton>
|
|
|
|
|
<ToolbarButton
|
|
|
|
|
active={editor.isActive("codeBlock")}
|
|
|
|
|
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
|
|
|
|
|
title="Code block"
|
|
|
|
|
>
|
|
|
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
|
|
|
<polyline points="16 18 22 12 16 6" /><polyline points="8 6 2 12 8 18" />
|
|
|
|
|
</svg>
|
|
|
|
|
</ToolbarButton>
|
|
|
|
|
</ToolbarGroup>
|
|
|
|
|
|
|
|
|
|
<ToolbarDivider />
|
|
|
|
|
|
|
|
|
|
{/* Insert items */}
|
|
|
|
|
<ToolbarGroup>
|
|
|
|
|
<ToolbarButton
|
|
|
|
|
active={false}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
const url = window.prompt("Link URL:");
|
|
|
|
|
if (url) {
|
|
|
|
|
editor.chain().focus().setLink({ href: url }).run();
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
title="Insert link"
|
|
|
|
|
>
|
|
|
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
|
|
|
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
|
|
|
|
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
|
|
|
|
</svg>
|
|
|
|
|
</ToolbarButton>
|
|
|
|
|
<ToolbarButton
|
|
|
|
|
active={false}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
// Open file picker for local images; shift-click for URL input
|
|
|
|
|
if (onUploadImages) {
|
|
|
|
|
imageInputRef.current?.click();
|
|
|
|
|
} else {
|
|
|
|
|
const url = window.prompt("Image URL:");
|
|
|
|
|
if (url) {
|
|
|
|
|
editor.chain().focus().setImage({ src: url }).run();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
title="Insert image (click to upload, or drag & drop)"
|
|
|
|
|
>
|
|
|
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
|
|
|
<rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
|
|
|
|
|
<circle cx="9" cy="9" r="2" />
|
|
|
|
|
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
|
|
|
|
|
</svg>
|
|
|
|
|
</ToolbarButton>
|
|
|
|
|
{/* Hidden file input for image upload */}
|
|
|
|
|
<input
|
|
|
|
|
ref={imageInputRef}
|
|
|
|
|
type="file"
|
|
|
|
|
accept="image/*"
|
|
|
|
|
multiple
|
|
|
|
|
className="hidden"
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
const files = Array.from(e.target.files ?? []);
|
|
|
|
|
if (files.length > 0 && onUploadImages) {
|
|
|
|
|
onUploadImages(files);
|
|
|
|
|
}
|
|
|
|
|
// Reset so the same file can be picked again
|
|
|
|
|
e.target.value = "";
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<ToolbarButton
|
|
|
|
|
active={false}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
|
|
|
|
|
}}
|
|
|
|
|
title="Insert table"
|
|
|
|
|
>
|
|
|
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
|
|
|
<path d="M12 3v18" /><rect width="18" height="18" x="3" y="3" rx="2" /><path d="M3 9h18" /><path d="M3 15h18" />
|
|
|
|
|
</svg>
|
|
|
|
|
</ToolbarButton>
|
|
|
|
|
<ToolbarButton
|
|
|
|
|
active={false}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
editor.chain().focus().setHorizontalRule().run();
|
|
|
|
|
}}
|
|
|
|
|
title="Horizontal rule"
|
|
|
|
|
>
|
|
|
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
|
|
|
<line x1="2" x2="22" y1="12" y2="12" />
|
|
|
|
|
</svg>
|
|
|
|
|
</ToolbarButton>
|
|
|
|
|
</ToolbarGroup>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Toolbar primitives ---
|
|
|
|
|
|
|
|
|
|
function ToolbarGroup({ children }: { children: React.ReactNode }) {
|
|
|
|
|
return <div className="editor-toolbar-group">{children}</div>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ToolbarDivider() {
|
|
|
|
|
return <div className="editor-toolbar-divider" />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ToolbarButton({
|
|
|
|
|
active,
|
|
|
|
|
onClick,
|
|
|
|
|
title,
|
|
|
|
|
children,
|
|
|
|
|
}: {
|
|
|
|
|
active: boolean;
|
|
|
|
|
onClick: () => void;
|
|
|
|
|
title: string;
|
|
|
|
|
children: React.ReactNode;
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className={`editor-toolbar-btn ${active ? "editor-toolbar-btn-active" : ""}`}
|
|
|
|
|
onClick={onClick}
|
|
|
|
|
title={title}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Bubble menu button ---
|
|
|
|
|
|
|
|
|
|
function BubbleButton({
|
|
|
|
|
active,
|
|
|
|
|
onClick,
|
|
|
|
|
title,
|
|
|
|
|
children,
|
|
|
|
|
}: {
|
|
|
|
|
active: boolean;
|
|
|
|
|
onClick: () => void;
|
|
|
|
|
title: string;
|
|
|
|
|
children: React.ReactNode;
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className={`bubble-menu-btn ${active ? "bubble-menu-btn-active" : ""}`}
|
|
|
|
|
onClick={onClick}
|
|
|
|
|
title={title}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
}
|