Просмотр исходного кода

Merge branch 'dev' into sqlite2

Dax Raad 2 месяцев назад
Родитель
Сommit
34ebb3d051

Разница между файлами не показана из-за своего большого размера
+ 200 - 206
bun.lock


+ 1 - 1
packages/app/src/components/session/session-header.tsx

@@ -130,7 +130,7 @@ export function SessionHeader() {
           <Portal mount={mount()}>
             <button
               type="button"
-              class="hidden md:flex w-[320px] p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus-visible:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
+              class="hidden md:flex w-[320px] max-w-full min-w-0 p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus-visible:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
               onClick={() => command.trigger("file.open")}
               aria-label={language.t("session.header.searchFiles")}
             >

+ 23 - 12
packages/app/src/components/titlebar.tsx

@@ -132,12 +132,14 @@ export function Titlebar() {
   }
 
   return (
-    <header class="h-10 shrink-0 bg-background-base flex items-center relative" data-tauri-drag-region>
+    <header
+      class="h-10 shrink-0 bg-background-base relative grid grid-cols-[auto_minmax(0,1fr)_auto] items-center"
+      data-tauri-drag-region
+    >
       <div
         classList={{
-          "flex items-center w-full min-w-0": true,
+          "flex items-center min-w-0": true,
           "pl-2": !mac(),
-          "pr-6": !windows(),
         }}
         onMouseDown={drag}
         data-tauri-drag-region
@@ -218,20 +220,29 @@ export function Titlebar() {
           </div>
         </div>
         <div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" data-tauri-drag-region />
-        <div class="flex-1 h-full" data-tauri-drag-region />
-        <div
-          id="opencode-titlebar-right"
-          class="flex items-center gap-3 shrink-0 flex-1 justify-end"
-          data-tauri-drag-region
-        />
+      </div>
+
+      <div
+        class="min-w-0 flex items-center justify-center pointer-events-none lg:absolute lg:inset-0 lg:flex lg:items-center lg:justify-center"
+        data-tauri-drag-region
+      >
+        <div id="opencode-titlebar-center" class="pointer-events-auto w-full min-w-0 flex justify-center lg:w-fit" />
+      </div>
+
+      <div
+        classList={{
+          "flex items-center min-w-0 justify-end": true,
+          "pr-6": !windows(),
+        }}
+        onMouseDown={drag}
+        data-tauri-drag-region
+      >
+        <div id="opencode-titlebar-right" class="flex items-center gap-3 shrink-0 justify-end" data-tauri-drag-region />
         <Show when={windows()}>
           <div class="w-6 shrink-0" />
           <div data-tauri-decorum-tb class="flex flex-row" />
         </Show>
       </div>
-      <div class="absolute inset-0 flex items-center justify-center pointer-events-none">
-        <div id="opencode-titlebar-center" class="pointer-events-auto" />
-      </div>
     </header>
   )
 }

+ 1 - 1
packages/opencode/package.json

@@ -52,7 +52,7 @@
   "dependencies": {
     "@actions/core": "1.11.1",
     "@actions/github": "6.0.1",
-    "@agentclientprotocol/sdk": "0.12.0",
+    "@agentclientprotocol/sdk": "0.13.0",
     "@ai-sdk/amazon-bedrock": "3.0.73",
     "@ai-sdk/anthropic": "2.0.57",
     "@ai-sdk/azure": "2.0.91",

+ 186 - 56
packages/opencode/src/acp/agent.ts

@@ -26,6 +26,7 @@ import {
   type ToolCallContent,
   type ToolKind,
 } from "@agentclientprotocol/sdk"
+
 import { Log } from "../util/log"
 import { ACPSessionManager } from "./session"
 import type { ACPConfig } from "./types"
@@ -40,6 +41,11 @@ import { LoadAPIKeyError } from "ai"
 import type { Event, OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2"
 import { applyPatch } from "diff"
 
+type ModeOption = { id: string; name: string; description?: string }
+type ModelOption = { modelId: string; name: string }
+
+const DEFAULT_VARIANT_VALUE = "default"
+
 export namespace ACP {
   const log = Log.create({ service: "acp-agent" })
 
@@ -476,7 +482,7 @@ export namespace ACP {
           sessionId,
           models: load.models,
           modes: load.modes,
-          _meta: {},
+          _meta: load._meta,
         }
       } catch (e) {
         const error = MessageV2.fromError(e, {
@@ -529,7 +535,7 @@ export namespace ACP {
             providerID: lastUser.model.providerID,
             modelID: lastUser.model.modelID,
           })
-          if (result.modes.availableModes.some((m) => m.id === lastUser.agent)) {
+          if (result.modes?.availableModes.some((m) => m.id === lastUser.agent)) {
             result.modes.currentModeId = lastUser.agent
             this.sessionManager.setMode(sessionId, lastUser.agent)
           }
@@ -956,27 +962,7 @@ export namespace ACP {
       }
     }
 
-    private async loadSessionMode(params: LoadSessionRequest) {
-      const directory = params.cwd
-      const model = await defaultModel(this.config, directory)
-      const sessionId = params.sessionId
-
-      const providers = await this.sdk.config.providers({ directory }).then((x) => x.data!.providers)
-      const entries = providers.sort((a, b) => {
-        const nameA = a.name.toLowerCase()
-        const nameB = b.name.toLowerCase()
-        if (nameA < nameB) return -1
-        if (nameA > nameB) return 1
-        return 0
-      })
-      const availableModels = entries.flatMap((provider) => {
-        const models = Provider.sort(Object.values(provider.models))
-        return models.map((model) => ({
-          modelId: `${provider.id}/${model.id}`,
-          name: `${provider.name}/${model.name}`,
-        }))
-      })
-
+    private async loadAvailableModes(directory: string): Promise<ModeOption[]> {
       const agents = await this.config.sdk.app
         .agents(
           {
@@ -986,6 +972,56 @@ export namespace ACP {
         )
         .then((resp) => resp.data!)
 
+      return agents
+        .filter((agent) => agent.mode !== "subagent" && !agent.hidden)
+        .map((agent) => ({
+          id: agent.name,
+          name: agent.name,
+          description: agent.description,
+        }))
+    }
+
+    private async resolveModeState(
+      directory: string,
+      sessionId: string,
+    ): Promise<{ availableModes: ModeOption[]; currentModeId?: string }> {
+      const availableModes = await this.loadAvailableModes(directory)
+      const currentModeId =
+        this.sessionManager.get(sessionId).modeId ||
+        (await (async () => {
+          if (!availableModes.length) return undefined
+          const defaultAgentName = await AgentModule.defaultAgent()
+          const resolvedModeId =
+            availableModes.find((mode) => mode.name === defaultAgentName)?.id ?? availableModes[0].id
+          this.sessionManager.setMode(sessionId, resolvedModeId)
+          return resolvedModeId
+        })())
+
+      return { availableModes, currentModeId }
+    }
+
+    private async loadSessionMode(params: LoadSessionRequest) {
+      const directory = params.cwd
+      const model = await defaultModel(this.config, directory)
+      const sessionId = params.sessionId
+
+      const providers = await this.sdk.config.providers({ directory }).then((x) => x.data!.providers)
+      const entries = sortProvidersByName(providers)
+      const availableVariants = modelVariantsFromProviders(entries, model)
+      const currentVariant = this.sessionManager.getVariant(sessionId)
+      if (currentVariant && !availableVariants.includes(currentVariant)) {
+        this.sessionManager.setVariant(sessionId, undefined)
+      }
+      const availableModels = buildAvailableModels(entries, { includeVariants: true })
+      const modeState = await this.resolveModeState(directory, sessionId)
+      const currentModeId = modeState.currentModeId
+      const modes = currentModeId
+        ? {
+            availableModes: modeState.availableModes,
+            currentModeId,
+          }
+        : undefined
+
       const commands = await this.config.sdk.command
         .list(
           {
@@ -1006,20 +1042,6 @@ export namespace ACP {
           description: "compact the session",
         })
 
-      const availableModes = agents
-        .filter((agent) => agent.mode !== "subagent" && !agent.hidden)
-        .map((agent) => ({
-          id: agent.name,
-          name: agent.name,
-          description: agent.description,
-        }))
-
-      const defaultAgentName = await AgentModule.defaultAgent()
-      const currentModeId = availableModes.find((m) => m.name === defaultAgentName)?.id ?? availableModes[0].id
-
-      // Persist the default mode so prompt() uses it immediately
-      this.sessionManager.setMode(sessionId, currentModeId)
-
       const mcpServers: Record<string, Config.Mcp> = {}
       for (const server of params.mcpServers) {
         if ("type" in server) {
@@ -1073,40 +1095,46 @@ export namespace ACP {
       return {
         sessionId,
         models: {
-          currentModelId: `${model.providerID}/${model.modelID}`,
+          currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, true),
           availableModels,
         },
-        modes: {
-          availableModes,
-          currentModeId,
-        },
-        _meta: {},
+        modes,
+        _meta: buildVariantMeta({
+          model,
+          variant: this.sessionManager.getVariant(sessionId),
+          availableVariants,
+        }),
       }
     }
 
     async unstable_setSessionModel(params: SetSessionModelRequest) {
       const session = this.sessionManager.get(params.sessionId)
+      const providers = await this.sdk.config
+        .providers({ directory: session.cwd }, { throwOnError: true })
+        .then((x) => x.data!.providers)
 
-      const model = Provider.parseModel(params.modelId)
+      const selection = parseModelSelection(params.modelId, providers)
+      this.sessionManager.setModel(session.id, selection.model)
+      this.sessionManager.setVariant(session.id, selection.variant)
 
-      this.sessionManager.setModel(session.id, {
-        providerID: model.providerID,
-        modelID: model.modelID,
-      })
+      const entries = sortProvidersByName(providers)
+      const availableVariants = modelVariantsFromProviders(entries, selection.model)
 
       return {
-        _meta: {},
+        _meta: buildVariantMeta({
+          model: selection.model,
+          variant: selection.variant,
+          availableVariants,
+        }),
       }
     }
 
     async setSessionMode(params: SetSessionModeRequest): Promise<SetSessionModeResponse | void> {
-      this.sessionManager.get(params.sessionId)
-      await this.config.sdk.app
-        .agents({}, { throwOnError: true })
-        .then((x) => x.data)
-        .then((agent) => {
-          if (!agent) throw new Error(`Agent not found: ${params.modeId}`)
-        })
+      const session = this.sessionManager.get(params.sessionId)
+      const availableModes = await this.loadAvailableModes(session.cwd)
+      if (!availableModes.some((mode) => mode.id === params.modeId)) {
+        throw new Error(`Agent not found: ${params.modeId}`)
+      }
       this.sessionManager.setMode(params.sessionId, params.modeId)
     }
 
@@ -1223,6 +1251,7 @@ export namespace ACP {
             providerID: model.providerID,
             modelID: model.modelID,
           },
+          variant: this.sessionManager.getVariant(sessionID),
           parts,
           agent,
           directory,
@@ -1434,4 +1463,105 @@ export namespace ACP {
     }
     return result
   }
+
+  function sortProvidersByName<T extends { name: string }>(providers: T[]): T[] {
+    return [...providers].sort((a, b) => {
+      const nameA = a.name.toLowerCase()
+      const nameB = b.name.toLowerCase()
+      if (nameA < nameB) return -1
+      if (nameA > nameB) return 1
+      return 0
+    })
+  }
+
+  function modelVariantsFromProviders(
+    providers: Array<{ id: string; models: Record<string, { variants?: Record<string, any> }> }>,
+    model: { providerID: string; modelID: string },
+  ): string[] {
+    const provider = providers.find((entry) => entry.id === model.providerID)
+    if (!provider) return []
+    const modelInfo = provider.models[model.modelID]
+    if (!modelInfo?.variants) return []
+    return Object.keys(modelInfo.variants)
+  }
+
+  function buildAvailableModels(
+    providers: Array<{ id: string; name: string; models: Record<string, any> }>,
+    options: { includeVariants?: boolean } = {},
+  ): ModelOption[] {
+    const includeVariants = options.includeVariants ?? false
+    return providers.flatMap((provider) => {
+      const models = Provider.sort(Object.values(provider.models) as any)
+      return models.flatMap((model) => {
+        const base: ModelOption = {
+          modelId: `${provider.id}/${model.id}`,
+          name: `${provider.name}/${model.name}`,
+        }
+        if (!includeVariants || !model.variants) return [base]
+        const variants = Object.keys(model.variants).filter((variant) => variant !== DEFAULT_VARIANT_VALUE)
+        const variantOptions = variants.map((variant) => ({
+          modelId: `${provider.id}/${model.id}/${variant}`,
+          name: `${provider.name}/${model.name} (${variant})`,
+        }))
+        return [base, ...variantOptions]
+      })
+    })
+  }
+
+  function formatModelIdWithVariant(
+    model: { providerID: string; modelID: string },
+    variant: string | undefined,
+    availableVariants: string[],
+    includeVariant: boolean,
+  ) {
+    const base = `${model.providerID}/${model.modelID}`
+    if (!includeVariant || !variant || !availableVariants.includes(variant)) return base
+    return `${base}/${variant}`
+  }
+
+  function buildVariantMeta(input: {
+    model: { providerID: string; modelID: string }
+    variant?: string
+    availableVariants: string[]
+  }) {
+    return {
+      opencode: {
+        modelId: `${input.model.providerID}/${input.model.modelID}`,
+        variant: input.variant ?? null,
+        availableVariants: input.availableVariants,
+      },
+    }
+  }
+
+  function parseModelSelection(
+    modelId: string,
+    providers: Array<{ id: string; models: Record<string, { variants?: Record<string, any> }> }>,
+  ): { model: { providerID: string; modelID: string }; variant?: string } {
+    const parsed = Provider.parseModel(modelId)
+    const provider = providers.find((p) => p.id === parsed.providerID)
+    if (!provider) {
+      return { model: parsed, variant: undefined }
+    }
+
+    // Check if modelID exists directly
+    if (provider.models[parsed.modelID]) {
+      return { model: parsed, variant: undefined }
+    }
+
+    // Try to extract variant from end of modelID (e.g., "claude-sonnet-4/high" -> model: "claude-sonnet-4", variant: "high")
+    const segments = parsed.modelID.split("/")
+    if (segments.length > 1) {
+      const candidateVariant = segments[segments.length - 1]
+      const baseModelId = segments.slice(0, -1).join("/")
+      const baseModelInfo = provider.models[baseModelId]
+      if (baseModelInfo?.variants && candidateVariant in baseModelInfo.variants) {
+        return {
+          model: { providerID: parsed.providerID, modelID: baseModelId },
+          variant: candidateVariant,
+        }
+      }
+    }
+
+    return { model: parsed, variant: undefined }
+  }
 }

+ 12 - 0
packages/opencode/src/acp/session.ts

@@ -96,6 +96,18 @@ export class ACPSessionManager {
     return session
   }
 
+  getVariant(sessionId: string) {
+    const session = this.get(sessionId)
+    return session.variant
+  }
+
+  setVariant(sessionId: string, variant?: string) {
+    const session = this.get(sessionId)
+    session.variant = variant
+    this.sessions.set(sessionId, session)
+    return session
+  }
+
   setMode(sessionId: string, modeId: string) {
     const session = this.get(sessionId)
     session.modeId = modeId

+ 1 - 0
packages/opencode/src/acp/types.ts

@@ -10,6 +10,7 @@ export interface ACPSessionState {
     providerID: string
     modelID: string
   }
+  variant?: string
   modeId?: string
 }
 

Некоторые файлы не были показаны из-за большого количества измененных файлов