diff --git a/README.md b/README.md
index bb86cc4c9e5..dd6aa3d24de 100644
--- a/README.md
+++ b/README.md
@@ -178,6 +178,7 @@ Works with **any CLI agent** — if it runs in a terminal, it runs in Orca.
Cursor
GitHub Copilot
OpenCode
+
MiMo Code
Amp
OpenClaude
Antigravity
diff --git a/mobile/src/tasks/mobile-tui-agents.ts b/mobile/src/tasks/mobile-tui-agents.ts
index 50e58612075..1c9ef440ac0 100644
--- a/mobile/src/tasks/mobile-tui-agents.ts
+++ b/mobile/src/tasks/mobile-tui-agents.ts
@@ -11,6 +11,7 @@ export const MOBILE_TUI_AGENT_AUTO_PICK_ORDER = [
'grok',
'copilot',
'opencode',
+ 'mimo-code',
'ante',
'pi',
'omp',
@@ -47,6 +48,7 @@ export const MOBILE_TUI_AGENT_LABELS: Record = {
grok: 'Grok',
copilot: 'GitHub Copilot',
opencode: 'OpenCode',
+ 'mimo-code': 'MiMo Code',
ante: 'Ante',
pi: 'Pi',
omp: 'OMP',
@@ -80,6 +82,7 @@ export const MOBILE_TUI_AGENT_FAVICON_DOMAINS: Partial>
grok: 'x.ai',
copilot: 'github.com',
opencode: 'opencode.ai',
+ 'mimo-code': 'mimo.xiaomi.com',
ante: 'antigma.ai',
omp: 'omp.sh',
gemini: 'gemini.google.com',
@@ -114,6 +117,7 @@ export const MOBILE_TUI_AGENT_LAUNCH_COMMANDS: Record = {
grok: 'grok',
copilot: 'copilot',
opencode: 'opencode',
+ 'mimo-code': 'mimo',
ante: 'ante',
pi: 'pi',
omp: 'omp',
diff --git a/mobile/src/worktree/agent-row-display.ts b/mobile/src/worktree/agent-row-display.ts
index f052f603cf2..05391b88b89 100644
--- a/mobile/src/worktree/agent-row-display.ts
+++ b/mobile/src/worktree/agent-row-display.ts
@@ -80,7 +80,8 @@ export function agentIdentityLabel(agentType: string | null): string {
copilot: 'CP',
amp: 'AM',
aider: 'AI',
- opencode: 'OC'
+ opencode: 'OC',
+ 'mimo-code': 'MC'
}
return known[normalized] ?? normalized.slice(0, 2).toUpperCase()
}
diff --git a/src/main/daemon/pty-subprocess.test.ts b/src/main/daemon/pty-subprocess.test.ts
index 75afb290310..c0ac8d3a7ab 100644
--- a/src/main/daemon/pty-subprocess.test.ts
+++ b/src/main/daemon/pty-subprocess.test.ts
@@ -48,6 +48,7 @@ import { createPtySubprocess } from './pty-subprocess'
const ORCA_SHELL_WRAPPER_ENV = [
'ORCA_ATTRIBUTION_SHIM_DIR',
'ORCA_OPENCODE_CONFIG_DIR',
+ 'ORCA_MIMOCODE_HOME',
'ORCA_PI_CODING_AGENT_DIR',
'ORCA_OMP_CODING_AGENT_DIR',
'ORCA_CODEX_HOME'
@@ -906,6 +907,35 @@ describe('createPtySubprocess', () => {
expect(lastCall[2].env.ORCA_SHELL_READY_MARKER).toBe('0')
})
+ it('uses shell wrapper when MiMo home must survive shell startup', () => {
+ const proc = mockPtyProcess()
+ spawnMock.mockReturnValue(proc)
+ const platform = Object.getOwnPropertyDescriptor(process, 'platform')
+ Object.defineProperty(process, 'platform', { value: 'linux' })
+
+ try {
+ createPtySubprocess({
+ sessionId: 'test',
+ cols: 80,
+ rows: 24,
+ env: {
+ SHELL: '/bin/zsh',
+ MIMOCODE_HOME: '/tmp/orca-mimocode-overlay',
+ ORCA_MIMOCODE_HOME: '/tmp/orca-mimocode-overlay'
+ }
+ })
+ } finally {
+ if (platform) {
+ Object.defineProperty(process, 'platform', platform)
+ }
+ }
+
+ const lastCall = spawnMock.mock.calls.at(-1)!
+ expect(lastCall[1]).toEqual(['-l'])
+ expect(lastCall[2].env.ZDOTDIR).toMatch(ZSH_SHELL_READY_DIR)
+ expect(lastCall[2].env.ORCA_SHELL_READY_MARKER).toBe('0')
+ })
+
it('uses shell wrapper when typed OMP commands need the status extension', () => {
const proc = mockPtyProcess()
spawnMock.mockReturnValue(proc)
diff --git a/src/main/daemon/pty-subprocess.ts b/src/main/daemon/pty-subprocess.ts
index 22fd03412b6..0e6e817dc33 100644
--- a/src/main/daemon/pty-subprocess.ts
+++ b/src/main/daemon/pty-subprocess.ts
@@ -593,6 +593,7 @@ export function createPtySubprocess(opts: PtySubprocessOptions): SubprocessHandl
shellLaunch =
env.ORCA_ATTRIBUTION_SHIM_DIR ||
env.ORCA_OPENCODE_CONFIG_DIR ||
+ env.ORCA_MIMOCODE_HOME ||
env.ORCA_OMP_STATUS_EXTENSION ||
env.ORCA_CODEX_HOME ||
env.ORCA_AGENT_TEAMS_SHIM_DIR
diff --git a/src/main/daemon/shell-ready.test.ts b/src/main/daemon/shell-ready.test.ts
index 16111573417..ebe7474cd24 100644
--- a/src/main/daemon/shell-ready.test.ts
+++ b/src/main/daemon/shell-ready.test.ts
@@ -472,6 +472,8 @@ describePosix('daemon shell-ready launch config', () => {
const bashRc = readFileSync(join(userDataPath, 'shell-ready', 'bash', 'rcfile'), 'utf8')
const restoreLine =
'[[ -n "${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="${ORCA_OPENCODE_CONFIG_DIR}"'
+ const mimoRestoreLine =
+ '[[ -n "${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="${ORCA_MIMOCODE_HOME}"'
const codexRestoreLine =
'[[ -n "${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="${ORCA_CODEX_HOME}"'
const agentTeamsPathRestoreLine = '[[ -n "${ORCA_AGENT_TEAMS_SHIM_DIR:-}" ]] || return 0'
@@ -479,6 +481,9 @@ describePosix('daemon shell-ready launch config', () => {
expect(zshrc).toContain(restoreLine)
expect(zlogin).toContain(restoreLine)
expect(bashRc).toContain(restoreLine)
+ expect(zshrc).toContain(mimoRestoreLine)
+ expect(zlogin).toContain(mimoRestoreLine)
+ expect(bashRc).toContain(mimoRestoreLine)
expect(zshrc).not.toContain('ORCA_PI_CODING_AGENT_DIR')
expect(zlogin).not.toContain('ORCA_PI_CODING_AGENT_DIR')
expect(bashRc).not.toContain('ORCA_PI_CODING_AGENT_DIR')
diff --git a/src/main/daemon/shell-ready.ts b/src/main/daemon/shell-ready.ts
index 05f3c25e7d5..065edb603b8 100644
--- a/src/main/daemon/shell-ready.ts
+++ b/src/main/daemon/shell-ready.ts
@@ -114,6 +114,7 @@ __orca_restore_agent_teams_path
# Why: user startup files may set the default OpenCode config after Orca's
# spawn env; restore the Orca-managed config dir before the first prompt.
[[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}"
+[[ -n "\${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="\${ORCA_MIMOCODE_HOME}"
${getPosixOmpShellWrapper()}
# Why: Codex must keep using Orca's runtime CODEX_HOME after profile scripts.
[[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}"
@@ -226,6 +227,7 @@ __orca_restore_agent_teams_path() {
if [[ ! -o login ]]; then
# Why: ~/.zshrc can export the user's default OpenCode config after spawn.
[[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}"
+ [[ -n "\${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="\${ORCA_MIMOCODE_HOME}"
${getPosixOmpShellWrapper()}
[[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}"
fi
@@ -288,6 +290,7 @@ __orca_restore_agent_teams_path() {
__orca_restore_agent_teams_path
# Why: .zlogin is the final login startup file before the prompt is shown.
[[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}"
+[[ -n "\${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="\${ORCA_MIMOCODE_HOME}"
${getPosixOmpShellWrapper()}
[[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}"
${getZshShellReadyMarkerRegistrationBlock(SHELL_READY_MARKER)}
diff --git a/src/main/ipc/pty.test.ts b/src/main/ipc/pty.test.ts
index 5e0261c601a..0187f2afaef 100644
--- a/src/main/ipc/pty.test.ts
+++ b/src/main/ipc/pty.test.ts
@@ -38,6 +38,7 @@ const {
spawnMock,
openCodeBuildPtyEnvMock,
openCodeClearPtyMock,
+ mimoCodeBuildPtyEnvMock,
buildAgentHookEnvMock,
clearAgentHookPaneStateMock,
registerPaneKeyAliasMock,
@@ -67,6 +68,7 @@ const {
getPathMock: vi.fn(),
spawnMock: vi.fn(),
openCodeBuildPtyEnvMock: vi.fn(),
+ mimoCodeBuildPtyEnvMock: vi.fn(),
isPwshAvailableMock: vi.fn(),
openCodeClearPtyMock: vi.fn(),
buildAgentHookEnvMock: vi.fn(),
@@ -124,6 +126,12 @@ vi.mock('../opencode/hook-service', () => ({
}
}))
+vi.mock('../mimo/hook-service', () => ({
+ mimoCodeHookService: {
+ buildPtyEnv: mimoCodeBuildPtyEnvMock
+ }
+}))
+
vi.mock('../agent-hooks/server', () => ({
agentHookServer: {
buildPtyEnv: buildAgentHookEnvMock,
@@ -271,6 +279,7 @@ describe('registerPtyHandlers', () => {
getPathMock.mockReset()
spawnMock.mockReset()
openCodeBuildPtyEnvMock.mockReset()
+ mimoCodeBuildPtyEnvMock.mockReset()
openCodeClearPtyMock.mockReset()
buildAgentHookEnvMock.mockReset()
clearAgentHookPaneStateMock.mockReset()
@@ -304,6 +313,9 @@ describe('registerPtyHandlers', () => {
? '/tmp/orca-opencode-overlay'
: '/tmp/orca-opencode-config'
}))
+ mimoCodeBuildPtyEnvMock.mockImplementation((_ptyId: string, existingHome?: string) => ({
+ MIMOCODE_HOME: existingHome ? '/tmp/orca-mimocode-overlay' : '/tmp/orca-mimocode-shared'
+ }))
buildAgentHookEnvMock.mockReturnValue({
ORCA_AGENT_HOOK_PORT: '5678',
ORCA_AGENT_HOOK_TOKEN: 'agent-token'
@@ -758,6 +770,51 @@ describe('registerPtyHandlers', () => {
expect(env.ORCA_OPENCODE_SOURCE_CONFIG_DIR).toBeUndefined()
})
+ it('injects MiMo overlay env only when launch command is mimo', async () => {
+ const env = await spawnAndGetEnv(undefined, undefined, undefined, undefined, 'mimo')
+
+ expect(mimoCodeBuildPtyEnvMock).toHaveBeenCalledTimes(1)
+ expect(env.MIMOCODE_HOME).toBe('/tmp/orca-mimocode-shared')
+ expect(env.ORCA_MIMOCODE_HOME).toBe('/tmp/orca-mimocode-shared')
+ expect(env.ORCA_MIMOCODE_SOURCE_HOME).toBeUndefined()
+ })
+
+ it.each(['/usr/local/bin/mimo --prompt hi', '"C:\\Program Files\\MiMo\\mimo.cmd" --prompt hi'])(
+ 'injects MiMo overlay env for path-qualified launch command %s',
+ async (launchCommand) => {
+ const env = await spawnAndGetEnv(undefined, undefined, undefined, undefined, launchCommand)
+
+ expect(mimoCodeBuildPtyEnvMock).toHaveBeenCalledTimes(1)
+ expect(env.MIMOCODE_HOME).toBe('/tmp/orca-mimocode-shared')
+ expect(env.ORCA_MIMOCODE_HOME).toBe('/tmp/orca-mimocode-shared')
+ }
+ )
+
+ it('does not inject MiMo overlay for non-mimo launches', async () => {
+ await spawnAndGetEnv()
+
+ expect(mimoCodeBuildPtyEnvMock).not.toHaveBeenCalled()
+ })
+
+ it('restores user MiMo home when agent status hooks are disabled in a nested Orca shell', async () => {
+ const env = await spawnAndGetEnv(
+ {
+ MIMOCODE_HOME: '/tmp/parent-orca-mimocode-overlay',
+ ORCA_MIMOCODE_HOME: '/tmp/parent-orca-mimocode-overlay',
+ ORCA_MIMOCODE_SOURCE_HOME: '/tmp/user-mimocode-home'
+ },
+ undefined,
+ undefined,
+ () => ({ agentStatusHooksEnabled: false }),
+ 'mimo'
+ )
+
+ expect(mimoCodeBuildPtyEnvMock).not.toHaveBeenCalled()
+ expect(env.MIMOCODE_HOME).toBe('/tmp/user-mimocode-home')
+ expect(env.ORCA_MIMOCODE_HOME).toBeUndefined()
+ expect(env.ORCA_MIMOCODE_SOURCE_HOME).toBeUndefined()
+ })
+
posixOnlyIt(
'reproduces issue #1534: GUI-launched Orca mirrors zshrc-only OpenCode config',
async () => {
@@ -1903,6 +1960,9 @@ describe('registerPtyHandlers', () => {
expect(env.OPENCODE_CONFIG_DIR).toBeUndefined()
expect(env.ORCA_OPENCODE_CONFIG_DIR).toBeUndefined()
expect(env.ORCA_OPENCODE_SOURCE_CONFIG_DIR).toBeUndefined()
+ expect(env.MIMOCODE_HOME).toBeUndefined()
+ expect(env.ORCA_MIMOCODE_HOME).toBeUndefined()
+ expect(env.ORCA_MIMOCODE_SOURCE_HOME).toBeUndefined()
expect(env.PI_CODING_AGENT_DIR).toBeUndefined()
expect(env.ORCA_PI_CODING_AGENT_DIR).toBeUndefined()
expect(env.ORCA_PI_SOURCE_AGENT_DIR).toBeUndefined()
diff --git a/src/main/ipc/pty.ts b/src/main/ipc/pty.ts
index 1d02b3deb4c..b391e01ca40 100644
--- a/src/main/ipc/pty.ts
+++ b/src/main/ipc/pty.ts
@@ -24,6 +24,11 @@ import {
resolveLocalWindowsTerminalRuntimeOptions
} from '../../shared/local-windows-terminal-runtime'
import { openCodeHookService } from '../opencode/hook-service'
+import { mimoCodeHookService } from '../mimo/hook-service'
+import {
+ getCommandTokenPathBasename,
+ getFirstCommandToken
+} from '../../shared/command-token-scanner'
import { agentHookServer } from '../agent-hooks/server'
import { isAgentStatusHooksEnabled } from '../agent-hooks/managed-agent-hook-controls'
import { piTitlebarExtensionService } from '../pi/titlebar-extension-service'
@@ -611,6 +616,26 @@ function restoreOrStripOverlayEnv(
delete baseEnv[keys.source]
}
+function isMimoLaunchCommand(launchCommand: string | undefined): boolean {
+ const binary = getCommandTokenPathBasename(getFirstCommandToken(launchCommand ?? ''))
+ .toLowerCase()
+ .replace(/\.(?:cmd|exe|sh)$/, '')
+ return binary === 'mimo'
+}
+
+function resolveMimocodeSourceHome(baseEnv: Record): string | undefined {
+ const sourceHome = baseEnv.ORCA_MIMOCODE_SOURCE_HOME ?? process.env.ORCA_MIMOCODE_SOURCE_HOME
+ if (sourceHome) {
+ return sourceHome
+ }
+ const configHome = baseEnv.MIMOCODE_HOME ?? process.env.MIMOCODE_HOME
+ const orcaHome = baseEnv.ORCA_MIMOCODE_HOME ?? process.env.ORCA_MIMOCODE_HOME
+ if (configHome && orcaHome && configHome === orcaHome) {
+ return undefined
+ }
+ return configHome
+}
+
function resolveOpenCodeSourceConfigDir(baseEnv: Record): string | undefined {
const sourceDir =
baseEnv.ORCA_OPENCODE_SOURCE_CONFIG_DIR ?? process.env.ORCA_OPENCODE_SOURCE_CONFIG_DIR
@@ -697,12 +722,29 @@ export function buildPtyHostEnv(
delete baseEnv.ORCA_OPENCODE_SOURCE_CONFIG_DIR
}
}
+ if (isMimoLaunchCommand(opts.launchCommand)) {
+ const preexistingMimocodeHome = resolveMimocodeSourceHome(baseEnv)
+ Object.assign(baseEnv, mimoCodeHookService.buildPtyEnv(id, preexistingMimocodeHome))
+ if (baseEnv.MIMOCODE_HOME) {
+ baseEnv.ORCA_MIMOCODE_HOME = baseEnv.MIMOCODE_HOME
+ if (preexistingMimocodeHome) {
+ baseEnv.ORCA_MIMOCODE_SOURCE_HOME = preexistingMimocodeHome
+ } else {
+ delete baseEnv.ORCA_MIMOCODE_SOURCE_HOME
+ }
+ }
+ }
} else {
restoreOrStripOverlayEnv(baseEnv, {
primary: 'OPENCODE_CONFIG_DIR',
overlay: 'ORCA_OPENCODE_CONFIG_DIR',
source: 'ORCA_OPENCODE_SOURCE_CONFIG_DIR'
})
+ restoreOrStripOverlayEnv(baseEnv, {
+ primary: 'MIMOCODE_HOME',
+ overlay: 'ORCA_MIMOCODE_HOME',
+ source: 'ORCA_MIMOCODE_SOURCE_HOME'
+ })
}
// Why: Claude/Codex native hooks run inside the shell process, so Orca
diff --git a/src/main/mimo/hook-service.test.ts b/src/main/mimo/hook-service.test.ts
new file mode 100644
index 00000000000..0439c727339
--- /dev/null
+++ b/src/main/mimo/hook-service.test.ts
@@ -0,0 +1,78 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'
+import { tmpdir } from 'os'
+import { join } from 'path'
+
+const { getPathMock } = vi.hoisted(() => ({
+ getPathMock: vi.fn<(name: string) => string>()
+}))
+
+vi.mock('electron', () => ({
+ app: {
+ getPath: getPathMock
+ }
+}))
+
+import { MimoCodeHookService } from './hook-service'
+
+describe('MimoCodeHookService buildPtyEnv', () => {
+ let userDataDir: string
+ let mimocodeHome: string
+
+ beforeEach(() => {
+ userDataDir = mkdtempSync(join(tmpdir(), 'orca-mimocode-userdata-'))
+ getPathMock.mockImplementation((name) => {
+ if (name === 'userData') {
+ return userDataDir
+ }
+ throw new Error(`unexpected getPath: ${name}`)
+ })
+
+ mimocodeHome = mkdtempSync(join(tmpdir(), 'orca-mimocode-home-'))
+ const configDir = join(mimocodeHome, 'config')
+ mkdirSync(join(configDir, 'plugins'), { recursive: true })
+ writeFileSync(join(configDir, 'mimocode.json'), '{"theme":"dark"}')
+ writeFileSync(join(configDir, 'plugins', 'user-plugin.js'), 'export default () => {}')
+ writeFileSync(join(configDir, 'plugins', 'orca-mimocode-status.js'), 'USER PLUGIN')
+ })
+
+ afterEach(() => {
+ rmSync(userDataDir, { recursive: true, force: true })
+ rmSync(mimocodeHome, { recursive: true, force: true })
+ })
+
+ it('mirrors user config into shared overlay and installs Orca status plugin', () => {
+ const service = new MimoCodeHookService()
+ const env = service.buildPtyEnv('pty-1', mimocodeHome)
+
+ const overlayHome = join(userDataDir, 'mimocode-hooks', 'shared')
+ expect(env.MIMOCODE_HOME).toBe(overlayHome)
+ expect(readFileSync(join(overlayHome, 'config', 'mimocode.json'), 'utf8')).toBe(
+ '{"theme":"dark"}'
+ )
+ expect(readFileSync(join(overlayHome, 'config', 'plugins', 'user-plugin.js'), 'utf8')).toBe(
+ 'export default () => {}'
+ )
+
+ const orcaPlugin = join(overlayHome, 'config', 'plugins', 'orca-mimocode-status.js')
+ expect(existsSync(orcaPlugin)).toBe(true)
+ expect(readFileSync(orcaPlugin, 'utf8')).toContain('/hook/mimo-code')
+
+ expect(
+ readFileSync(join(mimocodeHome, 'config', 'plugins', 'orca-mimocode-status.js'), 'utf8')
+ ).toBe('USER PLUGIN')
+ })
+
+ it('reuses the overlay home on a second buildPtyEnv call', () => {
+ const service = new MimoCodeHookService()
+ const first = service.buildPtyEnv('pty-1', mimocodeHome)
+ const second = service.buildPtyEnv('pty-2', mimocodeHome)
+
+ const overlayHome = join(userDataDir, 'mimocode-hooks', 'shared')
+ expect(first.MIMOCODE_HOME).toBe(overlayHome)
+ expect(second.MIMOCODE_HOME).toBe(overlayHome)
+ expect(
+ readFileSync(join(overlayHome, 'config', 'plugins', 'orca-mimocode-status.js'), 'utf8')
+ ).toContain('/hook/mimo-code')
+ })
+})
diff --git a/src/main/mimo/hook-service.ts b/src/main/mimo/hook-service.ts
new file mode 100644
index 00000000000..2cc9f542b94
--- /dev/null
+++ b/src/main/mimo/hook-service.ts
@@ -0,0 +1,80 @@
+import { app } from 'electron'
+import { join } from 'path'
+import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'fs'
+import { homedir } from 'os'
+import { getOpenCodeFamilyPluginSource } from '../opencode/hook-service'
+import { mirrorEntry, safeRemoveTree } from '../pty/overlay-mirror'
+
+const ORCA_MIMOCODE_PLUGIN_FILE = 'orca-mimocode-status.js'
+const MIMOCODE_HOOKS_DIR = 'mimocode-hooks'
+const MIMOCODE_SHARED_HOME = 'shared'
+
+function defaultMimocodeConfigDir(): string {
+ return join(homedir(), '.config', 'mimocode')
+}
+
+function resolveSourceConfigDir(existingHome: string | undefined): string | undefined {
+ if (existingHome) {
+ const fromHome = join(existingHome, 'config')
+ if (existsSync(fromHome)) {
+ return fromHome
+ }
+ }
+ const xdg = defaultMimocodeConfigDir()
+ return existsSync(xdg) ? xdg : undefined
+}
+
+function mirrorConfigDir(sourceConfigDir: string, targetConfigDir: string): void {
+ mkdirSync(targetConfigDir, { recursive: true })
+ for (const entry of readdirSync(sourceConfigDir, { withFileTypes: true })) {
+ if (entry.name === 'plugins' && entry.isDirectory()) {
+ const overlayPlugins = join(targetConfigDir, 'plugins')
+ mkdirSync(overlayPlugins, { recursive: true })
+ for (const pluginEntry of readdirSync(join(sourceConfigDir, 'plugins'), {
+ withFileTypes: true
+ })) {
+ if (pluginEntry.name === ORCA_MIMOCODE_PLUGIN_FILE) {
+ continue
+ }
+ mirrorEntry(
+ join(sourceConfigDir, 'plugins', pluginEntry.name),
+ join(overlayPlugins, pluginEntry.name)
+ )
+ }
+ continue
+ }
+ mirrorEntry(join(sourceConfigDir, entry.name), join(targetConfigDir, entry.name))
+ }
+}
+
+export class MimoCodeHookService {
+ clearPty(_ptyId: string): void {}
+
+ buildPtyEnv(_ptyId: string, existingMimocodeHome?: string): Record {
+ // Why: MiMo currently uses a shared home; per-source subdirs can come
+ // later if concurrent MiMo panes need isolated runtime state.
+ const home = join(app.getPath('userData'), MIMOCODE_HOOKS_DIR, MIMOCODE_SHARED_HOME)
+ try {
+ for (const sub of ['config', 'data', 'cache', 'state'] as const) {
+ mkdirSync(join(home, sub), { recursive: true })
+ }
+ const overlayConfig = join(home, 'config')
+ const sourceConfig = resolveSourceConfigDir(existingMimocodeHome)
+ if (sourceConfig) {
+ safeRemoveTree(overlayConfig)
+ mirrorConfigDir(sourceConfig, overlayConfig)
+ }
+ const pluginsDir = join(home, 'config', 'plugins')
+ mkdirSync(pluginsDir, { recursive: true })
+ writeFileSync(
+ join(pluginsDir, ORCA_MIMOCODE_PLUGIN_FILE),
+ getOpenCodeFamilyPluginSource('/hook/mimo-code')
+ )
+ } catch {
+ return existingMimocodeHome ? { MIMOCODE_HOME: existingMimocodeHome } : {}
+ }
+ return { MIMOCODE_HOME: home }
+ }
+}
+
+export const mimoCodeHookService = new MimoCodeHookService()
diff --git a/src/main/opencode/hook-service.ts b/src/main/opencode/hook-service.ts
index 6a734d4a8d1..20bc863b5ce 100644
--- a/src/main/opencode/hook-service.ts
+++ b/src/main/opencode/hook-service.ts
@@ -58,7 +58,11 @@ function toSafeDirName(id: string): string {
return createHash('sha256').update(id).digest('hex').slice(0, 32)
}
-function getOpenCodePluginSource(): string {
+export function getOpenCodePluginSource(): string {
+ return getOpenCodeFamilyPluginSource('/hook/opencode')
+}
+
+export function getOpenCodeFamilyPluginSource(hookPathname: string): string {
// Why: the plugin runs inside the OpenCode Node process and POSTs to the
// unified agent-hooks server shared with Claude/Codex/Gemini. It reads the
// same ORCA_PANE_KEY / ORCA_TAB_ID / ORCA_WORKTREE_ID / ORCA_AGENT_HOOK_*
@@ -260,7 +264,7 @@ function getOpenCodePluginSource(): string {
' const coords = resolveHookCoords();',
' const paneKey = process.env.ORCA_PANE_KEY;',
' if (!coords.port || !coords.token || !paneKey) return;',
- ' const url = `http://127.0.0.1:${coords.port}/hook/opencode`;',
+ ` const url = \`http://127.0.0.1:\${coords.port}${hookPathname}\`;`,
' const body = JSON.stringify({',
' paneKey,',
' launchToken: process.env.ORCA_AGENT_LAUNCH_TOKEN || "",',
diff --git a/src/main/powershell-osc133-bootstrap.test.ts b/src/main/powershell-osc133-bootstrap.test.ts
index 80f9fd0880a..eedfb6159d7 100644
--- a/src/main/powershell-osc133-bootstrap.test.ts
+++ b/src/main/powershell-osc133-bootstrap.test.ts
@@ -10,6 +10,7 @@ describe('PowerShell OSC 133 bootstrap', () => {
expect(script).toContain('[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()')
expect(script).toContain('ORCA_OPENCODE_CONFIG_DIR')
+ expect(script).toContain('ORCA_MIMOCODE_HOME')
expect(script).not.toContain('ORCA_PI_CODING_AGENT_DIR')
expect(script).not.toContain('ORCA_OMP_CODING_AGENT_DIR')
expect(script).toContain('ORCA_OMP_STATUS_EXTENSION')
diff --git a/src/main/powershell-osc133-bootstrap.ts b/src/main/powershell-osc133-bootstrap.ts
index cb4e3177a46..bad6cbba6ea 100644
--- a/src/main/powershell-osc133-bootstrap.ts
+++ b/src/main/powershell-osc133-bootstrap.ts
@@ -24,6 +24,7 @@ try {
# Profiles can re-export user defaults after Orca's spawn env is set.
if ($env:ORCA_OPENCODE_CONFIG_DIR) { $env:OPENCODE_CONFIG_DIR = $env:ORCA_OPENCODE_CONFIG_DIR }
+if ($env:ORCA_MIMOCODE_HOME) { $env:MIMOCODE_HOME = $env:ORCA_MIMOCODE_HOME }
${getPowerShellOmpShellWrapper()}
if ($env:ORCA_CODEX_HOME) { $env:CODEX_HOME = $env:ORCA_CODEX_HOME }
diff --git a/src/main/providers/local-pty-provider.test.ts b/src/main/providers/local-pty-provider.test.ts
index c058fa57151..54a7838c87a 100644
--- a/src/main/providers/local-pty-provider.test.ts
+++ b/src/main/providers/local-pty-provider.test.ts
@@ -307,6 +307,23 @@ describe('LocalPtyProvider', () => {
expect(spawnCall[2].env.ORCA_ATTRIBUTION_SHIM_DIR).toBeUndefined()
})
+ it('uses shell wrapper when MiMo home must survive shell startup', async () => {
+ provider.configure({
+ buildSpawnEnv: (_id, env) => {
+ env.MIMOCODE_HOME = '/tmp/orca-mimocode-overlay'
+ env.ORCA_MIMOCODE_HOME = '/tmp/orca-mimocode-overlay'
+ return env
+ }
+ })
+
+ await provider.spawn({ cols: 80, rows: 24 })
+
+ const spawnCall = spawnMock.mock.calls.at(-1)!
+ expect(spawnCall[1]).toEqual(['-l'])
+ expect(spawnCall[2].env.ZDOTDIR).toMatch(/shell-ready[\\/]zsh/)
+ expect(spawnCall[2].env.ORCA_SHELL_READY_MARKER).toBe('0')
+ })
+
it('does not pass a Windows Codex home into WSL terminals', async () => {
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
provider.configure({
diff --git a/src/main/providers/local-pty-provider.ts b/src/main/providers/local-pty-provider.ts
index c64a5511315..5b61c20e5f3 100644
--- a/src/main/providers/local-pty-provider.ts
+++ b/src/main/providers/local-pty-provider.ts
@@ -539,6 +539,7 @@ export class LocalPtyProvider implements IPtyProvider {
const needsNoMarkerWrapper =
finalEnv.ORCA_ATTRIBUTION_SHIM_DIR ||
finalEnv.ORCA_OPENCODE_CONFIG_DIR ||
+ finalEnv.ORCA_MIMOCODE_HOME ||
finalEnv.ORCA_OMP_STATUS_EXTENSION ||
finalEnv.ORCA_CODEX_HOME ||
finalEnv.ORCA_AGENT_TEAMS_SHIM_DIR
diff --git a/src/main/providers/local-pty-shell-ready.test.ts b/src/main/providers/local-pty-shell-ready.test.ts
index 8eed31c3b93..f40db2c5931 100644
--- a/src/main/providers/local-pty-shell-ready.test.ts
+++ b/src/main/providers/local-pty-shell-ready.test.ts
@@ -437,6 +437,8 @@ describePosix('local PTY shell-ready launch config', () => {
const bashRc = getBashShellReadyRcfileContent()
const restoreLine =
'[[ -n "${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="${ORCA_OPENCODE_CONFIG_DIR}"'
+ const mimoRestoreLine =
+ '[[ -n "${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="${ORCA_MIMOCODE_HOME}"'
const codexRestoreLine =
'[[ -n "${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="${ORCA_CODEX_HOME}"'
const agentTeamsPathRestoreLine = '[[ -n "${ORCA_AGENT_TEAMS_SHIM_DIR:-}" ]] || return 0'
@@ -444,6 +446,9 @@ describePosix('local PTY shell-ready launch config', () => {
expect(zshrc).toContain(restoreLine)
expect(zlogin).toContain(restoreLine)
expect(bashRc).toContain(restoreLine)
+ expect(zshrc).toContain(mimoRestoreLine)
+ expect(zlogin).toContain(mimoRestoreLine)
+ expect(bashRc).toContain(mimoRestoreLine)
expect(zshrc).not.toContain('ORCA_PI_CODING_AGENT_DIR')
expect(zlogin).not.toContain('ORCA_PI_CODING_AGENT_DIR')
expect(bashRc).not.toContain('ORCA_PI_CODING_AGENT_DIR')
diff --git a/src/main/providers/local-pty-shell-ready.ts b/src/main/providers/local-pty-shell-ready.ts
index d8bbf94f69f..6a7c1d1fa73 100644
--- a/src/main/providers/local-pty-shell-ready.ts
+++ b/src/main/providers/local-pty-shell-ready.ts
@@ -140,6 +140,7 @@ __orca_restore_agent_teams_path
# Why: user startup files may set the default OpenCode config after Orca's
# spawn env; restore the Orca-managed config dir before the first prompt.
[[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}"
+[[ -n "\${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="\${ORCA_MIMOCODE_HOME}"
${getPosixOmpShellWrapper()}
# Why: Codex must keep using Orca's runtime CODEX_HOME after profile scripts.
[[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}"
@@ -255,6 +256,7 @@ __orca_restore_agent_teams_path() {
if [[ ! -o login ]]; then
# Why: ~/.zshrc can export the user's default OpenCode config after spawn.
[[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}"
+[[ -n "\${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="\${ORCA_MIMOCODE_HOME}"
${getPosixOmpShellWrapper()}
# Why: Codex must keep using Orca's runtime CODEX_HOME after rc files.
[[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}"
@@ -318,6 +320,7 @@ __orca_restore_agent_teams_path() {
__orca_restore_agent_teams_path
# Why: .zlogin is the final login startup file before the prompt is shown.
[[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}"
+[[ -n "\${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="\${ORCA_MIMOCODE_HOME}"
${getPosixOmpShellWrapper()}
[[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}"
${getZshShellReadyMarkerRegistrationBlock(SHELL_READY_MARKER_ESCAPED)}
diff --git a/src/main/runtime/orchestration/groups.test.ts b/src/main/runtime/orchestration/groups.test.ts
index 04799b7ec6f..483c8a1fe83 100644
--- a/src/main/runtime/orchestration/groups.test.ts
+++ b/src/main/runtime/orchestration/groups.test.ts
@@ -105,6 +105,16 @@ describe('resolveGroupAddress', () => {
expect(result).toEqual(['term_b'])
})
+ it('matches @mimo by terminal title', () => {
+ const terminals = [
+ makeSummary('term_a', { title: 'mimo' }),
+ makeSummary('term_b', { title: 'MiMo Code session' }),
+ makeSummary('term_c', { title: 'OpenCode' })
+ ]
+ const result = resolveGroupAddress('@mimo', 'term_a', terminals, noStatus)
+ expect(result).toEqual(['term_b'])
+ })
+
it('matches @openclaude by terminal title', () => {
const terminals = [
makeSummary('term_a', { title: 'OpenClaude' }),
diff --git a/src/main/runtime/orchestration/groups.ts b/src/main/runtime/orchestration/groups.ts
index baa3110f456..c897ae03866 100644
--- a/src/main/runtime/orchestration/groups.ts
+++ b/src/main/runtime/orchestration/groups.ts
@@ -4,7 +4,15 @@ import type { RuntimeTerminalSummary } from '../../../shared/runtime-types'
// Resolution is done at send-time: one message record per recipient, same thread_id,
// so each recipient gets their own read-tracking (Section 4.5).
-const AGENT_NAME_GROUPS = ['claude', 'openclaude', 'codex', 'opencode', 'gemini', 'droid'] as const
+const AGENT_NAME_GROUPS = [
+ 'claude',
+ 'openclaude',
+ 'codex',
+ 'opencode',
+ 'mimo',
+ 'gemini',
+ 'droid'
+] as const
export type GroupAddress =
| '@all'
diff --git a/src/relay/pty-shell-launch.test.ts b/src/relay/pty-shell-launch.test.ts
index 9f020b06e91..aec824b3758 100644
--- a/src/relay/pty-shell-launch.test.ts
+++ b/src/relay/pty-shell-launch.test.ts
@@ -137,6 +137,24 @@ describe('getRelayShellLaunchConfig', () => {
)
})
+ it.skipIf(process.platform === 'win32')(
+ 'wraps zsh when MiMo home must survive shell startup',
+ () => {
+ const config = getRelayShellLaunchConfig('/bin/zsh', {
+ HOME: homeDir,
+ ORCA_MIMOCODE_HOME: '/tmp/orca-mimocode-overlay'
+ })
+ const zshRoot = join(homeDir, '.orca-relay', 'shell-ready', 'zsh')
+ const zshrc = readFileSync(join(zshRoot, '.zshrc'), 'utf8')
+
+ expect(config.args).toEqual(['-l'])
+ expect(config.env.ZDOTDIR).toBe(zshRoot)
+ expect(zshrc).toContain(
+ '[[ -n "${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="${ORCA_MIMOCODE_HOME}"'
+ )
+ }
+ )
+
it.skipIf(process.platform === 'win32')(
'wraps bash even without overlay env for OSC 133 lifecycle markers',
() => {
diff --git a/src/relay/pty-shell-launch.ts b/src/relay/pty-shell-launch.ts
index a9a3e221c36..d2ee25e2d41 100644
--- a/src/relay/pty-shell-launch.ts
+++ b/src/relay/pty-shell-launch.ts
@@ -40,7 +40,10 @@ function windowsShellArgs(shellName: string): string[] | null {
function hasOverlayRestoreEnv(env: Record): boolean {
return Boolean(
- env.ORCA_OPENCODE_CONFIG_DIR || env.ORCA_REMOTE_CLI_BIN_DIR || env.ORCA_OMP_STATUS_EXTENSION
+ env.ORCA_OPENCODE_CONFIG_DIR ||
+ env.ORCA_MIMOCODE_HOME ||
+ env.ORCA_REMOTE_CLI_BIN_DIR ||
+ env.ORCA_OMP_STATUS_EXTENSION
)
}
@@ -100,6 +103,7 @@ ${getZshStartupFileSourceBlock({
if [[ ! -o login ]]; then
# Why: remote startup files can re-export user defaults after relay spawn.
[[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}"
+ [[ -n "\${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="\${ORCA_MIMOCODE_HOME}"
[[ -n "\${ORCA_REMOTE_CLI_BIN_DIR:-}" ]] && case ":$PATH:" in *:"\${ORCA_REMOTE_CLI_BIN_DIR}":*) ;; *) export PATH="\${ORCA_REMOTE_CLI_BIN_DIR}:$PATH" ;; esac
${getPosixOmpShellWrapper()}
fi
@@ -115,6 +119,7 @@ ${getZshStartupFileSourceBlock({
})}
# Why: .zlogin is the final zsh login startup file before the prompt.
[[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}"
+[[ -n "\${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="\${ORCA_MIMOCODE_HOME}"
[[ -n "\${ORCA_REMOTE_CLI_BIN_DIR:-}" ]] && case ":$PATH:" in *:"\${ORCA_REMOTE_CLI_BIN_DIR}":*) ;; *) export PATH="\${ORCA_REMOTE_CLI_BIN_DIR}:$PATH" ;; esac
${getPosixOmpShellWrapper()}
${getZshFinalZdotdirRestoreBlock('"${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"')}
@@ -131,6 +136,7 @@ elif [[ -f "$HOME/.profile" ]]; then
fi
# Why: remote startup files can re-export user defaults after relay spawn.
[[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}"
+[[ -n "\${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="\${ORCA_MIMOCODE_HOME}"
[[ -n "\${ORCA_REMOTE_CLI_BIN_DIR:-}" ]] && case ":$PATH:" in *:"\${ORCA_REMOTE_CLI_BIN_DIR}":*) ;; *) export PATH="\${ORCA_REMOTE_CLI_BIN_DIR}:$PATH" ;; esac
${getPosixOmpShellWrapper()}
# Why: SSH bash sessions need the same command lifecycle markers as local
diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json
index db36d7b51b5..7e004df6698 100644
--- a/src/renderer/src/i18n/locales/en.json
+++ b/src/renderer/src/i18n/locales/en.json
@@ -289,7 +289,8 @@
"bf53f09bf8": "Claude Agent Teams",
"0708ed89f1": "Claude",
"fc80296033": "Devin",
- "da41abbdd4": "Ante"
+ "da41abbdd4": "Ante",
+ "mimo_code_label": "MiMo Code"
},
"skill": {
"cli": {
diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json
index 495c24dfeaf..d01aecb6416 100644
--- a/src/renderer/src/i18n/locales/es.json
+++ b/src/renderer/src/i18n/locales/es.json
@@ -289,7 +289,8 @@
"bf53f09bf8": "Claude Agent Teams",
"0708ed89f1": "Claude",
"fc80296033": "Devin",
- "da41abbdd4": "Ante"
+ "da41abbdd4": "Ante",
+ "mimo_code_label": "MiMo Code"
},
"skill": {
"cli": {
diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json
index 78a9c4dffad..138e10813fd 100644
--- a/src/renderer/src/i18n/locales/ja.json
+++ b/src/renderer/src/i18n/locales/ja.json
@@ -289,7 +289,8 @@
"bf53f09bf8": "Claude Agent Teams",
"0708ed89f1": "Claude",
"fc80296033": "Devin",
- "da41abbdd4": "Ante"
+ "da41abbdd4": "Ante",
+ "mimo_code_label": "MiMo Code"
},
"skill": {
"cli": {
diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json
index 00981aea841..64bf5cd45da 100644
--- a/src/renderer/src/i18n/locales/ko.json
+++ b/src/renderer/src/i18n/locales/ko.json
@@ -289,7 +289,8 @@
"bf53f09bf8": "Claude Agent Teams",
"0708ed89f1": "Claude",
"fc80296033": "Devin",
- "da41abbdd4": "Ante"
+ "da41abbdd4": "Ante",
+ "mimo_code_label": "MiMo Code"
},
"skill": {
"cli": {
diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json
index 36f73ea9abb..1e46ef2fb90 100644
--- a/src/renderer/src/i18n/locales/zh.json
+++ b/src/renderer/src/i18n/locales/zh.json
@@ -289,7 +289,8 @@
"bf53f09bf8": "Claude Agent Teams",
"0708ed89f1": "Claude",
"fc80296033": "Devin",
- "da41abbdd4": "Ante"
+ "da41abbdd4": "Ante",
+ "mimo_code_label": "MiMo Code"
},
"skill": {
"cli": {
diff --git a/src/renderer/src/lib/agent-catalog.tsx b/src/renderer/src/lib/agent-catalog.tsx
index 5b4d0ca2587..e93f32d03db 100644
--- a/src/renderer/src/lib/agent-catalog.tsx
+++ b/src/renderer/src/lib/agent-catalog.tsx
@@ -89,6 +89,13 @@ export const getAgentCatalog = createLocalizedCatalog((): AgentCatalogEntry[] =>
faviconDomain: 'opencode.ai',
homepageUrl: 'https://opencode.ai/docs/cli/'
},
+ {
+ id: 'mimo-code',
+ label: translate('auto.lib.agent.catalog.mimo_code_label', 'MiMo Code'),
+ cmd: 'mimo',
+ faviconDomain: 'mimo.xiaomi.com',
+ homepageUrl: 'https://mimo.xiaomi.com/coder'
+ },
{
id: 'ante',
label: translate('auto.lib.agent.catalog.da41abbdd4', 'Ante'),
diff --git a/src/renderer/src/lib/agent-status.ts b/src/renderer/src/lib/agent-status.ts
index 05461437229..39618185154 100644
--- a/src/renderer/src/lib/agent-status.ts
+++ b/src/renderer/src/lib/agent-status.ts
@@ -121,6 +121,7 @@ const WELL_KNOWN_LABELS: Record = {
amp: 'Amp',
copilot: 'GitHub Copilot',
opencode: 'OpenCode',
+ 'mimo-code': 'MiMo Code',
cursor: 'Cursor',
aider: 'Aider',
pi: 'Pi',
@@ -162,6 +163,7 @@ const ICONABLE_AGENT_TYPES: Record = {
codex: true,
autohand: true,
opencode: true,
+ 'mimo-code': true,
pi: true,
omp: true,
gemini: true,
diff --git a/src/shared/agent-detection.test.ts b/src/shared/agent-detection.test.ts
index 6154e9be242..1a42e362013 100644
--- a/src/shared/agent-detection.test.ts
+++ b/src/shared/agent-detection.test.ts
@@ -1,6 +1,12 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
-import { extractAllOscTitles, extractLastOscTitle, MAX_OSC_TITLE_CHARS } from './agent-detection'
+import {
+ detectAgentStatusFromTitle,
+ extractAllOscTitles,
+ extractLastOscTitle,
+ getAgentLabel,
+ MAX_OSC_TITLE_CHARS
+} from './agent-detection'
afterEach(() => {
vi.restoreAllMocks()
@@ -51,3 +57,23 @@ describe('OSC title extraction', () => {
expect(extractAllOscTitles(data)).toEqual([extracted])
})
})
+
+describe('MiMo title detection', () => {
+ it.each([
+ ['MiMo Code', 'idle'],
+ ['mimo ready', 'idle'],
+ ['mimo working', 'working'],
+ ['\u280b MiMo Code', 'working']
+ ] as const)('classifies %s', (title, expectedStatus) => {
+ expect(getAgentLabel(title)).toBe('MiMo Code')
+ expect(detectAgentStatusFromTitle(title)).toBe(expectedStatus)
+ })
+
+ it.each(['~/mimo/working', 'mimo-code-fixtures ready'])(
+ 'does not classify path or hyphen false positive %s',
+ (title) => {
+ expect(getAgentLabel(title)).toBeNull()
+ expect(detectAgentStatusFromTitle(title)).toBeNull()
+ }
+ )
+})
diff --git a/src/shared/agent-detection.ts b/src/shared/agent-detection.ts
index 27412a53529..4b808935142 100644
--- a/src/shared/agent-detection.ts
+++ b/src/shared/agent-detection.ts
@@ -350,6 +350,9 @@ export function getAgentLabel(title: string): string | null {
if (titleHasAgentName(title, 'opencode')) {
return 'OpenCode'
}
+ if (titleHasAgentName(title, 'mimo')) {
+ return 'MiMo Code'
+ }
if (titleHasAgentName(title, 'aider')) {
return 'Aider'
}
diff --git a/src/shared/agent-hook-listener.test.ts b/src/shared/agent-hook-listener.test.ts
index 55f005facb9..f208450a7a7 100644
--- a/src/shared/agent-hook-listener.test.ts
+++ b/src/shared/agent-hook-listener.test.ts
@@ -91,6 +91,7 @@ describe('shared agent-hook-listener', () => {
expect(resolveHookSource('/hook/pi')).toBe('pi')
expect(resolveHookSource('/hook/omp')).toBe('omp')
expect(resolveHookSource('/hook/command-code')).toBe('command-code')
+ expect(resolveHookSource('/hook/mimo-code')).toBe('mimo-code')
expect(resolveHookSource('/hook/unknown')).toBeNull()
expect(resolveHookSource('/')).toBeNull()
})
@@ -544,6 +545,55 @@ describe('shared agent-hook-listener', () => {
expect(stopped?.providerSession).toMatchObject({ key: 'session_id', id: 'session_abc' })
})
+ it('normalizes MiMo Code OpenCode-compatible lifecycle events as mimo-code status', () => {
+ const message = normalizeHookPayload(
+ state,
+ 'mimo-code',
+ {
+ paneKey: PANE_KEY,
+ payload: {
+ hook_event_name: 'MessagePart',
+ sessionID: 'mimo-session',
+ messageID: 'message-1',
+ role: 'user',
+ text: 'ship the fix'
+ }
+ },
+ 'production'
+ )
+ const tool = normalizeHookPayload(
+ state,
+ 'mimo-code',
+ {
+ paneKey: PANE_KEY,
+ payload: {
+ hook_event_name: 'SessionBusy',
+ sessionID: 'mimo-session'
+ }
+ },
+ 'production'
+ )
+ const idle = normalizeHookPayload(
+ state,
+ 'mimo-code',
+ {
+ paneKey: PANE_KEY,
+ payload: { hook_event_name: 'SessionIdle', sessionID: 'mimo-session' }
+ },
+ 'production'
+ )
+
+ expect(message?.payload).toMatchObject({
+ agentType: 'mimo-code',
+ state: 'working',
+ prompt: 'ship the fix'
+ })
+ expect(message?.promptInteractionKey).toBe('mimo-code-message-message-1')
+ expect(message?.providerSession).toMatchObject({ key: 'session_id', id: 'mimo-session' })
+ expect(tool?.payload).toMatchObject({ agentType: 'mimo-code', state: 'working' })
+ expect(idle?.payload).toMatchObject({ agentType: 'mimo-code', state: 'done' })
+ })
+
it('maps Kimi AskUserQuestion PreToolUse to waiting, then back to working on answer', () => {
const question = normalizeHookPayload(
state,
diff --git a/src/shared/agent-hook-listener.ts b/src/shared/agent-hook-listener.ts
index ba0d5acde1f..f3149fd79b0 100644
--- a/src/shared/agent-hook-listener.ts
+++ b/src/shared/agent-hook-listener.ts
@@ -1895,6 +1895,7 @@ function isNewTurnEvent(source: AgentHookSource, eventName: unknown): boolean {
case 'amp':
return eventName === 'agent.start'
case 'opencode':
+ case 'mimo-code':
return false
case 'cursor':
return eventName === 'beforeSubmitPrompt' || eventName === 'sessionStart'
@@ -1947,7 +1948,7 @@ function hasExplicitUserPrompt(
return true
}
if (extractedPrompt.source === 'role_user_text') {
- return source === 'opencode' && eventName === 'MessagePart'
+ return (source === 'opencode' || source === 'mimo-code') && eventName === 'MessagePart'
}
if (extractedPrompt.text.length === 0) {
return false
@@ -1988,6 +1989,7 @@ function extractToolFields(
case 'amp':
return extractAmpToolFields(eventName, hookPayload)
case 'opencode':
+ case 'mimo-code':
return extractOpenCodeToolFields(eventName, hookPayload)
case 'cursor':
return extractCursorToolFields(eventName, hookPayload)
@@ -2520,7 +2522,8 @@ function normalizeCodexEvent(
)
}
-function normalizeOpenCodeEvent(
+function normalizeOpenCodeFamilyEvent(
+ source: 'opencode' | 'mimo-code',
state: HookListenerState,
eventName: unknown,
promptText: string,
@@ -2543,17 +2546,17 @@ function normalizeOpenCodeEvent(
const snapshot = resolveToolState(
state,
paneKey,
- extractToolFields('opencode', eventName, hookPayload),
- { resetOnNewTurn: isNewTurnEvent('opencode', eventName) }
+ extractToolFields(source, eventName, hookPayload),
+ { resetOnNewTurn: isNewTurnEvent(source, eventName) }
)
return parseAgentStatusPayload(
JSON.stringify({
state: stateName,
prompt: resolvePrompt(state, paneKey, promptText, {
- resetOnNewTurn: isNewTurnEvent('opencode', eventName)
+ resetOnNewTurn: isNewTurnEvent(source, eventName)
}),
- agentType: 'opencode',
+ agentType: source,
toolName: snapshot.toolName,
toolInput: snapshot.toolInput,
lastAssistantMessage: snapshot.lastAssistantMessage
@@ -3054,15 +3057,24 @@ export function normalizeHookPayload(
payload = normalizeAmpEvent(state, eventName, promptText, paneKey, hookPayloadRecord)
break
case 'opencode':
+ case 'mimo-code':
if (extractedPrompt.source === 'role_user_text') {
const messageId = readFirstString(hookPayloadRecord, [
'messageID',
'messageId',
'message_id'
])
- promptInteractionKey = messageId ? `opencode-message-${messageId}` : undefined
+ const prefix = source === 'mimo-code' ? 'mimo-code-message' : 'opencode-message'
+ promptInteractionKey = messageId ? `${prefix}-${messageId}` : undefined
}
- payload = normalizeOpenCodeEvent(state, eventName, promptText, paneKey, hookPayloadRecord)
+ payload = normalizeOpenCodeFamilyEvent(
+ source,
+ state,
+ eventName,
+ promptText,
+ paneKey,
+ hookPayloadRecord
+ )
break
case 'cursor':
payload = normalizeCursorEvent(state, eventName, promptText, paneKey, hookPayloadRecord)
@@ -3171,6 +3183,7 @@ export const HOOK_SOURCE_BY_PATHNAME: Readonly>
'/hook/antigravity': 'antigravity',
'/hook/amp': 'amp',
'/hook/opencode': 'opencode',
+ '/hook/mimo-code': 'mimo-code',
'/hook/cursor': 'cursor',
'/hook/pi': 'pi',
'/hook/omp': 'omp',
diff --git a/src/shared/agent-hook-relay.ts b/src/shared/agent-hook-relay.ts
index df3ddcf9910..4500dc3f26e 100644
--- a/src/shared/agent-hook-relay.ts
+++ b/src/shared/agent-hook-relay.ts
@@ -38,6 +38,7 @@ export type AgentHookSource =
| 'antigravity'
| 'amp'
| 'opencode'
+ | 'mimo-code'
| 'cursor'
| 'pi'
| 'omp'
diff --git a/src/shared/agent-kind.ts b/src/shared/agent-kind.ts
index fa933dbe3c8..ddacd75659b 100644
--- a/src/shared/agent-kind.ts
+++ b/src/shared/agent-kind.ts
@@ -20,6 +20,7 @@ const TUI_AGENT_KIND_BY_AGENT = {
codex: 'codex',
autohand: 'autohand',
opencode: 'opencode',
+ 'mimo-code': 'mimo-code',
pi: 'pi',
omp: 'omp',
gemini: 'gemini',
diff --git a/src/shared/agent-name-token-match.ts b/src/shared/agent-name-token-match.ts
index c99d2dc3bcb..157f1c8f608 100644
--- a/src/shared/agent-name-token-match.ts
+++ b/src/shared/agent-name-token-match.ts
@@ -22,6 +22,7 @@ export const AGENT_NAMES = [
'gemini',
'antigravity',
'opencode',
+ 'mimo',
'openclaw',
'aider',
'grok',
diff --git a/src/shared/agent-session-resume.test.ts b/src/shared/agent-session-resume.test.ts
index 720f4a48c8b..8469a9c0cd5 100644
--- a/src/shared/agent-session-resume.test.ts
+++ b/src/shared/agent-session-resume.test.ts
@@ -21,6 +21,7 @@ describe('agent session resume metadata', () => {
{ key: 'conversation_id', id: 'agy-conversation' }
],
['opencode', { sessionID: 'opencode-session' }, { key: 'session_id', id: 'opencode-session' }],
+ ['mimo-code', { sessionID: 'mimo-session' }, { key: 'session_id', id: 'mimo-session' }],
['droid', { session_id: 'droid-session' }, { key: 'session_id', id: 'droid-session' }],
['grok', { sessionId: 'grok-session' }, { key: 'session_id', id: 'grok-session' }],
['devin', { session_id: 'devin-session' }, { key: 'session_id', id: 'devin-session' }]
@@ -34,6 +35,7 @@ describe('agent session resume metadata', () => {
['gemini', { key: 'session_id', id: 's1' }, ['gemini', '--resume', 's1']],
['antigravity', { key: 'conversation_id', id: 's1' }, ['agy', '--conversation', 's1']],
['opencode', { key: 'session_id', id: 's1' }, ['opencode', '--session', 's1']],
+ ['mimo-code', { key: 'session_id', id: 's1' }, ['mimo', '--session', 's1']],
['droid', { key: 'session_id', id: 's1' }, ['droid', '--resume', 's1']],
['grok', { key: 'session_id', id: 's1' }, ['grok', '--resume', 's1']],
['devin', { key: 'session_id', id: 'abc12345' }, ['devin', '--resume', 'abc12345']]
diff --git a/src/shared/agent-session-resume.ts b/src/shared/agent-session-resume.ts
index aacd548dfed..f135e90d6be 100644
--- a/src/shared/agent-session-resume.ts
+++ b/src/shared/agent-session-resume.ts
@@ -8,6 +8,7 @@ export const RESUMABLE_TUI_AGENTS = [
'gemini',
'antigravity',
'opencode',
+ 'mimo-code',
'droid',
'grok',
'devin'
@@ -125,7 +126,8 @@ export function extractAgentProviderSession(
const id = readSessionId(payload, ['conversationId'])
return id ? { key: 'conversation_id', id } : null
}
- case 'opencode': {
+ case 'opencode':
+ case 'mimo-code': {
const id = readSessionId(payload, ['sessionID'])
return id ? { key: 'session_id', id } : null
}
@@ -164,6 +166,8 @@ export function getAgentResumeArgv(
return providerSession.key === 'conversation_id' ? ['agy', '--conversation', id] : null
case 'opencode':
return providerSession.key === 'session_id' ? ['opencode', '--session', id] : null
+ case 'mimo-code':
+ return providerSession.key === 'session_id' ? ['mimo', '--session', id] : null
case 'droid':
return providerSession.key === 'session_id' ? ['droid', '--resume', id] : null
case 'grok':
diff --git a/src/shared/agent-status-types.ts b/src/shared/agent-status-types.ts
index 9c199550d53..75257f8aab4 100644
--- a/src/shared/agent-status-types.ts
+++ b/src/shared/agent-status-types.ts
@@ -21,6 +21,7 @@ export type WellKnownAgentType =
| 'antigravity'
| 'amp'
| 'opencode'
+ | 'mimo-code'
| 'cursor'
| 'copilot'
| 'aider'
diff --git a/src/shared/telemetry-events.ts b/src/shared/telemetry-events.ts
index e2083eaf2e2..7ea6439b81e 100644
--- a/src/shared/telemetry-events.ts
+++ b/src/shared/telemetry-events.ts
@@ -75,6 +75,7 @@ export const AGENT_KIND_VALUES = [
'codex',
'autohand',
'opencode',
+ 'mimo-code',
'pi',
'omp',
'gemini',
diff --git a/src/shared/tui-agent-config.ts b/src/shared/tui-agent-config.ts
index 8f1488a0ec9..bea54d857bc 100644
--- a/src/shared/tui-agent-config.ts
+++ b/src/shared/tui-agent-config.ts
@@ -116,6 +116,12 @@ export const TUI_AGENT_CONFIG: Record = {
expectedProcess: 'opencode',
promptInjectionMode: 'flag-prompt'
},
+ 'mimo-code': {
+ detectCmd: 'mimo',
+ launchCmd: 'mimo',
+ expectedProcess: 'mimo',
+ promptInjectionMode: 'flag-prompt'
+ },
pi: {
detectCmd: 'pi',
launchCmd: 'pi',
diff --git a/src/shared/tui-agent-display-names.ts b/src/shared/tui-agent-display-names.ts
index 8d16ae926e2..4d62d35f961 100644
--- a/src/shared/tui-agent-display-names.ts
+++ b/src/shared/tui-agent-display-names.ts
@@ -14,6 +14,7 @@ export const TUI_AGENT_DISPLAY_NAMES: Record = {
ante: 'Ante',
autohand: 'Autohand Code',
opencode: 'OpenCode',
+ 'mimo-code': 'MiMo Code',
pi: 'Pi',
omp: 'OMP',
gemini: 'Gemini',
diff --git a/src/shared/tui-agent-selection.ts b/src/shared/tui-agent-selection.ts
index caedd8a0338..7ac9d252713 100644
--- a/src/shared/tui-agent-selection.ts
+++ b/src/shared/tui-agent-selection.ts
@@ -11,6 +11,7 @@ export const TUI_AGENT_AUTO_PICK_ORDER = [
'grok',
'copilot',
'opencode',
+ 'mimo-code',
'ante',
'pi',
'omp',
diff --git a/src/shared/types.ts b/src/shared/types.ts
index c56b9af4acc..9d904f1c6a7 100644
--- a/src/shared/types.ts
+++ b/src/shared/types.ts
@@ -2221,6 +2221,7 @@ export type TuiAgent =
| 'codex' // OpenAI Codex
| 'autohand' // Autohand Code CLI
| 'opencode' // OpenCode
+ | 'mimo-code'
| 'pi' // Pi (pi.dev)
| 'omp' // OMP (omp.sh)
| 'gemini' // Gemini CLI