fix: use correct project directory for Windows subagents (#1718)
This commit is contained in:
parent
fcf26d9898
commit
5ae45c8c8e
@ -0,0 +1,33 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
|
||||||
|
import { resolveParentDirectory } from "./parent-directory-resolver"
|
||||||
|
|
||||||
|
describe("background-agent parent-directory-resolver", () => {
|
||||||
|
const originalPlatform = process.platform
|
||||||
|
|
||||||
|
test("uses current working directory on Windows when parent session directory is AppData", async () => {
|
||||||
|
//#given
|
||||||
|
Object.defineProperty(process, "platform", { value: "win32" })
|
||||||
|
try {
|
||||||
|
const client = {
|
||||||
|
session: {
|
||||||
|
get: async () => ({
|
||||||
|
data: { directory: "C:\\Users\\test\\AppData\\Local\\ai.opencode.desktop" },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = await resolveParentDirectory({
|
||||||
|
client: client as Parameters<typeof resolveParentDirectory>[0]["client"],
|
||||||
|
parentSessionID: "ses_parent",
|
||||||
|
defaultDirectory: "C:\\Users\\test\\AppData\\Roaming\\opencode",
|
||||||
|
})
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toBe(process.cwd())
|
||||||
|
} finally {
|
||||||
|
Object.defineProperty(process, "platform", { value: originalPlatform })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import type { OpencodeClient } from "../constants"
|
import type { OpencodeClient } from "../constants"
|
||||||
import { log } from "../../../shared"
|
import { log, resolveSessionDirectory } from "../../../shared"
|
||||||
|
|
||||||
export async function resolveParentDirectory(options: {
|
export async function resolveParentDirectory(options: {
|
||||||
client: OpencodeClient
|
client: OpencodeClient
|
||||||
@ -15,7 +15,10 @@ export async function resolveParentDirectory(options: {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
const parentDirectory = parentSession?.data?.directory ?? defaultDirectory
|
const parentDirectory = resolveSessionDirectory({
|
||||||
|
parentDirectory: parentSession?.data?.directory,
|
||||||
|
fallbackDirectory: defaultDirectory,
|
||||||
|
})
|
||||||
log(`[background-agent] Parent dir: ${parentSession?.data?.directory}, using: ${parentDirectory}`)
|
log(`[background-agent] Parent dir: ${parentSession?.data?.directory}, using: ${parentDirectory}`)
|
||||||
return parentDirectory
|
return parentDirectory
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,3 +54,4 @@ export * from "./truncate-description"
|
|||||||
export * from "./opencode-storage-paths"
|
export * from "./opencode-storage-paths"
|
||||||
export * from "./opencode-message-dir"
|
export * from "./opencode-message-dir"
|
||||||
export * from "./normalize-sdk-response"
|
export * from "./normalize-sdk-response"
|
||||||
|
export * from "./session-directory-resolver"
|
||||||
|
|||||||
79
src/shared/session-directory-resolver.test.ts
Normal file
79
src/shared/session-directory-resolver.test.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
|
||||||
|
import { isWindowsAppDataDirectory, resolveSessionDirectory } from "./session-directory-resolver"
|
||||||
|
|
||||||
|
describe("session-directory-resolver", () => {
|
||||||
|
describe("isWindowsAppDataDirectory", () => {
|
||||||
|
test("returns true when path is under AppData Local", () => {
|
||||||
|
//#given
|
||||||
|
const directory = "C:/Users/test/AppData/Local/opencode"
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = isWindowsAppDataDirectory(directory)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns false when path is outside AppData", () => {
|
||||||
|
//#given
|
||||||
|
const directory = "D:/projects/oh-my-opencode"
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = isWindowsAppDataDirectory(directory)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("resolveSessionDirectory", () => {
|
||||||
|
test("uses process working directory on Windows when parent directory drifts to AppData", () => {
|
||||||
|
//#given
|
||||||
|
const options = {
|
||||||
|
parentDirectory: "C:\\Users\\test\\AppData\\Local\\ai.opencode.desktop",
|
||||||
|
fallbackDirectory: "C:\\Users\\test\\AppData\\Roaming\\opencode",
|
||||||
|
platform: "win32" as const,
|
||||||
|
currentWorkingDirectory: "D:\\projects\\oh-my-opencode",
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = resolveSessionDirectory(options)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toBe("D:\\projects\\oh-my-opencode")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("keeps AppData directory when current working directory is also AppData", () => {
|
||||||
|
//#given
|
||||||
|
const options = {
|
||||||
|
parentDirectory: "C:\\Users\\test\\AppData\\Local\\ai.opencode.desktop",
|
||||||
|
fallbackDirectory: "C:\\Users\\test\\AppData\\Roaming\\opencode",
|
||||||
|
platform: "win32" as const,
|
||||||
|
currentWorkingDirectory: "C:\\Users\\test\\AppData\\Local\\Temp",
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = resolveSessionDirectory(options)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toBe("C:\\Users\\test\\AppData\\Local\\ai.opencode.desktop")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("keeps original directory outside Windows", () => {
|
||||||
|
//#given
|
||||||
|
const options = {
|
||||||
|
parentDirectory: "/tmp/opencode",
|
||||||
|
fallbackDirectory: "/workspace/project",
|
||||||
|
platform: "darwin" as const,
|
||||||
|
currentWorkingDirectory: "/workspace/project",
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = resolveSessionDirectory(options)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toBe("/tmp/opencode")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
39
src/shared/session-directory-resolver.ts
Normal file
39
src/shared/session-directory-resolver.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
const WINDOWS_APPDATA_SEGMENTS = ["\\appdata\\local\\", "\\appdata\\roaming\\", "\\appdata\\locallow\\"]
|
||||||
|
|
||||||
|
function normalizeWindowsPath(directory: string): string {
|
||||||
|
return directory.replaceAll("/", "\\").toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isWindowsAppDataDirectory(directory: string): boolean {
|
||||||
|
const normalizedDirectory = normalizeWindowsPath(directory)
|
||||||
|
return WINDOWS_APPDATA_SEGMENTS.some((segment) => normalizedDirectory.includes(segment))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveSessionDirectory(options: {
|
||||||
|
parentDirectory: string | null | undefined
|
||||||
|
fallbackDirectory: string
|
||||||
|
platform?: NodeJS.Platform
|
||||||
|
currentWorkingDirectory?: string
|
||||||
|
}): string {
|
||||||
|
const {
|
||||||
|
parentDirectory,
|
||||||
|
fallbackDirectory,
|
||||||
|
platform = process.platform,
|
||||||
|
currentWorkingDirectory = process.cwd(),
|
||||||
|
} = options
|
||||||
|
|
||||||
|
const sessionDirectory = parentDirectory ?? fallbackDirectory
|
||||||
|
if (platform !== "win32") {
|
||||||
|
return sessionDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isWindowsAppDataDirectory(sessionDirectory)) {
|
||||||
|
return sessionDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWindowsAppDataDirectory(currentWorkingDirectory)) {
|
||||||
|
return sessionDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentWorkingDirectory
|
||||||
|
}
|
||||||
@ -4,44 +4,88 @@ import { resolveOrCreateSessionId } from "./subagent-session-creator"
|
|||||||
import { _resetForTesting, subagentSessions } from "../../features/claude-code-session-state"
|
import { _resetForTesting, subagentSessions } from "../../features/claude-code-session-state"
|
||||||
|
|
||||||
describe("call-omo-agent resolveOrCreateSessionId", () => {
|
describe("call-omo-agent resolveOrCreateSessionId", () => {
|
||||||
test("tracks newly created child session as subagent session", async () => {
|
const originalPlatform = process.platform
|
||||||
// given
|
|
||||||
_resetForTesting()
|
function buildInput(options: {
|
||||||
|
parentDirectory?: string
|
||||||
|
contextDirectory: string
|
||||||
|
}): {
|
||||||
|
ctx: Parameters<typeof resolveOrCreateSessionId>[0]
|
||||||
|
args: Parameters<typeof resolveOrCreateSessionId>[1]
|
||||||
|
toolContext: Parameters<typeof resolveOrCreateSessionId>[2]
|
||||||
|
createCalls: Array<{ query?: { directory?: string } }>
|
||||||
|
} {
|
||||||
|
const createCalls: Array<{ query?: { directory?: string } }> = []
|
||||||
|
const { parentDirectory, contextDirectory } = options
|
||||||
|
const parentSessionData = parentDirectory ? { data: { directory: parentDirectory } } : { data: {} }
|
||||||
|
|
||||||
const createCalls: Array<unknown> = []
|
|
||||||
const ctx = {
|
const ctx = {
|
||||||
directory: "/project",
|
directory: contextDirectory,
|
||||||
client: {
|
client: {
|
||||||
session: {
|
session: {
|
||||||
get: async () => ({ data: { directory: "/parent" } }),
|
get: async () => parentSessionData,
|
||||||
create: async (args: unknown) => {
|
create: async (createInput: unknown) => {
|
||||||
createCalls.push(args)
|
const payload = createInput as { query?: { directory?: string } }
|
||||||
|
createCalls.push(payload)
|
||||||
return { data: { id: "ses_child_sync" } }
|
return { data: { id: "ses_child_sync" } }
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
} as unknown as Parameters<typeof resolveOrCreateSessionId>[0]
|
||||||
|
|
||||||
const args = {
|
const args = {
|
||||||
description: "sync test",
|
description: "sync test",
|
||||||
prompt: "hello",
|
prompt: "hello",
|
||||||
subagent_type: "explore",
|
subagent_type: "explore",
|
||||||
run_in_background: false,
|
run_in_background: false,
|
||||||
}
|
} satisfies Parameters<typeof resolveOrCreateSessionId>[1]
|
||||||
|
|
||||||
const toolContext = {
|
const toolContext = {
|
||||||
sessionID: "ses_parent",
|
sessionID: "ses_parent",
|
||||||
messageID: "msg_parent",
|
messageID: "msg_parent",
|
||||||
agent: "sisyphus",
|
agent: "sisyphus",
|
||||||
abort: new AbortController().signal,
|
abort: new AbortController().signal,
|
||||||
}
|
} satisfies Parameters<typeof resolveOrCreateSessionId>[2]
|
||||||
|
|
||||||
// when
|
return { ctx, args, toolContext, createCalls }
|
||||||
const result = await resolveOrCreateSessionId(ctx as any, args as any, toolContext as any)
|
}
|
||||||
|
|
||||||
// then
|
test("tracks newly created child session as subagent session", async () => {
|
||||||
|
//#given
|
||||||
|
_resetForTesting()
|
||||||
|
|
||||||
|
const { ctx, args, toolContext, createCalls } = buildInput({
|
||||||
|
parentDirectory: "/parent",
|
||||||
|
contextDirectory: "/project",
|
||||||
|
})
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = await resolveOrCreateSessionId(ctx, args, toolContext)
|
||||||
|
|
||||||
|
//#then
|
||||||
expect(result).toEqual({ ok: true, sessionID: "ses_child_sync" })
|
expect(result).toEqual({ ok: true, sessionID: "ses_child_sync" })
|
||||||
expect(createCalls).toHaveLength(1)
|
expect(createCalls).toHaveLength(1)
|
||||||
expect(subagentSessions.has("ses_child_sync")).toBe(true)
|
expect(subagentSessions.has("ses_child_sync")).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("uses current working directory on Windows when parent directory is under AppData", async () => {
|
||||||
|
//#given
|
||||||
|
_resetForTesting()
|
||||||
|
Object.defineProperty(process, "platform", { value: "win32" })
|
||||||
|
try {
|
||||||
|
const { ctx, args, toolContext, createCalls } = buildInput({
|
||||||
|
parentDirectory: "C:\\Users\\test\\AppData\\Local\\ai.opencode.desktop",
|
||||||
|
contextDirectory: "C:\\Users\\test\\AppData\\Roaming\\opencode",
|
||||||
|
})
|
||||||
|
|
||||||
|
//#when
|
||||||
|
await resolveOrCreateSessionId(ctx, args, toolContext)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(createCalls).toHaveLength(1)
|
||||||
|
expect(createCalls[0]?.query?.directory).toBe(process.cwd())
|
||||||
|
} finally {
|
||||||
|
Object.defineProperty(process, "platform", { value: originalPlatform })
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import type { PluginInput } from "@opencode-ai/plugin"
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
import { log } from "../../shared"
|
import { log } from "../../shared"
|
||||||
|
import { resolveSessionDirectory } from "../../shared"
|
||||||
import { subagentSessions } from "../../features/claude-code-session-state"
|
import { subagentSessions } from "../../features/claude-code-session-state"
|
||||||
import type { CallOmoAgentArgs } from "./types"
|
import type { CallOmoAgentArgs } from "./types"
|
||||||
import type { ToolContextWithMetadata } from "./tool-context-with-metadata"
|
import type { ToolContextWithMetadata } from "./tool-context-with-metadata"
|
||||||
@ -27,11 +28,14 @@ export async function resolveOrCreateSessionId(
|
|||||||
log(`[call_omo_agent] Creating new session with parent: ${toolContext.sessionID}`)
|
log(`[call_omo_agent] Creating new session with parent: ${toolContext.sessionID}`)
|
||||||
const parentSession = await ctx.client.session
|
const parentSession = await ctx.client.session
|
||||||
.get({ path: { id: toolContext.sessionID } })
|
.get({ path: { id: toolContext.sessionID } })
|
||||||
.catch((err) => {
|
.catch((err: unknown) => {
|
||||||
log("[call_omo_agent] Failed to get parent session", { error: String(err) })
|
log("[call_omo_agent] Failed to get parent session", { error: String(err) })
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
const parentDirectory = parentSession?.data?.directory ?? ctx.directory
|
const parentDirectory = resolveSessionDirectory({
|
||||||
|
parentDirectory: parentSession?.data?.directory,
|
||||||
|
fallbackDirectory: ctx.directory,
|
||||||
|
})
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
parentID: toolContext.sessionID,
|
parentID: toolContext.sessionID,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user