diff --git a/packages/kilo-ui/src/stories/dock-prompt.stories.tsx b/packages/kilo-ui/src/stories/dock-prompt.stories.tsx new file mode 100644 index 00000000000..df4b32ae6da --- /dev/null +++ b/packages/kilo-ui/src/stories/dock-prompt.stories.tsx @@ -0,0 +1,60 @@ +/** @jsxImportSource solid-js */ +import type { Meta, StoryObj } from "storybook-solidjs-vite" +import { DockPrompt } from "@opencode-ai/ui/dock-prompt" +import { Button } from "@opencode-ai/ui/button" + +const meta: Meta = { + title: "Components/DockPrompt", + parameters: { layout: "padded" }, +} +export default meta +type Story = StoryObj + +export const Question: Story = { + name: "Question kind", + render: () => ( + Which testing framework should I use?} + footer={ +
+ + +
+ } + > +
+

Choose one of the following options:

+ +
+
+ ), +} + +export const Permission: Story = { + name: "Permission kind", + render: () => ( + Permission required — write} + footer={ +
+ + + +
+ } + > +
+ src/main.tsx +
+ src/utils.ts +
+
+ ), +} diff --git a/packages/kilo-ui/src/stories/dock-surface.stories.tsx b/packages/kilo-ui/src/stories/dock-surface.stories.tsx new file mode 100644 index 00000000000..e602659c5de --- /dev/null +++ b/packages/kilo-ui/src/stories/dock-surface.stories.tsx @@ -0,0 +1,60 @@ +/** @jsxImportSource solid-js */ +import type { Meta, StoryObj } from "storybook-solidjs-vite" +import { DockShell, DockShellForm, DockTray } from "@opencode-ai/ui/dock-surface" + +const meta: Meta = { + title: "Components/DockSurface", + parameters: { layout: "padded" }, +} +export default meta +type Story = StoryObj + +export const Shell: Story = { + name: "DockShell", + render: () => ( + +

DockShell — main container body for the dock surface.

+
+ ), +} + +export const Tray: Story = { + name: "DockTray", + render: () => ( + +

DockTray — footer tray area (attach=none).

+
+ ), +} + +export const TrayAttachTop: Story = { + name: "DockTray attach=top", + render: () => ( + +

DockTray — header tray area (attach=top).

+
+ ), +} + +export const ShellWithTray: Story = { + name: "DockShell + DockTray", + render: () => ( +
+ +

Body content goes here inside DockShell.

+
+ + Footer actions slot + +
+ ), +} + +export const ShellForm: Story = { + name: "DockShellForm", + render: () => ( + +

DockShellForm — shell variant using a form element.

+
+ ), +} diff --git a/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx b/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx index dbbe0f64bea..2f2610e1cae 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx @@ -4,10 +4,16 @@ * * Instead of instantiating the full VSCodeProvider → ServerProvider → SessionProvider * chain (which requires a real extension host / SSE connection), we provide mock - * context values directly. + * context values directly. Where a real provider is safe to instantiate without an + * extension host (VSCodeProvider, ServerProvider, ProviderProvider), we use the real + * thing so components that call useVSCode()/useServer()/useProvider() don't throw. */ import { createSignal, type ParentComponent } from "solid-js" +import { VSCodeProvider } from "../context/vscode" +import { ServerProvider } from "../context/server" +import { ProviderProvider } from "../context/provider" +import { ConfigProvider } from "../context/config" import { DataProvider } from "@kilocode/kilo-ui/context/data" import { DiffComponentProvider } from "@kilocode/kilo-ui/context/diff" import { CodeComponentProvider } from "@kilocode/kilo-ui/context/code" @@ -144,29 +150,37 @@ export const StoryProviders: ParentComponent = (props) => { const [locale] = createSignal<"en">("en") return ( - - "" as any, - t, - }} - > - "en", t }}> - - - - - - {props.noPadding ? props.children :
{props.children}
} -
-
-
-
-
-
-
-
+ + + + + + "" as any, + t, + }} + > + "en", t }}> + + + + + + {props.noPadding ? props.children :
{props.children}
} +
+
+
+
+
+
+
+
+
+
+
+
) } diff --git a/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx new file mode 100644 index 00000000000..0c75731604d --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx @@ -0,0 +1,188 @@ +/** @jsxImportSource solid-js */ +/** + * Stories for Agent Manager components: + * AgentManagerApp, FileTree, DiffPanel, FullScreenDiffView + * + * FileTree, DiffPanel, and FullScreenDiffView accept data-only props and only + * require a LanguageContext, so they are storied with StoryProviders. + * AgentManagerApp sets up its own full provider chain and is rendered directly. + */ + +import type { Meta, StoryObj } from "storybook-solidjs-vite" +import { StoryProviders } from "./StoryProviders" +import { AgentManagerApp } from "../../agent-manager/AgentManagerApp" +import { FileTree } from "../../agent-manager/FileTree" +import { DiffPanel } from "../../agent-manager/DiffPanel" +import { FullScreenDiffView } from "../../agent-manager/FullScreenDiffView" +import type { WorktreeFileDiff } from "../types/messages" + +// --------------------------------------------------------------------------- +// Shared mock data +// --------------------------------------------------------------------------- + +const mockDiffs: WorktreeFileDiff[] = [ + { + file: "src/components/chat/ChatView.tsx", + status: "modified", + additions: 12, + deletions: 4, + before: `import { Component } from "solid-js"\n\nexport const ChatView: Component = () => {\n return
\n}\n`, + after: `import { Component, createSignal } from "solid-js"\n\nexport const ChatView: Component = () => {\n const [open, setOpen] = createSignal(false)\n return
\n}\n`, + }, + { + file: "src/components/chat/MessageList.tsx", + status: "modified", + additions: 3, + deletions: 1, + before: `export const MessageList = () =>
\n`, + after: `export const MessageList = () => (\n
\n)\n`, + }, + { + file: "src/stories/chat.stories.tsx", + status: "added", + additions: 80, + deletions: 0, + before: "", + after: `/** @jsxImportSource solid-js */\nimport type { Meta } from "storybook-solidjs-vite"\nconst meta: Meta = { title: "Chat" }\nexport default meta\n`, + }, +] + +// --------------------------------------------------------------------------- +// Meta +// --------------------------------------------------------------------------- + +const meta: Meta = { + title: "AgentManager", + parameters: { layout: "padded" }, +} +export default meta +type Story = StoryObj + +// --------------------------------------------------------------------------- +// FileTree +// --------------------------------------------------------------------------- + +export const FileTreeWithChanges: Story = { + name: "FileTree — with modifications and additions", + render: () => ( + +
+ {}} + showSummary + /> +
+
+ ), +} + +export const FileTreeEmpty: Story = { + name: "FileTree — no changes", + render: () => ( + +
+ {}} /> +
+
+ ), +} + +// --------------------------------------------------------------------------- +// DiffPanel +// --------------------------------------------------------------------------- + +export const DiffPanelLoading: Story = { + name: "DiffPanel — loading", + render: () => ( + +
+ {}} + onClose={() => {}} + /> +
+
+ ), +} + +export const DiffPanelWithDiffs: Story = { + name: "DiffPanel — with diffs (unified)", + render: () => ( + +
+ {}} + comments={[]} + onCommentsChange={() => {}} + onClose={() => {}} + onExpand={() => {}} + /> +
+
+ ), +} + +// --------------------------------------------------------------------------- +// FullScreenDiffView +// --------------------------------------------------------------------------- + +export const FullScreenDiffLoading: Story = { + name: "FullScreenDiffView — loading", + render: () => ( + +
+ {}} + comments={[]} + onCommentsChange={() => {}} + onClose={() => {}} + /> +
+
+ ), +} + +export const FullScreenDiffWithChanges: Story = { + name: "FullScreenDiffView — with changes", + render: () => ( + +
+ {}} + comments={[]} + onCommentsChange={() => {}} + onClose={() => {}} + /> +
+
+ ), +} + +// --------------------------------------------------------------------------- +// AgentManagerApp +// --------------------------------------------------------------------------- + +export const AgentManagerRoot: Story = { + name: "AgentManagerApp — initial connecting state", + render: () => ( + // AgentManagerApp sets up its own VSCodeProvider + ThemeProvider + layout. + // In Storybook it renders its empty/connecting state (no extension host). +
+ +
+ ), +} diff --git a/packages/kilo-vscode/webview-ui/src/stories/chat.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/chat.stories.tsx new file mode 100644 index 00000000000..d34da6baa86 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/stories/chat.stories.tsx @@ -0,0 +1,162 @@ +/** @jsxImportSource solid-js */ +/** + * Stories for high-priority chat components: + * ChatView, MessageList, QuestionDock + * + * These render with mocked session/server/provider contexts — the components + * will show their "idle / empty" states since no real extension host is connected. + */ + +import type { Meta, StoryObj } from "storybook-solidjs-vite" +import { StoryProviders } from "./StoryProviders" +import { ChatView } from "../components/chat/ChatView" +import { MessageList } from "../components/chat/MessageList" +import { QuestionDock } from "../components/chat/QuestionDock" +import type { QuestionRequest } from "../types/messages" + +const SESSION_ID = "story-session-chat-001" + +// --------------------------------------------------------------------------- +// Question fixtures +// --------------------------------------------------------------------------- + +const singleQuestion: QuestionRequest = { + id: "q-single-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-001", callID: "call-question-001" }, +} + +const multiQuestion: QuestionRequest = { + id: "q-multi-001", + sessionID: SESSION_ID, + questions: [ + { + question: "Which testing framework?", + options: [{ label: "Vitest" }, { label: "Jest" }, { label: "Bun test" }], + }, + { + question: "Should I include coverage reporting?", + options: [{ label: "Yes, Istanbul" }, { label: "Yes, V8" }, { label: "No" }], + }, + ], + tool: { messageID: "asst-msg-001", callID: "call-question-002" }, +} + +// --------------------------------------------------------------------------- +// Meta +// --------------------------------------------------------------------------- + +const meta: Meta = { + title: "Chat", + parameters: { layout: "padded" }, +} +export default meta +type Story = StoryObj + +// --------------------------------------------------------------------------- +// ChatView stories +// --------------------------------------------------------------------------- + +export const ChatViewIdle: Story = { + name: "ChatView — idle (empty)", + render: () => ( + +
+ +
+
+ ), +} + +export const ChatViewReadonly: Story = { + name: "ChatView — readonly", + render: () => ( + +
+ +
+
+ ), +} + +export const ChatViewWithQuestion: Story = { + name: "ChatView — with QuestionDock", + render: () => ( + +
+ +
+
+ ), +} + +// --------------------------------------------------------------------------- +// MessageList stories +// --------------------------------------------------------------------------- + +export const MessageListEmpty: Story = { + name: "MessageList — empty", + render: () => ( + +
+ +
+
+ ), +} + +export const MessageListLoading: Story = { + name: "MessageList — loading", + render: () => { + const loadingSession = { + loading: () => true, + } + // Render empty state — the real "loading" state requires overriding session.loading() + // which we approximate here: the component will show its empty-state logo instead. + return ( + +
+ +
+
+ ) + }, +} + +// --------------------------------------------------------------------------- +// QuestionDock stories +// --------------------------------------------------------------------------- + +export const QuestionDockSingle: Story = { + name: "QuestionDock — single question", + render: () => ( + +
+ +
+
+ ), +} + +export const QuestionDockMulti: Story = { + name: "QuestionDock — multi-question wizard", + render: () => ( + +
+ +
+
+ ), +} diff --git a/packages/kilo-vscode/webview-ui/src/stories/history.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/history.stories.tsx new file mode 100644 index 00000000000..82ea82fa7f6 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/stories/history.stories.tsx @@ -0,0 +1,152 @@ +/** @jsxImportSource solid-js */ +/** + * Stories for the SessionList component (history panel). + */ + +import type { Meta, StoryObj } from "storybook-solidjs-vite" +import { createSignal, type ParentComponent } from "solid-js" +import { DialogProvider } from "@kilocode/kilo-ui/context/dialog" +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 { 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 { VSCodeProvider } from "../context/vscode" +import { ServerProvider } from "../context/server" +import { ConfigProvider } from "../context/config" +import { ProviderProvider } from "../context/provider" +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 { SessionList } from "../components/history/SessionList" + +const dict: Record = { ...appEn, ...uiEn, ...kiloEn } +function t(key: string) { return dict[key] ?? key } +function noop() {} + +const now = new Date().toISOString() +const yesterday = new Date(Date.now() - 86400000).toISOString() +const weekAgo = new Date(Date.now() - 7 * 86400000).toISOString() + +const mockSessions = [ + { id: "s1", title: "Refactor authentication module", createdAt: now, updatedAt: now }, + { id: "s2", title: "Add screenshot test coverage", createdAt: yesterday, updatedAt: yesterday }, + { id: "s3", title: "Fix TypeScript errors in webview", createdAt: weekAgo, updatedAt: weekAgo }, + { id: "s4", title: undefined, createdAt: weekAgo, updatedAt: weekAgo }, +] + +const WithSessions: ParentComponent<{ sessions?: typeof mockSessions }> = (props) => { + const [locale] = createSignal<"en">("en") + const sessions = props.sessions ?? [] + const session = { + currentSessionID: () => "s1", + currentSession: () => sessions[0], + setCurrentSessionID: noop, + sessions: () => sessions as any, + status: () => "idle" as const, + statusInfo: () => ({ type: "idle" }), + statusText: () => undefined, + busySince: () => undefined, + loading: () => false, + messages: () => [], + userMessages: () => [], + allMessages: () => ({}), + allParts: () => ({}), + allStatusMap: () => ({}), + getParts: () => [], + todos: () => [], + permissions: () => [], + questions: () => [], + questionErrors: () => new Set(), + 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, + } + + return ( + + + + + + "" as any, t }}> + "en", t }}> + + + + + +
{props.children}
+
+
+
+
+
+
+
+
+
+
+
+
+ ) +} + +const meta: Meta = { + title: "History/SessionList", + parameters: { layout: "padded" }, +} +export default meta +type Story = StoryObj + +export const Empty: Story = { + name: "Empty list", + render: () => ( + +
+ +
+
+ ), +} + +export const WithItems: Story = { + name: "With sessions", + render: () => ( + +
+ +
+
+ ), +} diff --git a/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx new file mode 100644 index 00000000000..7bc720d822c --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx @@ -0,0 +1,38 @@ +/** @jsxImportSource solid-js */ +/** + * Stories for Settings and ProvidersTab components. + */ + +import type { Meta, StoryObj } from "storybook-solidjs-vite" +import { StoryProviders } from "./StoryProviders" +import Settings from "../components/settings/Settings" +import ProvidersTab from "../components/settings/ProvidersTab" + +const meta: Meta = { + title: "Settings", + parameters: { layout: "fullscreen" }, +} +export default meta +type Story = StoryObj + +export const SettingsPanel: Story = { + name: "Settings — full panel", + render: () => ( + +
+ {}} /> +
+
+ ), +} + +export const ProvidersConfigure: Story = { + name: "ProvidersTab — no providers configured", + render: () => ( + +
+ +
+
+ ), +} diff --git a/packages/kilo-vscode/webview-ui/src/stories/shared.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/shared.stories.tsx new file mode 100644 index 00000000000..1c17d51ab50 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/stories/shared.stories.tsx @@ -0,0 +1,103 @@ +/** @jsxImportSource solid-js */ +/** + * Stories for shared controls: ModelSelector and ModeSwitcher. + */ + +import type { Meta, StoryObj } from "storybook-solidjs-vite" +import { StoryProviders } from "./StoryProviders" +import { ModelSelectorBase } from "../components/shared/ModelSelector" +import { ModeSwitcherBase, ModeSwitcher } from "../components/shared/ModeSwitcher" + +const agents = [ + { name: "code", description: "Write, edit and review code", mode: "primary" as const }, + { name: "ask", description: "Answer questions without making changes", mode: "primary" as const }, + { name: "architect", description: "Plan and design before implementation", mode: "primary" as const }, + { name: "debug", description: "Diagnose and fix issues", mode: "primary" as const }, +] + +const meta: Meta = { + title: "Shared", + parameters: { layout: "padded" }, +} +export default meta +type Story = StoryObj + +// --------------------------------------------------------------------------- +// ModelSelector +// --------------------------------------------------------------------------- + +export const ModelSelectorNoProviders: Story = { + name: "ModelSelector — no providers", + render: () => ( + +
+ {}} + placement="bottom-start" + /> +
+
+ ), +} + +export const ModelSelectorAllowClear: Story = { + name: "ModelSelector — allow clear", + render: () => ( + +
+ {}} + placement="bottom-start" + allowClear + clearLabel="Use default model" + /> +
+
+ ), +} + +// --------------------------------------------------------------------------- +// ModeSwitcher +// --------------------------------------------------------------------------- + +export const ModeSwitcherSingle: Story = { + name: "ModeSwitcherBase — single agent (hidden)", + render: () => ( + + {}} + /> + + ), +} + +export const ModeSwitcherMultiple: Story = { + name: "ModeSwitcherBase — multiple agents", + render: () => ( + + {}} /> + + ), +} + +export const ModeSwitcherAskSelected: Story = { + name: "ModeSwitcherBase — ask mode selected", + render: () => ( + + {}} /> + + ), +} + +export const ModeSwitcherFromSession: Story = { + name: "ModeSwitcher — wired to session context", + render: () => ( + + + + ), +}