diff --git a/src/create-managers.test.ts b/src/create-managers.test.ts
index e246e1dcb..94a18460d 100644
--- a/src/create-managers.test.ts
+++ b/src/create-managers.test.ts
@@ -1,23 +1,66 @@
///
-import { beforeEach, describe, expect, it, mock } from "bun:test"
+import { afterAll, beforeEach, describe, expect, it, mock, spyOn } from "bun:test"
-import { createManagers } from "./create-managers"
+import * as openclawRuntimeDispatch from "./openclaw/runtime-dispatch"
-class MockBackgroundManager {
- constructor(..._args: unknown[]) {}
-}
+const markServerRunningInProcess = mock(() => {})
+let backgroundManagerOptions: {
+ onSubagentSessionCreated?: (event: { sessionID: string; parentID: string; title: string }) => Promise
+} | null = null
+const trackedPaneBySession = new Map()
-class MockSkillMcpManager {
- constructor(..._args: unknown[]) {}
-}
+mock.module("./features/background-agent", () => ({
+ BackgroundManager: class BackgroundManager {
+ constructor(_ctx: unknown, _config: unknown, options: typeof backgroundManagerOptions) {
+ backgroundManagerOptions = options
+ }
+ },
+}))
-class MockTmuxSessionManager {
- constructor(..._args: unknown[]) {}
+mock.module("./features/skill-mcp-manager", () => ({
+ SkillMcpManager: class SkillMcpManager {
+ constructor(..._args: unknown[]) {}
+ },
+}))
- async cleanup(): Promise {}
- async onSessionCreated(..._args: unknown[]): Promise {}
-}
+mock.module("./features/task-toast-manager", () => ({
+ initTaskToastManager: mock(() => {}),
+}))
+
+mock.module("./features/tmux-subagent", () => ({
+ TmuxSessionManager: class TmuxSessionManager {
+ constructor(..._args: unknown[]) {}
+
+ async cleanup(): Promise {}
+ async onSessionCreated(event: { properties?: { info?: { id?: string } } }): Promise {
+ const sessionID = event.properties?.info?.id
+ if (sessionID) {
+ trackedPaneBySession.set(sessionID, `%pane-${sessionID}`)
+ }
+ }
+
+ getTrackedPaneId(sessionID: string): string | undefined {
+ return trackedPaneBySession.get(sessionID)
+ }
+ },
+}))
+
+mock.module("./features/background-agent/process-cleanup", () => ({
+ registerManagerForCleanup: mock(() => {}),
+}))
+
+mock.module("./plugin-handlers", () => ({
+ createConfigHandler: mock(() => ({ kind: "config-handler" })),
+}))
+
+mock.module("./shared/tmux/tmux-utils/server-health", () => ({
+ isServerRunning: mock(async () => true),
+ markServerRunningInProcess,
+ resetServerCheck: mock(() => {}),
+}))
+
+const { createManagers } = await import("./create-managers")
function createTmuxConfig(enabled: boolean) {
return {
@@ -31,63 +74,79 @@ function createTmuxConfig(enabled: boolean) {
}
describe("createManagers", () => {
- const markServerRunningInProcess = mock(() => {})
- const initTaskToastManager = mock(() => ({}) as never)
- const registerManagerForCleanup = mock(() => {})
- const createConfigHandler = mock(() => (async () => {}) as never)
-
- function createMockArgs(enabled: boolean): Parameters[0] {
- return {
- ctx: {
- directory: "/tmp",
- client: {} as never,
- project: {} as never,
- worktree: "/tmp",
- serverUrl: new URL("https://example.com"),
- $: Bun.$,
- },
- pluginConfig: {} as never,
- tmuxConfig: createTmuxConfig(enabled),
- modelCacheState: {} as never,
- backgroundNotificationHookEnabled: false,
- deps: {
- BackgroundManagerClass: MockBackgroundManager as never,
- SkillMcpManagerClass: MockSkillMcpManager as never,
- TmuxSessionManagerClass: MockTmuxSessionManager as never,
- initTaskToastManagerFn: initTaskToastManager,
- registerManagerForCleanupFn: registerManagerForCleanup,
- createConfigHandlerFn: createConfigHandler,
- markServerRunningInProcessFn: markServerRunningInProcess,
- },
- }
- }
+ const dispatchOpenClawEvent = spyOn(openclawRuntimeDispatch, "dispatchOpenClawEvent")
beforeEach(() => {
markServerRunningInProcess.mockClear()
- initTaskToastManager.mockClear()
- registerManagerForCleanup.mockClear()
- createConfigHandler.mockClear()
+ dispatchOpenClawEvent.mockReset()
+ backgroundManagerOptions = null
+ trackedPaneBySession.clear()
+ })
+
+ afterAll(() => {
+ mock.restore()
})
it("#given tmux integration is disabled #when managers are created #then it does not mark the tmux server as running", () => {
- // #given
- const args = createMockArgs(false)
+ const args = {
+ ctx: { directory: "/tmp", client: {} },
+ pluginConfig: {},
+ tmuxConfig: createTmuxConfig(false),
+ modelCacheState: {},
+ backgroundNotificationHookEnabled: false,
+ } as Parameters[0]
- // #when
createManagers(args)
- // #then
expect(markServerRunningInProcess).not.toHaveBeenCalled()
})
it("#given tmux integration is enabled #when managers are created #then it marks the tmux server as running", () => {
- // #given
- const args = createMockArgs(true)
+ const args = {
+ ctx: { directory: "/tmp", client: {} },
+ pluginConfig: {},
+ tmuxConfig: createTmuxConfig(true),
+ modelCacheState: {},
+ backgroundNotificationHookEnabled: false,
+ } as Parameters[0]
- // #when
createManagers(args)
- // #then
expect(markServerRunningInProcess).toHaveBeenCalledTimes(1)
})
+
+ it("#given openclaw is enabled #when the background session-created callback runs #then it dispatches openclaw with the tracked pane id", async () => {
+ const args = {
+ ctx: { directory: "/tmp/project", client: {} },
+ pluginConfig: {
+ openclaw: {
+ enabled: true,
+ gateways: {},
+ hooks: {},
+ },
+ },
+ tmuxConfig: createTmuxConfig(true),
+ modelCacheState: {},
+ backgroundNotificationHookEnabled: false,
+ } as Parameters[0]
+
+ createManagers(args)
+
+ await backgroundManagerOptions?.onSubagentSessionCreated?.({
+ sessionID: "ses-bg-1",
+ parentID: "ses-parent",
+ title: "child task",
+ })
+
+ expect(dispatchOpenClawEvent).toHaveBeenCalledTimes(1)
+ expect(dispatchOpenClawEvent).toHaveBeenCalledWith({
+ config: args.pluginConfig.openclaw,
+ rawEvent: "session.created",
+ context: {
+ sessionId: "ses-bg-1",
+ projectPath: "/tmp/project",
+ tmuxPaneId: "%pane-ses-bg-1",
+ },
+ })
+ })
})
diff --git a/src/create-managers.ts b/src/create-managers.ts
index 51b93270e..dc2a99a98 100644
--- a/src/create-managers.ts
+++ b/src/create-managers.ts
@@ -7,6 +7,7 @@ import { BackgroundManager } from "./features/background-agent"
import { SkillMcpManager } from "./features/skill-mcp-manager"
import { initTaskToastManager } from "./features/task-toast-manager"
import { TmuxSessionManager } from "./features/tmux-subagent"
+import * as openclawRuntimeDispatch from "./openclaw/runtime-dispatch"
import { registerManagerForCleanup } from "./features/background-agent/process-cleanup"
import { createConfigHandler } from "./plugin-handlers"
import { log } from "./shared"
@@ -86,6 +87,18 @@ export function createManagers(args: {
},
})
+ if (pluginConfig.openclaw) {
+ await openclawRuntimeDispatch.dispatchOpenClawEvent({
+ config: pluginConfig.openclaw,
+ rawEvent: "session.created",
+ context: {
+ sessionId: event.sessionID,
+ projectPath: ctx.directory,
+ tmuxPaneId: tmuxSessionManager.getTrackedPaneId?.(event.sessionID) ?? process.env.TMUX_PANE,
+ },
+ })
+ }
+
log("[index] onSubagentSessionCreated callback completed")
},
onShutdown: async () => {
diff --git a/src/features/tmux-subagent/manager.ts b/src/features/tmux-subagent/manager.ts
index 7bab2d215..a31f668bf 100644
--- a/src/features/tmux-subagent/manager.ts
+++ b/src/features/tmux-subagent/manager.ts
@@ -182,6 +182,10 @@ export class TmuxSessionManager {
}))
}
+ getTrackedPaneId(sessionId: string): string | undefined {
+ return this.sessions.get(sessionId)?.paneId
+ }
+
private removeTrackedSession(sessionId: string): void {
this.sessions.delete(sessionId)