diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 1ca56ea2b3f..6365424886b 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -1,6 +1,16 @@ // Agent Manager root component -import { Component, For, Show, createSignal, createMemo, createEffect, onMount, onCleanup } from "solid-js" +import { + Component, + For, + Show, + createSignal, + createMemo, + createEffect, + onMount, + onCleanup, + type Accessor, +} from "solid-js" import type { ExtensionMessage, AgentManagerRepoInfoMessage, @@ -49,6 +59,71 @@ type SidebarSelection = typeof LOCAL | string | null const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent) const modKey = isMac ? "\u2318" : "Ctrl+" +/** Manages horizontal scroll for the tab list: hides the scrollbar, converts + * vertical wheel events to horizontal scroll, tracks overflow to show/hide + * fade indicators, and auto-scrolls the active tab into view. */ +function useTabScroll(activeTabs: Accessor, activeId: Accessor) { + const [ref, setRef] = createSignal() + const [showLeft, setShowLeft] = createSignal(false) + const [showRight, setShowRight] = createSignal(false) + + const update = () => { + const el = ref() + if (!el) return + setShowLeft(el.scrollLeft > 2) + setShowRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 2) + } + + // Wheel → horizontal scroll conversion + const onWheel = (e: WheelEvent) => { + const el = ref() + if (!el) return + if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return + e.preventDefault() + el.scrollLeft += e.deltaY > 0 ? 60 : -60 + } + + // Recalculate on scroll, resize, or tab changes + createEffect(() => { + const el = ref() + if (!el) return + el.addEventListener("scroll", update, { passive: true }) + el.addEventListener("wheel", onWheel, { passive: false }) + const ro = new ResizeObserver(update) + ro.observe(el) + const mo = new MutationObserver(update) + mo.observe(el, { childList: true, subtree: true }) + onCleanup(() => { + el.removeEventListener("scroll", update) + el.removeEventListener("wheel", onWheel) + ro.disconnect() + mo.disconnect() + }) + }) + + // Auto-scroll active tab into view + createEffect(() => { + const id = activeId() + const el = ref() + // depend on tabs length to trigger on tab add/remove + activeTabs() + if (!id || !el) return + requestAnimationFrame(() => { + const tab = el.querySelector(`[data-tab-id="${id}"]`) as HTMLElement | null + if (!tab) return + const left = tab.offsetLeft + const right = left + tab.offsetWidth + if (left < el.scrollLeft) { + el.scrollTo({ left: left - 8, behavior: "smooth" }) + } else if (right > el.scrollLeft + el.clientWidth) { + el.scrollTo({ left: right - el.clientWidth + 8, behavior: "smooth" }) + } + }) + }) + + return { setRef, showLeft, showRight } +} + const AgentManagerContent: Component = () => { const session = useSession() const vscode = useVSCode() @@ -163,6 +238,10 @@ const AgentManagerContent: Component = () => { // Read-only mode: viewing an unassigned session (not in a worktree or local) const readOnly = createMemo(() => selection() === null && !!session.currentSessionID()) + // Tab scroll: hidden scrollbar with fade overflow indicators + const visibleTabId = createMemo(() => session.currentSessionID() ?? activePendingId()) + const tabScroll = useTabScroll(activeTabs, visibleTabId) + // Display name for worktree const worktreeLabel = (wt: WorktreeState): string => { const managed = managedSessions().filter((ms) => ms.worktreeId === wt.id) @@ -615,43 +694,48 @@ const AgentManagerContent: Component = () => { {/* Tab bar — visible when a section is selected and has tabs or a pending new session */}
-
- - {(s) => { - const pending = isPending(s.id) - const active = () => - pending - ? s.id === activePendingId() && !session.currentSessionID() - : s.id === session.currentSessionID() - return ( - -
{ - if (pending) { - setActivePendingId(s.id) - session.clearCurrentSession() - } else { - setActivePendingId(undefined) - session.selectSession(s.id) - } - }} - onMouseDown={(e: MouseEvent) => handleTabMouseDown(s.id, e)} - > - {s.title || "Untitled"} - handleCloseTab(s.id, e)} - /> -
-
- ) - }} -
+
+
+
+ + {(s) => { + const pending = isPending(s.id) + const active = () => + pending + ? s.id === activePendingId() && !session.currentSessionID() + : s.id === session.currentSessionID() + return ( + +
{ + if (pending) { + setActivePendingId(s.id) + session.clearCurrentSession() + } else { + setActivePendingId(undefined) + session.selectSession(s.id) + } + }} + onMouseDown={(e: MouseEvent) => handleTabMouseDown(s.id, e)} + > + {s.title || "Untitled"} + handleCloseTab(s.id, e)} + /> +
+
+ ) + }} +
+
+