diff --git a/core/engine.go b/core/engine.go index 6235bb89..58a48d88 100644 --- a/core/engine.go +++ b/core/engine.go @@ -5093,7 +5093,13 @@ func (e *Engine) cmdReasoning(p Platform, msg *Message, args []string) { } func (e *Engine) cmdMode(p Platform, msg *Message, args []string) { - switcher, ok := e.agent.(ModeSwitcher) + agent, sessions, interactiveKey, err := e.commandContext(p, msg) + if err != nil { + e.reply(p, msg.ReplyCtx, e.i18n.Tf(MsgWsResolutionError, err)) + return + } + + switcher, ok := agent.(ModeSwitcher) if !ok { e.reply(p, msg.ReplyCtx, e.i18n.T(MsgModeNotSupported)) return @@ -5141,7 +5147,7 @@ func (e *Engine) cmdMode(p Platform, msg *Message, args []string) { e.replyWithButtons(p, msg.ReplyCtx, sb.String(), buttons) return } - e.replyWithCard(p, msg.ReplyCtx, e.renderModeCard()) + e.replyWithCard(p, msg.ReplyCtx, e.renderModeCardForAgent(agent)) return } @@ -5151,7 +5157,12 @@ func (e *Engine) cmdMode(p Platform, msg *Message, args []string) { appliedLive := e.applyLiveModeChange(msg.SessionKey, newMode) if !appliedLive { - e.cleanupInteractiveState(e.interactiveKeyForSessionKey(msg.SessionKey)) + e.cleanupInteractiveState(interactiveKey) + // Live switch unavailable: recreate the session so the new mode takes effect. + s := sessions.GetOrCreateActive(msg.SessionKey) + s.SetAgentSessionID("", "") + s.ClearHistory() + sessions.Save() } modes := switcher.PermissionModes() @@ -6215,7 +6226,8 @@ func (e *Engine) handleCardNav(action string, sessionKey string) *Card { case "/reasoning": return e.renderReasoningCard() case "/mode": - return e.renderModeCard() + agent, _ := e.sessionContextForKey(sessionKey) + return e.renderModeCardForAgent(agent) case "/lang": return e.renderLangCard() case "/status": @@ -6349,7 +6361,8 @@ func (e *Engine) executeCardAction(cmd, args, sessionKey string) { if args == "" { return } - switcher, ok := e.agent.(ModeSwitcher) + agent, sessions := e.sessionContextForKey(sessionKey) + switcher, ok := agent.(ModeSwitcher) if !ok { return } @@ -6362,10 +6375,10 @@ func (e *Engine) executeCardAction(cmd, args, sessionKey string) { } e.cleanupInteractiveState(interactiveKey) // Mode change requires a new session to take effect - s := e.sessions.GetOrCreateActive(sessionKey) + s := sessions.GetOrCreateActive(sessionKey) s.SetAgentSessionID("", "") s.ClearHistory() - e.sessions.Save() + sessions.Save() case "/lang": if args == "" { @@ -6931,8 +6944,8 @@ func (e *Engine) renderReasoningCard() *Card { return cb.Build() } -func (e *Engine) renderModeCard() *Card { - switcher, ok := e.agent.(ModeSwitcher) +func (e *Engine) renderModeCardForAgent(agent Agent) *Card { + switcher, ok := agent.(ModeSwitcher) if !ok { return e.simpleCard(e.i18n.T(MsgCardTitleMode), "violet", e.i18n.T(MsgModeNotSupported)) } diff --git a/core/engine_test.go b/core/engine_test.go index a75b539e..6999c910 100644 --- a/core/engine_test.go +++ b/core/engine_test.go @@ -3495,6 +3495,39 @@ func TestCmdMode_AppliesLiveModeWithoutReset(t *testing.T) { } } +func TestCmdMode_MultiWorkspaceUpdatesWorkspaceAgent(t *testing.T) { + p := &stubPlatformEngine{n: "plain"} + globalAgent := &stubModelModeAgent{mode: "default"} + e := NewEngine("test", globalAgent, []Platform{p}, "", LangEnglish) + + baseDir := t.TempDir() + bindingPath := filepath.Join(t.TempDir(), "bindings.json") + e.SetMultiWorkspace(baseDir, bindingPath) + + wsDir := filepath.Join(baseDir, "ws1") + if err := os.MkdirAll(wsDir, 0o755); err != nil { + t.Fatal(err) + } + normalizedWsDir := normalizeWorkspacePath(wsDir) + channelID := "C123" + e.workspaceBindings.Bind("project:test", channelID, "chan", normalizedWsDir) + + wsAgent := &stubModelModeAgent{mode: "default"} + ws := e.workspacePool.GetOrCreate(normalizedWsDir) + ws.agent = wsAgent + ws.sessions = NewSessionManager("") + + msg := &Message{SessionKey: "slack:" + channelID + ":U1", ReplyCtx: "ctx"} + e.cmdMode(p, msg, []string{"yolo"}) + + if wsAgent.mode != "yolo" { + t.Fatalf("workspace agent mode = %q, want yolo", wsAgent.mode) + } + if globalAgent.mode == "yolo" { + t.Fatalf("global agent mode = %q, want unchanged", globalAgent.mode) + } +} + func TestCmdStatus_UsesLegacyTextOnPlatformWithoutCardSupport(t *testing.T) { p := &stubPlatformEngine{n: "plain"} e := NewEngine("test", &stubAgent{}, []Platform{p}, "", LangEnglish) @@ -5605,6 +5638,51 @@ func TestExecuteCardAction_ModeCleansUpWithInteractiveKey(t *testing.T) { } } +func TestExecuteCardAction_ModeUsesWorkspaceAgent(t *testing.T) { + p := &stubPlatformEngine{n: "plain"} + globalAgent := &stubModelModeAgent{mode: "default"} + e := NewEngine("test", globalAgent, []Platform{p}, "", LangEnglish) + + baseDir := t.TempDir() + bindingPath := filepath.Join(t.TempDir(), "bindings.json") + e.SetMultiWorkspace(baseDir, bindingPath) + + wsDir := filepath.Join(baseDir, "ws1") + if err := os.MkdirAll(wsDir, 0o755); err != nil { + t.Fatal(err) + } + normalizedWsDir := normalizeWorkspacePath(wsDir) + channelID := "channel1" + sessionKey := "feishu:" + channelID + ":user1" + e.workspaceBindings.Bind("project:test", channelID, "chan", normalizedWsDir) + + wsAgent := &stubModelModeAgent{mode: "default"} + ws := e.workspacePool.GetOrCreate(normalizedWsDir) + ws.agent = wsAgent + ws.sessions = NewSessionManager("") + + interactiveKey := normalizedWsDir + ":" + sessionKey + e.interactiveMu.Lock() + e.interactiveStates[interactiveKey] = &interactiveState{} + e.interactiveMu.Unlock() + + e.executeCardAction("/mode", "yolo", sessionKey) + + if wsAgent.mode != "yolo" { + t.Fatalf("workspace agent mode = %q, want yolo", wsAgent.mode) + } + if globalAgent.mode == "yolo" { + t.Fatalf("global agent mode = %q, want unchanged", globalAgent.mode) + } + + e.interactiveMu.Lock() + _, exists := e.interactiveStates[interactiveKey] + e.interactiveMu.Unlock() + if exists { + t.Error("expected workspace interactive state to be cleaned up after /mode") + } +} + // =========================================================================== // P0 Beta release tests // ===========================================================================