feat(agents): native Xiaomi MiMo Code support (#6239)

* feat(agents): native Xiaomi MiMo Code support

Add mimo-code TUI agent (detect mimo, --prompt, --session resume).
Inject MIMOCODE_HOME overlay and /hook/mimo-code status plugin on mimo
launch when agent status hooks are enabled; restore via shell-ready
wrappers. Reuse OpenCode-family hook normalization in the listener.

SSH remote MiMo hook overlays are not included (local/daemon first).

Closes #6220

* fix(mimo): remirror overlay config idempotently

rmSync overlay config before mirror so a second mimo launch does not
hit EEXIST in mirrorEntry and fall back to the user MIMOCODE_HOME.

* review: harden mimo code support

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Doan Bac Tam <24356000+doanbactam@users.noreply.github.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Doan Bac Tam
2026-06-24 00:17:49 -07:00
committed by GitHub
co-authored by Orca Doan Bac Tam Jinwoo-H
parent b76a2ddae6
commit 0c0367ea88
44 changed files with 525 additions and 20 deletions
+1
View File
@@ -178,6 +178,7 @@ Works with **any CLI agent** — if it runs in a terminal, it runs in Orca.
<a href="https://cursor.com/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=cursor.com&sz=64" alt="Cursor logo" width="16" valign="middle" /> Cursor</kbd></a> &nbsp;
<a href="https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli"><kbd><img src="https://www.google.com/s2/favicons?domain=github.com&sz=64" alt="GitHub Copilot logo" width="16" valign="middle" /> GitHub Copilot</kbd></a> &nbsp;
<a href="https://opencode.ai/docs/cli/"><kbd><img src="https://www.google.com/s2/favicons?domain=opencode.ai&sz=64" alt="OpenCode logo" width="16" valign="middle" /> OpenCode</kbd></a> &nbsp;
<a href="https://mimo.xiaomi.com/coder"><kbd><img src="https://www.google.com/s2/favicons?domain=mimo.xiaomi.com&sz=64" alt="MiMo Code logo" width="16" valign="middle" /> MiMo Code</kbd></a> &nbsp;
<a href="https://ampcode.com/manual#install"><kbd><img src="https://www.google.com/s2/favicons?domain=ampcode.com&sz=64" alt="Amp logo" width="16" valign="middle" /> Amp</kbd></a> &nbsp;
<a href="https://openclaude.gitlawb.com/"><kbd><img src="resources/openclaude-logo.png" alt="OpenClaude logo" width="16" valign="middle" /> OpenClaude</kbd></a> &nbsp;
<a href="https://antigravity.google/docs/cli-overview"><kbd><img src="https://www.google.com/s2/favicons?domain=antigravity.google&sz=64" alt="Antigravity logo" width="16" valign="middle" /> Antigravity</kbd></a> &nbsp;
+4
View File
@@ -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<TuiAgent, string> = {
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<Record<TuiAgent, string>>
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<TuiAgent, string> = {
grok: 'grok',
copilot: 'copilot',
opencode: 'opencode',
'mimo-code': 'mimo',
ante: 'ante',
pi: 'pi',
omp: 'omp',
+2 -1
View File
@@ -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()
}
+30
View File
@@ -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)
+1
View File
@@ -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
+5
View File
@@ -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')
+3
View File
@@ -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)}
+60
View File
@@ -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()
+42
View File
@@ -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, string>): 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, string>): 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
+78
View File
@@ -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')
})
})
+80
View File
@@ -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<string, string> {
// 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()
+6 -2
View File
@@ -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 || "",',
@@ -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')
+1
View File
@@ -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 }
@@ -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({
+1
View File
@@ -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
@@ -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')
@@ -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)}
@@ -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' }),
+9 -1
View File
@@ -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'
+18
View File
@@ -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',
() => {
+7 -1
View File
@@ -40,7 +40,10 @@ function windowsShellArgs(shellName: string): string[] | null {
function hasOverlayRestoreEnv(env: Record<string, string>): 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
+2 -1
View File
@@ -289,7 +289,8 @@
"bf53f09bf8": "Claude Agent Teams",
"0708ed89f1": "Claude",
"fc80296033": "Devin",
"da41abbdd4": "Ante"
"da41abbdd4": "Ante",
"mimo_code_label": "MiMo Code"
},
"skill": {
"cli": {
+2 -1
View File
@@ -289,7 +289,8 @@
"bf53f09bf8": "Claude Agent Teams",
"0708ed89f1": "Claude",
"fc80296033": "Devin",
"da41abbdd4": "Ante"
"da41abbdd4": "Ante",
"mimo_code_label": "MiMo Code"
},
"skill": {
"cli": {
+2 -1
View File
@@ -289,7 +289,8 @@
"bf53f09bf8": "Claude Agent Teams",
"0708ed89f1": "Claude",
"fc80296033": "Devin",
"da41abbdd4": "Ante"
"da41abbdd4": "Ante",
"mimo_code_label": "MiMo Code"
},
"skill": {
"cli": {
+2 -1
View File
@@ -289,7 +289,8 @@
"bf53f09bf8": "Claude Agent Teams",
"0708ed89f1": "Claude",
"fc80296033": "Devin",
"da41abbdd4": "Ante"
"da41abbdd4": "Ante",
"mimo_code_label": "MiMo Code"
},
"skill": {
"cli": {
+2 -1
View File
@@ -289,7 +289,8 @@
"bf53f09bf8": "Claude Agent Teams",
"0708ed89f1": "Claude",
"fc80296033": "Devin",
"da41abbdd4": "Ante"
"da41abbdd4": "Ante",
"mimo_code_label": "MiMo Code"
},
"skill": {
"cli": {
+7
View File
@@ -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'),
+2
View File
@@ -121,6 +121,7 @@ const WELL_KNOWN_LABELS: Record<string, string> = {
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<TuiAgent, true> = {
codex: true,
autohand: true,
opencode: true,
'mimo-code': true,
pi: true,
omp: true,
gemini: true,
+27 -1
View File
@@ -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()
}
)
})
+3
View File
@@ -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'
}
+50
View File
@@ -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,
+21 -8
View File
@@ -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<Record<string, AgentHookSource>>
'/hook/antigravity': 'antigravity',
'/hook/amp': 'amp',
'/hook/opencode': 'opencode',
'/hook/mimo-code': 'mimo-code',
'/hook/cursor': 'cursor',
'/hook/pi': 'pi',
'/hook/omp': 'omp',
+1
View File
@@ -38,6 +38,7 @@ export type AgentHookSource =
| 'antigravity'
| 'amp'
| 'opencode'
| 'mimo-code'
| 'cursor'
| 'pi'
| 'omp'
+1
View File
@@ -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',
+1
View File
@@ -22,6 +22,7 @@ export const AGENT_NAMES = [
'gemini',
'antigravity',
'opencode',
'mimo',
'openclaw',
'aider',
'grok',
+2
View File
@@ -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']]
+5 -1
View File
@@ -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':
+1
View File
@@ -21,6 +21,7 @@ export type WellKnownAgentType =
| 'antigravity'
| 'amp'
| 'opencode'
| 'mimo-code'
| 'cursor'
| 'copilot'
| 'aider'
+1
View File
@@ -75,6 +75,7 @@ export const AGENT_KIND_VALUES = [
'codex',
'autohand',
'opencode',
'mimo-code',
'pi',
'omp',
'gemini',
+6
View File
@@ -116,6 +116,12 @@ export const TUI_AGENT_CONFIG: Record<TuiAgent, TuiAgentConfig> = {
expectedProcess: 'opencode',
promptInjectionMode: 'flag-prompt'
},
'mimo-code': {
detectCmd: 'mimo',
launchCmd: 'mimo',
expectedProcess: 'mimo',
promptInjectionMode: 'flag-prompt'
},
pi: {
detectCmd: 'pi',
launchCmd: 'pi',
+1
View File
@@ -14,6 +14,7 @@ export const TUI_AGENT_DISPLAY_NAMES: Record<TuiAgent, string> = {
ante: 'Ante',
autohand: 'Autohand Code',
opencode: 'OpenCode',
'mimo-code': 'MiMo Code',
pi: 'Pi',
omp: 'OMP',
gemini: 'Gemini',
+1
View File
@@ -11,6 +11,7 @@ export const TUI_AGENT_AUTO_PICK_ORDER = [
'grok',
'copilot',
'opencode',
'mimo-code',
'ante',
'pi',
'omp',
+1
View File
@@ -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