Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/compact-agent-tabs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Make Agent Manager session tabs compact, status-aware, and easier to scan when many tabs are open.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
123 changes: 77 additions & 46 deletions packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
/** @jsxImportSource solid-js */

import { For, Show, createSignal, createMemo, createEffect, on, onMount, onCleanup, type Component } from "solid-js"
import {
For,
Show,
createSignal,
createMemo,
createEffect,
on,
onMount,
onCleanup,
type Component,
type JSX,
} from "solid-js"
import type {
ExtensionMessage,
AgentManagerRepoInfoMessage,
Expand Down Expand Up @@ -108,6 +119,7 @@ import { ConstrainDragXAxis } from "./constrain-drag-x"
import { mergeWorktreeDiffs } from "./diff-state"
import { initialMessage, seedInitialVariant } from "./initial-message"
import { createMarkdownRender } from "./review-preferences"
import { setTabWidths } from "./tab-widths"
import "./agent-manager.css"
import "./agent-manager-review.css"

Expand Down Expand Up @@ -582,6 +594,13 @@ const AgentManagerContent: Component = () => {

// Drag-and-drop state for tab reordering
const [draggingTab, setDraggingTab] = createSignal<string | undefined>()

const freezeTabs = () => {
const bar = document.querySelector(".am-tab-bar")
if (bar instanceof HTMLElement && bar.matches(":hover")) setTabWidths(true)
}

const releaseTabs = () => setTabWidths(false)
// Tab ordering: context key → ordered session ID array (recovered from extension state)
const [worktreeTabOrder, setWorktreeTabOrder] = createSignal<Record<string, string[]>>({})
// Sidebar worktree order (persisted to extension state)
Expand Down Expand Up @@ -825,6 +844,8 @@ const AgentManagerContent: Component = () => {
/** True when a local session is actively working. */
const isLocalBusy = (): boolean => isAnySessionBusy(localSessionIDs())

const isSessionBusy = (id: string): boolean => isAnySessionBusy([id])

/** Worktrees sorted so that grouped items are always adjacent, respecting custom order if set. */
const sortedWorktrees = createMemo(() => {
const ordered = applyTabOrder(worktrees(), sidebarWorktreeOrder())
Expand Down Expand Up @@ -1516,6 +1537,7 @@ const AgentManagerContent: Component = () => {
// the <Show> unmount triggers heavy FileDiff cleanup but the tab bar
// and chat view are already visible before that work runs.
const closeReviewTab = () => {
freezeTabs()
setReviewActive(false)
setReviewOpenForSelection(false)
}
Expand Down Expand Up @@ -1881,6 +1903,7 @@ const AgentManagerContent: Component = () => {
vscode.postMessage({ ...msg, worktreeId: sel })
}
const handleCloseTab = (sessionId: string) => {
freezeTabs()
const pending = isPending(sessionId)
const isActive = pending ? sessionId === activePendingId() : session.currentSessionID() === sessionId
if (isActive) {
Expand Down Expand Up @@ -1909,6 +1932,7 @@ const AgentManagerContent: Component = () => {
const handleTabMouseDown = (sessionId: string, e: MouseEvent) => {
if (e.button === 1) {
e.preventDefault()
e.stopPropagation()
handleCloseTab(sessionId)
}
}
Expand Down Expand Up @@ -1937,6 +1961,7 @@ const AgentManagerContent: Component = () => {
isPendingId: isPending,
findTab: (id) => tabLookup().get(id),
postMessage: (msg) => vscode.postMessage(msg as never),
onRemove: freezeTabs,
getSelection: selection,
LOCAL,
REVIEW_TAB_ID,
Expand Down Expand Up @@ -1967,7 +1992,6 @@ const AgentManagerContent: Component = () => {
worktreeTabOrder()[key],
).map((item) => item.id)
})

const handleDragStart = (event: DragEvent) => {
const id = event.draggable?.id
if (typeof id === "string") setDraggingTab(id)
Expand Down Expand Up @@ -2640,55 +2664,62 @@ const AgentManagerContent: Component = () => {
>
<DragDropSensors />
<ConstrainDragYAxis />
<div class="am-tab-bar">
<div class="am-tab-bar" onPointerLeave={releaseTabs}>
<div class="am-tab-scroll-area">
<div class={`am-tab-fade am-tab-fade-left ${tabScroll.showLeft() ? "am-tab-fade-visible" : ""}`} />
<div class="am-tab-list" ref={tabScroll.setRef}>
<SortableProvider ids={tabIds()}>
<For each={tabIds()}>
{(id) =>
renderTab(id, {
terms,
REVIEW_TAB_ID,
tabIds,
kb,
reviewActive,
currentSessionID: () => session.currentSessionID(),
activePendingId,
visibleTabId,
isPending,
tabLookup,
adjacentHint,
activateTerminal: termHandlers.activate,
deactivateTerminal: termHandlers.deactivate,
closeTerminal: termHandlers.closeTerminal,
terminalMiddleClick: termHandlers.middleClick,
closeReview: closeReviewTab,
reviewMiddleClick: handleReviewTabMouseDown,
selectReviewTab: () => setReviewActive(true),
selectSessionTab,
sessionMiddleClick: handleTabMouseDown,
sessionClose: handleCloseTab,
sessionFork: handleForkSession,
reviewLabel: t("session.tab.review"),
reviewTooltip: t("command.review.toggle"),
})
}
</For>
</SortableProvider>
<div class="am-tab-list-wrap">
<div
class="am-tab-list"
ref={tabScroll.setRef}
style={{ "--tab-count": `${tabIds().length}` } as JSX.CSSProperties}
>
<SortableProvider ids={tabIds()}>
<For each={tabIds()}>
{(id) =>
renderTab(id, {
terms,
REVIEW_TAB_ID,
tabIds,
kb,
reviewActive,
currentSessionID: () => session.currentSessionID(),
activePendingId,
visibleTabId,
isPending,
isBusy: isSessionBusy,
tabLookup,
adjacentHint,
activateTerminal: termHandlers.activate,
deactivateTerminal: termHandlers.deactivate,
closeTerminal: termHandlers.closeTerminal,
terminalMiddleClick: termHandlers.middleClick,
closeReview: closeReviewTab,
reviewMiddleClick: handleReviewTabMouseDown,
selectReviewTab: () => setReviewActive(true),
selectSessionTab,
sessionMiddleClick: handleTabMouseDown,
sessionClose: handleCloseTab,
sessionFork: handleForkSession,
reviewLabel: t("session.tab.review"),
reviewTooltip: t("command.review.toggle"),
})
}
</For>
</SortableProvider>
</div>
{renderNewTabButton({
contextSelected: () => selection() !== null,
kb,
newSessionLabel: t("agentManager.session.new"),
newTerminalLabel: t("agentManager.terminal.new"),
newSessionMenuLabel: t("agentManager.session.newSession"),
moreOptionsLabel: t("agentManager.tab.newOptions"),
onNewSession: handleAddSession,
onNewTerminal: () => termHandlers.requestNew(),
})}
</div>
<div class={`am-tab-fade am-tab-fade-right ${tabScroll.showRight() ? "am-tab-fade-visible" : ""}`} />
</div>
{renderNewTabButton({
contextSelected: () => selection() !== null,
kb,
newSessionLabel: t("agentManager.session.new"),
newTerminalLabel: t("agentManager.terminal.new"),
newSessionMenuLabel: t("agentManager.session.newSession"),
moreOptionsLabel: t("agentManager.tab.newOptions"),
onNewSession: handleAddSession,
onNewTerminal: () => termHandlers.requestNew(),
})}
<div class="am-tab-actions">
<CurrentTabsMenu
items={tabMenuItems}
Expand Down
87 changes: 75 additions & 12 deletions packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css
Original file line number Diff line number Diff line change
Expand Up @@ -1033,8 +1033,11 @@ button.am-section-toggle:hover .am-section-label {
}

.am-tab-list {
--am-tab-max-width: 240px;
--am-tab-width: clamp(72px, calc(100% / var(--tab-count, 1)), var(--am-tab-max-width));
display: flex;
align-items: stretch;
flex: 0 1 calc(var(--tab-count, 1) * var(--am-tab-max-width));
min-width: 0;
overflow-x: auto;
height: 100%;
Expand All @@ -1044,6 +1047,16 @@ button.am-section-toggle:hover .am-section-label {
-ms-overflow-style: none;
}

.am-tab-list-wrap {
display: flex;
align-items: stretch;
gap: 4px;
min-width: 0;
flex: 0 1 auto;
max-width: 100%;
height: 100%;
}

.am-tab-list::-webkit-scrollbar {
display: none;
}
Expand All @@ -1053,7 +1066,7 @@ button.am-section-toggle:hover .am-section-label {
position: relative;
display: flex;
min-width: 0;
flex-shrink: 1;
flex: 1 1 auto;
align-items: stretch;
height: 100%;
overflow: hidden;
Expand Down Expand Up @@ -1086,10 +1099,11 @@ button.am-section-toggle:hover .am-section-label {
}

.am-tab {
position: relative;
display: flex;
align-items: center;
gap: 6px;
padding: 0 12px;
gap: 5px;
padding: 0 8px;
background: none;
color: var(--text-weak);
font-size: var(--kilo-font-size-12);
Expand All @@ -1098,7 +1112,12 @@ button.am-section-toggle:hover .am-section-label {
border: none;
border-bottom: 2px solid transparent;
min-width: 0;
width: 100%;
height: 100%;
transition:
background-color 120ms ease,
border-color 120ms ease,
color 120ms ease;
}

.am-tab:hover {
Expand All @@ -1108,45 +1127,88 @@ button.am-section-toggle:hover .am-section-label {
.am-tab-active {
color: var(--text-base);
border-bottom-color: var(--surface-interactive-base);
background: color-mix(in srgb, var(--surface-interactive-base) 10%, transparent);
}

.am-tab-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
flex-shrink: 0;
}

.am-tab-icon [data-component="icon"] {
color: currentColor;
}

.am-tab-label {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.am-tab-close {
.am-tab-close-wrap {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
flex-shrink: 0;
opacity: 0;
pointer-events: none;
transition: opacity 100ms ease;
}

.am-tab .am-tab-close {
opacity: 0;
.am-tab:hover .am-tab-close-wrap,
.am-tab-active .am-tab-close-wrap {
opacity: 1;
pointer-events: auto;
}

.am-tab:hover .am-tab-close {
opacity: 0.6;
.am-tab-close-wrap:hover {
opacity: 1;
}

.am-tab-active .am-tab-close {
opacity: 0.6;
.am-tab-close[data-component="icon-button"] {
width: 20px;
height: 20px;
}

.am-tab-close:hover {
opacity: 1;
.am-tab-close[data-component="icon-button"] [data-slot="icon-svg"] {
color: var(--text-base);
}

/* Drag-and-drop sortable tab wrapper */

.am-tab-sortable {
display: flex;
height: 100%;
width: var(--am-tab-width);
min-width: var(--am-tab-width);
max-width: var(--am-tab-width);
flex: 0 0 var(--am-tab-width);
touch-action: none;
transition:
flex-basis 140ms ease,
max-width 140ms ease,
min-width 140ms ease,
width 140ms ease;
}

.am-tab-list[data-tab-widths-frozen] .am-tab-sortable,
.am-tab-dragging {
transition: none;
}

.am-tab-sortable > [data-component="tooltip-trigger"],
.am-tab-sortable > [data-slot="context-menu-trigger"] > [data-component="tooltip-trigger"] {
height: 100%;
min-width: 0;
width: 100%;
}

.am-tab-dragging {
Expand Down Expand Up @@ -1211,6 +1273,7 @@ body.am-wt-dragging-active * {
.am-tab-add-split {
flex-shrink: 0;
align-self: center;
margin-right: 2px;
}

.am-tab-actions {
Expand Down
Loading
Loading