mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
23039c0fb1
* feat(agent-manager): support multiple side-panel terminals Add a terminal tab strip to the side panel header so a context can own several side terminals: click to switch, drag to reorder, X to close a single terminal, + to open another one. The strip reuses the top tab bar's TerminalTabChrome and solid-dnd stack (with a width-pinned DragOverlay so drags track the cursor without offset). Terminal numbers fill gaps left by closed terminals, and tabs pick up live titles from OSC escape codes so the shell or running program names its own tab. Closes #12597 * fix(agent-manager): address side terminal strip review findings Split the strip into a scrollable tab list and a fixed add-button area (mirroring .am-tab-list-wrap / .am-tab-add-wrap) so the + action never scrolls away, and scope role=tablist to the tab list so axe aria-required-children passes on the empty strip. Validate closeSide targets a live side terminal before mutating state, so a stray non-side id can no longer drop a record while leaking its backend PTY. Give SortableTabContainer a class prop and reuse it for side tabs instead of a duplicated sortable wrapper.
138 lines
4.8 KiB
TypeScript
138 lines
4.8 KiB
TypeScript
import { test, expect, type Page } from "@playwright/test"
|
|
import { platform } from "node:os"
|
|
|
|
const IS_DARWIN = platform() === "darwin"
|
|
|
|
// Screenshot baselines are captured on Linux CI — font rendering and anti-aliasing
|
|
// differ on macOS, which causes false-positive diffs. Skip the entire suite there.
|
|
if (IS_DARWIN) {
|
|
console.warn("Visual regression tests must be run on CI, skipping on local macOS.")
|
|
test.skip()
|
|
}
|
|
|
|
type Story = {
|
|
id: string
|
|
title: string
|
|
name: string
|
|
}
|
|
|
|
type StoriesIndex = {
|
|
stories?: Record<string, Story>
|
|
entries?: Record<string, Story>
|
|
}
|
|
|
|
const STORYBOOK_URL = "http://localhost:6007"
|
|
|
|
// Fetched once per worker process — cheap HTTP call to the already-running Storybook
|
|
async function fetchStories(): Promise<Story[]> {
|
|
const res = await fetch(`${STORYBOOK_URL}/index.json`).catch(() => fetch(`${STORYBOOK_URL}/stories.json`))
|
|
if (!res.ok) throw new Error(`Storybook index fetch failed: ${res.status} ${res.statusText}`)
|
|
const data = (await res.json()) as StoriesIndex
|
|
const map = data.entries ?? data.stories ?? {}
|
|
return Object.values(map).filter((s) => s.id && !s.id.endsWith("--docs"))
|
|
}
|
|
|
|
async function disableAnimations(page: Page) {
|
|
await page.addStyleTag({
|
|
content: `
|
|
*, *::before, *::after {
|
|
animation: none !important;
|
|
animation-duration: 0s !important;
|
|
animation-delay: 0s !important;
|
|
transition: none !important;
|
|
transition-duration: 0s !important;
|
|
transition-delay: 0s !important;
|
|
}
|
|
`,
|
|
})
|
|
}
|
|
|
|
async function settle(page: Page) {
|
|
const frames = () =>
|
|
page.evaluate(
|
|
() =>
|
|
new Promise<void>((resolve) => {
|
|
requestAnimationFrame(() => requestAnimationFrame(() => resolve()))
|
|
}),
|
|
)
|
|
|
|
await page.evaluate(async () => {
|
|
await document.fonts.ready
|
|
})
|
|
await frames()
|
|
await page.waitForFunction(
|
|
() => {
|
|
const root = document.querySelector("#storybook-root")
|
|
return root && !root.querySelector('pre > code[data-lang]:not([data-lang="mermaid"])')
|
|
},
|
|
undefined,
|
|
{ timeout: 5_000 },
|
|
)
|
|
await frames()
|
|
}
|
|
|
|
// Stories to skip from visual regression (add IDs here if needed)
|
|
// Spinner animation captures at an indeterminate frame, causing flaky diffs.
|
|
// Permission dock config-preloaded has non-deterministic toggle rendering.
|
|
// Sandboxing rows can settle at different scroll heights after settings context updates.
|
|
// Side terminal tabs mount live xterm instances whose websocket error text
|
|
// lands at indeterminate times.
|
|
const SKIP = new Set<string>([
|
|
"agentmanager--worktree-item-busy",
|
|
"agentmanager--full-screen-diff-agent-edit-scroll",
|
|
"agentmanager--side-terminal-panel-tabs",
|
|
"composite-webview--permission-dock-config-preloaded",
|
|
"settings--sandboxing-allowlist",
|
|
"settings--sandboxing-panel",
|
|
])
|
|
|
|
const DOCS = new Map<string, string[]>([
|
|
[
|
|
"chat--task-header-with-todos",
|
|
[
|
|
"packages/kilo-docs/pages/code-with-ai/features/task-todo-list.md:/docs/img/screenshot-tests/kilo-vscode/visual-regression/chat/task-header-with-todos-chromium-linux.png",
|
|
],
|
|
],
|
|
[
|
|
"composite-webview--todo-write-docs-overview",
|
|
[
|
|
"packages/kilo-docs/pages/code-with-ai/features/task-todo-list.md:/docs/img/screenshot-tests/kilo-vscode/visual-regression/composite-webview/todo-write-docs-overview-chromium-linux.png",
|
|
],
|
|
],
|
|
[
|
|
"settings--agent-behaviour-workflows",
|
|
[
|
|
"packages/kilo-docs/pages/customize/workflows.md:/docs/img/screenshot-tests/kilo-vscode/visual-regression/settings/agent-behaviour-workflows-chromium-linux.png",
|
|
],
|
|
],
|
|
])
|
|
|
|
// Generate one test() per story so Playwright's scheduler can distribute
|
|
// them freely across workers — no manual sharding needed.
|
|
// Skip fetching stories on macOS since test.skip() above already marks the file skipped.
|
|
const stories = IS_DARWIN ? [] : (await fetchStories()).filter((s) => !SKIP.has(s.id))
|
|
|
|
for (const story of stories) {
|
|
test(`${story.title} / ${story.name}`, async ({ page }) => {
|
|
for (const ref of DOCS.get(story.id) ?? []) {
|
|
test.info().annotations.push({ type: "docs", description: ref })
|
|
}
|
|
|
|
// Width-suffixed stories cover layouts outside the default sidebar viewport.
|
|
const width = story.id.endsWith("-200") ? 200 : story.id.endsWith("-1280") ? 1280 : 420
|
|
await page.setViewportSize({ width, height: 720 })
|
|
|
|
await page.goto(
|
|
`/iframe.html?id=${story.id}&viewMode=story&globals=colorScheme:dark;theme:kilo-vscode;vscodeTheme:dark-modern`,
|
|
{ waitUntil: "load" },
|
|
)
|
|
await disableAnimations(page)
|
|
await page.waitForSelector("#storybook-root *", { state: "attached" })
|
|
await settle(page)
|
|
|
|
const [component, variant] = story.id.split("--")
|
|
const root = page.locator("#storybook-root")
|
|
await expect(root).toHaveScreenshot(["visual-regression", component!, `${variant!}-chromium-linux.png`])
|
|
})
|
|
}
|