summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-29 20:33:08 +0900
committerAdam Malczewski <[email protected]>2026-05-29 20:33:08 +0900
commitc8e76ef506da32884ccf9ea2ac83a4d344c62943 (patch)
treec0a04721d9898ed868520d86c07762b87c522632
parentfd08733f751650b93d54c5cecb33556e95e0274d (diff)
downloaddispatch-c8e76ef506da32884ccf9ea2ac83a4d344c62943.tar.gz
dispatch-c8e76ef506da32884ccf9ea2ac83a4d344c62943.zip
Make chat input an auto-growing textarea
Replace the single-line input with a textarea that starts at one line, grows vertically as text wraps (up to 7 lines), then scrolls. Override daisyUI's .textarea min-height:5rem so the resting state is one line. Enter sends, Shift+Enter inserts a newline.
-rw-r--r--packages/frontend/src/lib/components/ChatInput.svelte42
1 files changed, 34 insertions, 8 deletions
diff --git a/packages/frontend/src/lib/components/ChatInput.svelte b/packages/frontend/src/lib/components/ChatInput.svelte
index 098856a..0c99078 100644
--- a/packages/frontend/src/lib/components/ChatInput.svelte
+++ b/packages/frontend/src/lib/components/ChatInput.svelte
@@ -1,7 +1,9 @@
<script lang="ts">
import { tabStore } from "../tabs.svelte.js";
-let inputEl: HTMLInputElement | undefined;
+const MAX_LINES = 7;
+
+let inputEl: HTMLTextAreaElement | undefined;
let inputValue = $state("");
const agentStatus = $derived(tabStore.activeTab?.agentStatus ?? "idle");
@@ -11,6 +13,29 @@ $effect(() => {
inputEl?.focus();
});
+function resize() {
+ const el = inputEl;
+ if (!el) return;
+ // Reset height so scrollHeight reflects the content's natural height.
+ el.style.height = "auto";
+ const style = getComputedStyle(el);
+ const lineHeight = Number.parseFloat(style.lineHeight) || 20;
+ const paddingY = Number.parseFloat(style.paddingTop) + Number.parseFloat(style.paddingBottom);
+ const borderY =
+ Number.parseFloat(style.borderTopWidth) + Number.parseFloat(style.borderBottomWidth);
+ const maxHeight = lineHeight * MAX_LINES + paddingY + borderY;
+ const next = Math.min(el.scrollHeight, maxHeight);
+ el.style.height = `${next}px`;
+ el.style.overflowY = el.scrollHeight > maxHeight ? "auto" : "hidden";
+}
+
+// Re-run resize whenever the value changes (covers programmatic clears too).
+$effect(() => {
+ // Touch inputValue so this effect tracks it.
+ void inputValue;
+ resize();
+});
+
function handleKeydown(e: KeyboardEvent) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
@@ -26,7 +51,7 @@ function submit() {
}
</script>
-<div class="flex items-center gap-2 p-3">
+<div class="flex items-end gap-2 p-3">
{#if agentStatus === "running"}
<button
type="button"
@@ -38,24 +63,25 @@ function submit() {
<span class="text-xs">Stop</span>
</button>
{:else if agentStatus === "idle"}
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="w-5 h-5 text-success">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="w-5 h-5 text-success shrink-0 mb-2">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
{:else if agentStatus === "error"}
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-5 h-5 text-error">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-5 h-5 text-error shrink-0 mb-2">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
{/if}
- <input
+ <textarea
bind:this={inputEl}
bind:value={inputValue}
- type="text"
+ rows="1"
placeholder="Type a message..."
- class="input input-ghost flex-1"
+ class="textarea textarea-ghost flex-1 resize-none leading-normal !min-h-0 h-auto"
onkeydown={handleKeydown}
- />
+ oninput={resize}
+ ></textarea>
<button
type="button"
class="btn btn-primary"