diff --git a/src/lib/actions/autogrow.ts b/src/lib/actions/autogrow.ts
new file mode 100644
index 0000000..b3db3c8
--- /dev/null
+++ b/src/lib/actions/autogrow.ts
@@ -0,0 +1,48 @@
+import type { Action } from "svelte/action";
+
+type AutogrowParams = {
+ // When false the action is inert, leaving the textarea at its native size.
+ enabled?: boolean;
+ // Max height in lines before the textarea starts scrolling.
+ maxRows?: number;
+ // A value to watch; resize is re-run whenever it changes (e.g. after a reset).
+ value?: unknown;
+};
+
+// Grow a textarea with its content up to maxRows lines, then scroll.
+export const autogrow: Action<
+ HTMLTextAreaElement,
+ AutogrowParams | undefined
+> = (node, params) => {
+ let enabled = params?.enabled ?? true;
+ let maxRows = params?.maxRows ?? 10;
+
+ function resize() {
+ if (!enabled) return;
+ const cs = getComputedStyle(node);
+ const lh = parseFloat(cs.lineHeight) || 20;
+ const vPad = parseFloat(cs.paddingTop) + parseFloat(cs.paddingBottom);
+ const vBorder =
+ parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth);
+ const max = lh * maxRows + vPad + vBorder;
+ node.style.height = "auto";
+ const next = Math.min(node.scrollHeight, max);
+ node.style.height = `${next}px`;
+ node.style.overflowY = node.scrollHeight > max ? "auto" : "hidden";
+ }
+
+ node.addEventListener("input", resize);
+ resize();
+
+ return {
+ update(next) {
+ enabled = next?.enabled ?? true;
+ maxRows = next?.maxRows ?? 10;
+ // Re-run on watched-value changes, including programmatic resets.
+ resize();
+ },
+ destroy() {
+ node.removeEventListener("input", resize);
+ },
+ };
+};
diff --git a/src/lib/components/ChatSidebar.svelte b/src/lib/components/ChatSidebar.svelte
index 67f3d37..4639113 100644
--- a/src/lib/components/ChatSidebar.svelte
+++ b/src/lib/components/ChatSidebar.svelte
@@ -207,11 +207,13 @@
sendError = e instanceof Error ? e.message : "Failed to send";
} finally {
sending = false;
+ // Re-enable runs first, then focus lands on the now-interactive textarea.
+ tick().then(() => inputEl?.focus({ caretAtEnd: true }));
}
}
function onKeydown(e: KeyboardEvent) {
- if (e.key === "Enter" && !e.shiftKey) {
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
submit();
} else if (e.key === "Escape" && replyTarget) {
@@ -462,16 +464,43 @@
Join to chat
{:else}
-