Skip to content

fix(chat): enable scrolling in input field and fix height reset#1143

Merged
fbricon merged 1 commit intokortex-hub:mainfrom
fbricon:GH-662
Mar 23, 2026
Merged

fix(chat): enable scrolling in input field and fix height reset#1143
fbricon merged 1 commit intokortex-hub:mainfrom
fbricon:GH-662

Conversation

@fbricon
Copy link
Copy Markdown
Contributor

@fbricon fbricon commented Mar 20, 2026

Resolves issue where long messages couldn't be scrolled or reviewed.
The input field now becomes scrollable when text exceeds 25% of viewport
height. Restructured layout to keep buttons in a separate row within the
chat box container, preventing text overlap when scrolling.

limit-message-box-height.mp4

Fixes #662

@fbricon fbricon requested a review from a team as a code owner March 20, 2026 15:41
@fbricon fbricon requested review from benoitf and gastoner and removed request for a team March 20, 2026 15:41
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 20, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2e2b63ca-0f05-4855-a1db-6ca393a579e0

📥 Commits

Reviewing files that changed from the base of the PR and between cb479e5 and 6dd9f2b.

📒 Files selected for processing (2)
  • packages/renderer/src/lib/chat/components/messages/preview-message.svelte
  • packages/renderer/src/lib/chat/components/multimodal-input.svelte
✅ Files skipped from review due to trivial changes (1)
  • packages/renderer/src/lib/chat/components/messages/preview-message.svelte
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/renderer/src/lib/chat/components/multimodal-input.svelte

📝 Walkthrough

Walkthrough

Textarea autosize and edit/submit/cancel flows were refactored: height now uses scrollHeight capped to 25% viewport; cancel/submit assign input directly and await requestAnimationFrame before resizing; Escape handling moved to a window-level listener; send/stop controls and layout were reorganized. (50 words)

Changes

Cohort / File(s) Summary
Multimodal input component
packages/renderer/src/lib/chat/components/multimodal-input.svelte
Autosize: adjustHeight() reads textareaRef.scrollHeight, caps at window.innerHeight * 0.25, sets textareaRef.style.height. resetHeight() delegates to adjustHeight(). cancelEditing() and submit flows are async, assign input directly, and await requestAnimationFrame() before resizing. Entering edit mode assigns input = text then schedules adjustHeight() and textareaRef?.focus() in rAF. Per-textarea Escape interception removed; component-scoped window keydown cancels on Escape while editing. Textarea onkeydown now only intercepts Enter (no-shift, non-composing). UI layout reorganized; stop now await chatClient.stop(); send disabled when !canSubmit(). Attention: rAF timing, global Escape listener scope, focus behavior, and async stop/send.
Message preview edit control
packages/renderer/src/lib/chat/components/messages/preview-message.svelte
Edit button for user messages is always rendered when not readonly; visibility/interactivity controlled via class (adds invisible) and disabled={editState.isEditing} instead of conditional DOM removal. Whitespace/indentation formatting updated. Attention: CSS/ARIA and focus/interaction while editing.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant User
participant MultimodalInput
participant DOM
participant ChatClient

User->>MultimodalInput: Type text / press Enter / press Escape
MultimodalInput->>DOM: read `textareaRef.scrollHeight`
DOM-->>MultimodalInput: return scrollHeight
MultimodalInput->>DOM: set `textareaRef.style.height` (min(scrollHeight+2, maxHeight))
alt User presses Enter (no-shift)
    User->>MultimodalInput: Enter
    MultimodalInput->>MultimodalInput: canSubmit() check
    MultimodalInput->>MultimodalInput: `input = ''` then await rAF
    MultimodalInput->>DOM: resetHeight()/adjustHeight()
    MultimodalInput->>ChatClient: send message / await response
end
alt User presses Escape while editing
    User->>MultimodalInput: Escape (window keydown)
    MultimodalInput->>MultimodalInput: cancelEditing() -> `input = savedInput`; `savedInput = ''`; await rAF
    MultimodalInput->>DOM: adjustHeight() and focus/blur updates
