From 3ebcd49c404ed287a97af159ac8adfa63d572849 Mon Sep 17 00:00:00 2001
From: Adam Malczewski <github@tradam.dev>
Date: Tue, 2 Jun 2026 14:43:16 +0900
Subject: feat(tabs): drag-reorder + double-click rename + per-tab chat draft

- TabBar: HTML5 drag-and-drop to reorder user tabs (subagent tabs untouched);
  double-click a tab title to rename (Enter/blur confirm, Escape cancel).
- Store: add reorderTabs/renameTab/setDraft; per-tab in-memory `draft` and
  `manualTitle` fields. Manual rename suppresses first-message auto-title.
- ChatInput: bind to the active tab's draft so switching tabs saves/restores
  unsent text instead of clobbering it.
- Backend: updateTabPositions() + PATCH /tabs/reorder persist tab order to the
  existing `position` column; tabs without a stored position fall to the end
  then get explicit positions on first reorder.
- Tests: store reorder/rename/auto-title-guard/draft coverage; core
  updateTabPositions coverage (FakeDatabase extended with transaction support).
---
 .../frontend/src/lib/components/ChatInput.svelte    | 21 ++++++++++++++++-----
 1 file changed, 16 insertions(+), 5 deletions(-)

(limited to 'packages/frontend/src/lib/components/ChatInput.svelte')

diff --git a/packages/frontend/src/lib/components/ChatInput.svelte b/packages/frontend/src/lib/components/ChatInput.svelte
index 0c99078..71eb496 100644
--- a/packages/frontend/src/lib/components/ChatInput.svelte
+++ b/packages/frontend/src/lib/components/ChatInput.svelte
@@ -4,12 +4,17 @@ import { tabStore } from "../tabs.svelte.js";
 const MAX_LINES = 7;
 
 let inputEl: HTMLTextAreaElement | undefined;
-let inputValue = $state("");
 
 const agentStatus = $derived(tabStore.activeTab?.agentStatus ?? "idle");
 const tabId = $derived(tabStore.activeTab?.id ?? "");
+// The current input text lives on the active tab (in-memory draft), so
+// switching tabs saves the current draft and restores the target tab's text
+// automatically — drafts are never lost or clobbered by tab switching.
+const inputValue = $derived(tabStore.activeTab?.draft ?? "");
 
 $effect(() => {
+	// Re-focus when switching tabs.
+	void tabId;
 	inputEl?.focus();
 });
 
@@ -29,13 +34,19 @@ function resize() {
 	el.style.overflowY = el.scrollHeight > maxHeight ? "auto" : "hidden";
 }
 
-// Re-run resize whenever the value changes (covers programmatic clears too).
+// Re-run resize whenever the value changes (covers tab switches and
+// programmatic clears too).
 $effect(() => {
 	// Touch inputValue so this effect tracks it.
 	void inputValue;
 	resize();
 });
 
+function handleInput(e: Event) {
+	if (!tabId) return;
+	tabStore.setDraft(tabId, (e.currentTarget as HTMLTextAreaElement).value);
+}
+
 function handleKeydown(e: KeyboardEvent) {
 	if (e.key === "Enter" && !e.shiftKey) {
 		e.preventDefault();
@@ -46,7 +57,7 @@ function handleKeydown(e: KeyboardEvent) {
 function submit() {
 	const text = inputValue.trim();
 	if (!text) return;
-	inputValue = "";
+	if (tabId) tabStore.setDraft(tabId, "");
 	tabStore.sendMessage(text);
 }
 </script>
@@ -75,12 +86,12 @@ function submit() {
 	{/if}
 	<textarea
 		bind:this={inputEl}
-		bind:value={inputValue}
+		value={inputValue}
 		rows="1"
 		placeholder="Type a message..."
 		class="textarea textarea-ghost flex-1 resize-none leading-normal !min-h-0 h-auto"
 		onkeydown={handleKeydown}
-		oninput={resize}
+		oninput={handleInput}
 	></textarea>
 	<button
 		type="button"
-- 
cgit v1.2.3

