fix(vscode): address local tab review feedback

This commit is contained in:
marius-kilocode
2026-05-21 11:32:15 +02:00
parent e4a03db371
commit 2ef897a0ba
4 changed files with 26 additions and 15 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
---
"kilo-code": patch
"kilo-code": minor
---
Open multiple same-repository sessions as tabs from Kilo sidebar and editor-tab chats.
@@ -13,6 +13,10 @@ import {
} from "../../webview-ui/src/utils/local-tabs"
const pending = (id = "sidebar-pending:1") => id
const makePending =
(id = "sidebar-pending:1") =>
() =>
id
function state(ids: string[], active?: string): LocalTabState {
return { ids, active }
@@ -36,11 +40,11 @@ const inventory = (local: string[], external: string[] = []) => ({ local, extern
describe("local session tabs", () => {
it("restores a fresh pending tab when no sessions were persisted", () => {
expect(restoreTabs(undefined, undefined, pending())).toEqual({ ids: [pending()], active: pending() })
expect(restoreTabs(undefined, undefined, makePending())).toEqual({ ids: [pending()], active: pending() })
})
it("restores persisted local sessions and their active tab", () => {
expect(restoreTabs(["s1", "s2"], "s2", pending())).toEqual({ ids: ["s1", "s2"], active: "s2" })
expect(restoreTabs(["s1", "s2"], "s2", makePending())).toEqual({ ids: ["s1", "s2"], active: "s2" })
})
it("promotes a pending tab into the created session without moving it", () => {
@@ -61,18 +65,18 @@ describe("local session tabs", () => {
})
it("selects the neighboring tab after closing the active one", () => {
expect(closeTab(state(["s1", "s2", "s3"], "s2"), "s2", pending())).toEqual({
expect(closeTab(state(["s1", "s2", "s3"], "s2"), "s2", makePending())).toEqual({
ids: ["s1", "s3"],
active: "s3",
})
})
it("keeps an empty chat available after closing the final tab", () => {
expect(closeTab(state(["s1"], "s1"), "s1", pending())).toEqual({ ids: [pending()], active: pending() })
expect(closeTab(state(["s1"], "s1"), "s1", makePending())).toEqual({ ids: [pending()], active: pending() })
})
it("drops missing persisted sessions while preserving pending work", () => {
expect(reconcileTabs(state(["s1", pending(), "gone"], "gone"), ["s1"], "sidebar-pending:2")).toEqual({
expect(reconcileTabs(state(["s1", pending(), "gone"], "gone"), ["s1"], makePending("sidebar-pending:2"))).toEqual({
ids: ["s1", pending()],
active: "s1",
})
@@ -50,7 +50,7 @@ export const LocalTabsProvider: ParentComponent = (props) => {
const saved = vscode.getState<LocalTabsState>()
let count = 0
const pending = () => `${PENDING_TAB_PREFIX}${++count}`
const init = restoreTabs(saved?.sidebarSessionTabIDs, saved?.sidebarActiveSessionTabID, pending())
const init = restoreTabs(saved?.sidebarSessionTabIDs, saved?.sidebarActiveSessionTabID, pending)
const [ids, setIds] = createSignal(init.ids)
const [active, setActive] = createSignal(init.active)
const fresh = new Set<string>()
@@ -92,7 +92,7 @@ export const LocalTabsProvider: ParentComponent = (props) => {
const close = (id: string) => {
const before = active()
const next = closeTab(current(), id, pending())
const next = closeTab(current(), id, pending)
apply(next)
if (before === id || before !== next.active) focus(next.active)
}
@@ -144,7 +144,7 @@ export const LocalTabsProvider: ParentComponent = (props) => {
const before = active()
const listed = message.sessions.map((item) => item.id)
for (const id of listed) fresh.delete(id)
const next = reconcileTabs(current(), [...listed, ...(message.preserveSessionIds ?? []), ...fresh], pending())
const next = reconcileTabs(current(), [...listed, ...(message.preserveSessionIds ?? []), ...fresh], pending)
apply(next)
if (before !== next.active) focus(next.active)
return
@@ -152,7 +152,7 @@ export const LocalTabsProvider: ParentComponent = (props) => {
if (message.type === "sessionDeleted") {
fresh.delete(message.sessionID)
const before = active()
const next = closeTab(current(), message.sessionID, pending())
const next = closeTab(current(), message.sessionID, pending)
apply(next)
if (before !== next.active) focus(next.active)
}
@@ -22,16 +22,21 @@ export const isPendingTab = (id: string) => id.startsWith(PENDING_TAB_PREFIX)
const unique = (ids: string[]) => [...new Set(ids.filter(Boolean))]
function normalize(ids: string[], active: string | undefined, pending: string): LocalTabState {
type PendingTabFactory = () => string
function normalize(ids: string[], active: string | undefined, pending: PendingTabFactory): LocalTabState {
const tabs = unique(ids)
if (tabs.length === 0) return { ids: [pending], active: pending }
if (tabs.length === 0) {
const id = pending()
return { ids: [id], active: id }
}
return { ids: tabs, active: active && tabs.includes(active) ? active : tabs[0] }
}
export function restoreTabs(
ids: string[] | undefined,
active: string | undefined,
pending: string,
pending: PendingTabFactory,
check: PendingTabCheck = isPendingTab,
): LocalTabState {
const tabs = ids?.filter((id) => !check(id)) ?? []
@@ -39,10 +44,12 @@ export function restoreTabs(
return normalize(tabs, tab, pending)
}
// New composers become active immediately even before the backend creates a session.
export function addPendingTab(state: LocalTabState, id: string): LocalTabState {
return { ids: unique([...state.ids, id]), active: id }
}
// Existing sessions use the same state shape, but callers keep their open-or-focus intent explicit.
export function openSessionTab(state: LocalTabState, id: string): LocalTabState {
return { ids: unique([...state.ids, id]), active: id }
}
@@ -61,7 +68,7 @@ export function nextTabAfterClose(ids: readonly string[], id: string): string |
return tabs[Math.min(index, tabs.length - 1)]
}
export function closeTab(state: LocalTabState, id: string, pending: string): LocalTabState {
export function closeTab(state: LocalTabState, id: string, pending: PendingTabFactory): LocalTabState {
if (!state.ids.includes(id)) return state
const ids = state.ids.filter((tab) => tab !== id)
if (state.active !== id) return normalize(ids, state.active, pending)
@@ -71,7 +78,7 @@ export function closeTab(state: LocalTabState, id: string, pending: string): Loc
export function reconcileTabs(
state: LocalTabState,
loaded: string[],
pending: string,
pending: PendingTabFactory,
check: PendingTabCheck = isPendingTab,
): LocalTabState {
const seen = new Set(loaded)