+
= ({
>
- {renderSavedComments()}
+ {portals}
-
- {/* Render Input overlay at the bottom */}
- {selection && (
-
0 ? `${inputBarOffset}px` : '16px')
- : '16px'
- }}
- data-keyboard-avoid="true"
- data-comment-ui="true"
- ref={commentContainerRef}
- >
- {commentContent}
-
- )}
);
}
- // Fallback for 'inline' layout: use Portal behavior
- // Use simple div with overflow-x-auto to avoid nested ScrollableOverlay issues in Chrome
+ // Fallback for 'inline' layout
return (
- {renderSavedComments()}
+ {portals}
-
- {selection && createPortal(
-
-
0 ? `${inputBarOffset}px` : '16px')
- : '16px'
- }}
- data-keyboard-avoid="true"
- data-comment-ui="true"
- ref={commentContainerRef}
- >
- {commentContent}
-
-
,
- document.body
- )}
);
};
+
+
diff --git a/packages/ui/src/components/views/PlanView.tsx b/packages/ui/src/components/views/PlanView.tsx
index 44a6c9db..43cb1e08 100644
--- a/packages/ui/src/components/views/PlanView.tsx
+++ b/packages/ui/src/components/views/PlanView.tsx
@@ -1,33 +1,26 @@
import React from 'react';
-import { CodeMirrorEditor } from '@/components/ui/CodeMirrorEditor';
+import { CodeMirrorEditor, type BlockWidgetDef } from '@/components/ui/CodeMirrorEditor';
+import { InlineCommentCard, InlineCommentInput } from '@/components/comments';
import { PreviewToggleButton } from './PreviewToggleButton';
import { SimpleMarkdownRenderer } from '@/components/chat/MarkdownRenderer';
import { ErrorBoundary } from '@/components/ui/ErrorBoundary';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
-import { Textarea } from '@/components/ui/textarea';
import { Button } from '@/components/ui/button';
import { useUIStore } from '@/stores/useUIStore';
-import { cn, getModifierLabel } from '@/lib/utils';
+
import { getLanguageFromExtension } from '@/lib/toolHelpers';
import { useDeviceInfo } from '@/lib/device';
import { useThemeSystem } from '@/contexts/useThemeSystem';
import { generateSyntaxTheme } from '@/lib/theme/syntaxThemeGenerator';
import { createFlexokiCodeMirrorTheme } from '@/lib/codemirror/flexokiTheme';
import { languageByExtension } from '@/lib/codemirror/languageByExtension';
-import { RiCheckLine, RiClipboardLine, RiFileCopy2Line, RiMoreLine, RiDeleteBinLine, RiEditLine } from '@remixicon/react';
+import { RiCheckLine, RiClipboardLine, RiFileCopy2Line } from '@remixicon/react';
import { useSessionStore } from '@/stores/useSessionStore';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { EditorView } from '@codemirror/view';
import { useInlineCommentDraftStore } from '@/stores/useInlineCommentDraftStore';
import { toast } from '@/components/ui';
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from '@/components/ui/dropdown-menu';
-import { createPortal } from 'react-dom';
const normalize = (value: string): string => {
if (!value) return '';
@@ -93,11 +86,10 @@ export const PlanView: React.FC = () => {
const homeDirectory = useDirectoryStore((state) => state.homeDirectory);
const runtimeApis = useRuntimeAPIs();
const newSessionDraftOpen = useSessionStore((state) => state.newSessionDraft?.open);
- const { inputBarOffset, isKeyboardOpen } = useUIStore();
+ useUIStore();
const { isMobile } = useDeviceInfo();
const { currentTheme } = useThemeSystem();
React.useMemo(() => generateSyntaxTheme(currentTheme), [currentTheme]);
- const [editorView, setEditorView] = React.useState
(null);
// Inline comment drafts
const addDraft = useInlineCommentDraftStore((state) => state.addDraft);
@@ -138,8 +130,6 @@ export const PlanView: React.FC = () => {
const [lineSelection, setLineSelection] = React.useState(null);
const [commentText, setCommentText] = React.useState('');
const [editingDraftId, setEditingDraftId] = React.useState(null);
- const [pendingFocus, setPendingFocus] = React.useState(false);
- const commentInputRef = React.useRef(null);
const MD_VIEWER_MODE_KEY = 'openchamber:plan:md-viewer-mode';
@@ -180,37 +170,35 @@ export const PlanView: React.FC = () => {
setLineSelection(null);
setCommentText('');
setEditingDraftId(null);
- setPendingFocus(false);
}, [content]);
- React.useEffect(() => {
- if (!pendingFocus || !lineSelection || isMobile) return;
- if (typeof window === 'undefined') return;
- const frame = window.requestAnimationFrame(() => {
- const input = commentInputRef.current;
- input?.focus();
- if (input) {
- const length = input.value.length;
- input.setSelectionRange(length, length);
- }
- setPendingFocus(false);
- });
- return () => window.cancelAnimationFrame(frame);
- }, [pendingFocus, lineSelection, isMobile]);
-
React.useEffect(() => {
if (!lineSelection) return;
+
+ // Auto-scroll input into view on mobile
+ if (isMobile && !editingDraftId) {
+ // We rely on InlineCommentInput doing this now via useEffect
+ }
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as HTMLElement;
- const commentUI = document.querySelector('[data-comment-ui]');
- if (commentUI?.contains(target)) return;
+ // Check if click is inside any comment component
+ if (
+ target.closest('[data-comment-card="true"]') ||
+ target.closest('[data-comment-input="true"]') ||
+ target.closest('.oc-block-widget')
+ ) {
+ return;
+ }
+
if (target.closest('.cm-gutterElement')) return;
if (target.closest('[data-sonner-toast]') || target.closest('[data-sonner-toaster]')) return;
+
+ // If clicking outside while editing, maybe we should save or ask confirmation?
+ // For now, cancel edit
setLineSelection(null);
setCommentText('');
setEditingDraftId(null);
- setPendingFocus(false);
};
const timeoutId = window.setTimeout(() => {
@@ -221,7 +209,7 @@ export const PlanView: React.FC = () => {
window.clearTimeout(timeoutId);
document.removeEventListener('click', handleClickOutside);
};
- }, [lineSelection]);
+ }, [lineSelection, editingDraftId, isMobile]);
const extractSelectedCode = React.useCallback((text: string, range: SelectedLineRange): string => {
const lines = text.split('\n');
@@ -235,11 +223,12 @@ export const PlanView: React.FC = () => {
setCommentText('');
setLineSelection(null);
setEditingDraftId(null);
- setPendingFocus(false);
}, []);
- const handleSaveComment = React.useCallback(() => {
- if (!lineSelection || !commentText.trim()) return;
+ const handleSaveComment = React.useCallback((textToSave: string, rangeOverride?: { start: number; end: number }) => {
+ // Use provided range override or fall back to current selection
+ const targetRange = rangeOverride ?? lineSelection;
+ if (!targetRange || !textToSave.trim()) return;
const sessionKey = getSessionKey();
if (!sessionKey) {
@@ -247,29 +236,29 @@ export const PlanView: React.FC = () => {
return;
}
- const code = extractSelectedCode(content, lineSelection);
+ const code = extractSelectedCode(content, targetRange);
const fileLabel = displayPath ? displayPath.split('/').pop() || 'plan' : 'plan';
const language = resolvedPath ? getLanguageFromExtension(resolvedPath) || 'markdown' : 'markdown';
if (editingDraftId) {
updateDraft(sessionKey, editingDraftId, {
fileLabel,
- startLine: lineSelection.start,
- endLine: lineSelection.end,
+ startLine: targetRange.start,
+ endLine: targetRange.end,
code,
language,
- text: commentText.trim(),
+ text: textToSave.trim(),
});
} else {
addDraft({
sessionKey,
source: 'plan',
fileLabel,
- startLine: lineSelection.start,
- endLine: lineSelection.end,
+ startLine: targetRange.start,
+ endLine: targetRange.end,
code,
language,
- text: commentText.trim(),
+ text: textToSave.trim(),
});
}
@@ -278,7 +267,8 @@ export const PlanView: React.FC = () => {
setEditingDraftId(null);
toast.success(editingDraftId ? 'Comment updated' : 'Comment saved');
- }, [lineSelection, commentText, content, displayPath, resolvedPath, addDraft, updateDraft, getSessionKey, extractSelectedCode, editingDraftId]);
+ }, [lineSelection, content, displayPath, resolvedPath, addDraft, updateDraft, getSessionKey, extractSelectedCode, editingDraftId]);
+
const editorExtensions = React.useMemo(() => {
const extensions = [createFlexokiCodeMirrorTheme(currentTheme)];
@@ -387,170 +377,87 @@ export const PlanView: React.FC = () => {
};
}, []);
- const renderCommentUI = () => {
- if (!lineSelection) return null;
- return (
-
- );
- };
-
- // Render saved comment cards
- const renderSavedComments = () => {
- if (mdViewMode === 'preview') return null;
- if (!editorView) return null;
+ const blockWidgets = React.useMemo(() => {
+ if (mdViewMode === 'preview') return [];
const sessionKey = getSessionKey();
- if (!sessionKey) return null;
+ if (!sessionKey) return [];
const sessionDrafts = allDrafts[sessionKey] ?? [];
const fileLabel = displayPath ? displayPath.split('/').pop() || 'plan' : 'plan';
const fileDrafts = sessionDrafts.filter((d) => d.source === 'plan' && d.fileLabel === fileLabel);
- if (fileDrafts.length === 0) return null;
+ const widgets: BlockWidgetDef[] = [];
- return createPortal(
-
- {fileDrafts.map((draft) => {
- const maxLine = editorView.state.doc.lines;
- const safeLine = Math.min(Math.max(1, draft.startLine), maxLine);
- const line = editorView.state.doc.line(safeLine);
- const top = editorView.lineBlockAt(line.from).top;
-
- return (
-
{
+ if (draft.id === editingDraftId) {
+ // Always show edit input (even on mobile)
+ widgets.push({
+ afterLine: draft.endLine,
+ id: `edit-${draft.id}`,
+ content: (
+
+ ),
+ });
+ } else {
+ // Show saved cards on all devices
+ widgets.push({
+ afterLine: draft.endLine,
+ id: `draft-${draft.id}`,
+ content: (
+
{
+ setLineSelection({ start: draft.startLine, end: draft.endLine });
+ setCommentText(draft.text);
+ setEditingDraftId(draft.id);
}}
- >
-
-
-
-
- {draft.fileLabel}:{draft.startLine}-{draft.endLine}
-
-
{draft.text}
-
-
-
-
-
- event.preventDefault()}
- >
- {
- setLineSelection({ start: draft.startLine, end: draft.endLine });
- setCommentText(draft.text);
- setEditingDraftId(draft.id);
- setPendingFocus(true);
- }}
- >
-
- Edit comment
-
- removeDraft(draft.sessionKey, draft.id)}
- className="text-destructive"
- >
-
- Delete comment
-
-
-
-
-
-
- );
- })}
-
,
- editorView.scrollDOM
- );
- };
+ onDelete={() => removeDraft(draft.sessionKey, draft.id)}
+ />
+ ),
+ });
+ }
+ });
+
+ // Add new comment input if selecting AND not editing an existing draft
+ if (lineSelection && !editingDraftId) {
+ widgets.push({
+ afterLine: lineSelection.end,
+ id: 'new-comment-input',
+ content: (
+
+ ),
+ });
+ }
+
+ return widgets;
+ }, [
+ mdViewMode,
+ getSessionKey,
+ allDrafts,
+ displayPath,
+ editingDraftId,
+ lineSelection,
+ commentText,
+ handleSaveComment,
+ handleCancelComment,
+ removeDraft,
+ ]);
return (
@@ -636,11 +543,7 @@ export const PlanView: React.FC = () => {
@@ -669,14 +572,13 @@ export const PlanView: React.FC = () => {
readOnly={true}
className="h-full [&_.cm-scroller]:pb-[var(--oc-plan-comment-pad)] [&_.cm-scroller]:relative"
extensions={editorExtensions}
- onViewReady={setEditorView}
- onViewDestroy={() => setEditorView(null)}
highlightLines={lineSelection
? {
start: Math.min(lineSelection.start, lineSelection.end),
end: Math.max(lineSelection.start, lineSelection.end),
}
: undefined}
+ blockWidgets={blockWidgets}
lineNumbersConfig={{
domEventHandlers: {
mousedown: (view, line, event) => {
@@ -725,7 +627,6 @@ export const PlanView: React.FC = () => {
},
}}
/>
- {renderSavedComments()}
)}
@@ -734,28 +635,6 @@ export const PlanView: React.FC = () => {
)}
-
- {lineSelection && mdViewMode !== 'preview' && (
-
-
0 ? `${inputBarOffset}px` : '16px')
- : '16px'
- }}
- data-keyboard-avoid="true"
- >
- {renderCommentUI()}
-
-
- )}
);
};