mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-21 05:52:35 +08:00
feat(agent-manager): filter sessions by worktree
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user