Merge remote-tracking branch 'origin/dev' into opencode/mighty-wolf

This commit is contained in:
Choi Kijin / 최 기진 / チョイ キジン
2026-04-28 15:47:58 +09:00
141 changed files with 6764 additions and 932 deletions
+14
View File
@@ -767,4 +767,18 @@ describe("createChatMessageHandler - TUI variant passthrough", () => {
expect(output.message["model"]).toBeUndefined()
expect(getSessionModel("test-session")).toEqual(nextModel)
})
test("strips legacy ZWSP-prefixed agent names from persisted prompt body session state (GH-3259)", async () => {
//#given - persisted prompt body from v3.14.0-v3.16.0 may contain ZWSP-prefixed agent
const args = createMockHandlerArgs()
const handler = createChatMessageHandler(args)
const input = createMockInput("\u200B\u200BHephaestus - Deep Agent")
const output = createMockOutput()
//#when
await handler(input, output)
//#then
expect(getSessionAgent("test-session")).toBe("Hephaestus - Deep Agent")
})
})
+3 -3
View File
@@ -81,9 +81,9 @@ function getStoredMainSessionModel(
return undefined
}
if (output.message["model"] !== undefined) {
return undefined
}
// Removed: `output.message["model"] !== undefined` guard was unreachable.
// OpenCode always populates output.message.model before triggering chat.message,
// so the guard short-circuited every time, preventing session model recovery.
if (hasExplicitAgentModelOverride(input.agent, pluginConfig)) {
return undefined
+4 -1
View File
@@ -1,4 +1,5 @@
import type { HookName, OhMyOpenCodeConfig } from "../../config"
import type { BackgroundManager } from "../../features/background-agent"
import type { ModelFallbackControllerAccessor } from "../../hooks/model-fallback"
import type { PluginContext } from "../types"
import type { ModelCacheState } from "../../plugin-state"
@@ -11,16 +12,18 @@ export function createCoreHooks(args: {
ctx: PluginContext
pluginConfig: OhMyOpenCodeConfig
modelCacheState: ModelCacheState
backgroundManager: BackgroundManager
modelFallbackControllerAccessor?: ModelFallbackControllerAccessor
isHookEnabled: (hookName: HookName) => boolean
safeHookEnabled: boolean
}) {
const { ctx, pluginConfig, modelCacheState, modelFallbackControllerAccessor, isHookEnabled, safeHookEnabled } = args
const { ctx, pluginConfig, modelCacheState, backgroundManager, modelFallbackControllerAccessor, isHookEnabled, safeHookEnabled } = args
const session = createSessionHooks({
ctx,
pluginConfig,
modelCacheState,
backgroundManager,
modelFallbackControllerAccessor,
isHookEnabled,
safeHookEnabled,
+4 -1
View File
@@ -1,4 +1,5 @@
import type { OhMyOpenCodeConfig, HookName } from "../../config"
import type { BackgroundManager } from "../../features/background-agent"
import type { ModelFallbackControllerAccessor } from "../../hooks/model-fallback"
import type { ModelCacheState } from "../../plugin-state"
import type { PluginContext } from "../types"
@@ -70,11 +71,12 @@ export function createSessionHooks(args: {
ctx: PluginContext
pluginConfig: OhMyOpenCodeConfig
modelCacheState: ModelCacheState
backgroundManager: BackgroundManager
modelFallbackControllerAccessor?: ModelFallbackControllerAccessor
isHookEnabled: (hookName: HookName) => boolean
safeHookEnabled: boolean
}): SessionHooks {
const { ctx, pluginConfig, modelCacheState, modelFallbackControllerAccessor, isHookEnabled, safeHookEnabled } = args
const { ctx, pluginConfig, modelCacheState, backgroundManager, modelFallbackControllerAccessor, isHookEnabled, safeHookEnabled } = args
const safeHook = <T>(hookName: HookName, factory: () => T): T | null =>
safeCreateHook(hookName, factory, { enabled: safeHookEnabled })
@@ -211,6 +213,7 @@ export function createSessionHooks(args: {
createRalphLoopHook(ctx, {
config: pluginConfig.ralph_loop,
checkSessionExists: async (sessionId) => await sessionExists(sessionId),
backgroundManager,
}))
: null
+64
View File
@@ -85,4 +85,68 @@ describe("createSkillContext", () => {
getSystemMcpServerNamesSpy.mockRestore()
}
})
it("excludes discovered dev-browser skill when browser provider is playwright", async () => {
// given
const discoveredDevBrowserSkill = {
name: "dev-browser",
definition: { description: "Discovered dev-browser skill" },
scope: "user" as const,
}
const discoverConfigSourceSkillsSpy = spyOn(
skillLoader,
"discoverConfigSourceSkills",
).mockResolvedValue([])
const discoverUserClaudeSkillsSpy = spyOn(
skillLoader,
"discoverUserClaudeSkills",
).mockResolvedValue([discoveredDevBrowserSkill])
const discoverProjectClaudeSkillsSpy = spyOn(
skillLoader,
"discoverProjectClaudeSkills",
).mockResolvedValue([])
const discoverOpencodeGlobalSkillsSpy = spyOn(
skillLoader,
"discoverOpencodeGlobalSkills",
).mockResolvedValue([])
const discoverProjectAgentsSkillsSpy = spyOn(
skillLoader,
"discoverProjectAgentsSkills",
).mockResolvedValue([])
const discoverGlobalAgentsSkillsSpy = spyOn(
skillLoader,
"discoverGlobalAgentsSkills",
).mockResolvedValue([])
const getSystemMcpServerNamesSpy = spyOn(
mcpLoader,
"getSystemMcpServerNames",
).mockReturnValue(new Set<string>())
const pluginConfig = OhMyOpenCodeConfigSchema.parse({
browser_automation_engine: { provider: "playwright" },
})
try {
// when
const result = await createSkillContext({
directory: testDirectory,
pluginConfig,
})
// then
expect(result.browserProvider).toBe("playwright")
expect(result.mergedSkills.some((skill) => skill.name === "playwright")).toBe(true)
expect(result.mergedSkills.some((skill) => skill.name === "dev-browser")).toBe(false)
expect(result.availableSkills.some((skill) => skill.name === "dev-browser")).toBe(false)
} finally {
discoverConfigSourceSkillsSpy.mockRestore()
discoverUserClaudeSkillsSpy.mockRestore()
discoverProjectClaudeSkillsSpy.mockRestore()
discoverOpencodeGlobalSkillsSpy.mockRestore()
discoverProjectAgentsSkillsSpy.mockRestore()
discoverGlobalAgentsSkillsSpy.mockRestore()
getSystemMcpServerNamesSpy.mockRestore()
}
})
})
+1 -1
View File
@@ -26,7 +26,7 @@ export type SkillContext = {
disabledSkills: Set<string>
}
const PROVIDER_GATED_SKILL_NAMES = new Set(["agent-browser", "playwright"])
const PROVIDER_GATED_SKILL_NAMES = new Set(["agent-browser", "dev-browser", "playwright"])
function mapScopeToLocation(scope: SkillScope): AvailableSkill["location"] {
if (scope === "user" || scope === "opencode") return "user"