diff --git a/src/modes/manager.test.ts b/src/modes/manager.test.ts index 338168d..4653a56 100644 --- a/src/modes/manager.test.ts +++ b/src/modes/manager.test.ts @@ -41,6 +41,9 @@ class MockModeManager { /** Tracks whether a drift-toast was shown during initialize */ lastDriftToast: string | null = null + /** Tracks whether a no-drift info toast was shown during initialize */ + lastInfoToast: string | null = null + constructor(client: OpencodeClient) { this.client = client } @@ -79,30 +82,35 @@ class MockModeManager { } const drifted = this.hasConfigDrift(preset) - if (!drifted) { - return - } - // Apply preset to in-memory configs using recursive merge - if (this.opencodeConfig) { - if (preset.model) { - this.opencodeConfig.model = preset.model + if (drifted) { + // Apply preset to in-memory configs using recursive merge + if (this.opencodeConfig) { + if (preset.model) { + this.opencodeConfig.model = preset.model + } + this.opencodeConfig.agent = this.opencodeConfig.agent || {} + deepMergeModel( + this.opencodeConfig.agent as Record, + preset.opencode + ) } - this.opencodeConfig.agent = this.opencodeConfig.agent || {} - deepMergeModel( - this.opencodeConfig.agent as Record, - preset.opencode - ) - } - if (this.ohMyConfig && preset['oh-my-opencode']) { - deepMergeModel( - this.ohMyConfig as Record, - preset['oh-my-opencode'] - ) + if (this.ohMyConfig && preset['oh-my-opencode']) { + deepMergeModel( + this.ohMyConfig as Record, + preset['oh-my-opencode'] + ) + } + + this.lastDriftToast = `applied "${this.config.currentMode}" mode.\nrestart opencode to take effect.` + return } - this.lastDriftToast = `Applied "${this.config.currentMode}" mode. Restart opencode to take effect.` + // No drift: show informational toast if configured + if (this.config.showToastOnStartup) { + this.lastInfoToast = `current mode: ${this.config.currentMode}` + } } private hasConfigDrift(preset: ModePreset): boolean { @@ -495,7 +503,7 @@ describe('ModeManager', () => { await manager.initialize() expect(manager.lastDriftToast).toContain('economy') - expect(manager.lastDriftToast).toContain('Restart opencode') + expect(manager.lastDriftToast).toContain('restart opencode') }) test('applies preset when oh-my-opencode.json has drifted', async () => { @@ -546,6 +554,88 @@ describe('ModeManager', () => { expect(manager.lastDriftToast).toBeNull() }) + test('shows info toast when no drift and showToastOnStartup is true', async () => { + const config = clonePluginConfig() + config.currentMode = 'economy' + config.showToastOnStartup = true + manager.setConfig(config) + + manager.setOpencodeConfig({ + model: 'opencode/glm-4.7-free', + agent: { + build: { model: 'opencode/glm-4.7-free' }, + plan: { model: 'opencode/glm-4.7-free' }, + }, + }) + + manager.setOhMyConfig({ + agents: { + sisyphus: { model: 'opencode/glm-4.7-free' }, + oracle: { model: 'opencode/glm-4.7-free' }, + }, + categories: { + 'visual-engineering': { model: 'opencode/glm-4.7-free' }, + quick: { model: 'opencode/glm-4.7-free' }, + }, + }) + + await manager.initialize() + + expect(manager.lastDriftToast).toBeNull() + expect(manager.lastInfoToast).toBe('current mode: economy') + }) + + test('does not show info toast when no drift and showToastOnStartup is false', async () => { + const config = clonePluginConfig() + config.currentMode = 'economy' + config.showToastOnStartup = false + manager.setConfig(config) + + manager.setOpencodeConfig({ + model: 'opencode/glm-4.7-free', + agent: { + build: { model: 'opencode/glm-4.7-free' }, + plan: { model: 'opencode/glm-4.7-free' }, + }, + }) + + manager.setOhMyConfig({ + agents: { + sisyphus: { model: 'opencode/glm-4.7-free' }, + oracle: { model: 'opencode/glm-4.7-free' }, + }, + categories: { + 'visual-engineering': { model: 'opencode/glm-4.7-free' }, + quick: { model: 'opencode/glm-4.7-free' }, + }, + }) + + await manager.initialize() + + expect(manager.lastDriftToast).toBeNull() + expect(manager.lastInfoToast).toBeNull() + }) + + test('does not show info toast when drift is detected (only drift toast)', async () => { + const config = clonePluginConfig() + config.currentMode = 'economy' + config.showToastOnStartup = true + manager.setConfig(config) + + manager.setOpencodeConfig({ + model: 'anthropic/claude-sonnet-4', // Mismatch: drift + agent: { + build: { model: 'anthropic/claude-sonnet-4' }, + plan: { model: 'anthropic/claude-sonnet-4' }, + }, + }) + + await manager.initialize() + + expect(manager.lastDriftToast).not.toBeNull() + expect(manager.lastInfoToast).toBeNull() + }) + test('does nothing when preset is not found', async () => { const config: ModeSwitcherConfig = { currentMode: 'nonexistent', diff --git a/src/modes/manager.ts b/src/modes/manager.ts index bfe1490..53f9c66 100644 --- a/src/modes/manager.ts +++ b/src/modes/manager.ts @@ -240,6 +240,12 @@ export class ModeManager { * files are updated to match the expected preset values, * and a toast notification prompts the user to restart. * + * When no drift is detected and `showToastOnStartup` is enabled, + * an informational toast displays the current mode name. + * + * Toast notifications are delayed via `setTimeout` to allow the + * OpenCode UI to fully initialize before sending them. + * * @private */ private async applyCurrentModeIfNeeded(): Promise { @@ -253,34 +259,53 @@ export class ModeManager { } const drifted = await this.hasConfigDrift(preset) - if (!drifted) { + + if (drifted) { + // Apply the preset to actual config files + await this.updateOpencodeConfig(preset.model, preset.opencode) + if (preset['oh-my-opencode']) { + await this.updateOhMyOpencodeConfig(preset['oh-my-opencode']) + } + + // Delay toast to allow UI to initialize before displaying. + // This is always shown regardless of showToastOnStartup since + // config drift is an important warning the user needs to see. + const modeName = this.config.currentMode + setTimeout(() => { + this.client.tui + .showToast({ + body: { + title: 'agent-mode-switcher', + message: `applied "${modeName}" mode.\nrestart opencode to take effect.`, + variant: 'warning', + duration: 5000, + }, + }) + .catch(() => { + // Toast might not be available if UI failed to initialize + }) + }, 3000) return } - // Apply the preset to actual config files - await this.updateOpencodeConfig(preset.model, preset.opencode) - if (preset['oh-my-opencode']) { - await this.updateOhMyOpencodeConfig(preset['oh-my-opencode']) + // No drift: show informational toast if configured + if (this.config.showToastOnStartup) { + const modeName = this.config.currentMode + setTimeout(() => { + this.client.tui + .showToast({ + body: { + title: 'agent-mode-switcher', + message: `current mode: ${modeName}`, + variant: 'info', + duration: 3000, + }, + }) + .catch(() => { + // Toast might not be available if UI failed to initialize + }) + }, 3000) } - - // Notify user to restart (fire-and-forget to avoid blocking - // plugin initialization when UI is not yet ready). - // TODO: Currently toast is likely not displayed because UI is - // not initialized at this point. To reliably show the toast, - // use setTimeout for delayed execution or an onReady lifecycle - // hook if OpenCode adds one in the future. - this.client.tui - .showToast({ - body: { - title: 'Mode Applied', - message: `Applied "${this.config.currentMode}" mode. Restart opencode to take effect.`, - variant: 'warning', - duration: 5000, - }, - }) - .catch(() => { - // Toast might not be available during early initialization - }) } /**