feat(agent-manager): filter sessions by worktree

This commit is contained in:
marius-kilocode
2026-07-24 13:15:08 +02:00
parent 1a3c719175
commit 99c04c7163
27 changed files with 157 additions and 18 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---
Filter `/sessions` history to sessions in the current Agent Manager worktree.
@@ -99,4 +99,29 @@ test.describe("history session accessibility", () => {
await expect(local).toHaveAttribute("aria-selected", "true")
await expect(page.getByRole("tabpanel", { name: "Local" })).toBeVisible()
})
test("filters sessions to the current worktree and includes it in keyboard navigation", async ({ page }) => {
await story(page, "history-sessionlist--worktree-sources")
const local = page.getByRole("tab", { name: "Local" })
const worktree = page.getByRole("tab", { name: "Worktree" })
await local.focus()
await page.keyboard.press("End")
await expect(worktree).toBeFocused()
await page.keyboard.press("Enter")
await expect(worktree).toHaveAttribute("aria-selected", "true")
await expect(page.getByRole("tabpanel", { name: "Worktree" })).toBeVisible()
const rows = page.locator('[data-slot="list-item"]')
await expect(rows.filter({ hasText: "Refactor authentication module" })).toBeVisible()
await expect(rows.filter({ hasText: "Fix TypeScript errors in webview" })).toBeVisible()
await expect(rows.filter({ hasText: "Add screenshot test coverage" })).toHaveCount(0)
await rows.filter({ hasText: "Refactor authentication module" }).click()
await expect(page.locator('[data-slot="selected-session"]')).toHaveText("s1")
await worktree.focus()
await page.keyboard.press("ArrowRight")
await expect(local).toBeFocused()
})
})
@@ -761,6 +761,16 @@ const AgentManagerContent: Component = () => {
return sessionsForWorktree(sel)
})
const activeWorktreeSessionIds = createMemo<ReadonlySet<string> | undefined>(() => {
const sel = selection()
if (!sel || sel === LOCAL) return undefined
return new Set(
managedSessions()
.filter((item) => item.worktreeId === sel)
.map((item) => item.id),
)
})
const activeTabs = createMemo((): SessionInfo[] => {
const sel = selection()
if (sel === LOCAL) return localSessions()
@@ -2898,6 +2908,7 @@ const AgentManagerContent: Component = () => {
openLocally(id)
}}
onBack={() => setHistory(false)}
worktreeSessionIds={activeWorktreeSessionIds}
/>
</Show>
<Show when={!contextEmpty() && !history()}>
@@ -1,10 +1,10 @@
/**
* HistoryView component
* Unified panel for local and cloud session history.
* Contains a tab bar ("Local" | "Cloud") and an always-visible "Import session" button.
* Unified panel for local, cloud, and optional worktree session history.
* Contains a source tab bar and an always-visible "Import session" button.
*/
import { Component, createEffect, createSignal, onCleanup } from "solid-js"
import { Component, Show, createEffect, createSignal, onCleanup, type Accessor } from "solid-js"
import { Button } from "@kilocode/kilo-ui/button"
import { useDialog } from "@kilocode/kilo-ui/context/dialog"
import { useLanguage } from "../../context/language"
@@ -17,21 +17,34 @@ import CloudSessionList from "./CloudSessionList"
interface HistoryViewProps {
onSelectSession: (id: string) => void
onBack?: () => void
worktreeSessionIds?: Accessor<ReadonlySet<string> | undefined>
}
type Source = "local" | "cloud" | "worktree"
const EMPTY_SESSION_IDS = new Set<string>()
const HistoryView: Component<HistoryViewProps> = (props) => {
const language = useLanguage()
const dialog = useDialog()
const session = useSession()
const tabs = useLocalTabs()
const [tab, setTab] = createSignal<"local" | "cloud">("local")
const [tab, setTab] = createSignal<Source>("local")
let local: HTMLButtonElement | undefined
let cloud: HTMLButtonElement | undefined
let worktree: HTMLButtonElement | undefined
let localPanel: HTMLDivElement | undefined
let cloudPanel: HTMLDivElement | undefined
let worktreePanel: HTMLDivElement | undefined
const worktreeIds = () => props.worktreeSessionIds?.()
createEffect(() => {
const panel = tab() === "local" ? localPanel : cloudPanel
if (tab() === "worktree" && !worktreeIds()) setTab("local")
})
createEffect(() => {
const panel = tab() === "local" ? localPanel : tab() === "cloud" ? cloudPanel : worktreePanel
const frame = requestAnimationFrame(() => {
panel
@@ -60,17 +73,20 @@ const HistoryView: Component<HistoryViewProps> = (props) => {
props.onBack?.()
}
function move(event: KeyboardEvent, current: "local" | "cloud") {
const next =
function move(event: KeyboardEvent, current: Source) {
const sources: Source[] = worktreeIds() ? ["local", "cloud", "worktree"] : ["local", "cloud"]
const index = sources.indexOf(current)
const source =
event.key === "Home"
? local
? sources[0]
: event.key === "End"
? cloud
: event.key === "ArrowLeft" || event.key === "ArrowRight"
? current === "local"
? cloud
: local
: undefined
? sources.at(-1)
: event.key === "ArrowLeft"
? sources[(index - 1 + sources.length) % sources.length]
: event.key === "ArrowRight"
? sources[(index + 1) % sources.length]
: undefined
const next = source === "local" ? local : source === "cloud" ? cloud : source === "worktree" ? worktree : undefined
if (!next) return
event.preventDefault()
next.focus()
@@ -113,6 +129,23 @@ const HistoryView: Component<HistoryViewProps> = (props) => {
>
{language.t("session.tab.cloud")}
</button>
<Show when={worktreeIds()}>
<button
ref={worktree}
id="history-tab-worktree"
class="history-tab-btn"
classList={{ "history-tab-btn--active": tab() === "worktree" }}
type="button"
role="tab"
aria-selected={tab() === "worktree"}
aria-controls="history-panel-worktree"
tabIndex={tab() === "worktree" ? 0 : -1}
onClick={() => setTab("worktree")}
onKeyDown={(event) => move(event, "worktree")}
>
{language.t("session.tab.worktree")}
</button>
</Show>
</div>
<Button variant="secondary" size="small" onClick={openImport} class="history-import-btn">
{language.t("session.cloud.import")}
@@ -139,6 +172,23 @@ const HistoryView: Component<HistoryViewProps> = (props) => {
>
{tab() === "cloud" && <CloudSessionList onSelectSession={selectCloudSession} />}
</div>
<Show when={worktreeIds()}>
<div
class="history-view-content"
ref={worktreePanel}
id="history-panel-worktree"
role="tabpanel"
aria-labelledby="history-tab-worktree"
hidden={tab() !== "worktree"}
>
{tab() === "worktree" && (
<SessionList
onSelectSession={props.onSelectSession}
sessionIds={() => worktreeIds() ?? EMPTY_SESSION_IDS}
/>
)}
</div>
</Show>
</div>
)
}
@@ -5,7 +5,7 @@
* Header/back button are owned by the parent HistoryView.
*/
import { Component, Show, createSignal, onMount, type JSX } from "solid-js"
import { Component, Show, createMemo, createSignal, onMount, type Accessor, type JSX } from "solid-js"
import { List } from "@kilocode/kilo-ui/list"
import { ContextMenu } from "@kilocode/kilo-ui/context-menu"
import { Dialog } from "@kilocode/kilo-ui/dialog"
@@ -38,6 +38,7 @@ function dateGroupKey(iso: string): (typeof DATE_GROUP_KEYS)[number] {
interface SessionListProps {
onSelectSession: (id: string) => void
sessionIds?: Accessor<ReadonlySet<string>>
}
const SessionList: Component<SessionListProps> = (props) => {
@@ -50,6 +51,12 @@ const SessionList: Component<SessionListProps> = (props) => {
const [notice, setNotice] = createSignal("")
let seq = 0
const items = createMemo(() => {
const ids = props.sessionIds?.()
if (!ids) return session.sessions()
return session.sessions().filter((item) => ids.has(item.id))
})
onMount(() => {
console.log("[Kilo New] SessionList mounted, loading sessions")
session.loadSessions()
@@ -57,7 +64,7 @@ const SessionList: Component<SessionListProps> = (props) => {
const currentSession = (): SessionInfo | undefined => {
const id = session.currentSessionID()
return session.sessions().find((s) => s.id === id)
return items().find((s) => s.id === id)
}
function startRename(s: SessionInfo) {
@@ -189,7 +196,7 @@ const SessionList: Component<SessionListProps> = (props) => {
return (
<div class="session-list">
<List<SessionInfo>
items={session.sessions()}
items={items()}
key={(s) => s.id}
filterKeys={["title"]}
current={currentSession()}
+1
View File
@@ -1109,6 +1109,7 @@ export const dict = {
"session.tabs.switcher.busy": "جارٍ العمل",
"session.tab.local": "محلي",
"session.tab.cloud": "السحابة",
"session.tab.worktree": "شجرة العمل",
"session.cloud.repoOnly": "هذا المستودع فقط",
"session.cloud.import": "استيراد من السحابة",
"feedback.button": "التغذية الراجعة والدعم",
+1
View File
@@ -1130,6 +1130,7 @@ export const dict = {
"session.tabs.switcher.busy": "Trabalhando",
"session.tab.local": "Local",
"session.tab.cloud": "Nuvem",
"session.tab.worktree": "Árvore de trabalho",
"session.cloud.repoOnly": "Apenas este repositório",
"session.cloud.import": "Importar da nuvem",
"feedback.button": "Feedback e suporte",
+1
View File
@@ -1177,6 +1177,7 @@ export const dict = {
"session.tabs.switcher.busy": "Radi",
"session.tab.local": "Lokalno",
"session.tab.cloud": "Oblak",
"session.tab.worktree": "Radno stablo",
"session.cloud.repoOnly": "Samo ovaj repozitorij",
"session.cloud.import": "Uvezi iz oblaka",
"feedback.button": "Povratne informacije i podrška",
+1
View File
@@ -1169,6 +1169,7 @@ export const dict = {
"session.tabs.switcher.busy": "Arbejder",
"session.tab.local": "Lokal",
"session.tab.cloud": "Sky",
"session.tab.worktree": "Arbejdstræ",
"session.cloud.repoOnly": "Kun dette repository",
"session.cloud.import": "Importér fra skyen",
"feedback.button": "Feedback & support",
@@ -1189,6 +1189,7 @@ export const dict = {
"session.tabs.switcher.busy": "In Arbeit",
"session.tab.local": "Lokal",
"session.tab.cloud": "Cloud",
"session.tab.worktree": "Arbeitsbaum",
"session.cloud.repoOnly": "Nur dieses Repository",
"session.cloud.import": "Aus der Cloud importieren",
"feedback.button": "Feedback & Support",
@@ -1084,6 +1084,7 @@ export const dict = {
"session.tabs.switcher.busy": "Working",
"session.tab.local": "Local",
"session.tab.cloud": "Cloud",
"session.tab.worktree": "Worktree",
"session.cloud.repoOnly": "Only this repository",
"session.cloud.import": "Import session",
"feedback.button": "Feedback & Support",
+1
View File
@@ -1181,6 +1181,7 @@ export const dict = {
"session.tabs.switcher.busy": "Trabajando",
"session.tab.local": "Local",
"session.tab.cloud": "Nube",
"session.tab.worktree": "Árbol de trabajo",
"session.cloud.repoOnly": "Solo este repositorio",
"session.cloud.import": "Importar desde la nube",
"feedback.button": "Comentarios y soporte",
+1
View File
@@ -1188,6 +1188,7 @@ export const dict = {
"session.tabs.switcher.busy": "En cours",
"session.tab.local": "Local",
"session.tab.cloud": "Cloud",
"session.tab.worktree": "Arbre de travail",
"session.cloud.repoOnly": "Uniquement ce dépôt",
"session.cloud.import": "Importer depuis le cloud",
"feedback.button": "Commentaires & support",
+1
View File
@@ -935,6 +935,7 @@ export const dict = {
"session.tabs.switcher.busy": "In corso",
"session.tab.local": "Locale",
"session.tab.cloud": "Cloud",
"session.tab.worktree": "Albero di lavoro",
"session.cloud.repoOnly": "Solo questa repository",
"session.cloud.import": "Importa sessione",
"feedback.button": "Feedback e supporto",
+1
View File
@@ -1164,6 +1164,7 @@ export const dict = {
"session.tabs.switcher.busy": "作業中",
"session.tab.local": "ローカル",
"session.tab.cloud": "クラウド",
"session.tab.worktree": "ワークツリー",
"session.cloud.repoOnly": "このリポジトリのみ",
"session.cloud.import": "クラウドからインポート",
"feedback.button": "フィードバック & サポート",
+1
View File
@@ -1118,6 +1118,7 @@ export const dict = {
"session.tabs.switcher.busy": "작업 중",
"session.tab.local": "로컬",
"session.tab.cloud": "클라우드",
"session.tab.worktree": "작업 트리",
"session.cloud.repoOnly": "이 저장소만",
"session.cloud.import": "클라우드에서 가져오기",
"feedback.button": "피드백 & 지원",
+1
View File
@@ -1124,6 +1124,7 @@ export const dict = {
"session.tabs.switcher.busy": "Bezig",
"session.tab.local": "Lokaal",
"session.tab.cloud": "Cloud",
"session.tab.worktree": "Werkboom",
"session.cloud.repoOnly": "Alleen deze repository",
"session.cloud.import": "Importeer uit de cloud",
"feedback.button": "Feedback & Ondersteuning",
+1
View File
@@ -1130,6 +1130,7 @@ export const dict = {
"session.tabs.switcher.busy": "Jobber",
"session.tab.local": "Lokal",
"session.tab.cloud": "Sky",
"session.tab.worktree": "Arbeidstre",
"session.cloud.repoOnly": "Kun dette repositoriet",
"session.cloud.import": "Importer fra skyen",
"feedback.button": "Tilbakemelding & støtte",
+1
View File
@@ -1128,6 +1128,7 @@ export const dict = {
"session.tabs.switcher.busy": "Pracuje",
"session.tab.local": "Lokalny",
"session.tab.cloud": "Chmura",
"session.tab.worktree": "Drzewo robocze",
"session.cloud.repoOnly": "Tylko to repozytorium",
"session.cloud.import": "Importuj z chmury",
"feedback.button": "Opinie i wsparcie",
+1
View File
@@ -1174,6 +1174,7 @@ export const dict = {
"session.tabs.switcher.busy": "В работе",
"session.tab.local": "Локальный",
"session.tab.cloud": "Облако",
"session.tab.worktree": "Рабочее дерево",
"session.cloud.repoOnly": "Только этот репозиторий",
"session.cloud.import": "Импорт из облака",
"feedback.button": "Отзывы и поддержка",
+1
View File
@@ -1158,6 +1158,7 @@ export const dict = {
"session.tabs.switcher.busy": "กำลังทำงาน",
"session.tab.local": "ในเครื่อง",
"session.tab.cloud": "คลาวด์",
"session.tab.worktree": "เวิร์กทรี Git",
"session.cloud.repoOnly": "เฉพาะรีโพซิทอรีนี้",
"session.cloud.import": "นำเข้าจากคลาวด์",
"feedback.button": "ข้อเสนอแนะและการสนับสนุน",
+1
View File
@@ -1120,6 +1120,7 @@ export const dict = {
"session.tabs.switcher.busy": "Çalışıyor",
"session.tab.local": "Local",
"session.tab.cloud": "Cloud",
"session.tab.worktree": "Çalışma ağacı",
"session.cloud.repoOnly": "Yalnızca bu depo",
"session.cloud.import": "Buluttan içe aktar",
"feedback.button": "Geri Bildirim ve Destek",
+1
View File
@@ -1121,6 +1121,7 @@ export const dict = {
"session.tabs.switcher.busy": "Працює",
"session.tab.local": "Локальний",
"session.tab.cloud": "Хмарний",
"session.tab.worktree": "Робоче дерево",
"session.cloud.repoOnly": "Лише цей репозиторій",
"session.cloud.import": "Імпортувати з хмари",
"feedback.button": "Зворотний зв'язок і підтримка",
+1
View File
@@ -1134,6 +1134,7 @@ export const dict = {
"session.tabs.switcher.busy": "工作中",
"session.tab.local": "本地",
"session.tab.cloud": "云端",
"session.tab.worktree": "工作树",
"session.cloud.repoOnly": "仅此仓库",
"session.cloud.import": "从云端导入",
"feedback.button": "反馈与支持",
+1
View File
@@ -1095,6 +1095,7 @@ export const dict = {
"session.tabs.switcher.busy": "工作中",
"session.tab.local": "本機",
"session.tab.cloud": "雲端",
"session.tab.worktree": "工作樹",
"session.cloud.repoOnly": "僅此儲存庫",
"session.cloud.import": "從雲端匯入",
"feedback.button": "意見回饋與支援",
@@ -185,3 +185,24 @@ export const Sources: Story = {
</WithSessions>
),
}
const WorktreeSourcesDemo = () => {
const [selected, setSelected] = createSignal("")
const ids = new Set(["s1", "s3"])
return (
<WithSessions sessions={mockSessions as any}>
<div style={{ height: "500px" }}>
<HistoryView onSelectSession={setSelected} onBack={noop} worktreeSessionIds={() => ids} />
<output class="sr-only" data-slot="selected-session">
{selected()}
</output>
</div>
</WithSessions>
)
}
export const WorktreeSources: Story = {
name: "Current worktree source",
render: () => <WorktreeSourcesDemo />,
}
@@ -103,7 +103,7 @@ body.vscode-light
}
}
/* History View (unified Local + Cloud tabs) */
/* History View (unified Local + Cloud + optional Worktree tabs) */
.history-view {
display: flex;
flex-direction: column;