-
Notifications
You must be signed in to change notification settings - Fork 30
Add markdown-like line formatting support to chat input #92
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- Implement line formatting that preserves indentation and spacing - Add auto-focus functionality for better user experience - Replace onChange with onInput for real-time formatting - Support maintaining leading whitespace when creating new lines - Improve text display with pre-wrap and word-wrap styling
WalkthroughAdds a textarea ref with autofocus, implements onKeyDown handling for Tab, Backspace, Enter and Shift+Enter to preserve indentation and manage caret, attaches the ref to Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Textarea as TextareaAutosize
participant ChatInput
participant DOM as textareaRef (DOM)
User->>Textarea: Type / press keys
Textarea->>ChatInput: onKeyDown(event)
alt Tab pressed
ChatInput->>DOM: insert "\t" at cursor, update value, set caret
else Backspace at auto-indent
ChatInput->>DOM: remove leading whitespace, update value, set caret
else Shift+Enter pressed
ChatInput->>DOM: compute current line indent -> insert "\n" + indent, update value, set caret
else Enter (desktop, no Shift)
ChatInput->>ChatInput: trigger send message
else Other keys
ChatInput-->>Textarea: forward to onChange / onKeyPress handlers
end
Note over ChatInput,DOM: On mount ChatInput -> focus textareaRef if not disabled
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
frontend/src/features/tasks/components/ChatInput.tsx(4 hunks)
🔇 Additional comments (6)
frontend/src/features/tasks/components/ChatInput.tsx (6)
7-7: LGTM: Required hooks imported correctly.The additions of
useRefanduseEffectare necessary for the new auto-focus and textarea reference functionality.
32-32: LGTM: Textarea ref properly typed.The ref is correctly typed for
HTMLTextAreaElementand will be used for focus management and potential cursor positioning.
89-93: Verify: Re-focus whendisabledbecomesfalse?The dependency array
[disabled]means the effect runs wheneverdisabledchanges. Ifdisabledtransitions fromtrue→false(e.g., chat becomes available after loading), the textarea will auto-focus again. This might be intentional UX, but it's non-standard—typically auto-focus only happens on mount with an empty[]dependency array.If refocusing on enable is desired, the current code is correct. Otherwise, change to:
- }, [disabled]); + }, []);and remove the
!disabledcondition inside, or keep it to avoid focusing when mounted in a disabled state.
98-98: LGTM: Ref correctly attached.The
textareaRefis properly forwarded to theTextareaAutosizecomponent for focus and cursor management.
100-100: LGTM:onInputis appropriate for real-time formatting.While
onChangeandonInputare functionally similar in React (both fire on every keystroke),onInputis the native DOM event and aligns with the PR intent for real-time formatting. This change is safe and works correctly with the existing composition event handlers.
109-109: LGTM: Styles correctly preserve formatted text.
whiteSpace: 'pre-wrap'preserves leading/trailing spaces and newlines while allowing wrapping, andwordWrap: 'break-word'prevents long tokens from overflowing. These are essential for displaying the indented multiline content correctly.
- Replace problematic onInput handler with proper onKeyDown approach - Implement auto-indentation only on Shift+Enter key combination - Preserve whitespace and normal input behavior for all other keys - Fix cursor positioning after auto-indentation - Maintain existing Enter key behavior for message sending
- Handle Tab key to insert \t placeholder instead of default focus switching - Prevent default Tab behavior and insert tab character at cursor position - Maintain proper cursor positioning after tab insertion - Preserve existing Shift+Enter auto-indentation functionality
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (1)
frontend/src/features/tasks/components/ChatInput.tsx (1)
106-110: Auto-focus re-triggers on everydisabledchange.The current effect will re-focus the textarea whenever
disabledtransitions fromtruetofalse, potentially interrupting user interactions elsewhere. If the intent is only to auto-focus on initial mount, use an empty dependency array.If auto-focus should only occur on mount:
- // Auto-focus on mount useEffect(() => { if (textareaRef.current && !disabled) { textareaRef.current.focus(); } - }, [disabled]); + }, []);If re-focusing when re-enabled is intentional, the current implementation is correct.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
frontend/src/features/tasks/components/ChatInput.tsx(3 hunks)
🔇 Additional comments (3)
frontend/src/features/tasks/components/ChatInput.tsx (3)
7-7: LGTM!The additional imports (
useRef,useEffect) are correctly added and used in the component.
32-32: LGTM!The
textareaRefis properly typed and initialized.
115-129: LGTM!The ref attachment (line 115), dual keyboard handlers (lines 120-121), and styling updates (line 129) are correctly implemented. The
whiteSpace: 'pre-wrap'andwordWrap: 'break-word'styles properly support the whitespace preservation objective.
| const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { | ||
| // Handle Tab key - insert \t placeholder instead of default tab behavior | ||
| if (e.key === 'Tab' && !disabled && !isComposing) { | ||
| e.preventDefault(); | ||
|
|
||
| const textarea = e.target as HTMLTextAreaElement; | ||
| const start = textarea.selectionStart; | ||
| const end = textarea.selectionEnd; | ||
| const value = textarea.value; | ||
|
|
||
| // Insert \t placeholder at cursor position | ||
| const newValue = value.substring(0, start) + '\t' + value.substring(end); | ||
|
|
||
| setMessage(newValue); | ||
|
|
||
| // Set cursor position after the inserted tab | ||
| setTimeout(() => { | ||
| textarea.selectionStart = start + 1; | ||
| textarea.selectionEnd = start + 1; | ||
| }, 0); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Use textareaRef.current in cursor restoration callback.
After setMessage triggers a parent re-render (line 68), the local textarea variable captured at line 60 may reference a stale DOM node. The setTimeout callback should access textareaRef.current instead to guarantee cursor restoration on the current element.
Apply this diff to fix cursor restoration:
- // Set cursor position after the inserted tab
- setTimeout(() => {
- textarea.selectionStart = start + 1;
- textarea.selectionEnd = start + 1;
- }, 0);
+ // Set cursor position after the inserted tab
+ requestAnimationFrame(() => {
+ if (textareaRef.current) {
+ textareaRef.current.selectionStart = start + 1;
+ textareaRef.current.selectionEnd = start + 1;
+ }
+ });Verify: Tab insertion feature is not mentioned in PR objectives.
The PR objectives describe line formatting, auto-focus, and Shift+Enter support, but don't mention Tab key handling. Confirm this feature is intentional and within scope.
🏁 Script executed:
#!/bin/bash
# Examine the full ChatInput.tsx component to understand structure and ref usage
cat frontend/src/features/tasks/components/ChatInput.tsxLength of output: 4308
🏁 Script executed:
#!/bin/bash
# Search for textareaRef definition and usage in the component file
rg -n "textareaRef" frontend/src/features/tasks/components/ChatInput.tsxLength of output: 176
🏁 Script executed:
#!/bin/bash
# Check for any PR description or objectives
fd -e md -e txt . --type f | xargs grep -l "Tab\|tab" 2>/dev/null | head -5Length of output: 98
🏁 Script executed:
#!/bin/bash
# Search for PR metadata (if available in git)
git log --all --oneline --grep="Tab\|tab" 2>/dev/null | head -10Length of output: 421
Use textareaRef.current for cursor restoration instead of stale event target reference.
Lines 71–74 and 95–98 capture the textarea reference from e.target, then call setMessage() which triggers a parent re-render before restoring cursor position. While the event target reference remains valid, using textareaRef.current is more robust and follows React patterns by always referencing the current DOM node through the ref.
Apply this diff to both Tab (lines 71–74) and Shift+Enter (lines 95–98) handlers:
- // Set cursor position after the inserted tab
- setTimeout(() => {
- textarea.selectionStart = start + 1;
- textarea.selectionEnd = start + 1;
- }, 0);
+ // Set cursor position after the inserted tab
+ setTimeout(() => {
+ if (textareaRef.current) {
+ textareaRef.current.selectionStart = start + 1;
+ textareaRef.current.selectionEnd = start + 1;
+ }
+ }, 0);Also fix lines 95–98 similarly for the Shift+Enter handler (start/end positions differ but same pattern applies).
Tab insertion feature is in scope (commit 721db18 confirms intentional addition).
| // Handle Enter key for auto-indentation (only when Shift+Enter) | ||
| if (e.key === 'Enter' && e.shiftKey && !disabled && !isComposing) { | ||
| e.preventDefault(); | ||
|
|
||
| const textarea = e.target as HTMLTextAreaElement; | ||
| const start = textarea.selectionStart; | ||
| const end = textarea.selectionEnd; | ||
| const value = textarea.value; | ||
|
|
||
| // Get the current line and its leading whitespace | ||
| const lines = value.substring(0, start).split('\n'); | ||
| const currentLine = lines[lines.length - 1]; | ||
| const leadingWhitespace = currentLine.match(/^\s*/)?.[0] || ''; | ||
|
|
||
| // Insert new line with preserved indentation | ||
| const newValue = value.substring(0, start) + '\n' + leadingWhitespace + value.substring(end); | ||
|
|
||
| setMessage(newValue); | ||
|
|
||
| // Set cursor position after the inserted whitespace | ||
| setTimeout(() => { | ||
| textarea.selectionStart = start + 1 + leadingWhitespace.length; | ||
| textarea.selectionEnd = start + 1 + leadingWhitespace.length; | ||
| }, 0); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use textareaRef.current in cursor restoration callback.
Similar to the Tab handler, after setMessage triggers a re-render (line 95), the local textarea variable captured at line 82 may reference a stale DOM node. The setTimeout callback should access textareaRef.current to ensure reliable cursor restoration.
Apply this diff to fix cursor restoration:
- // Set cursor position after the inserted whitespace
- setTimeout(() => {
- textarea.selectionStart = start + 1 + leadingWhitespace.length;
- textarea.selectionEnd = start + 1 + leadingWhitespace.length;
- }, 0);
+ // Set cursor position after the inserted whitespace
+ requestAnimationFrame(() => {
+ if (textareaRef.current) {
+ textareaRef.current.selectionStart = start + 1 + leadingWhitespace.length;
+ textareaRef.current.selectionEnd = start + 1 + leadingWhitespace.length;
+ }
+ });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Handle Enter key for auto-indentation (only when Shift+Enter) | |
| if (e.key === 'Enter' && e.shiftKey && !disabled && !isComposing) { | |
| e.preventDefault(); | |
| const textarea = e.target as HTMLTextAreaElement; | |
| const start = textarea.selectionStart; | |
| const end = textarea.selectionEnd; | |
| const value = textarea.value; | |
| // Get the current line and its leading whitespace | |
| const lines = value.substring(0, start).split('\n'); | |
| const currentLine = lines[lines.length - 1]; | |
| const leadingWhitespace = currentLine.match(/^\s*/)?.[0] || ''; | |
| // Insert new line with preserved indentation | |
| const newValue = value.substring(0, start) + '\n' + leadingWhitespace + value.substring(end); | |
| setMessage(newValue); | |
| // Set cursor position after the inserted whitespace | |
| setTimeout(() => { | |
| textarea.selectionStart = start + 1 + leadingWhitespace.length; | |
| textarea.selectionEnd = start + 1 + leadingWhitespace.length; | |
| }, 0); | |
| } | |
| }; | |
| // Handle Enter key for auto-indentation (only when Shift+Enter) | |
| if (e.key === 'Enter' && e.shiftKey && !disabled && !isComposing) { | |
| e.preventDefault(); | |
| const textarea = e.target as HTMLTextAreaElement; | |
| const start = textarea.selectionStart; | |
| const end = textarea.selectionEnd; | |
| const value = textarea.value; | |
| // Get the current line and its leading whitespace | |
| const lines = value.substring(0, start).split('\n'); | |
| const currentLine = lines[lines.length - 1]; | |
| const leadingWhitespace = currentLine.match(/^\s*/)?.[0] || ''; | |
| // Insert new line with preserved indentation | |
| const newValue = value.substring(0, start) + '\n' + leadingWhitespace + value.substring(end); | |
| setMessage(newValue); | |
| // Set cursor position after the inserted whitespace | |
| requestAnimationFrame(() => { | |
| if (textareaRef.current) { | |
| textareaRef.current.selectionStart = start + 1 + leadingWhitespace.length; | |
| textareaRef.current.selectionEnd = start + 1 + leadingWhitespace.length; | |
| } | |
| }); | |
| } | |
| }; |
🤖 Prompt for AI Agents
In frontend/src/features/tasks/components/ChatInput.tsx around lines 78 to 103,
the Enter+Shift auto-indent handler captures a local textarea variable and then
uses it inside the setTimeout callback, which can reference a stale DOM node
after setMessage triggers a re-render; replace uses of the captured textarea
inside the timeout with textareaRef.current, guard against null, and set
selectionStart/selectionEnd on textareaRef.current using the computed start + 1
+ leadingWhitespace.length so the cursor restoration is reliable after the
re-render.
- Handle Backspace key to remove auto-indented whitespace efficiently - Delete entire line of whitespace when cursor is at empty indented line - Remove all leading whitespace when Backspacing at line start - Maintain proper cursor positioning after deletion - Preserve existing Tab and Shift+Enter functionality
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (2)
frontend/src/features/tasks/components/ChatInput.tsx (2)
55-76: Tab handler still uses stale textarea reference in cursor restoration.Despite past review comments being marked as addressed, lines 71-74 still use the captured
textareavariable insidesetTimeoutinstead oftextareaRef.current. AftersetMessage(newValue)triggers a parent re-render, the captured reference may be stale.Apply this diff to fix cursor restoration:
setMessage(newValue); // Set cursor position after the inserted tab - setTimeout(() => { - textarea.selectionStart = start + 1; - textarea.selectionEnd = start + 1; - }, 0); + requestAnimationFrame(() => { + if (textareaRef.current) { + textareaRef.current.selectionStart = start + 1; + textareaRef.current.selectionEnd = start + 1; + } + }); return; }
134-159: Shift+Enter handler still uses stale textarea reference in cursor restoration.Despite past review comments being marked as addressed, lines 154-157 still use the captured
textareavariable instead oftextareaRef.currentin the cursor restoration callback.Apply this diff to fix cursor restoration:
setMessage(newValue); // Set cursor position after the inserted whitespace - setTimeout(() => { - textarea.selectionStart = start + 1 + leadingWhitespace.length; - textarea.selectionEnd = start + 1 + leadingWhitespace.length; - }, 0); + requestAnimationFrame(() => { + if (textareaRef.current) { + textareaRef.current.selectionStart = start + 1 + leadingWhitespace.length; + textareaRef.current.selectionEnd = start + 1 + leadingWhitespace.length; + } + }); } };
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
frontend/src/features/tasks/components/ChatInput.tsx(3 hunks)
🔇 Additional comments (4)
frontend/src/features/tasks/components/ChatInput.tsx (4)
7-7: LGTM!The addition of
useRefanduseEffectimports is correct and necessary for the auto-focus and cursor management features.
32-32: LGTM!The ref initialization is properly typed and follows React patterns.
161-166: LGTM!The auto-focus implementation correctly uses
textareaRef.currentwith proper null-checking and appropriate dependencies.
171-185: LGTM!The ref attachment, keyboard handler, and style changes correctly implement the multi-line formatting requirements. The
whiteSpace: 'pre-wrap'andwordWrap: 'break-word'styles properly preserve indentation and handle text wrapping.
| // Handle Backspace key - remove auto-indented whitespace | ||
| if (e.key === 'Backspace' && !disabled && !isComposing) { | ||
| const textarea = e.target as HTMLTextAreaElement; | ||
| const start = textarea.selectionStart; | ||
| const end = textarea.selectionEnd; | ||
| const value = textarea.value; | ||
|
|
||
| // Only handle when cursor is at a single position (no selection) | ||
| if (start === end && start > 0) { | ||
| // Get the current line and previous character | ||
| const lines = value.substring(0, start).split('\n'); | ||
| const currentLine = lines[lines.length - 1]; | ||
|
|
||
| // Check if we're at the beginning of a line and the line contains only whitespace | ||
| if (currentLine.trim() === '' && currentLine.length > 0) { | ||
| e.preventDefault(); | ||
|
|
||
| // Remove the entire line of whitespace and the newline character | ||
| const previousLines = lines.slice(0, -1); | ||
| const remainingText = value.substring(start); | ||
| const newValue = previousLines.join('\n') + (previousLines.length > 0 ? '\n' : '') + remainingText; | ||
|
|
||
| setMessage(newValue); | ||
|
|
||
| // Set cursor position at the end of the previous line | ||
| const newCursorPos = previousLines.length > 0 ? previousLines.join('\n').length + 1 : 0; | ||
| setTimeout(() => { | ||
| textarea.selectionStart = newCursorPos; | ||
| textarea.selectionEnd = newCursorPos; | ||
| }, 0); | ||
| return; | ||
| } | ||
|
|
||
| // Check if we're deleting whitespace that was auto-indented | ||
| const charBeforeCursor = value.substring(start - 1, start); | ||
| if (charBeforeCursor === ' ' || charBeforeCursor === '\t') { | ||
| // Get the line start position | ||
| const lineStart = value.lastIndexOf('\n', start - 1) + 1; | ||
| const lineContent = value.substring(lineStart, start); | ||
|
|
||
| // If the line contains only whitespace up to cursor, remove all whitespace | ||
| if (lineContent.trim() === '') { | ||
| e.preventDefault(); | ||
| const newValue = value.substring(0, lineStart) + value.substring(start); | ||
| setMessage(newValue); | ||
|
|
||
| setTimeout(() => { | ||
| textarea.selectionStart = lineStart; | ||
| textarea.selectionEnd = lineStart; | ||
| }, 0); | ||
| return; | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Backspace handler uses stale textarea reference in cursor restoration.
The Backspace handler has the same cursor restoration issue as the Tab handler. Lines 104-107 and 124-127 use the captured textarea variable in setTimeout callbacks after calling setMessage, which may reference a stale DOM node.
Apply this diff to fix both cursor restoration points:
setMessage(newValue);
// Set cursor position at the end of the previous line
const newCursorPos = previousLines.length > 0 ? previousLines.join('\n').length + 1 : 0;
- setTimeout(() => {
- textarea.selectionStart = newCursorPos;
- textarea.selectionEnd = newCursorPos;
- }, 0);
+ requestAnimationFrame(() => {
+ if (textareaRef.current) {
+ textareaRef.current.selectionStart = newCursorPos;
+ textareaRef.current.selectionEnd = newCursorPos;
+ }
+ });
return;
}And for the second cursor restoration:
setMessage(newValue);
- setTimeout(() => {
- textarea.selectionStart = lineStart;
- textarea.selectionEnd = lineStart;
- }, 0);
+ requestAnimationFrame(() => {
+ if (textareaRef.current) {
+ textareaRef.current.selectionStart = lineStart;
+ textareaRef.current.selectionEnd = lineStart;
+ }
+ });
return;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Handle Backspace key - remove auto-indented whitespace | |
| if (e.key === 'Backspace' && !disabled && !isComposing) { | |
| const textarea = e.target as HTMLTextAreaElement; | |
| const start = textarea.selectionStart; | |
| const end = textarea.selectionEnd; | |
| const value = textarea.value; | |
| // Only handle when cursor is at a single position (no selection) | |
| if (start === end && start > 0) { | |
| // Get the current line and previous character | |
| const lines = value.substring(0, start).split('\n'); | |
| const currentLine = lines[lines.length - 1]; | |
| // Check if we're at the beginning of a line and the line contains only whitespace | |
| if (currentLine.trim() === '' && currentLine.length > 0) { | |
| e.preventDefault(); | |
| // Remove the entire line of whitespace and the newline character | |
| const previousLines = lines.slice(0, -1); | |
| const remainingText = value.substring(start); | |
| const newValue = previousLines.join('\n') + (previousLines.length > 0 ? '\n' : '') + remainingText; | |
| setMessage(newValue); | |
| // Set cursor position at the end of the previous line | |
| const newCursorPos = previousLines.length > 0 ? previousLines.join('\n').length + 1 : 0; | |
| setTimeout(() => { | |
| textarea.selectionStart = newCursorPos; | |
| textarea.selectionEnd = newCursorPos; | |
| }, 0); | |
| return; | |
| } | |
| // Check if we're deleting whitespace that was auto-indented | |
| const charBeforeCursor = value.substring(start - 1, start); | |
| if (charBeforeCursor === ' ' || charBeforeCursor === '\t') { | |
| // Get the line start position | |
| const lineStart = value.lastIndexOf('\n', start - 1) + 1; | |
| const lineContent = value.substring(lineStart, start); | |
| // If the line contains only whitespace up to cursor, remove all whitespace | |
| if (lineContent.trim() === '') { | |
| e.preventDefault(); | |
| const newValue = value.substring(0, lineStart) + value.substring(start); | |
| setMessage(newValue); | |
| setTimeout(() => { | |
| textarea.selectionStart = lineStart; | |
| textarea.selectionEnd = lineStart; | |
| }, 0); | |
| return; | |
| } | |
| } | |
| } | |
| } | |
| // Handle Backspace key - remove auto-indented whitespace | |
| if (e.key === 'Backspace' && !disabled && !isComposing) { | |
| const textarea = e.target as HTMLTextAreaElement; | |
| const start = textarea.selectionStart; | |
| const end = textarea.selectionEnd; | |
| const value = textarea.value; | |
| // Only handle when cursor is at a single position (no selection) | |
| if (start === end && start > 0) { | |
| // Get the current line and previous character | |
| const lines = value.substring(0, start).split('\n'); | |
| const currentLine = lines[lines.length - 1]; | |
| // Check if we're at the beginning of a line and the line contains only whitespace | |
| if (currentLine.trim() === '' && currentLine.length > 0) { | |
| e.preventDefault(); | |
| // Remove the entire line of whitespace and the newline character | |
| const previousLines = lines.slice(0, -1); | |
| const remainingText = value.substring(start); | |
| const newValue = previousLines.join('\n') + (previousLines.length > 0 ? '\n' : '') + remainingText; | |
| setMessage(newValue); | |
| // Set cursor position at the end of the previous line | |
| const newCursorPos = previousLines.length > 0 ? previousLines.join('\n').length + 1 : 0; | |
| requestAnimationFrame(() => { | |
| if (textareaRef.current) { | |
| textareaRef.current.selectionStart = newCursorPos; | |
| textareaRef.current.selectionEnd = newCursorPos; | |
| } | |
| }); | |
| return; | |
| } | |
| // Check if we're deleting whitespace that was auto-indented | |
| const charBeforeCursor = value.substring(start - 1, start); | |
| if (charBeforeCursor === ' ' || charBeforeCursor === '\t') { | |
| // Get the line start position | |
| const lineStart = value.lastIndexOf('\n', start - 1) + 1; | |
| const lineContent = value.substring(lineStart, start); | |
| // If the line contains only whitespace up to cursor, remove all whitespace | |
| if (lineContent.trim() === '') { | |
| e.preventDefault(); | |
| const newValue = value.substring(0, lineStart) + value.substring(start); | |
| setMessage(newValue); | |
| requestAnimationFrame(() => { | |
| if (textareaRef.current) { | |
| textareaRef.current.selectionStart = lineStart; | |
| textareaRef.current.selectionEnd = lineStart; | |
| } | |
| }); | |
| return; | |
| } | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
In frontend/src/features/tasks/components/ChatInput.tsx around lines 78–132, the
Backspace handler captures the local `textarea` variable and uses it inside
setTimeout callbacks which can reference a stale DOM node; replace those usages
by reading the current textarea from a stable ref (e.g., textareaRef.current) or
by re-querying the element by id inside the timeout, then check for null before
setting selection. Concretely: ensure you store selection positions immediately,
call setMessage, and in the setTimeout use textareaRef.current (or
document.getElementById(savedId)) to restore selection (with null guard) instead
of the captured `textarea`; apply this to both cursor restoration points.
- Enable auto-indentation on mobile Enter key press - Add mobile Tab button for inserting \t placeholder - Differentiate Enter key behavior between desktop and mobile - Mobile: Enter creates new line with auto-indentation - Desktop: Enter sends message, Shift+Enter creates new line - Add mobile toolbar with Tab button for better UX
Summary
Test plan
Summary by CodeRabbit
New Features
Bug Fixes