end

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main changes: enabling scrolling in the input field and fixing height reset logic, which directly addresses the linked issue #662.
Description check ✅ Passed The description clearly relates to the changeset, explaining that the input field now becomes scrollable when text exceeds 25% of viewport height and layout was restructured to prevent text overlap.
Linked Issues check ✅ Passed The PR directly addresses issue #662 by implementing scrollable input field with a 25% viewport height cap and preventing text overlap through layout restructuring, meeting all coding requirements.
Out of Scope Changes check ✅ Passed All changes are focused on fixing the input field scrolling behavior and height reset logic as specified in issue #662; no unrelated modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a 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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/renderer/src/lib/chat/components/multimodal-input.svelte (1)

62-79: ⚠️ Potential issue | 🟠 Major

Unify the submit guard across keyboard and click paths.

Right now the disabled state only exists on the send button, but Enter still calls submitForm() directly. That lets one path submit drafts the other path blocks. Please move the predicate into a shared helper and reuse it in both places.

🔧 Suggested fix
+function canSubmit(): boolean {
+  return input.trim().length > 0;
+}
+
 async function submitForm(): Promise<void> {
+  if (!canSubmit()) {
+    return;
+  }
+
   const text = input;
   // Clear input directly instead of using setInput('') to avoid calling adjustHeight() before DOM updates
   input = '';
@@
 					if (loading) {
 						toast.error('Please wait for the model to finish its response!');
-					} else {
+					} else if (canSubmit()) {
 						await submitForm();
 					}
 				}
@@
-							disabled={input.trim().length === 0}
+							disabled={!canSubmit()}

Also applies to: 153-160, 196-204

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/lib/chat/components/multimodal-input.svelte` around
lines 62 - 79, Create a single shared predicate (e.g., canSubmit or
isSubmitDisabled) and use it everywhere the UI currently decides whether to
send: call it at the top of submitForm(), use it to set the send button's
disabled attribute, and call it in the Enter/keyboard handler before invoking
submitForm(); ensure the predicate checks the same conditions used by the button
(empty input, ongoing send state, or invalid attachments) so both click and
keyboard paths behave identically — update references to submitForm, the send
button disabled binding, and the keyboard handler (e.g., onKeyDown/onKeyPress)
to reuse this helper; also apply the same change to the other submit locations
noted (the blocks around lines 153-160 and 196-204).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/renderer/src/lib/chat/components/multimodal-input.svelte`:
- Around line 168-178: The attachment Button (the Button component wrapping
PaperclipIcon) is an icon-only control and needs an accessible name; add an
aria-label (e.g., "Attach file") to the Button element that invokes handleFile
to ensure screen readers announce it; update the Button props next to
onclick/disabled/variant (the same element that renders PaperclipIcon and calls
handleFile) to include aria-label="Attach file" (or a localized string) so
accessibility is satisfied.
- Around line 183-191: The handler calls a nonexistent standalone stop()
function; replace that call with the chatClient.stop() method so the generation
abort uses the chat client API (update the onclick callback where stop() is
invoked to call chatClient.stop() and keep the surrounding
event.preventDefault() and chatClient.messages reassignment intact).

---

Outside diff comments:
In `@packages/renderer/src/lib/chat/components/multimodal-input.svelte`:
- Around line 62-79: Create a single shared predicate (e.g., canSubmit or
isSubmitDisabled) and use it everywhere the UI currently decides whether to
send: call it at the top of submitForm(), use it to set the send button's
disabled attribute, and call it in the Enter/keyboard handler before invoking
submitForm(); ensure the predicate checks the same conditions used by the button
(empty input, ongoing send state, or invalid attachments) so both click and
keyboard paths behave identically — update references to submitForm, the send
button disabled binding, and the keyboard handler (e.g., onKeyDown/onKeyPress)
to reuse this helper; also apply the same change to the other submit locations
noted (the blocks around lines 153-160 and 196-204).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d746d38d-e5da-4494-9e92-154e1f329107

