mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-10-24 01:24:18 +08:00
Merge pull request #6546 from Kilo-Org/mark/csp-connect-src-fix
fix(vscode): CSP, inline permissions, and composite visual regression tests
This commit is contained in:
@@ -4,3 +4,4 @@
|
||||
|
||||
# Visual regression baseline snapshots
|
||||
packages/kilo-ui/tests/**/*.png filter=lfs diff=lfs merge=lfs -text
|
||||
packages/kilo-vscode/tests/**/*.png filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
@@ -7,6 +7,9 @@ on:
|
||||
- "packages/ui/**"
|
||||
- "packages/util/**"
|
||||
- "packages/sdk/js/**"
|
||||
- "packages/kilo-vscode/webview-ui/**"
|
||||
- "packages/kilo-vscode/.storybook/**"
|
||||
- "packages/kilo-vscode/tests/visual-regression*"
|
||||
- ".github/workflows/visual-regression.yml"
|
||||
|
||||
jobs:
|
||||
@@ -101,3 +104,95 @@ jobs:
|
||||
name: visual-regression-results
|
||||
path: packages/kilo-ui/test-results/
|
||||
retention-days: 7
|
||||
|
||||
visual-regression-vscode:
|
||||
name: Visual Regression (kilo-vscode webview)
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
ref: ${{ github.head_ref }}
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Cache Bun modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.bun/install/cache
|
||||
key: bun-${{ hashFiles('bun.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Cache Playwright browsers
|
||||
id: playwright-cache-vscode
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-vscode-${{ hashFiles('packages/kilo-vscode/package.json') }}
|
||||
|
||||
- name: Install Playwright browsers
|
||||
if: steps.playwright-cache-vscode.outputs.cache-hit != 'true'
|
||||
run: bunx playwright install chromium
|
||||
working-directory: packages/kilo-vscode
|
||||
|
||||
- name: Install Playwright system deps
|
||||
run: bunx playwright install-deps chromium
|
||||
working-directory: packages/kilo-vscode
|
||||
|
||||
- name: Cache Storybook build
|
||||
id: storybook-cache-vscode
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: packages/kilo-vscode/storybook-static
|
||||
key: storybook-vscode-${{ hashFiles('packages/kilo-vscode/webview-ui/src/**', 'packages/kilo-vscode/.storybook/**', 'packages/kilo-ui/src/**', 'packages/ui/src/**', 'packages/kilo-vscode/package.json') }}
|
||||
|
||||
- name: Build Storybook
|
||||
if: steps.storybook-cache-vscode.outputs.cache-hit != 'true'
|
||||
run: bun run build-storybook
|
||||
working-directory: packages/kilo-vscode
|
||||
|
||||
- name: Generate baselines for new/missing stories
|
||||
run: bun run test:visual:update
|
||||
working-directory: packages/kilo-vscode
|
||||
env:
|
||||
CI: true
|
||||
PLAYWRIGHT_WORKERS: "4"
|
||||
|
||||
- name: Commit and push new baselines (if any)
|
||||
id: commit-baselines-vscode
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/
|
||||
if git diff --cached --quiet; then
|
||||
echo "No new baselines — nothing to commit."
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
git commit -m "chore: update kilo-vscode visual regression baselines"
|
||||
git lfs push --all origin
|
||||
git push
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Fail if baselines changed
|
||||
if: steps.commit-baselines-vscode.outputs.changed == 'true'
|
||||
run: |
|
||||
echo "::error::Visual regression baselines changed. New baselines have been committed to the branch. Please pull and review."
|
||||
exit 1
|
||||
|
||||
- name: Upload test results on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: visual-regression-vscode-results
|
||||
path: packages/kilo-vscode/test-results/
|
||||
retention-days: 7
|
||||
|
||||
@@ -249,6 +249,8 @@
|
||||
"zod": "^3.24.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.58.2",
|
||||
"@storybook/addon-docs": "10.2.10",
|
||||
"@types/diff": "^6.0.0",
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "22.x",
|
||||
@@ -263,9 +265,13 @@
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"prettier": "^3.8.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"storybook": "10.2.10",
|
||||
"storybook-solidjs-vite": "10.0.9",
|
||||
"ts-morph": "27.0.2",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.54.0",
|
||||
"vite": "7.3.1",
|
||||
"vite-plugin-solid": "2.11.10",
|
||||
},
|
||||
},
|
||||
"packages/opencode": {
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6c67c7cc21cd458c632228bdcee310615fae82c6d4c76e4d1170fa00224a9ac3
|
||||
size 29215
|
||||
oid sha256:1de30d219cd299acf26c9fe473c00f18cf057297db2e9b1ae44fb3116b0392f0
|
||||
size 31305
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6c67c7cc21cd458c632228bdcee310615fae82c6d4c76e4d1170fa00224a9ac3
|
||||
size 29215
|
||||
oid sha256:1de30d219cd299acf26c9fe473c00f18cf057297db2e9b1ae44fb3116b0392f0
|
||||
size 31305
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
bin/
|
||||
out/
|
||||
.vscode-test/
|
||||
.vscode-test/
|
||||
storybook-static/
|
||||
test-results/
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { StorybookConfig } from "storybook-solidjs-vite"
|
||||
import { mergeConfig } from "vite"
|
||||
import solidPlugin from "vite-plugin-solid"
|
||||
|
||||
const config: StorybookConfig = {
|
||||
framework: "storybook-solidjs-vite",
|
||||
stories: ["../webview-ui/src/stories/**/*.stories.@(ts|tsx)"],
|
||||
addons: ["@storybook/addon-docs"],
|
||||
refs: {},
|
||||
viteFinal: async (config) => {
|
||||
return mergeConfig(config, {
|
||||
plugins: [solidPlugin()],
|
||||
resolve: {
|
||||
conditions: ["browser", "solid", "module", "import"],
|
||||
},
|
||||
esbuild: {
|
||||
jsxImportSource: "solid-js",
|
||||
jsx: "automatic",
|
||||
},
|
||||
worker: {
|
||||
format: "es",
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
@@ -0,0 +1,84 @@
|
||||
/** @jsxImportSource solid-js */
|
||||
import type { Preview, SolidRenderer } from "storybook-solidjs-vite"
|
||||
import type { DecoratorFunction } from "storybook/internal/types"
|
||||
// Reference kilo-ui stories helpers directly — not exported via package.json
|
||||
import { applyKiloTheme, applyVscodeTheme, clearVscodeTheme } from "../../kilo-ui/src/stories/theme-decorator"
|
||||
import "../../kilo-ui/.storybook/fonts.css"
|
||||
import "@kilocode/kilo-ui/styles"
|
||||
import "../webview-ui/src/styles/chat.css"
|
||||
|
||||
const themeDecorator: DecoratorFunction<SolidRenderer> = (Story, context) => {
|
||||
const themeId = (context.globals["theme"] as string) ?? "kilo-vscode"
|
||||
const vscodeThemeId = (context.globals["vscodeTheme"] as string) ?? "dark-modern"
|
||||
|
||||
const colorScheme = (() => {
|
||||
if (themeId === "kilo-vscode") return applyVscodeTheme(vscodeThemeId)
|
||||
clearVscodeTheme()
|
||||
return (context.globals["colorScheme"] as "light" | "dark") ?? "dark"
|
||||
})()
|
||||
|
||||
applyKiloTheme(themeId, colorScheme)
|
||||
document.body.style.background = "var(--background-base)"
|
||||
document.body.style.color = "var(--text-base)"
|
||||
return Story()
|
||||
}
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
layout: "fullscreen",
|
||||
},
|
||||
decorators: [themeDecorator],
|
||||
globalTypes: {
|
||||
theme: {
|
||||
description: "Theme",
|
||||
toolbar: {
|
||||
title: "Theme",
|
||||
icon: "paintbrush",
|
||||
items: [
|
||||
{ value: "kilo-vscode", title: "Kilo VSCode" },
|
||||
{ value: "kilo", title: "Kilo" },
|
||||
],
|
||||
dynamicTitle: true,
|
||||
},
|
||||
},
|
||||
colorScheme: {
|
||||
description: "Color Scheme",
|
||||
toolbar: {
|
||||
title: "Color Scheme",
|
||||
icon: "circlehollow",
|
||||
items: [
|
||||
{ value: "dark", title: "Dark", icon: "moon" },
|
||||
{ value: "light", title: "Light", icon: "sun" },
|
||||
],
|
||||
dynamicTitle: true,
|
||||
},
|
||||
},
|
||||
vscodeTheme: {
|
||||
description: "VSCode Theme",
|
||||
toolbar: {
|
||||
title: "VSCode Theme",
|
||||
icon: "browser",
|
||||
items: [
|
||||
{ value: "dark-modern", title: "Dark Modern (default)" },
|
||||
{ value: "dark-plus", title: "Dark+" },
|
||||
{ value: "light-modern", title: "Light Modern" },
|
||||
{ value: "hc-black", title: "High Contrast" },
|
||||
],
|
||||
dynamicTitle: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
initialGlobals: {
|
||||
theme: "kilo-vscode",
|
||||
colorScheme: "dark",
|
||||
vscodeTheme: "dark-modern",
|
||||
},
|
||||
}
|
||||
|
||||
export default preview
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../webview-ui/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js"
|
||||
},
|
||||
"include": ["./**/*", "../webview-ui/src/**/*"]
|
||||
}
|
||||
@@ -672,9 +672,15 @@
|
||||
"lint": "eslint src",
|
||||
"test": "vscode-test",
|
||||
"test:unit": "bun test tests/unit/",
|
||||
"rebuild-sdk": "bun run --cwd ../sdk/js build"
|
||||
"rebuild-sdk": "bun run --cwd ../sdk/js build",
|
||||
"storybook": "storybook dev -p 6007",
|
||||
"build-storybook": "storybook build -o storybook-static",
|
||||
"test:visual": "playwright test",
|
||||
"test:visual:update": "playwright test --update-snapshots"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.58.2",
|
||||
"@storybook/addon-docs": "10.2.10",
|
||||
"@types/diff": "^6.0.0",
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "22.x",
|
||||
@@ -688,9 +694,13 @@
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"prettier": "^3.8.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"storybook": "10.2.10",
|
||||
"storybook-solidjs-vite": "10.0.9",
|
||||
"ts-morph": "27.0.2",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.54.0",
|
||||
"vite": "7.3.1",
|
||||
"vite-plugin-solid": "2.11.10",
|
||||
"@vscode/vsce": "^3.7.1"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { defineConfig, devices } from "@playwright/test"
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
testMatch: "**/*.spec.ts",
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env["CI"],
|
||||
retries: process.env["CI"] ? 1 : 0,
|
||||
workers: process.env["PLAYWRIGHT_WORKERS"]
|
||||
? Number.parseInt(process.env["PLAYWRIGHT_WORKERS"]!, 10) || undefined
|
||||
: undefined,
|
||||
reporter: [["html", { open: "never" }], ["list"]],
|
||||
use: {
|
||||
baseURL: "http://localhost:6007",
|
||||
// VS Code sidebar is typically 350-450px wide
|
||||
viewport: { width: 420, height: 720 },
|
||||
reducedMotion: "reduce",
|
||||
screenshot: "only-on-failure",
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"], viewport: { width: 420, height: 720 } },
|
||||
},
|
||||
],
|
||||
webServer: {
|
||||
command:
|
||||
"bunx storybook build -o ./storybook-static && bunx http-server ./storybook-static -p 6007 --silent",
|
||||
url: "http://localhost:6007",
|
||||
reuseExistingServer: !process.env["CI"],
|
||||
timeout: 300_000,
|
||||
},
|
||||
timeout: 60_000,
|
||||
expect: {
|
||||
timeout: 10_000,
|
||||
toHaveScreenshot: {
|
||||
maxDiffPixelRatio: 0.01,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -27,7 +27,7 @@ export function buildCspString(cspSource: string, nonce: string, port?: number):
|
||||
`style-src 'unsafe-inline' ${cspSource}`,
|
||||
`script-src 'nonce-${nonce}' 'wasm-unsafe-eval'`,
|
||||
`font-src ${cspSource}`,
|
||||
`connect-src ${connectSrc}`,
|
||||
`connect-src ${cspSource} ${connectSrc}`,
|
||||
`img-src ${cspSource} data: https:`,
|
||||
]
|
||||
return joinCspDirectives(directives)
|
||||
|
||||
@@ -67,6 +67,11 @@ describe("buildCspString", () => {
|
||||
expect(result).not.toContain(":*")
|
||||
})
|
||||
|
||||
it("includes cspSource in connect-src for source map loading", () => {
|
||||
const result = buildCspString(cspSource, nonce)
|
||||
expect(result).toMatch(new RegExp(`connect-src\\s+${cspSource.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`))
|
||||
})
|
||||
|
||||
it("joins directives with semicolons", () => {
|
||||
const result = buildCspString(cspSource, nonce)
|
||||
const parts = result.split(";")
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
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-duration: 0s !important;
|
||||
animation-delay: 0s !important;
|
||||
transition-duration: 0s !important;
|
||||
transition-delay: 0s !important;
|
||||
}
|
||||
`,
|
||||
})
|
||||
}
|
||||
|
||||
// Stories to skip from visual regression (add IDs here if needed)
|
||||
const SKIP = new Set<string>([])
|
||||
|
||||
// Use test.describe + beforeAll to avoid top-level await which breaks
|
||||
// on Node.js versions with strict ESM/CJS boundaries.
|
||||
test.describe("Visual Regression", () => {
|
||||
let stories: Story[] = []
|
||||
|
||||
test.beforeAll(async () => {
|
||||
if (IS_DARWIN) return
|
||||
stories = (await fetchStories()).filter((s) => !SKIP.has(s.id))
|
||||
})
|
||||
|
||||
test("all stories", async ({ page }) => {
|
||||
if (IS_DARWIN) return
|
||||
for (const story of stories) {
|
||||
// Use kilo-vscode theme by default (matched to the preview initialGlobals)
|
||||
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" })
|
||||
|
||||
const [component, variant] = story.id.split("--")
|
||||
const root = page.locator("#storybook-root")
|
||||
await expect(root).toHaveScreenshot([component!, `${variant}.png`])
|
||||
}
|
||||
})
|
||||
})
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:04332222bf7bf3680954f967414f93e9e3d03747961fada79cd32d4a5e2a1431
|
||||
size 10279
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ee4898a628110edf572fa27433bbb905d260225be0dd2154ccdfd4e4a2ea4c1e
|
||||
size 4415
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4b6d383088e69f94aeed3e9291092d1278f8346b2d50889bd83ecc72db5d13b7
|
||||
size 2846
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b1c5266bca40764f48d0cdefa3398083708ed7deb354acf8a5a296f9f9e0b2a6
|
||||
size 9005
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c13ea96b21def4f7359426d9d8900bab511938d4f71bc6635daaba44a2bbd133
|
||||
size 25078
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cd8a53356f0aa0df5c07246296dc20273f5380a012cd579885b1974e3aeb99e1
|
||||
size 8496
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fa7bddaa15cf80565ef5b639bfe2de8d2f373019da4b72210c8ea82aaf47913f
|
||||
size 10384
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:64ff66248b1c07209757eb6e27ccac4967650b144317ee21076ba82c129bc17b
|
||||
size 8846
|
||||
@@ -3,12 +3,19 @@
|
||||
* Renders all parts of an assistant message as a flat list — no context grouping.
|
||||
* Unlike the upstream AssistantParts, this renders each read/glob/grep/list tool
|
||||
* individually for maximum verbosity in the VS Code sidebar context.
|
||||
*
|
||||
* Permissions and questions with a tool context are rendered inline with their
|
||||
* tool call rather than in the bottom dock.
|
||||
*/
|
||||
|
||||
import { Component, For, Show, createMemo } from "solid-js"
|
||||
import { Component, For, Show, createMemo, createSignal } from "solid-js"
|
||||
import { Part, PART_MAPPING } from "@kilocode/kilo-ui/message-part"
|
||||
import { Button } from "@kilocode/kilo-ui/button"
|
||||
import type { AssistantMessage as SDKAssistantMessage, Part as SDKPart, Message as SDKMessage } from "@kilocode/sdk/v2"
|
||||
import { useData } from "@kilocode/kilo-ui/context/data"
|
||||
import { useSession } from "../../context/session"
|
||||
import { useLanguage } from "../../context/language"
|
||||
import { QuestionDock } from "./QuestionDock"
|
||||
|
||||
const HIDDEN_TOOLS = new Set(["todowrite", "todoread"])
|
||||
|
||||
@@ -33,6 +40,8 @@ interface AssistantMessageProps {
|
||||
|
||||
export const AssistantMessage: Component<AssistantMessageProps> = (props) => {
|
||||
const data = useData()
|
||||
const session = useSession()
|
||||
const language = useLanguage()
|
||||
|
||||
const parts = createMemo(() => {
|
||||
const stored = data.store.part?.[props.message.id]
|
||||
@@ -40,18 +49,76 @@ export const AssistantMessage: Component<AssistantMessageProps> = (props) => {
|
||||
return (stored as SDKPart[]).filter(isRenderable)
|
||||
})
|
||||
|
||||
const id = () => session.currentSessionID()
|
||||
const permissions = () => session.permissions().filter((p) => p.sessionID === id() && p.tool)
|
||||
const questions = () => session.questions().filter((q) => q.sessionID === id() && q.tool)
|
||||
|
||||
const permissionForPart = (part: SDKPart) => {
|
||||
if (part.type !== "tool") return undefined
|
||||
const callID = (part as SDKPart & { callID: string }).callID
|
||||
return permissions().find((p) => p.tool!.callID === callID && p.tool!.messageID === props.message.id)
|
||||
}
|
||||
|
||||
// Questions linked to this message (rendered after the last part)
|
||||
const questionForMessage = () =>
|
||||
questions().find((q) => q.tool!.messageID === props.message.id)
|
||||
|
||||
const [responding, setResponding] = createSignal(false)
|
||||
|
||||
const decide = (permissionId: string, response: "once" | "always" | "reject") => {
|
||||
if (responding()) return
|
||||
setResponding(true)
|
||||
session.respondToPermission(permissionId, response)
|
||||
setResponding(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<For each={parts()}>
|
||||
{(part) => (
|
||||
<Show when={PART_MAPPING[part.type]}>
|
||||
<Part
|
||||
part={part}
|
||||
message={props.message as SDKMessage}
|
||||
showAssistantCopyPartID={props.showAssistantCopyPartID}
|
||||
turnDurationMs={props.turnDurationMs}
|
||||
/>
|
||||
</Show>
|
||||
)}
|
||||
</For>
|
||||
<>
|
||||
<For each={parts()}>
|
||||
{(part) => {
|
||||
const perm = () => permissionForPart(part)
|
||||
return (
|
||||
<Show when={PART_MAPPING[part.type]}>
|
||||
<div data-component="tool-part-wrapper" data-permission={!!perm()}>
|
||||
<Part
|
||||
part={part}
|
||||
message={props.message as SDKMessage}
|
||||
showAssistantCopyPartID={props.showAssistantCopyPartID}
|
||||
turnDurationMs={props.turnDurationMs}
|
||||
/>
|
||||
<Show when={perm()} keyed>
|
||||
{(p) => (
|
||||
<div data-component="permission-prompt">
|
||||
<Show when={p.patterns.length > 0}>
|
||||
<div class="permission-dock-patterns">
|
||||
<For each={p.patterns}>
|
||||
{(pattern) => <code class="permission-dock-pattern">{pattern}</code>}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
<div data-slot="permission-actions">
|
||||
<Button variant="ghost" size="small" onClick={() => decide(p.id, "reject")} disabled={responding()}>
|
||||
{language.t("ui.permission.deny")}
|
||||
</Button>
|
||||
<Button variant="secondary" size="small" onClick={() => decide(p.id, "always")} disabled={responding()}>
|
||||
{language.t("ui.permission.allowAlways")}
|
||||
</Button>
|
||||
<Button variant="primary" size="small" onClick={() => decide(p.id, "once")} disabled={responding()}>
|
||||
{language.t("ui.permission.allowOnce")}
|
||||
</Button>
|
||||
</div>
|
||||
<p data-slot="permission-hint">{language.t("ui.permission.sessionHint")}</p>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
<Show when={questionForMessage()} keyed>
|
||||
{(req) => <QuestionDock request={req} />}
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export const ChatView: Component<ChatViewProps> = (props) => {
|
||||
const sessionQuestions = () => session.questions().filter((q) => q.sessionID === id())
|
||||
const sessionPermissions = () => session.permissions().filter((p) => p.sessionID === id())
|
||||
|
||||
const questionRequest = () => sessionQuestions()[0]
|
||||
const questionRequest = () => sessionQuestions().find((q) => !q.tool)
|
||||
const permissionRequest = () => sessionPermissions().find((p) => !p.tool)
|
||||
const blocked = () => sessionPermissions().length > 0 || sessionQuestions().length > 0
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ interface LanguageContextValue {
|
||||
t: (key: string, params?: UiI18nParams) => string
|
||||
}
|
||||
|
||||
const LanguageContext = createContext<LanguageContextValue>()
|
||||
export const LanguageContext = createContext<LanguageContextValue>()
|
||||
|
||||
export function useLanguage() {
|
||||
const ctx = useContext(LanguageContext)
|
||||
|
||||
@@ -138,7 +138,7 @@ interface SessionContextValue {
|
||||
selectCloudSession: (cloudSessionId: string) => void
|
||||
}
|
||||
|
||||
const SessionContext = createContext<SessionContextValue>()
|
||||
export const SessionContext = createContext<SessionContextValue>()
|
||||
|
||||
export const SessionProvider: ParentComponent = (props) => {
|
||||
const vscode = useVSCode()
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
/** @jsxImportSource solid-js */
|
||||
/**
|
||||
* StoryProviders — wraps composite stories with all required contexts.
|
||||
*
|
||||
* Instead of instantiating the full VSCodeProvider → ServerProvider → SessionProvider
|
||||
* chain (which requires a real extension host / SSE connection), we provide mock
|
||||
* context values directly.
|
||||
*/
|
||||
|
||||
import { createSignal, type ParentComponent } from "solid-js"
|
||||
import { DataProvider } from "@kilocode/kilo-ui/context/data"
|
||||
import { DiffComponentProvider } from "@kilocode/kilo-ui/context/diff"
|
||||
import { CodeComponentProvider } from "@kilocode/kilo-ui/context/code"
|
||||
import { DialogProvider } from "@kilocode/kilo-ui/context/dialog"
|
||||
import { MarkedProvider } from "@kilocode/kilo-ui/context/marked"
|
||||
import { I18nProvider } from "@kilocode/kilo-ui/context"
|
||||
import { Diff } from "@kilocode/kilo-ui/diff"
|
||||
import { Code } from "@kilocode/kilo-ui/code"
|
||||
import { SessionContext } from "../context/session"
|
||||
import { LanguageContext } from "../context/language"
|
||||
import { dict as uiEn } from "@kilocode/kilo-ui/i18n/en"
|
||||
import { dict as appEn } from "../i18n/en"
|
||||
import { dict as kiloEn } from "@kilocode/kilo-i18n/en"
|
||||
import type { PermissionRequest, QuestionRequest } from "../types/messages"
|
||||
|
||||
// Merged English dictionary (same merge order as the real LanguageProvider)
|
||||
const dict: Record<string, string> = { ...appEn, ...uiEn, ...kiloEn }
|
||||
|
||||
function t(key: string) {
|
||||
return dict[key] ?? key
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Default mock data (empty session)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const defaultMockData = {
|
||||
session: [],
|
||||
session_status: {},
|
||||
session_diff: {},
|
||||
message: {} as Record<string, any[]>,
|
||||
part: {} as Record<string, any[]>,
|
||||
permission: {} as Record<string, any[]>,
|
||||
question: {},
|
||||
provider: { all: [], connected: false, default: {} },
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock SessionContext value — only the subset used by components
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function noop() {}
|
||||
|
||||
export function mockSessionValue(overrides?: {
|
||||
id?: string
|
||||
permissions?: PermissionRequest[]
|
||||
questions?: QuestionRequest[]
|
||||
status?: string
|
||||
}) {
|
||||
const id = overrides?.id ?? "story-session-001"
|
||||
const permissions = overrides?.permissions ?? []
|
||||
const qs = overrides?.questions ?? []
|
||||
const status = (overrides?.status ?? "idle") as "idle" | "busy"
|
||||
|
||||
return {
|
||||
currentSessionID: () => id,
|
||||
currentSession: () => ({ id, title: "Story session", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }),
|
||||
setCurrentSessionID: noop,
|
||||
sessions: () => [],
|
||||
status: () => status,
|
||||
statusInfo: () => ({ type: status }),
|
||||
statusText: () => (status === "idle" ? undefined : "Thinking…"),
|
||||
busySince: () => (status === "busy" ? Date.now() - 2000 : undefined),
|
||||
loading: () => false,
|
||||
messages: () => [],
|
||||
userMessages: () => [],
|
||||
allMessages: () => ({}),
|
||||
allParts: () => ({}),
|
||||
allStatusMap: () => ({}),
|
||||
getParts: () => [],
|
||||
todos: () => [],
|
||||
permissions: () => permissions,
|
||||
questions: () => qs,
|
||||
questionErrors: () => new Set<string>(),
|
||||
selected: () => ({ providerID: "anthropic", modelID: "claude-sonnet-4-20250514" }),
|
||||
selectModel: noop,
|
||||
totalCost: () => 0,
|
||||
contextUsage: () => undefined,
|
||||
agents: () => [{ name: "code", description: "Code mode", mode: "primary" as const }],
|
||||
selectedAgent: () => "code",
|
||||
selectAgent: noop,
|
||||
getSessionAgent: () => "code",
|
||||
getSessionModel: () => ({ providerID: "anthropic", modelID: "claude-sonnet-4-20250514" }),
|
||||
setSessionModel: noop,
|
||||
setSessionAgent: noop,
|
||||
variantList: () => [],
|
||||
currentVariant: () => undefined,
|
||||
selectVariant: noop,
|
||||
sendMessage: noop,
|
||||
abort: noop,
|
||||
compact: noop,
|
||||
respondToPermission: noop,
|
||||
replyToQuestion: noop,
|
||||
rejectQuestion: noop,
|
||||
createSession: noop,
|
||||
clearCurrentSession: noop,
|
||||
loadSessions: noop,
|
||||
selectSession: noop,
|
||||
deleteSession: noop,
|
||||
renameSession: noop,
|
||||
syncSession: noop,
|
||||
cloudPreviewId: () => null,
|
||||
selectCloudSession: noop,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// StoryProviders component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface StoryProvidersProps {
|
||||
data?: any
|
||||
permissions?: PermissionRequest[]
|
||||
questions?: QuestionRequest[]
|
||||
status?: string
|
||||
sessionID?: string
|
||||
}
|
||||
|
||||
export const StoryProviders: ParentComponent<StoryProvidersProps> = (props) => {
|
||||
const data = () => props.data ?? defaultMockData
|
||||
const session = mockSessionValue({
|
||||
id: props.sessionID,
|
||||
permissions: props.permissions,
|
||||
questions: props.questions,
|
||||
status: props.status,
|
||||
})
|
||||
const [locale] = createSignal<"en">("en")
|
||||
|
||||
return (
|
||||
<DialogProvider>
|
||||
<LanguageContext.Provider
|
||||
value={{
|
||||
locale,
|
||||
setLocale: noop,
|
||||
userOverride: () => "" as any,
|
||||
t,
|
||||
}}
|
||||
>
|
||||
<I18nProvider value={{ locale: () => "en", t }}>
|
||||
<SessionContext.Provider value={session as any}>
|
||||
<DataProvider data={data()} directory="/project/">
|
||||
<DiffComponentProvider component={Diff}>
|
||||
<CodeComponentProvider component={Code}>
|
||||
<MarkedProvider>
|
||||
<div style={{ padding: "12px" }}>{props.children}</div>
|
||||
</MarkedProvider>
|
||||
</CodeComponentProvider>
|
||||
</DiffComponentProvider>
|
||||
</DataProvider>
|
||||
</SessionContext.Provider>
|
||||
</I18nProvider>
|
||||
</LanguageContext.Provider>
|
||||
</DialogProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,463 @@
|
||||
/** @jsxImportSource solid-js */
|
||||
/**
|
||||
* Composite visual regression stories for the kilo-vscode webview.
|
||||
*
|
||||
* These test the *composed* UI — how kilo-ui components look together
|
||||
* in the extension webview context with extension-specific styling,
|
||||
* inline permission prompts, and tool card overrides.
|
||||
*/
|
||||
|
||||
import type { Meta, StoryObj } from "storybook-solidjs-vite"
|
||||
import { For, Show } from "solid-js"
|
||||
import { Part } from "@kilocode/kilo-ui/message-part"
|
||||
import { BasicTool } from "@kilocode/kilo-ui/basic-tool"
|
||||
import { Button } from "@kilocode/kilo-ui/button"
|
||||
import type {
|
||||
AssistantMessage as SDKAssistantMessage,
|
||||
TextPart,
|
||||
ToolPart,
|
||||
Message as SDKMessage,
|
||||
} from "@kilocode/sdk/v2"
|
||||
import { StoryProviders, defaultMockData, mockSessionValue } from "./StoryProviders"
|
||||
import { AssistantMessage } from "../components/chat/AssistantMessage"
|
||||
import { registerVscodeToolOverrides } from "../components/chat/VscodeToolOverrides"
|
||||
import { SessionContext } from "../context/session"
|
||||
import type { PermissionRequest, QuestionRequest } from "../types/messages"
|
||||
|
||||
// Register VS Code tool overrides (bash expanded by default, etc.)
|
||||
registerVscodeToolOverrides()
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const SESSION_ID = "story-session-001"
|
||||
const ASST_MSG_ID = "asst-msg-001"
|
||||
const now = Date.now()
|
||||
|
||||
const baseAssistantMessage: SDKAssistantMessage = {
|
||||
id: ASST_MSG_ID,
|
||||
sessionID: SESSION_ID,
|
||||
role: "assistant",
|
||||
parentID: "user-msg-001",
|
||||
time: { created: now - 9000, completed: now - 5000 },
|
||||
modelID: "claude-sonnet-4-20250514",
|
||||
providerID: "anthropic",
|
||||
mode: "default",
|
||||
agent: "default",
|
||||
path: { cwd: "/project", root: "/project" },
|
||||
cost: 0.0023,
|
||||
tokens: { total: 512, input: 256, output: 256, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tool parts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const globPending: ToolPart = {
|
||||
id: "part-glob-001",
|
||||
sessionID: SESSION_ID,
|
||||
messageID: ASST_MSG_ID,
|
||||
type: "tool",
|
||||
callID: "call-glob-001",
|
||||
tool: "glob",
|
||||
state: {
|
||||
status: "pending",
|
||||
input: { pattern: "**/*.md", path: "." },
|
||||
title: "Searching for files",
|
||||
metadata: {},
|
||||
time: { start: now - 3000 },
|
||||
},
|
||||
}
|
||||
|
||||
const readCompleted: ToolPart = {
|
||||
id: "part-read-001",
|
||||
sessionID: SESSION_ID,
|
||||
messageID: ASST_MSG_ID,
|
||||
type: "tool",
|
||||
callID: "call-read-001",
|
||||
tool: "read",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: { filePath: "src/main.tsx" },
|
||||
output: 'import { render } from "solid-js/web"\nrender(() => <App />, document.getElementById("root")!)',
|
||||
title: "Read file",
|
||||
metadata: {},
|
||||
time: { start: now - 8000, end: now - 7500 },
|
||||
},
|
||||
}
|
||||
|
||||
const grepCompleted: ToolPart = {
|
||||
id: "part-grep-001",
|
||||
sessionID: SESSION_ID,
|
||||
messageID: ASST_MSG_ID,
|
||||
type: "tool",
|
||||
callID: "call-grep-001",
|
||||
tool: "grep",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: { pattern: "TODO", path: "src/" },
|
||||
output: "src/main.tsx:12: // TODO: add error boundary\nsrc/utils.ts:5: // TODO: refactor",
|
||||
title: "Search",
|
||||
metadata: {},
|
||||
time: { start: now - 7000, end: now - 6500 },
|
||||
},
|
||||
}
|
||||
|
||||
const globCompleted: ToolPart = {
|
||||
id: "part-glob-002",
|
||||
sessionID: SESSION_ID,
|
||||
messageID: ASST_MSG_ID,
|
||||
type: "tool",
|
||||
callID: "call-glob-002",
|
||||
tool: "glob",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: { pattern: "src/**/*.ts", path: "." },
|
||||
output: "src/main.ts\nsrc/utils.ts\nsrc/types.ts",
|
||||
title: "Found 3 files",
|
||||
metadata: {},
|
||||
time: { start: now - 6000, end: now - 5800 },
|
||||
},
|
||||
}
|
||||
|
||||
const lsCompleted: ToolPart = {
|
||||
id: "part-ls-001",
|
||||
sessionID: SESSION_ID,
|
||||
messageID: ASST_MSG_ID,
|
||||
type: "tool",
|
||||
callID: "call-ls-001",
|
||||
tool: "ls",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: { path: "." },
|
||||
output: "src/\npackage.json\ntsconfig.json\nREADME.md",
|
||||
title: "List directory",
|
||||
metadata: {},
|
||||
time: { start: now - 5500, end: now - 5400 },
|
||||
},
|
||||
}
|
||||
|
||||
const bashPending: ToolPart = {
|
||||
id: "part-bash-001",
|
||||
sessionID: SESSION_ID,
|
||||
messageID: ASST_MSG_ID,
|
||||
type: "tool",
|
||||
callID: "call-bash-001",
|
||||
tool: "bash",
|
||||
state: {
|
||||
status: "pending",
|
||||
input: { description: "Run tests", command: "bun test" },
|
||||
title: "Execute command",
|
||||
metadata: {},
|
||||
time: { start: now - 2000 },
|
||||
},
|
||||
}
|
||||
|
||||
const textPart: TextPart = {
|
||||
id: "part-text-001",
|
||||
sessionID: SESSION_ID,
|
||||
messageID: ASST_MSG_ID,
|
||||
type: "text",
|
||||
text: "I found the relevant files and will now update them.",
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Permission fixtures
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const globPermission: PermissionRequest = {
|
||||
id: "perm-glob-001",
|
||||
sessionID: SESSION_ID,
|
||||
toolName: "glob",
|
||||
patterns: ["**/*.md"],
|
||||
args: { pattern: "**/*.md" },
|
||||
tool: { messageID: ASST_MSG_ID, callID: "call-glob-001" },
|
||||
}
|
||||
|
||||
const bashPermission: PermissionRequest = {
|
||||
id: "perm-bash-001",
|
||||
sessionID: SESSION_ID,
|
||||
toolName: "bash",
|
||||
patterns: ["bun test"],
|
||||
args: { command: "bun test" },
|
||||
tool: { messageID: ASST_MSG_ID, callID: "call-bash-001" },
|
||||
}
|
||||
|
||||
const dockPermission: PermissionRequest = {
|
||||
id: "perm-dock-001",
|
||||
sessionID: SESSION_ID,
|
||||
toolName: "write",
|
||||
patterns: ["src/main.tsx", "src/utils.ts"],
|
||||
args: {},
|
||||
// No `tool` field — this is a non-tool (dock) permission
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Question fixtures
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const questionRequest: QuestionRequest = {
|
||||
id: "question-001",
|
||||
sessionID: SESSION_ID,
|
||||
questions: [
|
||||
{
|
||||
question: "Which testing framework should I use for this project?",
|
||||
header: "Choose a framework",
|
||||
options: [
|
||||
{ label: "Vitest", description: "Fast, Vite-native unit testing" },
|
||||
{ label: "Jest", description: "Widely adopted, rich ecosystem" },
|
||||
{ label: "Playwright", description: "End-to-end browser testing" },
|
||||
{ label: "Bun test", description: "Built-in, zero config" },
|
||||
],
|
||||
},
|
||||
],
|
||||
tool: { messageID: ASST_MSG_ID, callID: "call-question-001" },
|
||||
}
|
||||
|
||||
const questionToolPart: ToolPart = {
|
||||
id: "part-question-001",
|
||||
sessionID: SESSION_ID,
|
||||
messageID: ASST_MSG_ID,
|
||||
type: "tool",
|
||||
callID: "call-question-001",
|
||||
tool: "question",
|
||||
state: {
|
||||
status: "running",
|
||||
input: { question: "Which testing framework?", options: [] },
|
||||
title: "Asking question",
|
||||
metadata: {},
|
||||
time: { start: now - 1000 },
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Data helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function dataWith(parts: any[], permissions?: PermissionRequest[]) {
|
||||
return {
|
||||
...defaultMockData,
|
||||
message: {
|
||||
[SESSION_ID]: [baseAssistantMessage],
|
||||
},
|
||||
part: {
|
||||
[ASST_MSG_ID]: parts,
|
||||
},
|
||||
permission: permissions
|
||||
? { [SESSION_ID]: permissions }
|
||||
: {},
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Meta
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const meta: Meta = {
|
||||
title: "Composite/Webview",
|
||||
parameters: { layout: "padded" },
|
||||
}
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 1. Tool with inline permission (glob)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const GlobWithPermission: Story = {
|
||||
name: "Glob + Inline Permission",
|
||||
render: () => {
|
||||
const perms = [globPermission]
|
||||
const data = dataWith([globPending], perms)
|
||||
return (
|
||||
<StoryProviders data={data} permissions={perms} sessionID={SESSION_ID}>
|
||||
<AssistantMessage message={baseAssistantMessage} />
|
||||
</StoryProviders>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 2. Tool with inline permission (bash)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const BashWithPermission: Story = {
|
||||
name: "Bash + Inline Permission",
|
||||
render: () => {
|
||||
const perms = [bashPermission]
|
||||
const data = dataWith([bashPending], perms)
|
||||
return (
|
||||
<StoryProviders data={data} permissions={perms} sessionID={SESSION_ID}>
|
||||
<AssistantMessage message={baseAssistantMessage} />
|
||||
</StoryProviders>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 3. Permission dock (non-tool bottom prompt)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const PermissionDock: Story = {
|
||||
name: "Permission Dock",
|
||||
render: () => {
|
||||
const perm = dockPermission
|
||||
return (
|
||||
<StoryProviders sessionID={SESSION_ID}>
|
||||
<div data-component="tool-part-wrapper" data-permission="true">
|
||||
<BasicTool
|
||||
icon="checklist"
|
||||
locked
|
||||
defaultOpen
|
||||
trigger={{
|
||||
title: "Permission required",
|
||||
subtitle: perm.toolName,
|
||||
}}
|
||||
>
|
||||
<Show when={perm.patterns.length > 0}>
|
||||
<div class="permission-dock-patterns">
|
||||
<For each={perm.patterns}>
|
||||
{(pattern) => <code class="permission-dock-pattern">{pattern}</code>}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</BasicTool>
|
||||
<div data-component="permission-prompt">
|
||||
<div data-slot="permission-actions">
|
||||
<Button variant="ghost" size="small">
|
||||
Deny
|
||||
</Button>
|
||||
<Button variant="secondary" size="small">
|
||||
Always Allow
|
||||
</Button>
|
||||
<Button variant="primary" size="small">
|
||||
Allow Once
|
||||
</Button>
|
||||
</div>
|
||||
<p data-slot="permission-hint">Approval applies only to the current session</p>
|
||||
</div>
|
||||
</div>
|
||||
</StoryProviders>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 4. Tool cards — read, glob, grep, ls
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const ToolCards: Story = {
|
||||
name: "Tool Cards",
|
||||
render: () => {
|
||||
const data = dataWith([readCompleted, globCompleted, grepCompleted, lsCompleted])
|
||||
return (
|
||||
<StoryProviders data={data} sessionID={SESSION_ID}>
|
||||
<AssistantMessage message={baseAssistantMessage} />
|
||||
</StoryProviders>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 5. Chat idle — prompt input placeholder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const ChatIdle: Story = {
|
||||
name: "Chat Idle",
|
||||
render: () => (
|
||||
<StoryProviders sessionID={SESSION_ID} status="idle">
|
||||
<div class="chat-view" style={{ width: "380px" }}>
|
||||
<div class="chat-input">
|
||||
<div
|
||||
style={{
|
||||
padding: "8px 12px",
|
||||
border: "1px solid var(--border-base)",
|
||||
"border-radius": "8px",
|
||||
color: "var(--text-dimmed)",
|
||||
"font-size": "13px",
|
||||
background: "var(--background-input)",
|
||||
}}
|
||||
>
|
||||
Ask anything… (⌘ Enter)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</StoryProviders>
|
||||
),
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 6. Chat busy — working indicator, no prompt
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const ChatBusy: Story = {
|
||||
name: "Chat Busy",
|
||||
render: () => {
|
||||
const data = dataWith([textPart])
|
||||
return (
|
||||
<StoryProviders data={data} sessionID={SESSION_ID} status="busy">
|
||||
<div class="chat-view" style={{ width: "380px" }}>
|
||||
<div class="vscode-session-turn-assistant">
|
||||
<AssistantMessage message={baseAssistantMessage} />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
padding: "8px 12px",
|
||||
display: "flex",
|
||||
"align-items": "center",
|
||||
gap: "8px",
|
||||
color: "var(--text-dimmed)",
|
||||
"font-size": "13px",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: "8px",
|
||||
height: "8px",
|
||||
"border-radius": "50%",
|
||||
background: "var(--accent-base)",
|
||||
animation: "pulse 1.5s infinite",
|
||||
}}
|
||||
/>
|
||||
Thinking…
|
||||
</div>
|
||||
</div>
|
||||
</StoryProviders>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 7. Multiple tool calls in one assistant message
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const MultipleToolCalls: Story = {
|
||||
name: "Multiple Tool Calls",
|
||||
render: () => {
|
||||
const data = dataWith([readCompleted, globCompleted, textPart])
|
||||
return (
|
||||
<StoryProviders data={data} sessionID={SESSION_ID}>
|
||||
<AssistantMessage message={baseAssistantMessage} />
|
||||
</StoryProviders>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 8. Inline question (tool-linked question rendered in message flow)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const InlineQuestion: Story = {
|
||||
name: "Inline Question",
|
||||
render: () => {
|
||||
const qs = [questionRequest]
|
||||
const data = dataWith([textPart, questionToolPart])
|
||||
return (
|
||||
<StoryProviders data={data} questions={qs} sessionID={SESSION_ID}>
|
||||
<AssistantMessage message={baseAssistantMessage} />
|
||||
</StoryProviders>
|
||||
)
|
||||
},
|
||||
}
|
||||
@@ -696,6 +696,33 @@
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Permission Prompt (inline with tool cards) */
|
||||
[data-component="tool-part-wrapper"][data-permission="true"] {
|
||||
/* When wrapping a Part + permission, the outer wrapper provides the card border.
|
||||
Remove the inner Part's tool-part-wrapper border so we don't get double borders. */
|
||||
> [data-component="tool-part-wrapper"] {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="permission-prompt"] {
|
||||
border-top: 1px solid var(--border-weak-base, var(--vscode-panel-border));
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
[data-component="permission-prompt"] [data-slot="permission-actions"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
[data-component="permission-prompt"] [data-slot="permission-hint"] {
|
||||
font-size: 11px;
|
||||
color: var(--text-weak, var(--vscode-descriptionForeground));
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
/* Dialog Confirmation */
|
||||
.dialog-confirm-body {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user