mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-30 17:14:40 +08:00
feat(agent-manager): assign models to workflows (#12729)
* feat(agent-manager): assign models to workflows * fix(agent-manager): address workflow model review findings * fix(cli): handle command source aliases safely * test(pty): allow slower macOS startup * test(cli): tolerate transient Windows response reads * test(cli): bound transient stall response retries * test(pty): include replayed output in wait helper * test(pty): stabilize buffered output waits * test(pty): avoid locale-dependent UTF-8 fixture
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@kilocode/cli": minor
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Configure a model and reasoning variant for each workflow from Agent Behaviour settings.
|
||||
@@ -11,6 +11,7 @@ export class Info extends Schema.Class<Info>("CommandV2.Info")({
|
||||
description: Schema.String.pipe(Schema.optional),
|
||||
agent: Schema.String.pipe(Schema.optional),
|
||||
model: ModelV2.Ref.pipe(Schema.optional),
|
||||
variant: ModelV2.VariantID.pipe(Schema.optional), // kilocode_change - support variant-only command overrides
|
||||
subtask: Schema.Boolean.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ export * as ConfigCommand from "./command"
|
||||
import { Schema } from "effect"
|
||||
|
||||
export class Info extends Schema.Class<Info>("ConfigV2.Command")({
|
||||
template: Schema.String,
|
||||
template: Schema.String.pipe(Schema.optional), // kilocode_change - allow partial command overrides
|
||||
description: Schema.String.pipe(Schema.optional),
|
||||
agent: Schema.String.pipe(Schema.optional),
|
||||
model: Schema.String.pipe(Schema.optional),
|
||||
|
||||
@@ -29,22 +29,33 @@ export const Plugin = PluginV2.define({
|
||||
}).pipe(Effect.map((documents) => documents.flat()))
|
||||
|
||||
yield* transform((editor) => {
|
||||
for (const document of documents) {
|
||||
for (const [name, command] of Object.entries(document.commands ?? {})) {
|
||||
editor.update(name, (item) => {
|
||||
item.template = command.template
|
||||
if (command.description !== undefined) item.description = command.description
|
||||
if (command.agent !== undefined) item.agent = command.agent
|
||||
if (command.model !== undefined) {
|
||||
const model = ModelV2.parse(command.model)
|
||||
item.model = { id: model.modelID, providerID: model.providerID, variant: item.model?.variant }
|
||||
}
|
||||
if (command.variant !== undefined && item.model !== undefined) {
|
||||
item.model.variant = ModelV2.VariantID.make(command.variant)
|
||||
}
|
||||
if (command.subtask !== undefined) item.subtask = command.subtask
|
||||
})
|
||||
const items = documents.flatMap((document) => Object.entries(document.commands ?? {}))
|
||||
// Register every template first, preserving the normal source priority for
|
||||
// metadata in the second pass. // kilocode_change
|
||||
for (const [name, command] of items) {
|
||||
if (command.template === undefined) {
|
||||
continue
|
||||
}
|
||||
const template = command.template
|
||||
editor.update(name, (item) => {
|
||||
item.template = template
|
||||
})
|
||||
}
|
||||
for (const [name, command] of items) {
|
||||
if (command.template === undefined && !editor.get(name)) continue // kilocode_change
|
||||
editor.update(name, (item) => {
|
||||
if (command.description !== undefined) item.description = command.description
|
||||
if (command.agent !== undefined) item.agent = command.agent
|
||||
if (command.model !== undefined) {
|
||||
const model = ModelV2.parse(command.model)
|
||||
item.model = { id: model.modelID, providerID: model.providerID, variant: item.model?.variant }
|
||||
}
|
||||
if (command.variant !== undefined) item.variant = ModelV2.VariantID.make(command.variant) // kilocode_change
|
||||
if (command.variant !== undefined && item.model !== undefined) {
|
||||
item.model.variant = ModelV2.VariantID.make(command.variant)
|
||||
}
|
||||
if (command.subtask !== undefined) item.subtask = command.subtask
|
||||
})
|
||||
}
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -3,7 +3,7 @@ export * as ConfigCommandV1 from "./command"
|
||||
import { Schema } from "effect"
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
template: Schema.String,
|
||||
template: Schema.optional(Schema.String), // kilocode_change - allow global workflow model/variant overrides
|
||||
description: Schema.optional(Schema.String),
|
||||
agent: Schema.optional(Schema.String),
|
||||
model: Schema.optional(Schema.String),
|
||||
|
||||
@@ -207,6 +207,7 @@ export const SubtaskPart = Schema.Struct({
|
||||
modelID: ModelV2.ID,
|
||||
}),
|
||||
),
|
||||
variant: Schema.optional(Schema.String), // kilocode_change - preserve workflow subtask variant
|
||||
command: Schema.optional(Schema.String),
|
||||
}).annotate({ identifier: "SubtaskPart" })
|
||||
export type SubtaskPart = Types.DeepMutable<Schema.Schema.Type<typeof SubtaskPart>>
|
||||
@@ -500,6 +501,7 @@ export const SubtaskPartInput = Schema.Struct({
|
||||
modelID: ModelV2.ID,
|
||||
}),
|
||||
),
|
||||
variant: Schema.optional(Schema.String), // kilocode_change - preserve workflow subtask variant
|
||||
command: Schema.optional(Schema.String),
|
||||
}).annotate({ identifier: "SubtaskPartInput" })
|
||||
export type SubtaskPartInput = Types.DeepMutable<Schema.Schema.Type<typeof SubtaskPartInput>>
|
||||
|
||||
@@ -69,6 +69,7 @@ Review files`,
|
||||
id: ModelV2.ID.make("claude"),
|
||||
variant: ModelV2.VariantID.make("high"),
|
||||
},
|
||||
variant: ModelV2.VariantID.make("high"),
|
||||
subtask: true,
|
||||
}),
|
||||
new CommandV2.Info({ name: "empty", template: "" }),
|
||||
@@ -78,4 +79,38 @@ Review files`,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("applies a global partial override after project command definitions", () =>
|
||||
Effect.gen(function* () {
|
||||
const command = yield* CommandV2.Service
|
||||
yield* ConfigCommandPlugin.Plugin.effect.pipe(
|
||||
Effect.provideService(CommandV2.Service, command),
|
||||
Effect.provideService(
|
||||
Config.Service,
|
||||
Config.Service.of({
|
||||
entries: () =>
|
||||
Effect.succeed([
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({ commands: { review: { model: "anthropic/claude", variant: "high" } } }),
|
||||
}),
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({ commands: { review: { template: "Review files" } } }),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
expect(yield* command.get("review")).toMatchObject({
|
||||
template: "Review files",
|
||||
model: {
|
||||
providerID: ProviderV2.ID.make("anthropic"),
|
||||
id: ModelV2.ID.make("claude"),
|
||||
variant: ModelV2.VariantID.make("high"),
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -11,6 +11,8 @@ import { testEffect } from "../lib/effect"
|
||||
|
||||
type PtyEvent = { type: "created" | "exited" | "deleted"; id: PtyID }
|
||||
|
||||
const PTY_TEST_TIMEOUT = "15 seconds" // kilocode_change - PTY startup can exceed the default test timeout on macOS CI
|
||||
|
||||
const locationLayer = Layer.succeed(
|
||||
Location.Service,
|
||||
Location.Service.of(location({ directory: AbsolutePath.make("/tmp") })),
|
||||
@@ -59,7 +61,7 @@ const waitForEvents = (events: Queue.Queue<PtyEvent>, id: PtyID, count: number)
|
||||
return picked
|
||||
}).pipe(
|
||||
Effect.timeoutOrElse({
|
||||
duration: "5 seconds",
|
||||
duration: PTY_TEST_TIMEOUT, // kilocode_change
|
||||
orElse: () => Effect.fail(new Error("timeout waiting for pty events")),
|
||||
}),
|
||||
)
|
||||
@@ -73,6 +75,7 @@ const attachCollecting = Effect.fn("PtySessionTest.attachCollecting")(function*
|
||||
onData: (chunk) => Queue.offerUnsafe(output, chunk),
|
||||
onEnd: (event) => Deferred.doneUnsafe(ended, Effect.succeed(event)),
|
||||
})
|
||||
if (attachment.replay) Queue.offerUnsafe(output, attachment.replay)
|
||||
attachment.activate()
|
||||
return { attachment, output, ended }
|
||||
})
|
||||
@@ -84,7 +87,7 @@ const waitForOutput = (output: Queue.Queue<string>, text: string) =>
|
||||
return received
|
||||
}).pipe(
|
||||
Effect.timeoutOrElse({
|
||||
duration: "5 seconds",
|
||||
duration: PTY_TEST_TIMEOUT, // kilocode_change
|
||||
orElse: () => Effect.fail(new Error(`timeout waiting for output containing ${JSON.stringify(text)}`)),
|
||||
}),
|
||||
)
|
||||
@@ -144,7 +147,7 @@ describe("pty", () => {
|
||||
Effect.gen(function* () {
|
||||
const pty = yield* Pty.Service
|
||||
const marker = "café-über-北京-🚀"
|
||||
const info = yield* createPty("sh", ["-c", `printf '${marker}\\n'`])
|
||||
const info = yield* createPty("sh", ["-c", "printf 'caf\\303\\251-\\303\\274ber-\\345\\214\\227\\344\\272\\254-\\360\\237\\232\\200\\n'"])
|
||||
const attached = yield* attachCollecting(info.id)
|
||||
expect(yield* waitForOutput(attached.output, marker)).toContain(marker)
|
||||
}),
|
||||
@@ -268,7 +271,7 @@ describe("pty", () => {
|
||||
attachment.write("ignored")
|
||||
yield* pty.remove(info.id)
|
||||
attachment.activate()
|
||||
expect(yield* Deferred.await(ended).pipe(Effect.timeout("5 seconds"))).toEqual({ exitCode: 7 })
|
||||
expect(yield* Deferred.await(ended).pipe(Effect.timeout(PTY_TEST_TIMEOUT))).toEqual({ exitCode: 7 })
|
||||
attachment.detach()
|
||||
}),
|
||||
)
|
||||
|
||||
+71
-1
@@ -4,10 +4,16 @@ import { IconButton } from "@kilocode/kilo-ui/icon-button"
|
||||
|
||||
import { useConfig } from "../../../context/config"
|
||||
import { useLanguage } from "../../../context/language"
|
||||
import { useProvider } from "../../../context/provider"
|
||||
import { ModelSelectorBase } from "../../shared/ModelSelector"
|
||||
import { ThinkingSelectorBase } from "../../shared/ThinkingSelector"
|
||||
import { parseModelString } from "../../../../../src/shared/provider-model"
|
||||
import type { CommandConfig } from "../../../types/messages"
|
||||
|
||||
const WorkflowsTab: Component = () => {
|
||||
const language = useLanguage()
|
||||
const { config } = useConfig()
|
||||
const { config, globalConfig, globalDraft, updateGlobalConfig } = useConfig()
|
||||
const provider = useProvider()
|
||||
|
||||
const cmds = createMemo(() => Object.entries(config().command ?? {}))
|
||||
const [expanded, setExpanded] = createSignal<Record<string, boolean>>({})
|
||||
@@ -16,6 +22,38 @@ const WorkflowsTab: Component = () => {
|
||||
setExpanded((prev) => ({ ...prev, [name]: !prev[name] }))
|
||||
}
|
||||
|
||||
const update = (name: string, patch: Partial<CommandConfig>) => {
|
||||
updateGlobalConfig({ command: { [name]: patch } })
|
||||
}
|
||||
|
||||
const scoped = (cmd: CommandConfig, name: string) => ({
|
||||
...cmd,
|
||||
...globalConfig().command?.[name],
|
||||
...globalDraft().command?.[name],
|
||||
})
|
||||
|
||||
const model = (cmd: CommandConfig, name: string) => {
|
||||
const value = scoped(cmd, name).model
|
||||
return value === null ? null : parseModelString(value ?? undefined)
|
||||
}
|
||||
|
||||
const variant = (cmd: CommandConfig, name: string) => {
|
||||
const value = scoped(cmd, name).variant
|
||||
return value === null ? undefined : (value ?? undefined)
|
||||
}
|
||||
|
||||
const variants = (cmd: CommandConfig, name: string) =>
|
||||
Object.keys(provider.findModel(model(cmd, name))?.variants ?? {})
|
||||
|
||||
const selectModel = (name: string, providerID: string, modelID: string) => {
|
||||
const list = Object.keys(provider.findModel({ providerID, modelID })?.variants ?? {})
|
||||
const current = variant(config().command?.[name] ?? {}, name)
|
||||
update(name, {
|
||||
model: providerID && modelID ? `${providerID}/${modelID}` : null,
|
||||
...(current && !list.includes(current) ? { variant: null } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Description */}
|
||||
@@ -118,6 +156,38 @@ const WorkflowsTab: Component = () => {
|
||||
{cmd.description}
|
||||
</div>
|
||||
</Show>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
"flex-wrap": "wrap",
|
||||
gap: "8px",
|
||||
"margin-bottom": "8px",
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ModelSelectorBase
|
||||
value={model(cmd, name)}
|
||||
onSelect={(providerID, modelID) => selectModel(name, providerID, modelID)}
|
||||
placement="bottom-start"
|
||||
allowClear
|
||||
clearLabel={language.t("settings.providers.notSet")}
|
||||
label={`${name} ${language.t("settings.agentBehaviour.workflows.model")}`}
|
||||
description={language.t("settings.agentBehaviour.workflows.modelDescription")}
|
||||
/>
|
||||
<Show when={variants(cmd, name).length > 0 || !!variant(cmd, name)}>
|
||||
<ThinkingSelectorBase
|
||||
variants={variants(cmd, name)}
|
||||
value={variant(cmd, name)}
|
||||
onSelect={(variant) => update(name, { variant })}
|
||||
onClear={() => update(name, { variant: null })}
|
||||
allowClear
|
||||
clearLabel={language.t("settings.providers.notSet")}
|
||||
placement="bottom-start"
|
||||
globalTrigger={false}
|
||||
label={`${name} ${language.t("settings.agentBehaviour.workflows.variant")}`}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={cmd.template}>
|
||||
<div>
|
||||
<span style={{ "font-weight": "500" }}>
|
||||
|
||||
@@ -43,6 +43,8 @@ export interface ThinkingSelectorBaseProps {
|
||||
globalTrigger?: boolean
|
||||
/** Show the Shift+Tab cycle hint in the trigger tooltip. */
|
||||
cycleHint?: boolean
|
||||
/** Accessible name for the selector trigger. */
|
||||
label?: string
|
||||
}
|
||||
|
||||
export const ThinkingSelectorBase: Component<ThinkingSelectorBaseProps> = (props) => {
|
||||
@@ -163,7 +165,7 @@ export const ThinkingSelectorBase: Component<ThinkingSelectorBaseProps> = (props
|
||||
open={open()}
|
||||
onOpenChange={onOpen}
|
||||
triggerAs={Button}
|
||||
triggerProps={{ variant: "ghost", size: "small" }}
|
||||
triggerProps={{ variant: "ghost", size: "small", "aria-label": props.label }}
|
||||
trigger={
|
||||
<>
|
||||
<span class="thinking-selector-trigger-label">{display(props.value)}</span>
|
||||
|
||||
@@ -40,6 +40,7 @@ export interface SaveError {
|
||||
interface ConfigContextValue {
|
||||
config: Accessor<Config>
|
||||
globalConfig: Accessor<Config>
|
||||
globalDraft: Accessor<Partial<Config>>
|
||||
projectConfig: Accessor<Config>
|
||||
collections: Accessor<ConfigCollections>
|
||||
settings: Accessor<Record<string, unknown>>
|
||||
@@ -397,6 +398,7 @@ export const ConfigProvider: ParentComponent = (props) => {
|
||||
const value: ConfigContextValue = {
|
||||
config,
|
||||
globalConfig,
|
||||
globalDraft,
|
||||
projectConfig,
|
||||
collections,
|
||||
settings,
|
||||
|
||||
+3
@@ -1001,6 +1001,9 @@ export const dict = {
|
||||
"settings.agentBehaviour.workflows.empty": "لم يتم تهيئة أوامر مخصصة. أضف أوامر إلى opencode.json لرؤيتها هنا.",
|
||||
"settings.agentBehaviour.workflows.detail.description": "الوصف",
|
||||
"settings.agentBehaviour.workflows.detail.template": "القالب",
|
||||
"settings.agentBehaviour.workflows.model": "النموذج",
|
||||
"settings.agentBehaviour.workflows.variant": "المتغير",
|
||||
"settings.agentBehaviour.workflows.modelDescription": "تجاوز النموذج العام",
|
||||
"settings.sandboxing.enabled.title": "Sandbox",
|
||||
"settings.sandboxing.enabled.description":
|
||||
"تشغيل أوامر shell الخاصة بالوكيل داخل sandbox على مستوى نظام التشغيل يقيّد الكتابة على مجلدات حالة المشروع و Kilo",
|
||||
|
||||
+3
@@ -1037,6 +1037,9 @@ export const dict = {
|
||||
"Nenhum comando personalizado configurado. Adicione comandos ao opencode.json para vê-los aqui.",
|
||||
"settings.agentBehaviour.workflows.detail.description": "Descrição",
|
||||
"settings.agentBehaviour.workflows.detail.template": "Modelo",
|
||||
"settings.agentBehaviour.workflows.model": "modelo",
|
||||
"settings.agentBehaviour.workflows.variant": "variante",
|
||||
"settings.agentBehaviour.workflows.modelDescription": "Substituição global do modelo",
|
||||
"settings.sandboxing.enabled.title": "Sandbox",
|
||||
"settings.sandboxing.enabled.description":
|
||||
"Executar os comandos shell do agente dentro de um sandbox a nível de sistema operacional que restringe escritas aos diretórios de estado do projeto e do Kilo",
|
||||
|
||||
+3
@@ -1027,6 +1027,9 @@ export const dict = {
|
||||
"Nema konfiguriranih prilagođenih komandi. Dodajte komande u opencode.json da ih vidite ovdje.",
|
||||
"settings.agentBehaviour.workflows.detail.description": "Opis",
|
||||
"settings.agentBehaviour.workflows.detail.template": "Predložak",
|
||||
"settings.agentBehaviour.workflows.model": "model",
|
||||
"settings.agentBehaviour.workflows.variant": "varijanta",
|
||||
"settings.agentBehaviour.workflows.modelDescription": "Globalno premošćivanje modela",
|
||||
"settings.sandboxing.enabled.title": "Sandbox",
|
||||
"settings.sandboxing.enabled.description":
|
||||
"Pokrenite shell komande agenta unutar sandboxa na nivou operativnog sistema koji ograničava pisanje na direktorije stanja projekta i Kilo",
|
||||
|
||||
+3
@@ -1025,6 +1025,9 @@ export const dict = {
|
||||
"Ingen brugerdefinerede kommandoer konfigureret. Tilføj kommandoer til opencode.json for at se dem her.",
|
||||
"settings.agentBehaviour.workflows.detail.description": "Beskrivelse",
|
||||
"settings.agentBehaviour.workflows.detail.template": "Skabelon",
|
||||
"settings.agentBehaviour.workflows.model": "model",
|
||||
"settings.agentBehaviour.workflows.variant": "variant",
|
||||
"settings.agentBehaviour.workflows.modelDescription": "Global modeloverskrivelse",
|
||||
"settings.sandboxing.enabled.title": "Sandbox",
|
||||
"settings.sandboxing.enabled.description":
|
||||
"Kør shell-kommandoer for agenten i en sandbox på operativsystemniveau, der begrænser skrivning til projekt- og Kilo-tilstandsmapperne",
|
||||
|
||||
@@ -1050,6 +1050,9 @@ export const dict = {
|
||||
"Keine benutzerdefinierten Befehle konfiguriert. Fügen Sie Befehle zu opencode.json hinzu, um sie hier zu sehen.",
|
||||
"settings.agentBehaviour.workflows.detail.description": "Beschreibung",
|
||||
"settings.agentBehaviour.workflows.detail.template": "Vorlage",
|
||||
"settings.agentBehaviour.workflows.model": "Modell",
|
||||
"settings.agentBehaviour.workflows.variant": "Variante",
|
||||
"settings.agentBehaviour.workflows.modelDescription": "Globale Modellüberschreibung",
|
||||
"settings.sandboxing.enabled.title": "Sandbox",
|
||||
"settings.sandboxing.enabled.description":
|
||||
"Shell-Befehle des Agenten in einer Sandbox auf Betriebssystemebene ausführen, die Schreibvorgänge auf die Projekt- und Kilo-Statusverzeichnisse beschränkt",
|
||||
|
||||
@@ -963,6 +963,9 @@ export const dict = {
|
||||
"No custom commands configured. Add commands to your opencode.json to see them here.",
|
||||
"settings.agentBehaviour.workflows.detail.description": "Description",
|
||||
"settings.agentBehaviour.workflows.detail.template": "Template",
|
||||
"settings.agentBehaviour.workflows.model": "model",
|
||||
"settings.agentBehaviour.workflows.variant": "variant",
|
||||
"settings.agentBehaviour.workflows.modelDescription": "Global model override",
|
||||
|
||||
"settings.agentBehaviour.createMode": "Create New Mode",
|
||||
"settings.agentBehaviour.createMode.name": "Name",
|
||||
|
||||
+3
@@ -1040,6 +1040,9 @@ export const dict = {
|
||||
"No hay comandos personalizados configurados. Añada comandos a opencode.json para verlos aquí.",
|
||||
"settings.agentBehaviour.workflows.detail.description": "Descripción",
|
||||
"settings.agentBehaviour.workflows.detail.template": "Plantilla",
|
||||
"settings.agentBehaviour.workflows.model": "modelo",
|
||||
"settings.agentBehaviour.workflows.variant": "variante",
|
||||
"settings.agentBehaviour.workflows.modelDescription": "Anulación global del modelo",
|
||||
"settings.sandboxing.enabled.title": "Sandbox",
|
||||
"settings.sandboxing.enabled.description":
|
||||
"Ejecutar los comandos de shell del agente dentro de un sandbox a nivel de sistema operativo que restringe las escrituras a los directorios de estado del proyecto y de Kilo",
|
||||
|
||||
+3
@@ -971,6 +971,9 @@ export const dict = {
|
||||
"هیچ دستور سفارشی پیکربندی نشده است. دستورات را به opencode.json خود اضافه کنید تا اینجا نمایش داده شوند.",
|
||||
"settings.agentBehaviour.workflows.detail.description": "توضیحات",
|
||||
"settings.agentBehaviour.workflows.detail.template": "قالب",
|
||||
"settings.agentBehaviour.workflows.model": "مدل",
|
||||
"settings.agentBehaviour.workflows.variant": "گونه",
|
||||
"settings.agentBehaviour.workflows.modelDescription": "بازنویسی مدل سراسری",
|
||||
|
||||
"settings.agentBehaviour.createMode": "ایجاد حالت جدید",
|
||||
"settings.agentBehaviour.createMode.name": "نام",
|
||||
|
||||
+3
@@ -1053,6 +1053,9 @@ export const dict = {
|
||||
"Aucune commande personnalisée configurée. Ajoutez des commandes à opencode.json pour les voir ici.",
|
||||
"settings.agentBehaviour.workflows.detail.description": "Description",
|
||||
"settings.agentBehaviour.workflows.detail.template": "Modèle",
|
||||
"settings.agentBehaviour.workflows.model": "modèle",
|
||||
"settings.agentBehaviour.workflows.variant": "variante",
|
||||
"settings.agentBehaviour.workflows.modelDescription": "Remplacement global du modèle",
|
||||
"settings.sandboxing.enabled.title": "Sandbox",
|
||||
"settings.sandboxing.enabled.description":
|
||||
"Exécuter les commandes shell de l'agent dans un sandbox au niveau du système d'exploitation qui restreint les écritures aux répertoires d'état du projet et de Kilo",
|
||||
|
||||
+3
@@ -850,6 +850,9 @@ export const dict = {
|
||||
"Nessun comando personalizzato configurato. Aggiungi comandi a opencode.json per vederli qui.",
|
||||
"settings.agentBehaviour.workflows.detail.description": "Descrizione",
|
||||
"settings.agentBehaviour.workflows.detail.template": "Template",
|
||||
"settings.agentBehaviour.workflows.model": "modello",
|
||||
"settings.agentBehaviour.workflows.variant": "variante",
|
||||
"settings.agentBehaviour.workflows.modelDescription": "Override globale del modello",
|
||||
"settings.agentBehaviour.createMode": "Crea nuova modalità",
|
||||
"settings.agentBehaviour.createMode.name": "Nome",
|
||||
"settings.agentBehaviour.createMode.name.placeholder": "es. reviewer",
|
||||
|
||||
+3
@@ -1019,6 +1019,9 @@ export const dict = {
|
||||
"カスタムコマンドが設定されていません。opencode.json にコマンドを追加するとここに表示されます。",
|
||||
"settings.agentBehaviour.workflows.detail.description": "説明",
|
||||
"settings.agentBehaviour.workflows.detail.template": "テンプレート",
|
||||
"settings.agentBehaviour.workflows.model": "モデル",
|
||||
"settings.agentBehaviour.workflows.variant": "バリアント",
|
||||
"settings.agentBehaviour.workflows.modelDescription": "グローバルモデルの上書き",
|
||||
"settings.sandboxing.enabled.title": "サンドボックス",
|
||||
"settings.sandboxing.enabled.description":
|
||||
"エージェントのシェルコマンドを、プロジェクトおよびKiloの状態ディレクトリへの書き込みを制限するOSレベルのサンドボックス内で実行",
|
||||
|
||||
+3
@@ -1013,6 +1013,9 @@ export const dict = {
|
||||
"구성된 사용자 정의 명령이 없습니다. opencode.json에 명령을 추가하면 여기에 표시됩니다.",
|
||||
"settings.agentBehaviour.workflows.detail.description": "설명",
|
||||
"settings.agentBehaviour.workflows.detail.template": "템플릿",
|
||||
"settings.agentBehaviour.workflows.model": "모델",
|
||||
"settings.agentBehaviour.workflows.variant": "변형",
|
||||
"settings.agentBehaviour.workflows.modelDescription": "전역 모델 재정의",
|
||||
"settings.sandboxing.enabled.title": "샌드박스",
|
||||
"settings.sandboxing.enabled.description":
|
||||
"에이전트 셸 명령을 프로젝트 및 Kilo 상태 디렉터리에 대한 쓰기를 제한하는 OS 수준의 샌드박스 내에서 실행",
|
||||
|
||||
+3
@@ -998,6 +998,9 @@ export const dict = {
|
||||
"Geen aangepaste commando's geconfigureerd. Voeg commando's toe aan opencode.json om ze hier te zien.",
|
||||
"settings.agentBehaviour.workflows.detail.description": "Beschrijving",
|
||||
"settings.agentBehaviour.workflows.detail.template": "Sjabloon",
|
||||
"settings.agentBehaviour.workflows.model": "model",
|
||||
"settings.agentBehaviour.workflows.variant": "variant",
|
||||
"settings.agentBehaviour.workflows.modelDescription": "Globale modeloverride",
|
||||
|
||||
"settings.autoApprove.description":
|
||||
"Definieer hoe tools mogen worden uitgevoerd. De meeste tools staan standaard op Toestaan. doom_loop en external_directory staan standaard op Vragen.",
|
||||
|
||||
+3
@@ -1025,6 +1025,9 @@ export const dict = {
|
||||
"Ingen egendefinerte kommandoer konfigurert. Legg til kommandoer i opencode.json for å se dem her.",
|
||||
"settings.agentBehaviour.workflows.detail.description": "Beskrivelse",
|
||||
"settings.agentBehaviour.workflows.detail.template": "Mal",
|
||||
"settings.agentBehaviour.workflows.model": "modell",
|
||||
"settings.agentBehaviour.workflows.variant": "variant",
|
||||
"settings.agentBehaviour.workflows.modelDescription": "Global modelloverstyring",
|
||||
"settings.sandboxing.enabled.title": "Sandbox",
|
||||
"settings.sandboxing.enabled.description":
|
||||
"Kjør shell-kommandoer for agenten i en sandbox på operativsystemnivå som begrenser skriving til prosjekt- og Kilo-tilstandsmapper",
|
||||
|
||||
+3
@@ -1026,6 +1026,9 @@ export const dict = {
|
||||
"Brak skonfigurowanych niestandardowych komend. Dodaj komendy do opencode.json, aby je tu zobaczyć.",
|
||||
"settings.agentBehaviour.workflows.detail.description": "Opis",
|
||||
"settings.agentBehaviour.workflows.detail.template": "Szablon",
|
||||
"settings.agentBehaviour.workflows.model": "model",
|
||||
"settings.agentBehaviour.workflows.variant": "wariant",
|
||||
"settings.agentBehaviour.workflows.modelDescription": "Globalne nadpisanie modelu",
|
||||
"settings.sandboxing.enabled.title": "Sandbox",
|
||||
"settings.sandboxing.enabled.description":
|
||||
"Uruchamiaj polecenia shell agenta w sandboxie na poziomie systemu operacyjnego, który ogranicza zapisy do katalogów stanu projektu i Kilo",
|
||||
|
||||
+3
@@ -1023,6 +1023,9 @@ export const dict = {
|
||||
"Пользовательские команды не настроены. Добавьте команды в opencode.json, чтобы увидеть их здесь.",
|
||||
"settings.agentBehaviour.workflows.detail.description": "Описание",
|
||||
"settings.agentBehaviour.workflows.detail.template": "Шаблон",
|
||||
"settings.agentBehaviour.workflows.model": "модель",
|
||||
"settings.agentBehaviour.workflows.variant": "вариант",
|
||||
"settings.agentBehaviour.workflows.modelDescription": "Глобальное переопределение модели",
|
||||
"settings.sandboxing.enabled.title": "Песочница",
|
||||
"settings.sandboxing.enabled.description":
|
||||
"Выполнять команды оболочки агента в песочнице на уровне ОС, которая ограничивает запись в каталоги состояния проекта и Kilo",
|
||||
|
||||
+3
@@ -1010,6 +1010,9 @@ export const dict = {
|
||||
"ไม่มีคำสั่งแบบกำหนดเองที่กำหนดค่าไว้ เพิ่มคำสั่งใน opencode.json เพื่อดูที่นี่",
|
||||
"settings.agentBehaviour.workflows.detail.description": "คำอธิบาย",
|
||||
"settings.agentBehaviour.workflows.detail.template": "เทมเพลต",
|
||||
"settings.agentBehaviour.workflows.model": "โมเดล",
|
||||
"settings.agentBehaviour.workflows.variant": "รูปแบบ",
|
||||
"settings.agentBehaviour.workflows.modelDescription": "การแทนที่โมเดลส่วนกลาง",
|
||||
"settings.sandboxing.enabled.title": "Sandbox",
|
||||
"settings.sandboxing.enabled.description":
|
||||
"เรียกใช้คำสั่ง shell ของ agent ใน sandbox ระดับระบบปฏิบัติการที่จำกัดการเขียนไปยังโฟลเดอร์สถานะของโปรเจ็กต์และ Kilo",
|
||||
|
||||
+3
@@ -986,6 +986,9 @@ export const dict = {
|
||||
"Yapılandırılmış özel komut yok. Burada görmek için opencode.json dosyasına komutlar ekleyin.",
|
||||
"settings.agentBehaviour.workflows.detail.description": "Açıklama",
|
||||
"settings.agentBehaviour.workflows.detail.template": "Şablon",
|
||||
"settings.agentBehaviour.workflows.model": "model",
|
||||
"settings.agentBehaviour.workflows.variant": "varyant",
|
||||
"settings.agentBehaviour.workflows.modelDescription": "Global model geçersiz kılması",
|
||||
|
||||
"settings.autoApprove.description":
|
||||
"Araçların nasıl çalıştırılacağını tanımlayın. Çoğu araç varsayılan olarak İzin Ver'dir. doom_loop ve external_directory varsayılan olarak Sor'dur.",
|
||||
|
||||
+3
@@ -986,6 +986,9 @@ export const dict = {
|
||||
"Власних команд не налаштовано. Додайте команди до opencode.json, щоб вони з'явилися тут.",
|
||||
"settings.agentBehaviour.workflows.detail.description": "Опис",
|
||||
"settings.agentBehaviour.workflows.detail.template": "Шаблон",
|
||||
"settings.agentBehaviour.workflows.model": "модель",
|
||||
"settings.agentBehaviour.workflows.variant": "варіант",
|
||||
"settings.agentBehaviour.workflows.modelDescription": "Глобальне перевизначення моделі",
|
||||
|
||||
"settings.autoApprove.description":
|
||||
"Визначте, як виконуються інструменти. Більшість інструментів за замовчуванням — Дозволити. doom_loop та external_directory за замовчуванням — Запитувати.",
|
||||
|
||||
+3
@@ -977,6 +977,9 @@ export const dict = {
|
||||
"settings.agentBehaviour.workflows.empty": "未配置自定义命令。将命令添加到 opencode.json 即可在此处看到。",
|
||||
"settings.agentBehaviour.workflows.detail.description": "描述",
|
||||
"settings.agentBehaviour.workflows.detail.template": "模板",
|
||||
"settings.agentBehaviour.workflows.model": "模型",
|
||||
"settings.agentBehaviour.workflows.variant": "变体",
|
||||
"settings.agentBehaviour.workflows.modelDescription": "全局模型覆盖",
|
||||
"settings.sandboxing.enabled.title": "沙盒",
|
||||
"settings.sandboxing.enabled.description":
|
||||
"在操作系统级沙盒中运行代理 shell 命令,将写入限制在项目和 Kilo 状态目录内",
|
||||
|
||||
+3
@@ -939,6 +939,9 @@ export const dict = {
|
||||
"settings.agentBehaviour.workflows.empty": "未設定自訂命令。將命令新增至 opencode.json 即可在此處看到。",
|
||||
"settings.agentBehaviour.workflows.detail.description": "描述",
|
||||
"settings.agentBehaviour.workflows.detail.template": "範本",
|
||||
"settings.agentBehaviour.workflows.model": "模型",
|
||||
"settings.agentBehaviour.workflows.variant": "變體",
|
||||
"settings.agentBehaviour.workflows.modelDescription": "全域模型覆寫",
|
||||
"settings.sandboxing.enabled.title": "沙盒",
|
||||
"settings.sandboxing.enabled.description":
|
||||
"在作業系統層級沙盒中執行代理 shell 指令,將寫入限制在專案和 Kilo 狀態目錄內",
|
||||
|
||||
@@ -345,6 +345,7 @@ const ConfigWrapper: ParentComponent<{
|
||||
const value = {
|
||||
config: createMemo(() => cfg()),
|
||||
globalConfig: createMemo(() => (scoped ? global() : cfg())),
|
||||
globalDraft: () => ({}),
|
||||
projectConfig: createMemo(() => (scoped ? project() : cfg())),
|
||||
collections: () => ({}),
|
||||
settings,
|
||||
|
||||
@@ -25,10 +25,12 @@ export interface ConfigCollectionEntry {
|
||||
export type ConfigCollections = Record<string, ConfigCollectionEntry[]>
|
||||
|
||||
export interface CommandConfig {
|
||||
template: string
|
||||
template?: string
|
||||
description?: string
|
||||
agent?: string
|
||||
model?: string
|
||||
model?: string | null
|
||||
variant?: string | null
|
||||
subtask?: boolean
|
||||
}
|
||||
|
||||
export interface SkillsConfig {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Config } from "@/config/config"
|
||||
import { MCP } from "../mcp"
|
||||
import { Skill } from "../skill"
|
||||
import { legacyReviewCommand, reviewCommand } from "@/kilocode/review/command" // kilocode_change
|
||||
import { apply as applyOverride, type Override } from "@/kilocode/command/override" // kilocode_change
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import PROMPT_INITIALIZE from "./template/initialize.txt"
|
||||
|
||||
@@ -32,6 +33,7 @@ export const Info = Schema.Struct({
|
||||
description: Schema.optional(Schema.String),
|
||||
agent: Schema.optional(Schema.String),
|
||||
model: Schema.optional(Schema.String),
|
||||
variant: Schema.optional(Schema.String), // kilocode_change
|
||||
source: Schema.optional(Schema.Literals(["command", "mcp", "skill"])),
|
||||
trusted: Schema.optional(Schema.Boolean), // kilocode_change - skill-sourced templates only run `!`cmd`` shell when trusted
|
||||
// Some command templates are lazy promises from MCP prompt resolution.
|
||||
@@ -114,20 +116,12 @@ export const layer = Layer.effect(
|
||||
commands["local-review-uncommitted"] = legacyReviewCommand("local-review-uncommitted")!
|
||||
// kilocode_change end
|
||||
|
||||
// kilocode_change start - defer partial overrides until all command sources are registered
|
||||
const overrides: Array<{ name: string; command: Override }> = []
|
||||
for (const [name, command] of Object.entries(cfg.command ?? {})) {
|
||||
commands[name] = {
|
||||
name,
|
||||
agent: command.agent,
|
||||
model: command.model,
|
||||
description: command.description,
|
||||
source: "command",
|
||||
get template() {
|
||||
return command.template
|
||||
},
|
||||
subtask: command.subtask,
|
||||
hints: hints(command.template),
|
||||
}
|
||||
if (!applyOverride(commands, name, command, hints)) overrides.push({ name, command }) // kilocode_change
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
for (const [name, prompt] of Object.entries(yield* mcp.prompts())) {
|
||||
commands[name] = {
|
||||
@@ -163,6 +157,31 @@ export const layer = Layer.effect(
|
||||
commands[item.name] = fromSkill(item) // kilocode_change
|
||||
}
|
||||
|
||||
// kilocode_change start - apply deferred overrides to their registered source
|
||||
for (const item of overrides) {
|
||||
const skillTarget = skillName(item.name)
|
||||
if (skillTarget) {
|
||||
const found = yield* skill.get(skillTarget)
|
||||
if (found) {
|
||||
if (commands[skillTarget]?.source !== "skill") {
|
||||
commands[item.name] = fromSkill(found)
|
||||
applyOverride(commands, item.name, item.command, hints) // kilocode_change
|
||||
} else {
|
||||
applyOverride(commands, skillTarget, item.command, hints) // kilocode_change
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
const mcpTarget = mcpName(item.name)
|
||||
if (mcpTarget) {
|
||||
if (commands[mcpTarget]?.source !== "mcp") continue
|
||||
applyOverride(commands, mcpTarget, item.command, hints) // kilocode_change
|
||||
continue
|
||||
}
|
||||
applyOverride(commands, item.name, item.command, hints) // kilocode_change
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
return {
|
||||
commands,
|
||||
}
|
||||
@@ -180,6 +199,8 @@ export const layer = Layer.effect(
|
||||
// kilocode_change start
|
||||
const target = skillName(name)
|
||||
if (target) {
|
||||
const exact = s.commands[target]
|
||||
if (exact?.source === "skill") return exact
|
||||
const item = yield* skill.get(target)
|
||||
if (item) return fromSkill(item)
|
||||
return undefined
|
||||
@@ -201,7 +222,7 @@ export const layer = Layer.effect(
|
||||
const result = Object.values(s.commands)
|
||||
const names = new Set(result.map((item) => item.name))
|
||||
for (const item of yield* skill.all()) {
|
||||
if (s.commands[item.name]?.source === "skill") continue
|
||||
if (s.commands[item.name]?.source === "skill" || s.commands[`${item.name}:skill`]?.source === "skill") continue
|
||||
if (names.has(item.name)) result.push(fromSkill(item))
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// kilocode_change - new file
|
||||
type Existing = {
|
||||
name: string
|
||||
description?: string
|
||||
agent?: string
|
||||
model?: string
|
||||
variant?: string
|
||||
source?: "command" | "mcp" | "skill"
|
||||
trusted?: boolean
|
||||
template: string | Promise<string>
|
||||
subtask?: boolean
|
||||
hints: readonly string[]
|
||||
}
|
||||
|
||||
export type Override = {
|
||||
template?: string
|
||||
description?: string
|
||||
agent?: string
|
||||
model?: string
|
||||
variant?: string
|
||||
subtask?: boolean
|
||||
}
|
||||
|
||||
type Hints = (template: string) => string[]
|
||||
|
||||
export function apply(commands: Record<string, Existing>, name: string, command: Override, hints: Hints) {
|
||||
const existing = commands[name]
|
||||
if (command.template === undefined) {
|
||||
if (!existing) return false
|
||||
if (command.description !== undefined) existing.description = command.description
|
||||
if (command.agent !== undefined) existing.agent = command.agent
|
||||
if (command.model !== undefined) existing.model = command.model
|
||||
if (command.variant !== undefined) existing.variant = command.variant
|
||||
if (command.subtask !== undefined) existing.subtask = command.subtask
|
||||
return true
|
||||
}
|
||||
|
||||
const template = command.template
|
||||
commands[name] = {
|
||||
name,
|
||||
agent: command.agent,
|
||||
model: command.model,
|
||||
variant: command.variant,
|
||||
description: command.description,
|
||||
source: "command",
|
||||
get template() {
|
||||
return template
|
||||
},
|
||||
subtask: command.subtask,
|
||||
hints: hints(template),
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// kilocode_change - new file
|
||||
import type { Agent } from "@/agent/agent"
|
||||
import type { Command } from "@/command"
|
||||
|
||||
export function resolve(input: {
|
||||
command: Pick<Command.Info, "model" | "agent" | "variant">
|
||||
agent: Pick<Agent.Info, "model" | "variant">
|
||||
model: { providerID: string; modelID: string }
|
||||
selected: { variants?: Record<string, unknown> }
|
||||
input?: string
|
||||
}) {
|
||||
if (input.command.variant && input.selected.variants?.[input.command.variant]) return input.command.variant
|
||||
|
||||
if (
|
||||
input.agent.model &&
|
||||
input.agent.model.providerID === input.model.providerID &&
|
||||
input.agent.model.modelID === input.model.modelID &&
|
||||
input.agent.variant &&
|
||||
input.selected.variants?.[input.agent.variant]
|
||||
) {
|
||||
return input.agent.variant
|
||||
}
|
||||
|
||||
if (
|
||||
!input.command.model &&
|
||||
(!input.command.agent || !input.agent.model) &&
|
||||
input.input &&
|
||||
input.selected.variants?.[input.input]
|
||||
) {
|
||||
return input.input
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
@@ -100,6 +100,7 @@ export namespace KiloTask {
|
||||
type Model = { providerID: ProviderV2.ID; modelID: ModelV2.ID }
|
||||
type Saved = Model & { variant?: string }
|
||||
type Choice = { model: Model; variant?: string; sticky?: boolean; direct?: boolean }
|
||||
type Workflow = { model: Model; variant?: string }
|
||||
|
||||
function key(model: Model) {
|
||||
return `${model.providerID}/${model.modelID}`
|
||||
@@ -141,12 +142,14 @@ export namespace KiloTask {
|
||||
config: Pick<Config.Info, "subagent_model" | "subagent_variant" | "subagent_variant_overrides">
|
||||
parent: Model
|
||||
variant?: string
|
||||
workflow?: Workflow
|
||||
provider: Provider.Interface
|
||||
}) {
|
||||
const state = yield* saved(input.name)
|
||||
const cfg = parse(input.config.subagent_model)
|
||||
const override = (model: Model) => input.config.subagent_variant_overrides?.[key(model)] ?? undefined
|
||||
const choices: Array<Choice | undefined> = [
|
||||
input.workflow ? { ...input.workflow, direct: true } : undefined,
|
||||
state
|
||||
? {
|
||||
model: { providerID: state.providerID, modelID: state.modelID },
|
||||
@@ -197,4 +200,20 @@ export namespace KiloTask {
|
||||
const variant = full?.variants?.[value] ? value : input.variant
|
||||
return { model: input.parent, variant }
|
||||
})
|
||||
|
||||
export function workflow(value: unknown): Workflow | undefined {
|
||||
if (!value || typeof value !== "object") return undefined
|
||||
const item = (value as { workflow?: unknown }).workflow
|
||||
if (!item || typeof item !== "object") return undefined
|
||||
const model = (item as { model?: unknown }).model
|
||||
if (!model || typeof model !== "object") return undefined
|
||||
const providerID = (model as { providerID?: unknown }).providerID
|
||||
const modelID = (model as { modelID?: unknown }).modelID
|
||||
if (typeof providerID !== "string" || typeof modelID !== "string") return undefined
|
||||
const variant = (item as { variant?: unknown }).variant
|
||||
return {
|
||||
model: { providerID: ProviderV2.ID.make(providerID), modelID: ModelV2.ID.make(modelID) },
|
||||
variant: typeof variant === "string" ? variant : undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { KiloSession } from "@/kilocode/session" // kilocode_change
|
||||
import { SessionTranscript } from "@/kilocode/session/transcript" // kilocode_change
|
||||
import { KiloCostPropagation } from "@/kilocode/session/cost-propagation" // kilocode_change
|
||||
import { KiloSessionProcessor } from "@/kilocode/session/processor" // kilocode_change
|
||||
import * as KiloWorkflowVariant from "@/kilocode/session/workflow-variant" // kilocode_change
|
||||
import { KiloSessionOverflow } from "@/kilocode/session/overflow" // kilocode_change
|
||||
import { KiloReference } from "@/kilocode/reference/contains" // kilocode_change
|
||||
import { KiloReadObject } from "@/kilocode/tool/read-object" // kilocode_change
|
||||
@@ -350,6 +351,7 @@ export const layer = Layer.effect(
|
||||
const promptOps = yield* ops()
|
||||
const { task: taskTool } = yield* registry.named()
|
||||
const taskModel = task.model ? yield* getModel(task.model.providerID, task.model.modelID, sessionID) : model
|
||||
const taskVariant = task.variant ?? lastUser.model.variant // kilocode_change
|
||||
const assistantMessage: SessionV1.Assistant = yield* sessions.updateMessage({
|
||||
id: MessageID.ascending(),
|
||||
role: "assistant",
|
||||
@@ -357,7 +359,7 @@ export const layer = Layer.effect(
|
||||
sessionID,
|
||||
mode: task.agent,
|
||||
agent: task.agent,
|
||||
variant: lastUser.model.variant,
|
||||
variant: taskVariant, // kilocode_change
|
||||
path: { cwd: ctx.directory, root: ctx.worktree },
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
@@ -404,6 +406,19 @@ export const layer = Layer.effect(
|
||||
throw error
|
||||
}
|
||||
|
||||
// kilocode_change start - distinguish explicit workflow model selection from the effective subtask model
|
||||
const workflow = yield* Effect.gen(function* () {
|
||||
if (!task.command) return undefined
|
||||
const command = yield* commands.get(task.command)
|
||||
if (!command) return undefined
|
||||
if (!command.model && !command.variant && !(command.agent && taskAgent.model)) return undefined
|
||||
return {
|
||||
model: task.model ?? { providerID: taskModel.providerID, modelID: taskModel.id },
|
||||
variant: task.variant,
|
||||
}
|
||||
})
|
||||
// kilocode_change end
|
||||
|
||||
let error: Error | undefined
|
||||
const taskAbort = new AbortController()
|
||||
// kilocode_change start - shared reader for the child session id written by task.ts ctx.metadata (#6321)
|
||||
@@ -419,7 +434,13 @@ export const layer = Layer.effect(
|
||||
sessionID,
|
||||
abort: taskAbort.signal,
|
||||
callID: part.callID,
|
||||
extra: { bypassAgentCheck: true, promptOps },
|
||||
// kilocode_change start
|
||||
extra: {
|
||||
bypassAgentCheck: true,
|
||||
promptOps,
|
||||
workflow, // kilocode_change
|
||||
},
|
||||
// kilocode_change end
|
||||
messages: msgs,
|
||||
metadata: (val: { title?: string; metadata?: Record<string, any> }) =>
|
||||
Effect.gen(function* () {
|
||||
@@ -2083,7 +2104,7 @@ export const layer = Layer.effect(
|
||||
return yield* currentModel(input.sessionID)
|
||||
})
|
||||
|
||||
yield* getModel(taskModel.providerID, taskModel.modelID, input.sessionID)
|
||||
const task = yield* getModel(taskModel.providerID, taskModel.modelID, input.sessionID) // kilocode_change
|
||||
|
||||
const agent = agentName ? yield* agents.get(agentName) : yield* agents.defaultInfo()
|
||||
if (!agent) {
|
||||
@@ -2095,6 +2116,16 @@ export const layer = Layer.effect(
|
||||
}
|
||||
yield* agents.guardRequirements(agent) // kilocode_change - command agent overrides must satisfy requirements
|
||||
|
||||
// kilocode_change start
|
||||
const variant = KiloWorkflowVariant.resolve({
|
||||
command: cmd,
|
||||
agent,
|
||||
model: taskModel,
|
||||
selected: task,
|
||||
input: input.variant,
|
||||
})
|
||||
// kilocode_change end
|
||||
|
||||
const templateParts = yield* resolvePromptParts(template)
|
||||
KiloSessionProcessor.markReviewTelemetry(templateParts, input.command) // kilocode_change - mark review commands for completion telemetry
|
||||
const inputFiles = new Set(
|
||||
@@ -2112,6 +2143,7 @@ export const layer = Layer.effect(
|
||||
description: cmd.description ?? "",
|
||||
command: input.command,
|
||||
model: { providerID: taskModel.providerID, modelID: taskModel.modelID },
|
||||
variant, // kilocode_change
|
||||
prompt: templateParts.find((y) => y.type === "text")?.text ?? "",
|
||||
},
|
||||
]
|
||||
@@ -2136,7 +2168,7 @@ export const layer = Layer.effect(
|
||||
model: userModel,
|
||||
agent: userAgent,
|
||||
parts,
|
||||
variant: input.variant,
|
||||
variant: isSubtask ? input.variant : variant, // kilocode_change
|
||||
snapshotInitialization: input.snapshotInitialization, // kilocode_change
|
||||
})
|
||||
yield* events.publish(Command.Event.Executed, {
|
||||
|
||||
@@ -213,6 +213,7 @@ export const TaskTool = Tool.define(
|
||||
providerID: msg.info.providerID,
|
||||
},
|
||||
variant: msg.info.variant,
|
||||
workflow: KiloTask.workflow(ctx.extra), // kilocode_change
|
||||
provider,
|
||||
})
|
||||
const model = selected.model
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { apply } from "../../../src/kilocode/command/override"
|
||||
|
||||
const hints = (template: string) => (template ? [template] : [])
|
||||
|
||||
describe("command overrides", () => {
|
||||
test("updates an existing command without replacing its template", () => {
|
||||
const commands = {
|
||||
review: {
|
||||
name: "review",
|
||||
template: "Review the changes",
|
||||
hints: ["existing"],
|
||||
},
|
||||
}
|
||||
|
||||
apply(commands, "review", { model: "anthropic/claude-sonnet", variant: "high" }, hints)
|
||||
|
||||
expect(commands.review).toMatchObject({
|
||||
template: "Review the changes",
|
||||
model: "anthropic/claude-sonnet",
|
||||
variant: "high",
|
||||
hints: ["existing"],
|
||||
})
|
||||
})
|
||||
|
||||
test("ignores a partial override for an unknown command", () => {
|
||||
const commands = {}
|
||||
|
||||
expect(apply(commands, "missing", { model: "anthropic/claude-sonnet" }, hints)).toBe(false)
|
||||
|
||||
expect(commands).toEqual({})
|
||||
})
|
||||
|
||||
test("preserves a lazy template while applying a partial override", () => {
|
||||
let reads = 0
|
||||
const commands = {
|
||||
review: {
|
||||
name: "review",
|
||||
get template() {
|
||||
reads++
|
||||
return "Review the changes"
|
||||
},
|
||||
hints: ["existing"],
|
||||
},
|
||||
}
|
||||
|
||||
apply(commands, "review", { variant: "high" }, hints)
|
||||
|
||||
expect(reads).toBe(0)
|
||||
expect((commands.review as { variant?: string }).variant).toBe("high")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Schema } from "effect"
|
||||
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
|
||||
|
||||
describe("workflow model overrides", () => {
|
||||
test("accepts a command entry containing only model and variant", () => {
|
||||
const value = Schema.decodeUnknownSync(ConfigV1.Info)({
|
||||
command: {
|
||||
review: {
|
||||
model: "anthropic/claude-sonnet-4-6",
|
||||
variant: "high",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(value.command?.review).toEqual({
|
||||
model: "anthropic/claude-sonnet-4-6",
|
||||
variant: "high",
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -68,9 +68,21 @@ function session(dir: string) {
|
||||
const headers = { "Content-Type": "application/json", "x-kilo-directory": dir }
|
||||
const query = `directory=${encodeURIComponent(dir)}`
|
||||
|
||||
const json = async (route: string, init?: RequestInit) => {
|
||||
const res = await app.request(route, { headers, ...init })
|
||||
return await res.json()
|
||||
const json = async (route: string, init?: RequestInit, retry = false) => {
|
||||
const tries = retry ? 5 : 1
|
||||
for (let attempt = 0; attempt < tries; attempt++) {
|
||||
const res = await app.request(route, { headers, ...init })
|
||||
const body = await res.text()
|
||||
try {
|
||||
return JSON.parse(body)
|
||||
} catch (error) {
|
||||
if (!retry || !res.ok || attempt === tries - 1) {
|
||||
throw new Error(`${route} -> ${res.status} ${body.slice(0, 200)}`, { cause: error })
|
||||
}
|
||||
await sleep(100)
|
||||
}
|
||||
}
|
||||
throw new Error(`failed to read JSON response from ${route}`)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -82,9 +94,9 @@ function session(dir: string) {
|
||||
body: JSON.stringify({ parts: [{ type: "text", text }] }),
|
||||
}),
|
||||
abort: (id: string) => app.request(`/session/${id}/abort`, { method: "POST", headers }),
|
||||
messages: (id: string) => json(`/session/${id}/message?${query}`) as Promise<Message[]>,
|
||||
messages: (id: string) => json(`/session/${id}/message?${query}`, undefined, true) as Promise<Message[]>,
|
||||
status: async (id: string) => {
|
||||
const all = (await json(`/session/status?${query}`)) as Record<string, { type: string }>
|
||||
const all = (await json(`/session/status?${query}`, undefined, true)) as Record<string, { type: string }>
|
||||
return all[id]?.type ?? "idle"
|
||||
},
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Server } from "../../../src/server/server"
|
||||
import { Config } from "../../../src/config/config"
|
||||
import { ConfigParse } from "../../../src/config/parse"
|
||||
import { KilocodeConfigOverlay } from "../../../src/kilocode/config/overlay"
|
||||
import { KilocodeConfigWriter } from "../../../src/kilocode/config/writer"
|
||||
import { Permission } from "../../../src/permission"
|
||||
@@ -24,6 +25,7 @@ type Overlay = {
|
||||
fields: Record<string, { source: string; inherited: boolean; overridden: boolean; value?: unknown }>
|
||||
collections: Record<string, Array<{ key: string; source: string; inherited: boolean; local?: unknown }>>
|
||||
targets: { project: Target; global: Target; active: Target }
|
||||
effective?: Config.Info
|
||||
}
|
||||
type Agent = {
|
||||
name: string
|
||||
@@ -598,6 +600,58 @@ describe("config overlay routes", () => {
|
||||
expect(Object.keys(saved.mcp)).toEqual(["local"])
|
||||
})
|
||||
|
||||
test.serial("writes partial global workflow overrides when both JSON and JSONC exist", async () => {
|
||||
await using global = await tmpdir()
|
||||
await using project = await tmpdir()
|
||||
;(Global.Path as { config: string }).config = global.path
|
||||
await Filesystem.write(path.join(global.path, "kilo.json"), JSON.stringify({ username: "legacy" }))
|
||||
await Filesystem.write(path.join(global.path, "kilo.jsonc"), "{\n // Keep JSONC as the active target.\n}\n")
|
||||
|
||||
await json(
|
||||
await req(project.path, "/config/overlay", {
|
||||
method: "PATCH",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
scope: "global",
|
||||
set: { command: { review: { model: "anthropic/claude-sonnet-4-6", variant: "high" } } },
|
||||
}),
|
||||
}),
|
||||
)
|
||||
const saved = ConfigParse.jsonc(await Bun.file(path.join(global.path, "kilo.jsonc")).text(), "kilo.jsonc")
|
||||
expect(saved).toMatchObject({
|
||||
command: { review: { model: "anthropic/claude-sonnet-4-6", variant: "high" } },
|
||||
})
|
||||
expect(await Bun.file(path.join(global.path, "kilo.json")).json()).toMatchObject({ username: "legacy" })
|
||||
})
|
||||
|
||||
test.serial("merges workflow overrides with a command body in the lower-precedence global file", async () => {
|
||||
await using global = await tmpdir()
|
||||
await using project = await tmpdir()
|
||||
;(Global.Path as { config: string }).config = global.path
|
||||
await Filesystem.write(
|
||||
path.join(global.path, "kilo.json"),
|
||||
JSON.stringify({ command: { review: { template: "Review the changes" } } }),
|
||||
)
|
||||
await Filesystem.write(path.join(global.path, "kilo.jsonc"), '{\n "username": "legacy"\n}\n')
|
||||
|
||||
const response = await json<Overlay>(
|
||||
await req(project.path, "/config/overlay", {
|
||||
method: "PATCH",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
scope: "global",
|
||||
set: { command: { review: { model: "anthropic/claude-sonnet-4-6", variant: "high" } } },
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(response.effective?.command?.review).toMatchObject({
|
||||
template: "Review the changes",
|
||||
model: "anthropic/claude-sonnet-4-6",
|
||||
variant: "high",
|
||||
})
|
||||
})
|
||||
|
||||
test.serial("disables inherited mcp server with a minimal local override", async () => {
|
||||
await using global = await tmpdir()
|
||||
await using project = await tmpdir()
|
||||
@@ -644,7 +698,7 @@ describe("config overlay routes", () => {
|
||||
const edit = body.effective.permission.edit
|
||||
const after = await json<Agent[]>(await req(project.path, "/agent"))
|
||||
|
||||
expect(typeof edit === "string" ? edit : edit["*"]).toBe("ask")
|
||||
expect(typeof edit === "string" ? edit : edit?.["*"]).toBe("ask")
|
||||
expect(
|
||||
Permission.evaluate("edit", "*", after.find((item) => item.name === "code")?.permission ?? []).action,
|
||||
).toBe("ask")
|
||||
@@ -678,7 +732,7 @@ describe("config overlay routes", () => {
|
||||
const edit = body.effective.permission.edit
|
||||
const after = await json<Agent[]>(await req(project.path, "/agent"))
|
||||
|
||||
expect(typeof edit === "string" ? edit : edit["*"]).toBe("ask")
|
||||
expect(typeof edit === "string" ? edit : edit?.["*"]).toBe("ask")
|
||||
expect(Permission.evaluate("edit", "*", after.find((item) => item.name === "code")?.permission ?? []).action).toBe(
|
||||
"ask",
|
||||
)
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { resolve } from "../../../src/kilocode/session/workflow-variant"
|
||||
|
||||
const selected = { variants: { high: {} } }
|
||||
const model = { providerID: ProviderV2.ID.make("anthropic"), modelID: ModelV2.ID.make("claude-sonnet") }
|
||||
|
||||
describe("workflow variant resolution", () => {
|
||||
test("prefers the command variant", () => {
|
||||
expect(
|
||||
resolve({
|
||||
command: { model: "anthropic/claude-sonnet", agent: undefined, variant: "high" },
|
||||
agent: { model: undefined, variant: undefined },
|
||||
model,
|
||||
selected,
|
||||
input: "high",
|
||||
}),
|
||||
).toBe("high")
|
||||
})
|
||||
|
||||
test("uses an agent variant only for the agent model", () => {
|
||||
expect(
|
||||
resolve({
|
||||
command: { model: undefined, agent: "reviewer", variant: undefined },
|
||||
agent: { model, variant: "high" },
|
||||
model,
|
||||
selected,
|
||||
}),
|
||||
).toBe("high")
|
||||
|
||||
expect(
|
||||
resolve({
|
||||
command: { model: undefined, agent: "reviewer", variant: undefined },
|
||||
agent: { model, variant: "high" },
|
||||
model: { providerID: ProviderV2.ID.make("openai"), modelID: ModelV2.ID.make("gpt-5") },
|
||||
selected,
|
||||
}),
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
test("uses chat variant when an agent does not select a model", () => {
|
||||
expect(
|
||||
resolve({
|
||||
command: { model: undefined, agent: undefined, variant: undefined },
|
||||
agent: { model: undefined, variant: undefined },
|
||||
model,
|
||||
selected,
|
||||
input: "high",
|
||||
}),
|
||||
).toBe("high")
|
||||
|
||||
expect(
|
||||
resolve({
|
||||
command: { model: "anthropic/claude-sonnet", agent: undefined, variant: undefined },
|
||||
agent: { model: undefined, variant: undefined },
|
||||
model,
|
||||
selected,
|
||||
input: "high",
|
||||
}),
|
||||
).toBeUndefined()
|
||||
|
||||
expect(
|
||||
resolve({
|
||||
command: { model: undefined, agent: "reviewer", variant: undefined },
|
||||
agent: { model: undefined, variant: undefined },
|
||||
model,
|
||||
selected,
|
||||
input: "high",
|
||||
}),
|
||||
).toBe("high")
|
||||
})
|
||||
})
|
||||
@@ -78,4 +78,90 @@ Skill content.
|
||||
{ git: true },
|
||||
),
|
||||
)
|
||||
|
||||
it.live("applies a partial override to a skill command", () =>
|
||||
provideTmpdirInstance(
|
||||
(dir) =>
|
||||
Effect.gen(function* () {
|
||||
yield* Effect.promise(() =>
|
||||
Bun.write(
|
||||
path.join(dir, ".kilo", "skill", "proj", "SKILL.md"),
|
||||
"---\nname: proj\ndescription: Project skill.\n---\n\nReview files.\n",
|
||||
),
|
||||
)
|
||||
|
||||
const command = yield* Command.Service
|
||||
const skill = yield* command.get("proj:skill")
|
||||
|
||||
expect(skill?.source).toBe("skill")
|
||||
expect(skill?.model).toBe("anthropic/claude-sonnet")
|
||||
expect(skill?.variant).toBe("high")
|
||||
}),
|
||||
{
|
||||
git: true,
|
||||
config: {
|
||||
command: {
|
||||
proj: {
|
||||
model: "anthropic/claude-sonnet",
|
||||
variant: "high",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
it.live("applies a skill alias override when a command has the same name", () =>
|
||||
provideTmpdirInstance(
|
||||
(dir) =>
|
||||
Effect.gen(function* () {
|
||||
yield* Effect.promise(() =>
|
||||
Bun.write(
|
||||
path.join(dir, ".kilo", "skill", "review", "SKILL.md"),
|
||||
"---\nname: review\ndescription: Review skill.\n---\n\nReview files.\n",
|
||||
),
|
||||
)
|
||||
|
||||
const command = yield* Command.Service
|
||||
const skill = yield* command.get("review:skill")
|
||||
const list = yield* command.list()
|
||||
|
||||
expect(skill?.source).toBe("skill")
|
||||
expect(skill?.model).toBe("anthropic/claude-sonnet")
|
||||
expect(list.filter((item) => item.source === "skill" && item.name === "review")).toHaveLength(1)
|
||||
}),
|
||||
{
|
||||
git: true,
|
||||
config: {
|
||||
command: {
|
||||
"review:skill": { model: "anthropic/claude-sonnet" },
|
||||
review: { template: "Command content." },
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
it.live("does not apply a missing MCP alias to a command with the same name", () =>
|
||||
provideTmpdirInstance(
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const command = yield* Command.Service
|
||||
const plain = yield* command.get("review")
|
||||
const missing = yield* command.get("review:mcp")
|
||||
|
||||
expect(plain?.model).toBeUndefined()
|
||||
expect(missing).toBeUndefined()
|
||||
}),
|
||||
{
|
||||
git: true,
|
||||
config: {
|
||||
command: {
|
||||
review: { template: "Command content." },
|
||||
"review:mcp": { model: "anthropic/claude-sonnet" },
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -633,6 +633,7 @@ export type SubtaskPart = {
|
||||
providerID: string
|
||||
modelID: string
|
||||
}
|
||||
variant?: string
|
||||
command?: string
|
||||
}
|
||||
|
||||
@@ -1555,7 +1556,7 @@ export type Config = {
|
||||
server?: ServerConfig
|
||||
command?: {
|
||||
[key: string]: {
|
||||
template: string
|
||||
template?: string
|
||||
description?: string
|
||||
agent?: string
|
||||
model?: string
|
||||
@@ -2114,6 +2115,7 @@ export type Command = {
|
||||
description?: string
|
||||
agent?: string
|
||||
model?: string
|
||||
variant?: string
|
||||
source?: "command" | "mcp" | "skill"
|
||||
trusted?: boolean
|
||||
template: string
|
||||
@@ -2769,6 +2771,7 @@ export type SubtaskPartInput = {
|
||||
providerID: string
|
||||
modelID: string
|
||||
}
|
||||
variant?: string
|
||||
command?: string
|
||||
}
|
||||
|
||||
@@ -6204,6 +6207,7 @@ export type CommandV2Info = {
|
||||
providerID: string
|
||||
variant?: string
|
||||
}
|
||||
variant?: string
|
||||
subtask?: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -26560,6 +26560,9 @@
|
||||
"required": ["providerID", "modelID"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"command": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -29178,7 +29181,6 @@
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["template"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
@@ -30824,6 +30826,9 @@
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"enum": ["command", "mcp", "skill"]
|
||||
@@ -32871,6 +32876,9 @@
|
||||
"required": ["providerID", "modelID"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"command": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -44436,6 +44444,9 @@
|
||||
"required": ["id", "providerID"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"subtask": {
|
||||
"type": "boolean"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user