📥 Commits

Reviewing files that changed from the base of the PR and between a16a9bb and 1e8d16d.

📒 Files selected for processing (1)
  • packages/renderer/src/lib/chat/components/multimodal-input.svelte

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 20, 2026

Codecov Report

❌ Patch coverage is 0% with 106 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...er/src/lib/chat/components/multimodal-input.svelte 0.00% 74 Missing ⚠️
...ib/chat/components/messages/preview-message.svelte 0.00% 32 Missing ⚠️

📢 Thoughts on this report? Let us know!

@fbricon fbricon force-pushed the GH-662 branch 2 times, most recently from f95909a to ef28038 Compare March 23, 2026 10:02
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/renderer/src/lib/chat/components/multimodal-input.svelte (1)

94-145: ⚠️ Potential issue | 🟠 Major

Mirror the disabled-state check inside submitForm().

Line 233 and Line 285 both invoke submitForm() directly, but the "cannot submit empty content" rule only exists on Line 287. Pressing Enter with an empty or whitespace-only draft still reaches this function. In edit mode that also means window.inferenceDeleteTrailingMessages(...) can run even though the send button is disabled.

🛠️ Minimal fix
 async function submitForm(): Promise<void> {
   const text = input;
+  if (text.trim().length === 0) {
+    return;
+  }
 
   if (editState.editingMessage) {

If attachment-only sends are meant to be valid, extract a shared canSubmit predicate and use it both here and on Line 287 instead of hardcoding trim() in only one path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/lib/chat/components/multimodal-input.svelte` around
lines 94 - 145, The submitForm() function currently accepts empty/whitespace
input even though the UI disables the send button; extract a shared predicate
(e.g., canSubmit(input, attachments)) and use it both where the send button is
disabled and at the top of submitForm(); ensure canSubmit mirrors the UI rule
(allow attachment-only sends if intended) and return early from submitForm()
before doing editState.editingMessage work or calling
window.inferenceDeleteTrailingMessages, chatClient.sendMessage, or modifying
attachments when canSubmit is false.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/renderer/src/lib/chat/components/multimodal-input.svelte`:
- Around line 94-145: The submitForm() function currently accepts
empty/whitespace input even though the UI disables the send button; extract a
shared predicate (e.g., canSubmit(input, attachments)) and use it both where the
send button is disabled and at the top of submitForm(); ensure canSubmit mirrors
the UI rule (allow attachment-only sends if intended) and return early from
submitForm() before doing editState.editingMessage work or calling
window.inferenceDeleteTrailingMessages, chatClient.sendMessage, or modifying
attachments when canSubmit is false.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6c97346c-d4ba-4539-bf4b-55f796e4858b

📥 Commits

Reviewing files that changed from the base of the PR and between 1e8d16d and b42c81f.

📒 Files selected for processing (2)
  • packages/renderer/src/lib/chat/components/messages/preview-message.svelte
  • packages/renderer/src/lib/chat/components/multimodal-input.svelte

@fbricon fbricon force-pushed the GH-662 branch 3 times, most recently from cb479e5 to ffe2128 Compare March 23, 2026 10:41
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/renderer/src/lib/chat/components/multimodal-input.svelte`:
- Around line 94-96: canSubmit() currently allows submission when attachments
exist even during an edit, but edits discard attachments and can therefore
enable an empty edit submit; update canSubmit() to respect editState.isEditing
by requiring non-empty trimmed input when editState.isEditing is true (i.e.,
only allow attachments to enable submit when not editing). Apply the same change
to other similar checks referenced around the 104-123 and 284-293 blocks so any
place that gates Send uses attachments, ensure those branches check
editState.isEditing and require input.trim().length > 0 when editing.
- Around line 194-208: The Escape handler is currently bound to window so any
Escape anywhere cancels editing; scope it to the active composer by either
attaching the keydown listener to the composer root element or guarding the
handler to only act when the event target is inside the composer. Update the
$effect block that registers handleKeyDown (and references editState.isEditing
and cancelEditing) so the listener is added to the composer DOM node (e.g.,
composerEl.addEventListener('keydown', handleKeyDown)) or, if keeping window,
start the handler with a check like if (!composerEl.contains(event.target as
Node)) return; then keep the existing Escape logic and cleanup via
removeEventListener.
- Line 236: Replace the Tailwind v3 important syntax in the textarea class
inside the multimodal-input.svelte component: locate the class attribute that
currently contains "!text-base" and change it to the Tailwind v4 trailing form
"text-base!" so the utility takes precedence (keep the rest of the class string
intact, e.g., the max-h, min-h, resize-none, overflow-y-auto, border-0,
bg-transparent, shadow-none, focus-visible:ring-0 utilities).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d1af52db-a695-463c-bec1-fa4ec28b407e

📥 Commits

Reviewing files that changed from the base of the PR and between ef28038 and cb479e5.

📒 Files selected for processing (2)
  • packages/renderer/src/lib/chat/components/messages/preview-message.svelte
  • packages/renderer/src/lib/chat/components/multimodal-input.svelte

Comment on lines +194 to +208
$effect((): (() => void) | void => {
if (editState.isEditing) {
const handleKeyDown = (event: KeyboardEvent): void => {
if (event.key === 'Escape') {
event.preventDefault();
cancelEditing().catch(console.error);
}
};

window.addEventListener('keydown', handleKeyDown);
return (): void => {
window.removeEventListener('keydown', handleKeyDown);
};
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Scope the Escape shortcut to the active composer.

Binding this on window means every Escape press cancels the edit while edit mode is open. If the user hits Escape to dismiss some other popover/dialog, the current edit is discarded too.

🔧 Possible guard
   if (editState.isEditing) {
     const handleKeyDown = (event: KeyboardEvent): void => {
-      if (event.key === 'Escape') {
+      if (
+        event.key === 'Escape' &&
+        !event.defaultPrevented &&
+        document.activeElement === textareaRef
+      ) {
         event.preventDefault();
         cancelEditing().catch(console.error);
       }
     };
📝 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.

Suggested change
$effect((): (() => void) | void => {
if (editState.isEditing) {
const handleKeyDown = (event: KeyboardEvent): void => {
if (event.key === 'Escape') {
event.preventDefault();
cancelEditing().catch(console.error);
}
};
window.addEventListener('keydown', handleKeyDown);
return (): void => {
window.removeEventListener('keydown', handleKeyDown);
};
}
});
$effect((): (() => void) | void => {
if (editState.isEditing) {
const handleKeyDown = (event: KeyboardEvent): void => {
if (
event.key === 'Escape' &&
!event.defaultPrevented &&
document.activeElement === textareaRef
) {
event.preventDefault();
cancelEditing().catch(console.error);
}
};
window.addEventListener('keydown', handleKeyDown);
return (): void => {
window.removeEventListener('keydown', handleKeyDown);
};
}
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/lib/chat/components/multimodal-input.svelte` around
lines 194 - 208, The Escape handler is currently bound to window so any Escape
anywhere cancels editing; scope it to the active composer by either attaching
the keydown listener to the composer root element or guarding the handler to
only act when the event target is inside the composer. Update the $effect block
that registers handleKeyDown (and references editState.isEditing and
cancelEditing) so the listener is added to the composer DOM node (e.g.,
composerEl.addEventListener('keydown', handleKeyDown)) or, if keeping window,
start the handler with a check like if (!composerEl.contains(event.target as
Node)) return; then keep the existing Escape logic and cleanup via
removeEventListener.

Resolves issue where long messages couldn't be scrolled or reviewed.
The input field now becomes scrollable when text exceeds 25% of viewport
height. Restructured layout to keep buttons in a separate row within the
chat box container, preventing text overlap when scrolling.

Fixes kortex-hub#662

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: Fred Bricon <fbricon@gmail.com>
@fbricon fbricon merged commit 5e429b2 into kortex-hub:main Mar 23, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Text input field is not scrollable when text overflows

2 participants