diff --git a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts index afcbae2f3d9..53536201df3 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts @@ -892,70 +892,73 @@ describe("Agent Manager — VS Code import boundary", () => { }) }) -// --------------------------------------------------------------------------- -// Provider chain parity — sidebar App.tsx vs AgentManagerApp.tsx -// -// The agent manager reuses ChatView (and therefore MessageList, etc.) from the -// sidebar. Any context provider that ChatView's tree may call useXxx() on must -// also be present in the agent manager's provider chain. A missing provider -// crashes the entire SolidJS component tree silently. -// -// Regression: PR #7473 moved KiloNotifications into MessageList. It calls -// useNotifications(), but NotificationsProvider was only in App.tsx — the agent -// manager rendered a blank screen. -// --------------------------------------------------------------------------- - const APP_FILE = path.join(ROOT, "webview-ui/src/App.tsx") const AGENT_MANAGER_APP_FILE = path.join(ROOT, "webview-ui/agent-manager/AgentManagerApp.tsx") +const PROVIDER_SHELL_FILE = path.join(ROOT, "webview-ui/src/context/provider-shell.tsx") -describe("Agent Manager — provider chain parity with sidebar", () => { - /** - * Extract provider component names used as JSX elements in a file. - * Matches `` patterns, returning the names. - */ - function extractProviders(content: string): string[] { - const matches = [...content.matchAll(/<(\w+Provider)\b/g)] - return [...new Set(matches.map((m) => m[1]!))] +describe("Shared webview provider shell", () => { + function ordered(source: string, names: string[]) { + const positions = names.map((name) => source.indexOf(`<${name}`)) + expect( + positions.every((position) => position >= 0), + `Missing provider from ${names.join(" -> ")}`, + ).toBe(true) + expect(positions).toEqual([...positions].sort((a, b) => a - b)) } - /** - * Providers that the agent manager intentionally omits because it does not - * use the components that depend on them. If a shared component (ChatView, - * MessageList, etc.) starts using one of these, the test will fail and - * force the developer to add the provider to AgentManagerApp.tsx. - */ - const KNOWN_EXCLUSIONS: string[] = [ - // These are wrapped by LanguageBridge and DataBridge respectively, - // which the agent manager already includes in its provider chain. - "LanguageProvider", - "DataProvider", - // Agent Manager owns its local session tabs and ChatView only reads this - // optional context in the standard sidebar/editor webview. - "LocalTabsProvider", - // Work-style onboarding is injected only into the sidebar empty state. - "WorkStyleProvider", - ] - - it("agent manager includes all context providers from sidebar App.tsx", () => { - const sidebar = fs.readFileSync(APP_FILE, "utf-8") - const agent = fs.readFileSync(AGENT_MANAGER_APP_FILE, "utf-8") - - const sidebarProviders = extractProviders(sidebar) - const agentProviders = extractProviders(agent) - const agentSet = new Set(agentProviders) - const excluded = new Set(KNOWN_EXCLUSIONS) - - const missing = sidebarProviders.filter((p) => !agentSet.has(p) && !excluded.has(p)) - - expect( - missing, - `These providers are in App.tsx but missing from AgentManagerApp.tsx.\n` + - `The agent manager reuses ChatView — any provider that ChatView's component\n` + - `tree depends on must be present in both provider chains.\n\n` + - `Missing providers:\n` + - missing.map((p) => ` - ${p}`).join("\n") + - `\n\nFix: add the missing <${missing[0]}> to AgentManagerApp.tsx's provider chain,\n` + - `or add it to KNOWN_EXCLUSIONS with a justification if it's truly unused.`, - ).toEqual([]) + it("owns the common provider order and bridges", () => { + const source = fs.readFileSync(PROVIDER_SHELL_FILE, "utf-8") + ordered(source, [ + "ThemeProvider", + "DialogProvider", + "VSCodeProvider", + "MermaidDownloadBridge", + "ServerProvider", + "LanguageBridge", + "MarkedProvider", + "DiffComponentProvider", + "CodeComponentProvider", + "FileComponentProvider", + "ProviderProvider", + "ConfigProvider", + "SpeechToTextPrewarm", + "DisplayProvider", + "IndexingProvider", + "KiloEmbeddingModelsProvider", + "ImageModelsProvider", + "NotificationsProvider", + "SessionProvider", + "AgentRequirementsProvider", + "MemoryProvider", + "FeedbackProvider", + ]) + expect(source.indexOf("")) + }) + + it("keeps sidebar-only providers in the sidebar root", () => { + const source = fs.readFileSync(APP_FILE, "utf-8") + ordered(source, [ + "ProviderShell.Root", + "WorkStyleProvider", + "ProviderShell.Session", + "LocalTabsProvider", + "ProviderShell.Chat", + "DataBridge", + "AppContent", + ]) + expect(fs.readFileSync(PROVIDER_SHELL_FILE, "utf-8")).not.toMatch(/WorkStyleProvider|LocalTabsProvider/) + }) + + it("keeps worktree mode in the Agent Manager root", () => { + const source = fs.readFileSync(AGENT_MANAGER_APP_FILE, "utf-8") + ordered(source, [ + "ProviderShell.Root", + "ProviderShell.Session", + "ProviderShell.Chat", + "WorktreeModeProvider", + "DataBridge", + "AgentManagerContent", + ]) + expect(fs.readFileSync(PROVIDER_SHELL_FILE, "utf-8")).not.toContain("WorktreeModeProvider") }) }) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 7f1773ba895..8397077e5d8 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -39,7 +39,6 @@ import type { SessionCreatedMessage, BranchInfo, } from "../src/types/messages" -import { IndexingProvider } from "../src/context/indexing" import { DragDropProvider, DragDropSensors, @@ -49,43 +48,24 @@ import { createSortable, } from "@thisbeyond/solid-dnd" import type { DragEvent } from "@thisbeyond/solid-dnd" -import { ThemeProvider } from "@kilocode/kilo-ui/theme" -import { DialogProvider, useDialog } from "@kilocode/kilo-ui/context/dialog" +import { useDialog } from "@kilocode/kilo-ui/context/dialog" import { Dialog } from "@kilocode/kilo-ui/dialog" import { DropdownMenu } from "@kilocode/kilo-ui/dropdown-menu" -import { MarkedProvider } from "@kilocode/kilo-ui/context/marked" -import { CodeComponentProvider } from "@kilocode/kilo-ui/context/code" -import { DiffComponentProvider } from "@kilocode/kilo-ui/context/diff" -import { FileComponentProvider } from "@kilocode/kilo-ui/context/file" -import { Code } from "@kilocode/kilo-ui/code" -import { Diff } from "@kilocode/kilo-ui/diff" -import { File } from "@kilocode/kilo-ui/file" -import { Toast, showToast } from "@kilocode/kilo-ui/toast" +import { showToast } from "@kilocode/kilo-ui/toast" import { ResizeHandle } from "@kilocode/kilo-ui/resize-handle" import { Icon } from "@kilocode/kilo-ui/icon" import { Button } from "@kilocode/kilo-ui/button" import { IconButton } from "@kilocode/kilo-ui/icon-button" import { Spinner } from "@kilocode/kilo-ui/spinner" import { Tooltip, TooltipKeybind } from "@kilocode/kilo-ui/tooltip" -import { VSCodeProvider, useVSCode } from "../src/context/vscode" -import { ServerProvider } from "../src/context/server" -import { ProviderProvider } from "../src/context/provider" -import { ConfigProvider } from "../src/context/config" -import { DisplayProvider } from "../src/context/display" -import { KiloEmbeddingModelsProvider } from "../src/context/kilo-embedding-models" -import { ImageModelsProvider } from "../src/context/image-models" -import { NotificationsProvider } from "../src/context/notifications" -import { FeedbackProvider } from "../src/context/feedback" -import { MemoryProvider } from "../src/context/memory" -import { SessionProvider, useSession } from "../src/context/session" -import { AgentRequirementsProvider } from "../src/context/agent-requirements" +import { useVSCode } from "../src/context/vscode" +import { useSession } from "../src/context/session" import { WorktreeModeProvider } from "../src/context/worktree-mode" +import { ProviderShell } from "../src/context/provider-shell" import { ChatView } from "../src/components/chat" -import { SpeechToTextPrewarm } from "../src/components/speech-to-text/SpeechToTextPrewarm" import HistoryView from "../src/components/history/HistoryView" import { NewWorktreeDialog } from "./NewWorktreeDialog" -import { DataBridge, MermaidDownloadBridge } from "../src/App" -import { LanguageBridge } from "../src/context/language-bridge" +import { DataBridge } from "../src/App" import { useLanguage } from "../src/context/language" import { createTabFocus } from "../src/utils/tab-navigation" import { @@ -2880,53 +2860,16 @@ const AgentManagerContent: Component = () => { export const AgentManagerApp: Component = () => { return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + ) } diff --git a/packages/kilo-vscode/webview-ui/src/App.tsx b/packages/kilo-vscode/webview-ui/src/App.tsx index 1aa67688238..7fa356e8482 100644 --- a/packages/kilo-vscode/webview-ui/src/App.tsx +++ b/packages/kilo-vscode/webview-ui/src/App.tsx @@ -1,34 +1,18 @@ import { Component, createSignal, createMemo, Switch, Match, Show, onMount, onCleanup } from "solid-js" -import { ThemeProvider } from "@kilocode/kilo-ui/theme" -import { DialogProvider } from "@kilocode/kilo-ui/context/dialog" -import { MarkedProvider } from "@kilocode/kilo-ui/context/marked" -import { CodeComponentProvider } from "@kilocode/kilo-ui/context/code" -import { DiffComponentProvider } from "@kilocode/kilo-ui/context/diff" -import { FileComponentProvider } from "@kilocode/kilo-ui/context/file" -import { Code } from "@kilocode/kilo-ui/code" -import { Diff } from "@kilocode/kilo-ui/diff" -import { File } from "@kilocode/kilo-ui/file" import { DataProvider } from "@kilocode/kilo-ui/context/data" -import { Toast } from "@kilocode/kilo-ui/toast" import Settings from "./components/settings/Settings" import ProfileView from "./components/profile/ProfileView" -import { VSCodeProvider, useVSCode } from "./context/vscode" -import { ServerProvider, useServer } from "./context/server" -import { ProviderProvider, useProvider } from "./context/provider" -import { ConfigProvider } from "./context/config" -import { DisplayProvider } from "./context/display" +import { useVSCode } from "./context/vscode" +import { useServer } from "./context/server" +import { useProvider } from "./context/provider" import { WorkStyleProvider } from "./context/work-style" -import { IndexingProvider } from "./context/indexing" -import { AgentRequirementsProvider } from "./context/agent-requirements" -import { MemoryProvider } from "./context/memory" -import { SessionProvider, useSession } from "./context/session" +import { useSession } from "./context/session" import { LocalTabsProvider, useLocalTabs } from "./context/local-tabs" -import { LanguageBridge } from "./context/language-bridge" +import { ProviderShell } from "./context/provider-shell" import { ChatView } from "./components/chat" import { SidebarEmptyState } from "./components/chat/SidebarEmptyState" import { registerExpandedTaskTool } from "./components/chat/TaskToolExpanded" import { registerVscodeToolOverrides } from "./components/chat/VscodeToolOverrides" -import { SpeechToTextPrewarm } from "./components/speech-to-text/SpeechToTextPrewarm" // Override the upstream "task" tool renderer with the fully-expanded version // that shows child session parts inline in the VS Code sidebar. @@ -37,10 +21,6 @@ registerExpandedTaskTool() registerVscodeToolOverrides() import HistoryView from "./components/history/HistoryView" import { MigrationWizard } from "./components/migration" // legacy-migration -import { NotificationsProvider } from "./context/notifications" -import { FeedbackProvider } from "./context/feedback" -import { KiloEmbeddingModelsProvider } from "./context/kilo-embedding-models" -import { ImageModelsProvider } from "./context/image-models" import type { Message as SDKMessage, Part as SDKPart } from "@kilocode/sdk/v2" import { cycleAgent as cycle } from "./context/session-agent" import "./styles/chat.css" @@ -210,27 +190,6 @@ export const DataBridge: Component<{ children: any }> = (props) => { ) } -type MermaidImageEvent = CustomEvent<{ dataUrl: string; filename: string }> - -export const MermaidDownloadBridge: Component = () => { - const vscode = useVSCode() - - onMount(() => { - const save = (event: Event) => { - const detail = (event as MermaidImageEvent).detail - if (!detail?.dataUrl || !detail.filename) return - event.preventDefault() - vscode.postMessage({ type: "saveImage", dataUrl: detail.dataUrl, filename: detail.filename }) - } - window.addEventListener("kilo:save-image", save) - onCleanup(() => { - window.removeEventListener("kilo:save-image", save) - }) - }) - - return null -} - // Inner app component that uses the contexts const AppContent: Component = () => { const [currentView, setCurrentView] = createSignal("newTask") @@ -412,59 +371,21 @@ const AppContent: Component = () => { ) } -// Main App component with context providers const App: Component = () => { return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + ) } diff --git a/packages/kilo-vscode/webview-ui/src/context/provider-shell.tsx b/packages/kilo-vscode/webview-ui/src/context/provider-shell.tsx new file mode 100644 index 00000000000..7a892052493 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/context/provider-shell.tsx @@ -0,0 +1,96 @@ +import { onCleanup, onMount, type Component, type ParentComponent } from "solid-js" +import { ThemeProvider } from "@kilocode/kilo-ui/theme" +import { DialogProvider } from "@kilocode/kilo-ui/context/dialog" +import { MarkedProvider } from "@kilocode/kilo-ui/context/marked" +import { CodeComponentProvider } from "@kilocode/kilo-ui/context/code" +import { DiffComponentProvider } from "@kilocode/kilo-ui/context/diff" +import { FileComponentProvider } from "@kilocode/kilo-ui/context/file" +import { Code } from "@kilocode/kilo-ui/code" +import { Diff } from "@kilocode/kilo-ui/diff" +import { File } from "@kilocode/kilo-ui/file" +import { Toast } from "@kilocode/kilo-ui/toast" +import { VSCodeProvider, useVSCode } from "./vscode" +import { ServerProvider } from "./server" +import { ProviderProvider } from "./provider" +import { ConfigProvider } from "./config" +import { DisplayProvider } from "./display" +import { IndexingProvider } from "./indexing" +import { AgentRequirementsProvider } from "./agent-requirements" +import { MemoryProvider } from "./memory" +import { SessionProvider } from "./session" +import { LanguageBridge } from "./language-bridge" +import { NotificationsProvider } from "./notifications" +import { FeedbackProvider } from "./feedback" +import { KiloEmbeddingModelsProvider } from "./kilo-embedding-models" +import { ImageModelsProvider } from "./image-models" +import { SpeechToTextPrewarm } from "../components/speech-to-text/SpeechToTextPrewarm" + +type MermaidImageEvent = CustomEvent<{ dataUrl: string; filename: string }> + +const MermaidDownloadBridge: Component = () => { + const vscode = useVSCode() + + onMount(() => { + const save = (event: Event) => { + const detail = (event as MermaidImageEvent).detail + if (!detail?.dataUrl || !detail.filename) return + event.preventDefault() + vscode.postMessage({ type: "saveImage", dataUrl: detail.dataUrl, filename: detail.filename }) + } + window.addEventListener("kilo:save-image", save) + onCleanup(() => window.removeEventListener("kilo:save-image", save)) + }) + + return null +} + +const Root: ParentComponent = (props) => ( + + + + + + + + + + + + + + {props.children} + + + + + + + + + + + + +) + +const Session: ParentComponent = (props) => ( + + + + + {props.children} + + + + +) + +const Chat: ParentComponent = (props) => ( + + + {props.children} + + +) + +export const ProviderShell = { Root, Session, Chat }