diff --git a/.changeset/session-tab-switcher.md b/.changeset/session-tab-switcher.md new file mode 100644 index 00000000000..5befe9b0668 --- /dev/null +++ b/.changeset/session-tab-switcher.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Add a searchable open-tabs switcher to the sidebar tab bar. diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/session-tabs/switcher-open-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/session-tabs/switcher-open-chromium-linux.png new file mode 100644 index 00000000000..4bb775859c9 --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/session-tabs/switcher-open-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:273c76976df0e83fa404ae2af2d561f8c38458e3ef1b32f4f5b1990a5906a195 +size 14239 diff --git a/packages/kilo-vscode/tests/accessibility.spec.ts b/packages/kilo-vscode/tests/accessibility.spec.ts index 153eeb1c34e..19ea7446ca9 100644 --- a/packages/kilo-vscode/tests/accessibility.spec.ts +++ b/packages/kilo-vscode/tests/accessibility.spec.ts @@ -13,6 +13,7 @@ const STORIES = [ { id: "settings--providers-configure", name: "Settings / providers empty state" }, { id: "marketplace--empty-list", name: "Marketplace / empty state" }, { id: "agentmanager--sidebar-search-open", name: "Agent Manager / sidebar search" }, + { id: "session-tabs--switcher-open", name: "Session tabs / switcher" }, ] function url(id: string) { @@ -178,4 +179,34 @@ test.describe("webview accessibility ratchet", () => { ).toBeVisible() await expect(page.getByText("⌘F", { exact: true })).toBeVisible() }) + + test("Session tab switcher restores chat focus after keyboard and mouse selection", async ({ page }) => { + await open(page, "session-tabs--switcher-open") + + const input = page.getByPlaceholder("Search open tabs") + const prompt = page.getByRole("textbox", { name: "Chat input" }) + await expect(page.locator('[data-slot="list-item"][data-active="true"]')).toHaveCount(0) + await expect(page.locator('[data-slot="list-item"][data-key="current"]')).toHaveAttribute("data-selected", "true") + await expect(page.locator('[data-slot="list-item"][data-key="refactor"]')).toHaveAttribute("data-selected", "false") + await input.press("ArrowDown") + await input.press("Enter") + await expect(prompt).toBeFocused() + + await page.getByRole("button", { name: "Show open tabs" }).click() + await page.locator('[data-slot="list-item"][data-key="current"]').click() + await expect(prompt).toBeFocused() + + // Enter without prior ArrowDown selects the first filtered result (noInitialSelection) + await page.getByRole("button", { name: "Show open tabs" }).click() + await input.fill("Review") + await input.press("Enter") + await expect(prompt).toBeFocused() + }) + + test("Search popovers expose accessible dialog names", async ({ page }) => { + for (const id of ["agentmanager--sidebar-search-open", "session-tabs--switcher-open"]) { + await open(page, id) + await expect(page.getByRole("dialog")).toHaveAccessibleName(/.+/) + } + }) }) diff --git a/packages/kilo-vscode/tests/fixtures/session-tab-switcher.tsx b/packages/kilo-vscode/tests/fixtures/session-tab-switcher.tsx new file mode 100644 index 00000000000..b1260dc8a27 --- /dev/null +++ b/packages/kilo-vscode/tests/fixtures/session-tab-switcher.tsx @@ -0,0 +1,194 @@ +import assert from "node:assert/strict" +import { Window } from "happy-dom" + +const window = new Window({ url: "http://localhost" }) +const style = window.getComputedStyle.bind(window) +Object.assign(globalThis, { + window, + document: window.document, + navigator: window.navigator, + Node: window.Node, + Element: window.Element, + HTMLElement: window.HTMLElement, + HTMLInputElement: window.HTMLInputElement, + HTMLTextAreaElement: window.HTMLTextAreaElement, + SVGElement: window.SVGElement, + MutationObserver: window.MutationObserver, + ResizeObserver: window.ResizeObserver, + CustomEvent: window.CustomEvent, + Event: window.Event, + FocusEvent: window.FocusEvent, + InputEvent: window.InputEvent, + KeyboardEvent: window.KeyboardEvent, + MouseEvent: window.MouseEvent, + PointerEvent: window.PointerEvent, + getComputedStyle: (node: Element) => { + const value = style(node) + Object.defineProperty(value, "animationName", { configurable: true, value: "none" }) + return value + }, + requestAnimationFrame: window.requestAnimationFrame.bind(window), + cancelAnimationFrame: window.cancelAnimationFrame.bind(window), +}) + +const { Show, createSignal } = await import("solid-js") +const { render } = await import("solid-js/web") +const { SessionTabSwitcher } = await import("../../webview-ui/src/components/chat/SessionTabSwitcher") + +const rows = [ + { id: "alpha", title: "Alpha", active: true, busy: false, pending: false }, + { id: "beta", title: "Beta", active: false, busy: true, pending: false }, + { id: "gamma", title: "Gamma", active: false, busy: false, pending: false }, +] +const [items, setItems] = createSignal(rows) +const selected: string[] = [] +const restored: boolean[] = [] +const closed: string[] = [] +const target = document.createElement("textarea") +const root = document.createElement("div") +document.body.append(root, target) + +const dispose = render( + () => ( + 1}> + selected.push(id)} + onRestore={() => { + restored.push(true) + target.focus() + }} + onClose={(id) => { + closed.push(id) + setItems((value) => value.filter((item) => item.id !== id)) + }} + portal={false} + /> + + ), + root, +) + +function query(selector: string, message: string) { + const node = root.querySelector(selector) + assert(node, message) + return node +} + +const settle = async () => { + await Promise.resolve() + await window.happyDOM.waitUntilComplete() +} + +const open = async () => { + query('[aria-label="Show open tabs"]', "Switcher trigger did not render").click() + await settle() + assert.equal(root.querySelector('[data-slot="list-item"][data-active="true"]'), null, "First tab was highlighted") + assert.equal( + query('[data-slot="list-item"][data-key="alpha"]', "Current tab did not render").getAttribute("data-selected"), + "true", + "Current tab was not selected", + ) +} + +async function closeFiltered() { + await open() + + const input = query('[data-slot="list-search"] input', "Switcher search did not render") + input.value = "be" + input.dispatchEvent(new InputEvent("input", { bubbles: true, data: "be", inputType: "insertText" })) + await settle() + + const close = query( + '[aria-label="Close tab: Beta"]', + "Filtered result close button did not render", + ) + assert.equal(close.tabIndex, 0, "Close button is not keyboard reachable") + close.click() + await settle() + + assert.deepEqual(closed, ["beta"], "Unexpected closed tabs") + assert.equal(input.value, "be", "Closing a result cleared the filter") + assert.equal(document.activeElement, input, "Search input was not refocused after closing a result") +} + +async function selectFiltered() { + setItems(rows) + await settle() + + query('[data-slot="list-item"][data-key="beta"]', "Filtered result did not return").click() + await settle() + + assert.deepEqual(selected, ["beta"], "Unexpected selected tabs") + assert.deepEqual(restored, [true], "Prompt focus was not restored") + assert.equal(document.activeElement, target, "Popover close stole focus from the prompt") +} + +async function enterSelectsFirst() { + setItems(rows) + selected.length = 0 + restored.length = 0 + await settle() + + await open() + + const input = query('[data-slot="list-search"] input', "Switcher search did not render") + input.value = "ga" + input.dispatchEvent(new InputEvent("input", { bubbles: true, data: "ga", inputType: "insertText" })) + await settle() + + input.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Enter" })) + await settle() + + assert.deepEqual(selected, ["gamma"], "Enter did not select the first filtered result") + assert.deepEqual(restored, [true], "Prompt focus was not restored after Enter") + assert.equal(document.activeElement, target, "Popover close stole focus from the prompt") +} + +async function deleteReopened() { + await open() + + const alpha = query( + '[data-slot="list-item"][data-key="alpha"]', + "Switcher did not reset its filter when reopened", + ) + alpha.focus() + alpha.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Delete" })) + await settle() + + assert.deepEqual(closed, ["beta", "alpha"], "Keyboard close failed") +} + +async function closeToOne() { + closed.length = 0 + restored.length = 0 + + const beta = query( + '[data-slot="list-item"][data-key="beta"]', + "Switcher did not retain the remaining tabs", + ) + beta.focus() + beta.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Delete" })) + await settle() + + assert.deepEqual(closed, ["beta"], "Final visible close failed") + assert.deepEqual(restored, [true], "Prompt did not receive the focus handoff") + assert.equal(root.querySelector('[aria-label="Show open tabs"]'), null, "Switcher did not unmount") + assert.equal(document.activeElement, target, "Prompt was not focused after the switcher unmounted") +} + +await closeFiltered() +await selectFiltered() +await enterSelectsFirst() +await deleteReopened() +await closeToOne() + +dispose() diff --git a/packages/kilo-vscode/tests/unit/session-tab-switcher.test.ts b/packages/kilo-vscode/tests/unit/session-tab-switcher.test.ts new file mode 100644 index 00000000000..2c621dc5858 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/session-tab-switcher.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from "bun:test" +import { unlinkSync } from "node:fs" +import path from "node:path" +import { build } from "esbuild" +import { solidPlugin } from "esbuild-plugin-solid" + +const ROOT = path.resolve(import.meta.dir, "../..") +const WEBVIEW = path.join(ROOT, "webview-ui") +const FIXTURE = path.join(ROOT, "tests/fixtures/session-tab-switcher.tsx") + +describe("SessionTabSwitcher", () => { + it("preserves filtering and restores focus across tab actions", async () => { + const solid = path.dirname(Bun.resolveSync("solid-js/package.json", WEBVIEW)) + const aliases: Record = { + "solid-js": path.join(solid, "dist/solid.js"), + "solid-js/web": path.join(solid, "web/dist/web.js"), + "solid-js/store": path.join(solid, "store/dist/store.js"), + } + const dedupe = { + name: "solid-dedupe", + setup(ctx: Parameters[0]["plugins"]>[number]["setup"]>[0]) { + ctx.onResolve({ filter: /^solid-js(\/web|\/store)?$/ }, (args) => ({ path: aliases[args.path] })) + }, + } + const result = await build({ + entryPoints: [FIXTURE], + bundle: true, + conditions: ["browser"], + external: ["happy-dom"], + format: "esm", + logLevel: "silent", + platform: "node", + plugins: [dedupe, solidPlugin()], + target: "es2022", + write: false, + }) + const file = path.join(ROOT, `.session-tab-switcher-${crypto.randomUUID()}.mjs`) + await Bun.write(file, result.outputFiles[0]!.contents) + const child = Bun.spawnSync(["bun", file], { cwd: WEBVIEW, stdout: "pipe", stderr: "pipe" }) + unlinkSync(file) + + const output = child.stdout.toString() + child.stderr.toString() + expect(child.exitCode, output).toBe(0) + }) + + it("uses logical properties for RTL layout", async () => { + const css = await Bun.file(path.join(WEBVIEW, "src/styles/session-tabs.css")).text() + const start = css.indexOf(".session-tab-switcher-wrap") + const end = css.indexOf("/* Match tab context menus", start) + const switcher = css.slice(start, end) + + expect(switcher).toContain("border-inline-start") + expect(switcher).toContain("inset-inline-end") + expect(switcher).toContain("padding-inline") + expect(switcher).not.toMatch(/\b(?:left|right|margin-left|margin-right|border-left|border-right)\s*:/) + }) +}) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/SidebarSearchMenu.tsx b/packages/kilo-vscode/webview-ui/agent-manager/SidebarSearchMenu.tsx index 7da9c9cb79f..e228c0f7d86 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/SidebarSearchMenu.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/SidebarSearchMenu.tsx @@ -3,6 +3,7 @@ import { Show, createEffect, createSignal } from "solid-js" import type { Accessor, Component } from "solid-js" import { Icon } from "@kilocode/kilo-ui/icon" +import { IconButton } from "@kilocode/kilo-ui/icon-button" import { List } from "@kilocode/kilo-ui/list" import type { ListRef } from "@kilocode/kilo-ui/list" import { Popover } from "@kilocode/kilo-ui/popover" @@ -71,16 +72,19 @@ export const SidebarSearchMenu: Component = (props) => { onOpenChange={close} modal={false} portal={props.portal} - class="am-sidebar-search-popover" - triggerAs="button" + class="search-menu-popover am-sidebar-search-popover" + contentLabel={props.labels.search} + triggerAs={IconButton} triggerProps={{ type: "button", - class: "am-sidebar-search-trigger", + icon: "magnifying-glass", + size: "normal", + variant: "ghost", + class: "search-menu-trigger", "aria-label": props.labels.search, }} - trigger={} > -
{announcement()} diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/SessionTabSwitcher.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/SessionTabSwitcher.tsx new file mode 100644 index 00000000000..3ba5618a02b --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/components/chat/SessionTabSwitcher.tsx @@ -0,0 +1,177 @@ +import { Icon } from "@kilocode/kilo-ui/icon" +import { IconButton } from "@kilocode/kilo-ui/icon-button" +import { List } from "@kilocode/kilo-ui/list" +import type { ListRef } from "@kilocode/kilo-ui/list" +import { Popover } from "@kilocode/kilo-ui/popover" +import { Spinner } from "@kilocode/kilo-ui/spinner" +import { Tooltip } from "@kilocode/kilo-ui/tooltip" +import { Show, createEffect, createMemo, createSignal, type Component, type JSX } from "solid-js" + +interface SessionTabSwitcherItem { + id: string + title: string + active: boolean + busy: boolean + pending: boolean +} + +interface SessionTabSwitcherProps { + items: () => SessionTabSwitcherItem[] + labels: { + open: string + search: string + close: string + current: string + pending: string + busy: string + } + onSelect: (id: string) => void + onRestore: () => void + onClose: (id: string) => void + defaultOpen?: boolean + portal?: boolean +} + +export const SessionTabSwitcher: Component = (props) => { + const [open, setOpen] = createSignal(props.defaultOpen ?? false) + const [notice, setNotice] = createSignal("") + let list: ListRef | undefined + let root: HTMLDivElement | undefined + + const current = createMemo(() => props.items().find((item) => item.active)) + + const focus = (reset = false) => + queueMicrotask(() => { + if (reset) list?.setFilter("") + root?.querySelector("input")?.focus({ preventScroll: true }) + }) + + createEffect(() => { + if (open()) focus(true) + }) + + const select = (item: SessionTabSwitcherItem) => { + setOpen(false) + props.onSelect(item.id) + // Restore the prompt after the closing popover finishes its current event. + queueMicrotask(props.onRestore) + } + + const remove = (item: SessionTabSwitcherItem) => { + const last = props.items().length === 2 + props.onClose(item.id) + setNotice(`${props.labels.close}: ${item.title}`) + if (last) { + queueMicrotask(props.onRestore) + return + } + focus() + } + + const key = (event: KeyboardEvent, item: SessionTabSwitcherItem | undefined) => { + const target = event.target + if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) return + const node = event.currentTarget + const id = node instanceof HTMLElement ? node.dataset.key : undefined + // With no initial cursor, a row reached by Tab may not be the List's active item. + const value = props.items().find((row) => row.id === id) ?? item + if (!value) return + if (event.key === "Enter") { + event.preventDefault() + select(value) + return + } + if (event.key !== "Delete" && event.key !== "Backspace") return + event.preventDefault() + remove(value) + } + + const wrap = (item: SessionTabSwitcherItem, node: JSX.Element) => ( +
+ {node} + { + event.preventDefault() + event.stopPropagation() + remove(item) + }} + /> +
+ ) + + return ( + + +
+ + ref={(value) => { + list = value + }} + items={props.items()} + key={(item) => item.id} + filterKeys={["title"]} + current={current()} + noInitialSelection + search={{ placeholder: props.labels.search, autofocus: true }} + onKeyEvent={key} + onMove={(item) => setNotice(item ? item.title : "")} + onSelect={(item) => { + if (item) select(item) + }} + itemWrapper={wrap} + > + {(item) => ( + + + }> + + + + + + {item.title} + + + + + {props.labels.busy} + + + + + + {props.labels.current} + + + )} + +
+ {notice()} +
+
+
+
+ ) +} diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index 5a04fd4eb7c..bef28b4e812 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -1102,6 +1102,11 @@ export const dict = { "session.showHistory": "عرض السجل", "session.search.placeholder": "البحث في الجلسات...", "session.empty": "لا توجد جلسات بعد. انقر + لبدء محادثة جديدة.", + "session.tabs.switcher.open": "إظهار التبويبات المفتوحة", + "session.tabs.switcher.search": "البحث في التبويبات المفتوحة...", + "session.tabs.switcher.current": "الحالي", + "session.tabs.switcher.pending": "جديد", + "session.tabs.switcher.busy": "جارٍ العمل", "session.tab.local": "محلي", "session.tab.cloud": "السحابة", "session.cloud.repoOnly": "هذا المستودع فقط", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index f153fec3c12..e76f7353d76 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -1123,6 +1123,11 @@ export const dict = { "session.showHistory": "Mostrar Histórico", "session.search.placeholder": "Buscar sessões...", "session.empty": "Nenhuma sessão ainda. Clique + para iniciar uma nova conversa.", + "session.tabs.switcher.open": "Mostrar abas abertas", + "session.tabs.switcher.search": "Buscar abas abertas...", + "session.tabs.switcher.current": "Atual", + "session.tabs.switcher.pending": "Nova", + "session.tabs.switcher.busy": "Trabalhando", "session.tab.local": "Local", "session.tab.cloud": "Nuvem", "session.cloud.repoOnly": "Apenas este repositório", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index 8a83fd8be41..91a11fc570a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -1170,6 +1170,11 @@ export const dict = { "session.showHistory": "Prikaži historiju", "session.search.placeholder": "Pretraži sesije...", "session.empty": "Još nema sesija. Kliknite + za početak novog razgovora.", + "session.tabs.switcher.open": "Prikaži otvorene kartice", + "session.tabs.switcher.search": "Pretraži otvorene kartice...", + "session.tabs.switcher.current": "Trenutno", + "session.tabs.switcher.pending": "Novo", + "session.tabs.switcher.busy": "Radi", "session.tab.local": "Lokalno", "session.tab.cloud": "Oblak", "session.cloud.repoOnly": "Samo ovaj repozitorij", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index d4c0d06ce86..16f9fb7c51e 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -1162,6 +1162,11 @@ export const dict = { "session.showHistory": "Vis historik", "session.search.placeholder": "Søg sessioner...", "session.empty": "Ingen sessioner endnu. Klik + for at starte en ny samtale.", + "session.tabs.switcher.open": "Vis åbne faner", + "session.tabs.switcher.search": "Søg i åbne faner...", + "session.tabs.switcher.current": "Aktuel", + "session.tabs.switcher.pending": "Ny", + "session.tabs.switcher.busy": "Arbejder", "session.tab.local": "Lokal", "session.tab.cloud": "Sky", "session.cloud.repoOnly": "Kun dette repository", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index bf036e817cf..de3a32ae916 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -1182,6 +1182,11 @@ export const dict = { "session.showHistory": "Verlauf anzeigen", "session.search.placeholder": "Sitzungen suchen...", "session.empty": "Noch keine Sitzungen. Klicke + um eine neue Unterhaltung zu starten.", + "session.tabs.switcher.open": "Offene Tabs anzeigen", + "session.tabs.switcher.search": "Offene Tabs suchen...", + "session.tabs.switcher.current": "Aktuell", + "session.tabs.switcher.pending": "Neu", + "session.tabs.switcher.busy": "In Arbeit", "session.tab.local": "Lokal", "session.tab.cloud": "Cloud", "session.cloud.repoOnly": "Nur dieses Repository", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index bd742755d4f..a9c1169571a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -1077,6 +1077,11 @@ export const dict = { "session.history.sources": "History source", "session.search.placeholder": "Search sessions...", "session.empty": "No sessions yet. Click + to start a new conversation.", + "session.tabs.switcher.open": "Show open tabs", + "session.tabs.switcher.search": "Search open tabs...", + "session.tabs.switcher.current": "Current", + "session.tabs.switcher.pending": "New", + "session.tabs.switcher.busy": "Working", "session.tab.local": "Local", "session.tab.cloud": "Cloud", "session.cloud.repoOnly": "Only this repository", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index 415c87f2acb..487fefbe2cf 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -1174,6 +1174,11 @@ export const dict = { "session.showHistory": "Mostrar historial", "session.search.placeholder": "Buscar sesiones...", "session.empty": "Aún no hay sesiones. Haz clic en + para iniciar una nueva conversación.", + "session.tabs.switcher.open": "Mostrar pestañas abiertas", + "session.tabs.switcher.search": "Buscar pestañas abiertas...", + "session.tabs.switcher.current": "Actual", + "session.tabs.switcher.pending": "Nueva", + "session.tabs.switcher.busy": "Trabajando", "session.tab.local": "Local", "session.tab.cloud": "Nube", "session.cloud.repoOnly": "Solo este repositorio", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index 05fac462ea6..ac24daab2a6 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -1181,6 +1181,11 @@ export const dict = { "session.showHistory": "Afficher l'historique", "session.search.placeholder": "Rechercher des sessions...", "session.empty": "Aucune session pour l'instant. Cliquez + pour démarrer une nouvelle conversation.", + "session.tabs.switcher.open": "Afficher les onglets ouverts", + "session.tabs.switcher.search": "Rechercher dans les onglets ouverts...", + "session.tabs.switcher.current": "Actuel", + "session.tabs.switcher.pending": "Nouveau", + "session.tabs.switcher.busy": "En cours", "session.tab.local": "Local", "session.tab.cloud": "Cloud", "session.cloud.repoOnly": "Uniquement ce dépôt", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/it.ts b/packages/kilo-vscode/webview-ui/src/i18n/it.ts index 43f32b66be8..2eaba4f7de9 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/it.ts @@ -928,6 +928,11 @@ export const dict = { "session.history.sources": "Origine cronologia", "session.search.placeholder": "Cerca sessioni...", "session.empty": "Ancora nessuna sessione. Fai clic su + per avviare una nuova conversazione.", + "session.tabs.switcher.open": "Mostra schede aperte", + "session.tabs.switcher.search": "Cerca schede aperte...", + "session.tabs.switcher.current": "Corrente", + "session.tabs.switcher.pending": "Nuova", + "session.tabs.switcher.busy": "In corso", "session.tab.local": "Locale", "session.tab.cloud": "Cloud", "session.cloud.repoOnly": "Solo questa repository", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index 8b63bbee619..e19b1ded6ee 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -1157,6 +1157,11 @@ export const dict = { "session.showHistory": "履歴を表示", "session.search.placeholder": "セッションを検索...", "session.empty": "セッションがありません。+ をクリックして新しい会話を始めましょう。", + "session.tabs.switcher.open": "開いているタブを表示", + "session.tabs.switcher.search": "開いているタブを検索...", + "session.tabs.switcher.current": "現在", + "session.tabs.switcher.pending": "新規", + "session.tabs.switcher.busy": "作業中", "session.tab.local": "ローカル", "session.tab.cloud": "クラウド", "session.cloud.repoOnly": "このリポジトリのみ", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index edcfa35ae65..d52d7e27b37 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -1111,6 +1111,11 @@ export const dict = { "session.showHistory": "기록 보기", "session.search.placeholder": "세션 검색...", "session.empty": "아직 세션이 없습니다. +를 클릭하여 새 대화를 시작하세요.", + "session.tabs.switcher.open": "열린 탭 표시", + "session.tabs.switcher.search": "열린 탭 검색...", + "session.tabs.switcher.current": "현재", + "session.tabs.switcher.pending": "새 항목", + "session.tabs.switcher.busy": "작업 중", "session.tab.local": "로컬", "session.tab.cloud": "클라우드", "session.cloud.repoOnly": "이 저장소만", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index 3508d0bd94a..1d0c12e55bb 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -1117,6 +1117,11 @@ export const dict = { "session.showHistory": "Geschiedenis weergeven", "session.search.placeholder": "Zoek sessies...", "session.empty": "Nog geen sessies. Klik op + om een nieuw gesprek te starten.", + "session.tabs.switcher.open": "Open tabbladen tonen", + "session.tabs.switcher.search": "Open tabbladen zoeken...", + "session.tabs.switcher.current": "Huidig", + "session.tabs.switcher.pending": "Nieuw", + "session.tabs.switcher.busy": "Bezig", "session.tab.local": "Lokaal", "session.tab.cloud": "Cloud", "session.cloud.repoOnly": "Alleen deze repository", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index 82375c64f6b..6a7cdb80906 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -1123,6 +1123,11 @@ export const dict = { "session.showHistory": "Vis historikk", "session.search.placeholder": "Søk i sesjoner...", "session.empty": "Ingen sesjoner ennå. Klikk + for å starte en ny samtale.", + "session.tabs.switcher.open": "Vis åpne faner", + "session.tabs.switcher.search": "Søk i åpne faner...", + "session.tabs.switcher.current": "Gjeldende", + "session.tabs.switcher.pending": "Ny", + "session.tabs.switcher.busy": "Jobber", "session.tab.local": "Lokal", "session.tab.cloud": "Sky", "session.cloud.repoOnly": "Kun dette repositoriet", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 762e3499eca..9bc5cce9c79 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -1121,6 +1121,11 @@ export const dict = { "session.showHistory": "Pokaż historię", "session.search.placeholder": "Szukaj sesji...", "session.empty": "Brak sesji. Kliknij + aby rozpocząć nową rozmowę.", + "session.tabs.switcher.open": "Pokaż otwarte karty", + "session.tabs.switcher.search": "Szukaj otwartych kart...", + "session.tabs.switcher.current": "Bieżąca", + "session.tabs.switcher.pending": "Nowa", + "session.tabs.switcher.busy": "Pracuje", "session.tab.local": "Lokalny", "session.tab.cloud": "Chmura", "session.cloud.repoOnly": "Tylko to repozytorium", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index e8f25868876..c39ad113591 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -1167,6 +1167,11 @@ export const dict = { "session.showHistory": "Показать историю", "session.search.placeholder": "Поиск сессий...", "session.empty": "Сессий пока нет. Нажмите + чтобы начать новый разговор.", + "session.tabs.switcher.open": "Показать открытые вкладки", + "session.tabs.switcher.search": "Поиск открытых вкладок...", + "session.tabs.switcher.current": "Текущая", + "session.tabs.switcher.pending": "Новая", + "session.tabs.switcher.busy": "В работе", "session.tab.local": "Локальный", "session.tab.cloud": "Облако", "session.cloud.repoOnly": "Только этот репозиторий", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index 9b4db932452..660f6d6cff5 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -1151,6 +1151,11 @@ export const dict = { "session.showHistory": "แสดงประวัติ", "session.search.placeholder": "ค้นหาเซสชัน...", "session.empty": "ยังไม่มีเซสชัน คลิก + เพื่อเริ่มการสนทนาใหม่", + "session.tabs.switcher.open": "แสดงแท็บที่เปิดอยู่", + "session.tabs.switcher.search": "ค้นหาแท็บที่เปิดอยู่...", + "session.tabs.switcher.current": "ปัจจุบัน", + "session.tabs.switcher.pending": "ใหม่", + "session.tabs.switcher.busy": "กำลังทำงาน", "session.tab.local": "ในเครื่อง", "session.tab.cloud": "คลาวด์", "session.cloud.repoOnly": "เฉพาะรีโพซิทอรีนี้", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index 997798a9f05..87e6850d0a8 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -1113,6 +1113,11 @@ export const dict = { "session.showHistory": "Geçmişi Göster", "session.search.placeholder": "Oturum ara...", "session.empty": "Henüz oturum yok. Yeni bir sohbet başlatmak için + tıklayın.", + "session.tabs.switcher.open": "Açık sekmeleri göster", + "session.tabs.switcher.search": "Açık sekmelerde ara...", + "session.tabs.switcher.current": "Geçerli", + "session.tabs.switcher.pending": "Yeni", + "session.tabs.switcher.busy": "Çalışıyor", "session.tab.local": "Local", "session.tab.cloud": "Cloud", "session.cloud.repoOnly": "Yalnızca bu depo", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index c7edb20c28b..9fe8f6c066e 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -1114,6 +1114,11 @@ export const dict = { "session.showHistory": "Показати історію", "session.search.placeholder": "Пошук сесій...", "session.empty": "Сесій поки немає. Натисніть +, щоб почати новий чат.", + "session.tabs.switcher.open": "Показати відкриті вкладки", + "session.tabs.switcher.search": "Пошук відкритих вкладок...", + "session.tabs.switcher.current": "Поточна", + "session.tabs.switcher.pending": "Нова", + "session.tabs.switcher.busy": "Працює", "session.tab.local": "Локальний", "session.tab.cloud": "Хмарний", "session.cloud.repoOnly": "Лише цей репозиторій", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 3444a21d8d9..aef89a31fd4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -1127,6 +1127,11 @@ export const dict = { "session.showHistory": "显示历史", "session.search.placeholder": "搜索会话...", "session.empty": "暂无会话。点击 + 开始新的对话。", + "session.tabs.switcher.open": "显示打开的标签页", + "session.tabs.switcher.search": "搜索打开的标签页...", + "session.tabs.switcher.current": "当前", + "session.tabs.switcher.pending": "新建", + "session.tabs.switcher.busy": "工作中", "session.tab.local": "本地", "session.tab.cloud": "云端", "session.cloud.repoOnly": "仅此仓库", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index dfa2dd85f31..5e5599ce9fa 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -1088,6 +1088,11 @@ export const dict = { "session.showHistory": "顯示歷史", "session.search.placeholder": "搜尋工作階段...", "session.empty": "尚無工作階段。點選 + 以開始新的對話。", + "session.tabs.switcher.open": "顯示開啟的分頁", + "session.tabs.switcher.search": "搜尋開啟的分頁...", + "session.tabs.switcher.current": "目前", + "session.tabs.switcher.pending": "新增", + "session.tabs.switcher.busy": "工作中", "session.tab.local": "本機", "session.tab.cloud": "雲端", "session.cloud.repoOnly": "僅此儲存庫", diff --git a/packages/kilo-vscode/webview-ui/src/stories/session-tabs.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/session-tabs.stories.tsx new file mode 100644 index 00000000000..26337440548 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/stories/session-tabs.stories.tsx @@ -0,0 +1,60 @@ +/** @jsxImportSource solid-js */ + +import type { Meta, StoryObj } from "storybook-solidjs-vite" +import { SessionTabSwitcher } from "../components/chat/SessionTabSwitcher" +import { StoryProviders } from "./StoryProviders" + +const rows = [ + { id: "refactor", title: "Refactor shared search menu styles", active: false, busy: false, pending: false }, + { id: "current", title: "Run the extension test suite", active: true, busy: true, pending: false }, + { id: "pending", title: "Untitled session", active: false, busy: false, pending: true }, + { id: "idle", title: "Review keyboard navigation behavior", active: false, busy: false, pending: false }, +] + +const noop = () => {} +const focus = () => document.querySelector('[data-slot="session-prompt-focus-target"]')?.focus() +const meta: Meta = { + title: "Session Tabs", + parameters: { layout: "fullscreen" }, +} + +export default meta +type Story = StoryObj + +export const SwitcherOpen: Story = { + name: "Session tab switcher — open", + render: () => ( + +
+