Step 1: Foundation & Cutover - SDK adapter layer

- Add @clinebot/core, @clinebot/llms, @clinebot/shared, @clinebot/agents
  as dependencies via file: protocol (linked to ../sdk-wip)
- Create src/sdk/ directory with SdkController stub and barrel export
- Replace src/core/controller/index.ts with re-export from SDK adapter
  (classic Controller accessible via origin/main)
- Extract getStateToPostToWebview() to standalone function for reuse
- Add vitest.config.sdk.ts for SDK adapter tests
- Fix implicit any types in handler modules
- Extension compiles and builds successfully (tsc + esbuild pass)
- Single entry point: no CLINE_SDK flag, SDK adapter is the only codepath

Replaces classic src/core/controller/index.ts (see origin/main)
This commit is contained in:
Dominic Cooney
2026-04-17 21:01:24 +09:00
parent df8d054cbb
commit 20cca8f526
11 changed files with 621 additions and 1051 deletions
+119
View File
@@ -20,6 +20,10 @@
"@azure/identity": "^4.13.0",
"@bufbuild/protobuf": "^2.2.5",
"@cerebras/cerebras_cloud_sdk": "^1.35.0",
"@clinebot/agents": "file:../sdk-wip/packages/agents",
"@clinebot/core": "file:../sdk-wip/packages/core",
"@clinebot/llms": "file:../sdk-wip/packages/llms",
"@clinebot/shared": "file:../sdk-wip/packages/shared",
"@google-cloud/vertexai": "^1.9.3",
"@google/genai": "^1.30.0",
"@grpc/grpc-js": "^1.9.15",
@@ -160,6 +164,105 @@
"vscode": "^1.84.0"
}
},
"../sdk-wip/packages/agents": {
"name": "@clinebot/agents",
"version": "0.0.28",
"license": "Apache-2.0",
"dependencies": {
"@clinebot/llms": "workspace:*",
"zod": "^4.3.6"
},
"devDependencies": {
"@clinebot/shared": "workspace:*"
},
"engines": {
"node": ">=20"
}
},
"../sdk-wip/packages/core": {
"name": "@clinebot/core",
"version": "0.0.28",
"dependencies": {
"@clinebot/agents": "workspace:*",
"@clinebot/llms": "workspace:*",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/api-logs": "^0.214.0",
"@opentelemetry/exporter-logs-otlp-http": "^0.214.0",
"@opentelemetry/exporter-metrics-otlp-http": "^0.214.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.214.0",
"@opentelemetry/resources": "^2.6.1",
"@opentelemetry/sdk-logs": "^0.214.0",
"@opentelemetry/sdk-metrics": "^2.6.1",
"@opentelemetry/sdk-trace-base": "^2.6.1",
"@opentelemetry/sdk-trace-node": "^2.6.1",
"@opentelemetry/semantic-conventions": "^1.40.0",
"jiti": "^1.21.7",
"nanoid": "^5.1.7",
"node-machine-id": "^1.1.12",
"simple-git": "^3.32.3",
"yaml": "^2.8.2",
"zod": "^4.3.6"
},
"devDependencies": {
"@clinebot/rpc": "workspace:*",
"@clinebot/shared": "workspace:*"
},
"engines": {
"node": ">=20"
}
},
"../sdk-wip/packages/llms": {
"name": "@clinebot/llms",
"version": "0.0.28",
"dependencies": {
"@ai-sdk/amazon-bedrock": "^4.0.89",
"@ai-sdk/anthropic": "^3.0.68",
"@ai-sdk/google": "^3.0.60",
"@ai-sdk/google-vertex": "^4.0.100",
"@ai-sdk/mistral": "^3.0.28",
"@ai-sdk/openai": "^3.0.52",
"@ai-sdk/openai-compatible": "^2.0.38",
"@aws-sdk/credential-providers": "^3.922.0",
"@clinebot/shared": "workspace:*",
"@langfuse/otel": "^4.0.0",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/sdk-trace-node": "^2.6.1",
"@streamparser/json": "^0.0.21",
"ai": "^6.0.144",
"ai-sdk-provider-claude-code": "^3.4.3",
"ai-sdk-provider-codex-cli": "^1.1.0",
"ai-sdk-provider-opencode-sdk": "^3.0.1",
"dify-ai-provider": "^1.1.0",
"nanoid": "^5.1.7",
"zod": "^4.3.6"
},
"engines": {
"node": ">=20"
},
"peerDependencies": {
"@aws-sdk/client-bedrock-runtime": "^3.0.0"
},
"peerDependenciesMeta": {
"@aws-sdk/client-bedrock-runtime": {
"optional": true
}
}
},
"../sdk-wip/packages/shared": {
"name": "@clinebot/shared",
"version": "0.0.28",
"dependencies": {
"jsonrepair": "^3.13.2",
"zod": "^4.3.6",
"zod-to-json-schema": "^3.25.1"
},
"engines": {
"node": ">=20"
},
"optionalDependencies": {
"better-sqlite3": "^12.8.0"
}
},
"cli": {
"name": "cline",
"version": "2.15.0",
@@ -2184,6 +2287,22 @@
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"license": "MIT"
},
"node_modules/@clinebot/agents": {
"resolved": "../sdk-wip/packages/agents",
"link": true
},
"node_modules/@clinebot/core": {
"resolved": "../sdk-wip/packages/core",
"link": true
},
"node_modules/@clinebot/llms": {
"resolved": "../sdk-wip/packages/llms",
"link": true
},
"node_modules/@clinebot/shared": {
"resolved": "../sdk-wip/packages/shared",
"link": true
},
"node_modules/@colors/colors": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
+4
View File
@@ -506,6 +506,10 @@
"typescript": "^5.4.5"
},
"dependencies": {
"@clinebot/core": "file:../sdk-wip/packages/core",
"@clinebot/llms": "file:../sdk-wip/packages/llms",
"@clinebot/shared": "file:../sdk-wip/packages/shared",
"@clinebot/agents": "file:../sdk-wip/packages/agents",
"@anthropic-ai/sdk": "^0.37.0",
"@anthropic-ai/vertex-sdk": "^0.6.4",
"@aws-sdk/client-bedrock-runtime": "^3.922.0",
+24 -1
View File
@@ -65,7 +65,30 @@ underlying patterns that caused them are relevant.
## New Issues
*Add issues found during this attempt below this line.*
### Step 1: Foundation & Cutover — Completed
- **Status**: 🟢 Verified Fixed
- **Description**: SDK adapter layer created as single entry point. Extension compiles and builds.
- **Verification**: `npx tsc --noEmit` returns 0 errors. `node esbuild.mjs` produces `dist/extension.js`.
- **Evidence**: Commit `3dec59fe9` on `sdk-migration-v3` branch.
### S1-1: SdkController stubs log warnings at runtime
- **Status**: 🟡 Minor
- **Description**: All unimplemented Controller methods log `[SdkController] STUB: <name> not yet implemented`. This is expected — functionality is added in Steps 4-8.
- **Root cause**: By design — stub pattern for incremental migration.
- **Fix**: Implement each method in its corresponding step.
### S1-2: Services not initialized (mcpHub, authService, etc.)
- **Status**: 🟡 Minor
- **Description**: The SdkController sets `mcpHub`, `accountService`, `authService`, `ocaAuthService` to `undefined`. Handler modules that access these will throw "not available" errors at runtime.
- **Root cause**: Services will be properly initialized in Steps 6-7.
- **Fix**: Wire up services as part of their respective migration steps.
### S1-3: Extension loads but sidebar shows errors
- **Status**: 🟢 Verified Fixed
- **Description**: Extension loads, sidebar renders correctly with full UI (chat input, model selector, announcements, auto-approve settings). No error elements in the webview.
- **Verification**: Debug harness launched with `--auto-launch`, sidebar opened, `document.querySelectorAll("[data-testid=error], .error, .codicon-error").length` returns 0. Sending a message via `ui.send_message` returns `{"sent": true, "method": "newTask"}` without crash. Task doesn't start (expected — `initTask` is a stub).
- **Evidence**: Debug harness session on 2026-04-13, commit `3dec59fe9`.
<!-- Template:
### [ID] Title
@@ -32,7 +32,7 @@ export async function getOrganizationCredits(
balance: balanceData ? { currentBalance: balanceData.balance / 100 } : { currentBalance: 0 },
organizationId: balanceData?.organizationId || "",
usageTransactions:
usageTransactions?.map((tx) =>
usageTransactions?.map((tx: any) =>
OrganizationUsageTransaction.create({
aiInferenceProviderName: tx.aiInferenceProviderName,
aiModelName: tx.aiModelName,
@@ -19,7 +19,7 @@ export async function getUserOrganizations(controller: Controller, _request: Emp
return UserOrganizationsResponse.create({
organizations:
organizations?.map((org) =>
organizations?.map((org: any) =>
UserOrganization.create({
active: org.active,
memberId: org.memberId,
@@ -25,7 +25,7 @@ export async function openFocusChainFile(controller: Controller, request: String
const lastProgressMessage = clineMessages
.slice()
.reverse()
.find((m) => m.say === "task_progress")
.find((m: any) => m.say === "task_progress")
if (lastProgressMessage && lastProgressMessage.text) {
initialFocusChainContent = extractFocusChainListFromText(lastProgressMessage.text) || undefined
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,197 @@
// Extracted from classic src/core/controller/index.ts (see origin/main)
//
// Standalone function to build ExtensionState from a Controller instance.
// This allows the SdkController to reuse the classic state-building logic
// without inheriting the entire classic Controller implementation.
import { getHooksEnabledSafe } from "@core/hooks/hooks-utils"
import type { ExtensionState, Platform } from "@shared/ExtensionMessage"
import { ClineEnv } from "@/config"
import { ExtensionRegistryInfo } from "@/registry"
import { BannerService } from "@/services/banner/BannerService"
import { featureFlagsService } from "@/services/feature-flags"
import { getDistinctId } from "@/services/logging/distinctId"
import { getLatestAnnouncementId } from "@/utils/announcements"
import { getClineOnboardingModels } from "../models/getClineOnboardingModels"
/**
* Builds the ExtensionState object to push to the webview.
* Extracted from the classic Controller.getStateToPostToWebview().
*/
export async function getStateToPostToWebview(controller: {
task?: any
stateManager: any
mcpHub?: any
backgroundCommandRunning?: boolean
backgroundCommandTaskId?: string
workspaceManager?: any
}): Promise<ExtensionState> {
const stateManager = controller.stateManager
// Get API configuration from cache for immediate access
const onboardingModels = getClineOnboardingModels()
const apiConfiguration = stateManager.getApiConfiguration()
const lastShownAnnouncementId = stateManager.getGlobalStateKey("lastShownAnnouncementId")
const taskHistory = stateManager.getGlobalStateKey("taskHistory")
const autoApprovalSettings = stateManager.getGlobalSettingsKey("autoApprovalSettings")
const browserSettings = stateManager.getGlobalSettingsKey("browserSettings")
const focusChainSettings = stateManager.getGlobalSettingsKey("focusChainSettings")
const preferredLanguage = stateManager.getGlobalSettingsKey("preferredLanguage")
const mode = stateManager.getGlobalSettingsKey("mode")
const strictPlanModeEnabled = stateManager.getGlobalSettingsKey("strictPlanModeEnabled")
const yoloModeToggled = stateManager.getGlobalSettingsKey("yoloModeToggled")
const useAutoCondense = stateManager.getGlobalSettingsKey("useAutoCondense")
const subagentsEnabled = stateManager.getGlobalSettingsKey("subagentsEnabled")
const userInfo = stateManager.getGlobalStateKey("userInfo")
const mcpMarketplaceEnabled = stateManager.getGlobalStateKey("mcpMarketplaceEnabled")
const mcpDisplayMode = stateManager.getGlobalStateKey("mcpDisplayMode")
const telemetrySetting = stateManager.getGlobalSettingsKey("telemetrySetting")
const planActSeparateModelsSetting = stateManager.getGlobalSettingsKey("planActSeparateModelsSetting")
const enableCheckpointsSetting = stateManager.getGlobalSettingsKey("enableCheckpointsSetting")
const globalClineRulesToggles = stateManager.getGlobalStateKey("globalClineRulesToggles")
const globalWorkflowToggles = stateManager.getGlobalStateKey("globalWorkflowToggles")
const globalSkillsToggles = stateManager.getGlobalStateKey("globalSkillsToggles")
const localSkillsToggles = stateManager.getWorkspaceStateKey("localSkillsToggles")
const remoteRulesToggles = stateManager.getGlobalStateKey("remoteRulesToggles")
const remoteWorkflowToggles = stateManager.getGlobalStateKey("remoteWorkflowToggles")
const shellIntegrationTimeout = stateManager.getGlobalSettingsKey("shellIntegrationTimeout")
const terminalReuseEnabled = stateManager.getGlobalStateKey("terminalReuseEnabled")
const vscodeTerminalExecutionMode = stateManager.getGlobalStateKey("vscodeTerminalExecutionMode")
const defaultTerminalProfile = stateManager.getGlobalSettingsKey("defaultTerminalProfile")
const isNewUser = stateManager.getGlobalStateKey("isNewUser")
const welcomeViewCompleted = !!stateManager.getGlobalStateKey("welcomeViewCompleted")
const customPrompt = stateManager.getGlobalSettingsKey("customPrompt")
const mcpResponsesCollapsed = stateManager.getGlobalStateKey("mcpResponsesCollapsed")
const terminalOutputLineLimit = stateManager.getGlobalSettingsKey("terminalOutputLineLimit")
const maxConsecutiveMistakes = stateManager.getGlobalSettingsKey("maxConsecutiveMistakes")
const favoritedModelIds = stateManager.getGlobalStateKey("favoritedModelIds")
const lastDismissedInfoBannerVersion = stateManager.getGlobalStateKey("lastDismissedInfoBannerVersion") || 0
const lastDismissedModelBannerVersion = stateManager.getGlobalStateKey("lastDismissedModelBannerVersion") || 0
const lastDismissedCliBannerVersion = stateManager.getGlobalStateKey("lastDismissedCliBannerVersion") || 0
const dismissedBanners = stateManager.getGlobalStateKey("dismissedBanners")
const doubleCheckCompletionEnabled = stateManager.getGlobalSettingsKey("doubleCheckCompletionEnabled")
const lazyTeammateModeEnabled = stateManager.getGlobalSettingsKey("lazyTeammateModeEnabled")
const showFeatureTips = stateManager.getGlobalSettingsKey("showFeatureTips")
const localClineRulesToggles = stateManager.getWorkspaceStateKey("localClineRulesToggles")
const localWindsurfRulesToggles = stateManager.getWorkspaceStateKey("localWindsurfRulesToggles")
const localCursorRulesToggles = stateManager.getWorkspaceStateKey("localCursorRulesToggles")
const localAgentsRulesToggles = stateManager.getWorkspaceStateKey("localAgentsRulesToggles")
const workflowToggles = stateManager.getWorkspaceStateKey("workflowToggles")
const currentTaskItem = controller.task?.taskId
? (taskHistory || []).find((item: any) => item.id === controller.task?.taskId)
: undefined
const clineMessages = [...(controller.task?.messageStateHandler?.getClineMessages?.() || [])]
const checkpointManagerErrorMessage = controller.task?.taskState?.checkpointManagerErrorMessage
const processedTaskHistory = (taskHistory || [])
.filter((item: any) => item.ts && item.task)
.sort((a: any, b: any) => b.ts - a.ts)
.slice(0, 100)
const latestAnnouncementId = getLatestAnnouncementId()
const shouldShowAnnouncement = lastShownAnnouncementId !== latestAnnouncementId
const platform = process.platform as Platform
const distinctId = getDistinctId()
const version = ExtensionRegistryInfo.version
const clineConfig = ClineEnv.config()
const environment = clineConfig.environment
const banners = BannerService.get().getActiveBanners() ?? []
const welcomeBanners = BannerService.get().getWelcomeBanners() ?? []
// Check OpenAI Codex authentication status
let openAiCodexIsAuthenticated = false
try {
const { openAiCodexOAuthManager } = await import("@/integrations/openai-codex/oauth")
openAiCodexIsAuthenticated = await openAiCodexOAuthManager.isAuthenticated()
} catch {
// Codex OAuth not available
}
return {
version,
apiConfiguration,
currentTaskItem,
clineMessages,
currentFocusChainChecklist: controller.task?.taskState?.currentFocusChainChecklist || null,
checkpointManagerErrorMessage,
autoApprovalSettings,
browserSettings,
focusChainSettings,
preferredLanguage,
mode,
strictPlanModeEnabled,
yoloModeToggled,
useAutoCondense,
subagentsEnabled,
userInfo,
mcpMarketplaceEnabled,
mcpDisplayMode,
telemetrySetting,
planActSeparateModelsSetting,
enableCheckpointsSetting: enableCheckpointsSetting ?? true,
platform,
environment,
distinctId,
globalClineRulesToggles: globalClineRulesToggles || {},
localClineRulesToggles: localClineRulesToggles || {},
localWindsurfRulesToggles: localWindsurfRulesToggles || {},
localCursorRulesToggles: localCursorRulesToggles || {},
localAgentsRulesToggles: localAgentsRulesToggles || {},
localWorkflowToggles: workflowToggles || {},
globalWorkflowToggles: globalWorkflowToggles || {},
globalSkillsToggles: globalSkillsToggles || {},
localSkillsToggles: localSkillsToggles || {},
remoteRulesToggles,
remoteWorkflowToggles,
shellIntegrationTimeout,
terminalReuseEnabled,
vscodeTerminalExecutionMode,
defaultTerminalProfile,
isNewUser,
welcomeViewCompleted,
onboardingModels,
mcpResponsesCollapsed,
terminalOutputLineLimit,
maxConsecutiveMistakes,
customPrompt,
taskHistory: processedTaskHistory,
shouldShowAnnouncement,
favoritedModelIds,
backgroundCommandRunning: controller.backgroundCommandRunning ?? false,
backgroundCommandTaskId: controller.backgroundCommandTaskId,
workspaceRoots: controller.workspaceManager?.getRoots?.() ?? [],
primaryRootIndex: controller.workspaceManager?.getPrimaryIndex?.() ?? 0,
isMultiRootWorkspace: (controller.workspaceManager?.getRoots?.()?.length ?? 0) > 1,
multiRootSetting: {
user: stateManager.getGlobalStateKey("multiRootEnabled"),
featureFlag: true,
},
clineWebToolsEnabled: {
user: stateManager.getGlobalSettingsKey("clineWebToolsEnabled"),
featureFlag: featureFlagsService.getWebtoolsEnabled(),
},
worktreesEnabled: {
user: stateManager.getGlobalSettingsKey("worktreesEnabled"),
featureFlag: featureFlagsService.getWorktreesEnabled(),
},
hooksEnabled: getHooksEnabledSafe(stateManager.getGlobalSettingsKey("hooksEnabled")),
lastDismissedInfoBannerVersion,
lastDismissedModelBannerVersion,
remoteConfigSettings: stateManager.getRemoteConfigSettings?.(),
lastDismissedCliBannerVersion,
dismissedBanners,
nativeToolCallSetting: stateManager.getGlobalStateKey("nativeToolCallEnabled"),
enableParallelToolCalling: stateManager.getGlobalSettingsKey("enableParallelToolCalling"),
backgroundEditEnabled: stateManager.getGlobalSettingsKey("backgroundEditEnabled"),
optOutOfRemoteConfig: stateManager.getGlobalSettingsKey("optOutOfRemoteConfig"),
doubleCheckCompletionEnabled,
lazyTeammateModeEnabled,
showFeatureTips,
banners,
welcomeBanners,
openAiCodexIsAuthenticated,
} as ExtensionState
}
+240
View File
@@ -0,0 +1,240 @@
// Replaces classic src/core/controller/index.ts (see origin/main)
//
// This is the SDK-backed Controller. It provides the same interface as the
// classic Controller but delegates to the Cline SDK (@clinebot/core).
//
// During migration, unimplemented methods log warnings. The extension will
// compile and load, but functionality is added incrementally in Steps 4-8.
import type { ModelInfo } from "@shared/api"
import type { ChatContent } from "@shared/ChatContent"
import type { ExtensionState } from "@shared/ExtensionMessage"
import type { HistoryItem } from "@shared/HistoryItem"
import type { McpMarketplaceCatalog } from "@shared/mcp"
import type { Settings } from "@shared/storage/state-keys"
import type { Mode } from "@shared/storage/types"
import type { TelemetrySetting } from "@shared/TelemetrySetting"
import type { UserInfo } from "@shared/UserInfo"
import { StateManager } from "@/core/storage/StateManager"
import { ClineExtensionContext } from "@/shared/cline"
import { Logger } from "@/shared/services/Logger"
/**
* Log a stub warning and return undefined.
*/
function stubWarn(name: string): void {
Logger.warn(`[SdkController] STUB: ${name} not yet implemented`)
}
export class Controller {
task?: any // Will be typed to SDK session when implemented in Step 4
mcpHub: any // Replaced by SDK MCP manager in Step 7
accountService: any // Replaced by SDK account service in Step 6
authService: any // Replaced by SDK auth in Step 6
ocaAuthService: any // Replaced by SDK auth in Step 6
readonly stateManager: StateManager
// Private state kept for stub compatibility
private backgroundCommandRunning = false
private backgroundCommandTaskId?: string
constructor(readonly context: ClineExtensionContext) {
// StateManager must be initialized before creating the Controller
this.stateManager = StateManager.get()
// Services will be properly initialized in later steps.
// For now, set to undefined so the extension compiles and loads.
this.mcpHub = undefined
this.accountService = undefined
this.authService = undefined
this.ocaAuthService = undefined
Logger.log("[SdkController] Initialized with SDK adapter layer")
}
async dispose(): Promise<void> {
await this.clearTask()
this.mcpHub?.dispose?.()
Logger.log("[SdkController] Disposed")
}
async handleSignOut(): Promise<void> {
stubWarn("handleSignOut")
await this.postStateToWebview()
}
async handleOcaSignOut(): Promise<void> {
stubWarn("handleOcaSignOut")
await this.postStateToWebview()
}
async setUserInfo(_info?: UserInfo): Promise<void> {
stubWarn("setUserInfo")
}
// ---- Task lifecycle (Step 4) ----
async initTask(
_task?: string,
_images?: string[],
_files?: string[],
_historyItem?: HistoryItem,
_taskSettings?: Partial<Settings>,
): Promise<string | undefined> {
stubWarn("initTask")
return undefined
}
async reinitExistingTaskFromId(_taskId: string): Promise<void> {
stubWarn("reinitExistingTaskFromId")
}
async cancelTask(): Promise<void> {
stubWarn("cancelTask")
}
async cancelBackgroundCommand(): Promise<void> {
stubWarn("cancelBackgroundCommand")
}
async clearTask(): Promise<void> {
stubWarn("clearTask")
}
async handleTaskCreation(_prompt: string): Promise<void> {
stubWarn("handleTaskCreation")
}
// ---- Mode switching (Step 8) ----
async toggleActModeForYoloMode(): Promise<boolean> {
stubWarn("toggleActModeForYoloMode")
return false
}
async togglePlanActMode(_modeToSwitchTo: Mode, _chatContent?: ChatContent): Promise<boolean> {
stubWarn("togglePlanActMode")
return false
}
// ---- Telemetry ----
async updateTelemetrySetting(telemetrySetting: TelemetrySetting): Promise<void> {
await this.stateManager.setGlobalState("telemetrySetting", telemetrySetting as any)
await this.postStateToWebview()
}
// ---- Auth callbacks (Step 6) ----
async handleAuthCallback(_customToken: string, _provider: string | null = null): Promise<void> {
stubWarn("handleAuthCallback")
await this.postStateToWebview()
}
async handleOcaAuthCallback(_code: string, _state: string): Promise<void> {
stubWarn("handleOcaAuthCallback")
await this.postStateToWebview()
}
async handleMcpOAuthCallback(_serverHash: string, _code: string, _state: string | null): Promise<void> {
stubWarn("handleMcpOAuthCallback")
}
// ---- MCP marketplace (Step 7) ----
async refreshMcpMarketplace(_sendCatalogEvent: boolean): Promise<McpMarketplaceCatalog | undefined> {
stubWarn("refreshMcpMarketplace")
return undefined
}
// ---- Provider auth callbacks (Step 6) ----
async handleOpenRouterCallback(_code: string): Promise<void> {
stubWarn("handleOpenRouterCallback")
}
async handleRequestyCallback(_code: string): Promise<void> {
stubWarn("handleRequestyCallback")
}
async handleHicapCallback(_code: string): Promise<void> {
stubWarn("handleHicapCallback")
}
async readOpenRouterModels(): Promise<Record<string, ModelInfo> | undefined> {
stubWarn("readOpenRouterModels")
return undefined
}
// ---- Task history (Step 4) ----
async getTaskWithId(_id: string): Promise<{
historyItem: HistoryItem
taskDirPath: string
apiConversationHistoryFilePath: string
uiMessagesFilePath: string
contextHistoryFilePath: string
taskMetadataFilePath: string
apiConversationHistory: any[]
}> {
stubWarn("getTaskWithId")
return undefined as any
}
async exportTaskWithId(_id: string): Promise<void> {
stubWarn("exportTaskWithId")
}
async deleteTaskFromState(_id: string): Promise<HistoryItem[]> {
stubWarn("deleteTaskFromState")
return []
}
async updateTaskHistory(_item: HistoryItem): Promise<HistoryItem[]> {
stubWarn("updateTaskHistory")
return []
}
// ---- Background command state ----
updateBackgroundCommandState(running: boolean, taskId?: string): void {
this.backgroundCommandRunning = running
this.backgroundCommandTaskId = taskId
}
// ---- State management ----
async postStateToWebview(): Promise<void> {
// Import dynamically to avoid circular deps
const { sendStateUpdate } = await import("@core/controller/state/subscribeToState")
const state = await this.getStateToPostToWebview()
await sendStateUpdate(state)
}
async getStateToPostToWebview(): Promise<ExtensionState> {
// Delegate to the classic implementation which reads from StateManager.
// This will be gradually replaced with SDK-sourced state in Steps 4-8.
// For now, we import the classic getStateToPostToWebview logic.
try {
const { getStateToPostToWebview: classicGetState } = await import("@core/controller/state/getStateToPostToWebview")
return await classicGetState({
task: this.task,
stateManager: this.stateManager,
mcpHub: this.mcpHub,
backgroundCommandRunning: this.backgroundCommandRunning,
backgroundCommandTaskId: this.backgroundCommandTaskId,
})
} catch {
// Fallback: return minimal state if classic impl not available
return {} as unknown as ExtensionState
}
}
// ---- Workspace (kept from classic) ----
async ensureWorkspaceManager(): Promise<any> {
stubWarn("ensureWorkspaceManager")
return undefined
}
}
+8
View File
@@ -0,0 +1,8 @@
// SDK Adapter Layer
// Replaces classic src/core/controller/ (see origin/main)
//
// This module provides the SDK-backed Controller and related adapters.
// The webview continues to communicate via gRPC; this layer translates
// between gRPC handlers and SDK calls.
export { Controller } from "./SdkController"
+20
View File
@@ -0,0 +1,20 @@
import path from "node:path"
import { defineConfig } from "vitest/config"
export default defineConfig({
test: {
include: ["src/sdk/**/*.test.ts"],
environment: "node",
},
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
"@core": path.resolve(__dirname, "src/core"),
"@integrations": path.resolve(__dirname, "src/integrations"),
"@services": path.resolve(__dirname, "src/services"),
"@shared": path.resolve(__dirname, "src/shared"),
"@utils": path.resolve(__dirname, "src/utils"),
"@packages": path.resolve(__dirname, "src/packages"),
},
},
})