diff --git a/server/modules/providers/README.md b/server/modules/providers/README.md
index baa8f43..f286d8c 100644
--- a/server/modules/providers/README.md
+++ b/server/modules/providers/README.md
@@ -38,6 +38,7 @@ Current provider ids accepted by the registry and `parseProvider` are:
- `opencode`
- `gjc`
- `omp`
+- `omo`
Those ids are mirrored in backend unions and frontend provider constants. If
adding a new provider, update every place that hardcodes this list.
diff --git a/server/modules/providers/list/gjc/gjc-session-synchronizer.provider.ts b/server/modules/providers/list/gjc/gjc-session-synchronizer.provider.ts
index 524e141..34fa1ba 100644
--- a/server/modules/providers/list/gjc/gjc-session-synchronizer.provider.ts
+++ b/server/modules/providers/list/gjc/gjc-session-synchronizer.provider.ts
@@ -14,7 +14,12 @@ import {
import type { IProviderSessionSynchronizer } from '@/shared/interfaces.js';
import type { AnyRecord } from '@/shared/types.js';
-type PiTranscriptProvider = 'gjc' | 'omp';
+/**
+ * CLIs that write the pi-derived transcript dialect: the same JSONL envelope
+ * (`message`, `model_change`, `thinking_level_change`, `custom`), so one reader
+ * and one synchronizer serve all of them.
+ */
+export type PiTranscriptProvider = 'gjc' | 'omp' | 'omo';
export type PiSessionSynchronizerOptions = {
provider?: PiTranscriptProvider;
diff --git a/server/modules/providers/list/gjc/gjc-sessions.provider.ts b/server/modules/providers/list/gjc/gjc-sessions.provider.ts
index f9d9b85..c76eebd 100644
--- a/server/modules/providers/list/gjc/gjc-sessions.provider.ts
+++ b/server/modules/providers/list/gjc/gjc-sessions.provider.ts
@@ -1,6 +1,7 @@
import fsSync from 'node:fs';
import { sessionsDb } from '@/modules/database/index.js';
+import type { PiTranscriptProvider } from '@/modules/providers/list/gjc/gjc-session-synchronizer.provider.js';
import type { IProviderSessions } from '@/shared/interfaces.js';
import type { AnyRecord, FetchHistoryOptions, FetchHistoryResult, NormalizedMessage } from '@/shared/types.js';
import { createNormalizedMessage, generateMessageId, readObjectRecord, sliceTailPage } from '@/shared/utils.js';
@@ -188,7 +189,7 @@ function normalizeGjcToolInput(toolName: string, value: unknown): unknown {
* its own intermediate record with a unique id so multi-part turns never collide.
*/
async function streamPiSessionMessages(
- provider: 'gjc' | 'omp',
+ provider: PiTranscriptProvider,
sessionId: string,
onMessage: (message: AnyRecord) => void,
): Promise {
@@ -337,7 +338,7 @@ async function streamPiSessionMessages(
}
export class GjcSessionsProvider implements IProviderSessions {
- constructor(private readonly provider: 'gjc' | 'omp' = 'gjc') {}
+ constructor(private readonly provider: PiTranscriptProvider = 'gjc') {}
/**
* Normalizes one flattened gjc content-part record into the shared envelope.
*/
diff --git a/server/modules/providers/list/omo/omo-auth.provider.ts b/server/modules/providers/list/omo/omo-auth.provider.ts
new file mode 100644
index 0000000..fadd7ce
--- /dev/null
+++ b/server/modules/providers/list/omo/omo-auth.provider.ts
@@ -0,0 +1,19 @@
+import spawn from 'cross-spawn';
+
+import type { IProviderAuth } from '@/shared/interfaces.js';
+import type { ProviderAuthStatus } from '@/shared/types.js';
+
+export class OmoProviderAuth implements IProviderAuth {
+ async getStatus(): Promise {
+ const result = spawn.sync('omo', ['--version'], { stdio: 'ignore', timeout: 5_000 });
+ const installed = !result.error && result.status === 0;
+ return {
+ installed,
+ provider: 'omo',
+ authenticated: installed,
+ email: installed ? 'CLI managed' : null,
+ method: installed ? 'cli' : null,
+ error: installed ? undefined : 'omo CLI is not installed',
+ };
+ }
+}
diff --git a/server/modules/providers/list/omo/omo-mcp.provider.ts b/server/modules/providers/list/omo/omo-mcp.provider.ts
new file mode 100644
index 0000000..7340d16
--- /dev/null
+++ b/server/modules/providers/list/omo/omo-mcp.provider.ts
@@ -0,0 +1,31 @@
+import { McpProvider } from '@/modules/providers/shared/mcp/mcp.provider.js';
+import type { ProviderMcpServer } from '@/shared/types.js';
+import { AppError } from '@/shared/utils.js';
+
+export class OmoMcpProvider extends McpProvider {
+ constructor() {
+ super('omo', ['user', 'project'], ['stdio', 'http']);
+ }
+
+ protected async readScopedServers(): Promise> {
+ return {};
+ }
+
+ protected async writeScopedServers(): Promise {
+ throw new AppError('omo MCP configuration is not supported by ChatMux.', {
+ code: 'MCP_WRITE_UNSUPPORTED',
+ statusCode: 400,
+ });
+ }
+
+ protected buildServerConfig(): Record {
+ throw new AppError('omo MCP configuration is not supported by ChatMux.', {
+ code: 'MCP_WRITE_UNSUPPORTED',
+ statusCode: 400,
+ });
+ }
+
+ protected normalizeServerConfig(): ProviderMcpServer | null {
+ return null;
+ }
+}
diff --git a/server/modules/providers/list/omo/omo-models.provider.ts b/server/modules/providers/list/omo/omo-models.provider.ts
new file mode 100644
index 0000000..81c7bae
--- /dev/null
+++ b/server/modules/providers/list/omo/omo-models.provider.ts
@@ -0,0 +1,92 @@
+import { execFile } from 'node:child_process';
+import { promisify } from 'node:util';
+
+import {
+ readLastTranscriptActiveModel,
+} from '@/modules/providers/list/omp/omp-models.provider.js';
+import { sessionsDb } from '@/modules/database/index.js';
+import type { IProviderModels } from '@/shared/interfaces.js';
+import type {
+ ProviderChangeActiveModelInput,
+ ProviderCurrentActiveModel,
+ ProviderModelOption,
+ ProviderModelsDefinition,
+ ProviderSessionActiveModelChange,
+} from '@/shared/types.js';
+import {
+ buildDefaultProviderCurrentActiveModel,
+ writeProviderSessionActiveModelChange,
+} from '@/shared/utils.js';
+
+const execFileAsync = promisify(execFile);
+
+const OMO_FALLBACK_MODELS: ProviderModelsDefinition = {
+ OPTIONS: [{ value: 'default', label: 'Current CLI model' }],
+ DEFAULT: 'default',
+};
+
+/**
+ * `omo --list-models` prints an aligned text table, not JSON:
+ *
+ * provider model context max-out thinking images
+ * alibaba-token-plan deepseek-v3.2 131.1K 65.5K yes no
+ *
+ * Every column is whitespace-free, so a row is exactly six tokens. Requiring
+ * that shape plus yes/no flags rejects the header and any startup chatter the
+ * CLI writes before the table.
+ */
+export function parseOmoModelCatalog(raw: string): ProviderModelsDefinition {
+ const options: ProviderModelOption[] = [];
+ const seen = new Set();
+ for (const line of raw.split(/\r?\n/)) {
+ const columns = line.trim().split(/\s+/);
+ if (columns.length !== 6) continue;
+ const [provider, model, context, , thinking, images] = columns;
+ if (!/^(?:yes|no)$/i.test(thinking) || !/^(?:yes|no)$/i.test(images)) continue;
+ const selector = `${provider}/${model}`;
+ if (seen.has(selector)) continue;
+ seen.add(selector);
+ options.push({
+ value: selector,
+ label: model,
+ description: `${context} context · ${provider}`,
+ });
+ }
+ return options.length > 0
+ ? { OPTIONS: [...OMO_FALLBACK_MODELS.OPTIONS, ...options], DEFAULT: OMO_FALLBACK_MODELS.DEFAULT }
+ : OMO_FALLBACK_MODELS;
+}
+
+async function loadOmoModelCatalog(): Promise {
+ try {
+ const { stdout } = await execFileAsync(
+ 'omo',
+ ['--list-models'],
+ { encoding: 'utf8', timeout: 30_000, maxBuffer: 8 * 1024 * 1024 },
+ );
+ return parseOmoModelCatalog(stdout);
+ } catch {
+ return OMO_FALLBACK_MODELS;
+ }
+}
+
+export class OmoProviderModels implements IProviderModels {
+ async getSupportedModels(): Promise {
+ return loadOmoModelCatalog();
+ }
+
+ async getCurrentActiveModel(sessionId?: string): Promise {
+ const row = sessionId ? sessionsDb.getSessionById(sessionId) : null;
+ if (row?.jsonl_path) {
+ const activeModel = await readLastTranscriptActiveModel(row.jsonl_path).catch(() => null);
+ if (activeModel) return activeModel;
+ }
+ return buildDefaultProviderCurrentActiveModel(await this.getSupportedModels());
+ }
+
+ async changeActiveModel(
+ input: ProviderChangeActiveModelInput,
+ ): Promise {
+ return writeProviderSessionActiveModelChange('omo', input);
+ }
+}
diff --git a/server/modules/providers/list/omo/omo-skills.provider.ts b/server/modules/providers/list/omo/omo-skills.provider.ts
new file mode 100644
index 0000000..e50bbff
--- /dev/null
+++ b/server/modules/providers/list/omo/omo-skills.provider.ts
@@ -0,0 +1,66 @@
+import os from 'node:os';
+import path from 'node:path';
+
+import { SkillsProvider } from '@/modules/providers/shared/skills/skills.provider.js';
+import type { ProviderSkillSource } from '@/shared/types.js';
+import { addUniqueProviderSkillSource, findTopmostGitRoot } from '@/shared/utils.js';
+
+const PROJECT_SKILL_DIRS = [
+ ['.omo', 'skills'],
+ ['.agent', 'skills'],
+ ['.agents', 'skills'],
+ ['.codex', 'skills'],
+ ['.claude', 'skills'],
+] as const;
+
+const USER_SKILL_DIRS = [
+ ['.omo', 'agent', 'skills'],
+ ['.agent', 'skills'],
+ ['.omo', 'agent', 'managed-skills'],
+ ['.agents', 'skills'],
+ ['.codex', 'skills'],
+ ['.claude', 'skills'],
+] as const;
+
+export class OmoSkillsProvider extends SkillsProvider {
+ constructor() {
+ super('omo');
+ }
+
+ protected async getSkillSources(workspacePath: string): Promise {
+ const sources: ProviderSkillSource[] = [];
+ const seenRootDirs = new Set();
+ const repoRoot = await findTopmostGitRoot(workspacePath);
+ const projectRoots = repoRoot && path.resolve(repoRoot) !== path.resolve(workspacePath)
+ ? [workspacePath, repoRoot]
+ : [workspacePath];
+
+ for (const projectRoot of projectRoots) {
+ for (const segments of PROJECT_SKILL_DIRS) {
+ addUniqueProviderSkillSource(sources, seenRootDirs, {
+ scope: 'project',
+ rootDir: path.join(projectRoot, ...segments),
+ commandPrefix: '/skill:',
+ });
+ }
+ }
+
+ for (const segments of USER_SKILL_DIRS) {
+ addUniqueProviderSkillSource(sources, seenRootDirs, {
+ scope: 'user',
+ rootDir: path.join(os.homedir(), ...segments),
+ commandPrefix: '/skill:',
+ });
+ }
+
+ return sources;
+ }
+
+ protected async getGlobalSkillSource(): Promise {
+ return {
+ scope: 'user',
+ rootDir: path.join(os.homedir(), '.omo', 'agent', 'skills'),
+ commandPrefix: '/skill:',
+ };
+ }
+}
diff --git a/server/modules/providers/list/omo/omo.provider.ts b/server/modules/providers/list/omo/omo.provider.ts
new file mode 100644
index 0000000..84e5369
--- /dev/null
+++ b/server/modules/providers/list/omo/omo.provider.ts
@@ -0,0 +1,29 @@
+import { OmoProviderAuth } from '@/modules/providers/list/omo/omo-auth.provider.js';
+import { OmoMcpProvider } from '@/modules/providers/list/omo/omo-mcp.provider.js';
+import { OmoProviderModels } from '@/modules/providers/list/omo/omo-models.provider.js';
+import { OmoSkillsProvider } from '@/modules/providers/list/omo/omo-skills.provider.js';
+import { GjcSessionSynchronizer } from '@/modules/providers/list/gjc/gjc-session-synchronizer.provider.js';
+import { GjcSessionsProvider } from '@/modules/providers/list/gjc/gjc-sessions.provider.js';
+import { AbstractProvider } from '@/modules/providers/shared/base/abstract.provider.js';
+import type {
+ IProviderAuth,
+ IProviderModels,
+ IProviderSessionSynchronizer,
+ IProviderSkills,
+ IProviderSessions,
+} from '@/shared/interfaces.js';
+
+export class OmoProvider extends AbstractProvider {
+ readonly models: IProviderModels = new OmoProviderModels();
+ readonly mcp = new OmoMcpProvider();
+ readonly auth: IProviderAuth = new OmoProviderAuth();
+ readonly skills: IProviderSkills = new OmoSkillsProvider();
+ readonly sessions: IProviderSessions = new GjcSessionsProvider('omo');
+ readonly sessionSynchronizer: IProviderSessionSynchronizer = new GjcSessionSynchronizer({
+ provider: 'omo',
+ });
+
+ constructor() {
+ super('omo');
+ }
+}
diff --git a/server/modules/providers/list/omp/omp-models.provider.ts b/server/modules/providers/list/omp/omp-models.provider.ts
index 8e7d914..3c6cdfc 100644
--- a/server/modules/providers/list/omp/omp-models.provider.ts
+++ b/server/modules/providers/list/omp/omp-models.provider.ts
@@ -102,7 +102,12 @@ export function parseOmpTranscriptActiveModelLine(
}
}
-async function readLastTranscriptActiveModel(
+/**
+ * Shared by every pi-derived CLI (omp, gjc, omo): they all record
+ * `model_change`, `thinking_level_change`, and `configured_model_chain` with
+ * the same shape, so the last active model is read the same way.
+ */
+export async function readLastTranscriptActiveModel(
filePath: string,
): Promise {
let model: string | null = null;
diff --git a/server/modules/providers/provider.registry.ts b/server/modules/providers/provider.registry.ts
index 1c0d8e2..76f00ed 100644
--- a/server/modules/providers/provider.registry.ts
+++ b/server/modules/providers/provider.registry.ts
@@ -3,6 +3,7 @@ import { CodexProvider } from '@/modules/providers/list/codex/codex.provider.js'
import { CursorProvider } from '@/modules/providers/list/cursor/cursor.provider.js';
import { GjcProvider } from '@/modules/providers/list/gjc/gjc.provider.js';
import { OpenCodeProvider } from '@/modules/providers/list/opencode/opencode.provider.js';
+import { OmoProvider } from '@/modules/providers/list/omo/omo.provider.js';
import { OmpProvider } from '@/modules/providers/list/omp/omp.provider.js';
import type { IProvider } from '@/shared/interfaces.js';
import type { LLMProvider } from '@/shared/types.js';
@@ -15,6 +16,7 @@ const providers: Record = {
opencode: new OpenCodeProvider(),
gjc: new GjcProvider(),
omp: new OmpProvider(),
+ omo: new OmoProvider(),
};
/**
diff --git a/server/modules/providers/provider.routes.ts b/server/modules/providers/provider.routes.ts
index c02166d..bba91a8 100644
--- a/server/modules/providers/provider.routes.ts
+++ b/server/modules/providers/provider.routes.ts
@@ -597,6 +597,7 @@ const parseProvider = (value: unknown): LLMProvider => {
|| normalized === 'opencode'
|| normalized === 'gjc'
|| normalized === 'omp'
+ || normalized === 'omo'
) {
return normalized;
}
diff --git a/server/modules/providers/services/external-cli-sessions.service.ts b/server/modules/providers/services/external-cli-sessions.service.ts
index fcc6585..ae7a4e5 100644
--- a/server/modules/providers/services/external-cli-sessions.service.ts
+++ b/server/modules/providers/services/external-cli-sessions.service.ts
@@ -42,12 +42,13 @@ const CODEX_RESUME_THREAD_RE = /(?:^|\s)resume\s+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-
const CLAUDE_RESUME_SESSION_RE = /(?:^|\s)--resume(?:=|\s+)([0-9a-f]{8}-[0-9a-f-]{27,})(?=\s|$)/i;
const CURSOR_RESUME_SESSION_RE = /(?:^|\s)(?:--resume|resume)(?:=|\s+)([A-Za-z0-9_-]{8,128})(?=\s|$)/;
const OPENCODE_SESSION_RE = /(?:^|\s)--session(?:=|\s+)([A-Za-z0-9_-]{8,128})(?=\s|$)/;
-const OMP_RESUME_SESSION_RE = /(?:^|\s)(?:--resume|-r)(?:=|\s+)([A-Za-z0-9_-]{8,128})(?=\s|$)/;
+// Oh My Pi and omo are both pi-derived and accept the identical `--resume|-r` form.
+const PI_RESUME_SESSION_RE = /(?:^|\s)(?:--resume|-r)(?:=|\s+)([A-Za-z0-9_-]{8,128})(?=\s|$)/;
const TRANSCRIPT_FILE_SESSION_ID_RE = /_([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
const CODEX_ROLLOUT_FILE_RE = /^rollout-.*-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
const MAX_RUNTIME_DESCRIPTORS = 2_048;
-export type ExternalLocalCliKind = 'claude' | 'codex' | 'cursor' | 'opencode' | 'omp';
+export type ExternalLocalCliKind = 'claude' | 'codex' | 'cursor' | 'opencode' | 'omp' | 'omo';
export type ExternalCliKind = ExternalLocalCliKind | 'ssh' | 'shell';
export type ExternalCliSession = {
tmuxName: string;
@@ -201,7 +202,7 @@ export function extractExternalResumeSessionId(
if (kind === 'codex') return extractCodexResumeThreadId(processArgs);
if (kind === 'cursor') return processArgs.match(CURSOR_RESUME_SESSION_RE)?.[1] ?? null;
if (kind === 'opencode') return processArgs.match(OPENCODE_SESSION_RE)?.[1] ?? null;
- if (kind === 'omp') return processArgs.match(OMP_RESUME_SESSION_RE)?.[1] ?? null;
+ if (kind === 'omp' || kind === 'omo') return processArgs.match(PI_RESUME_SESSION_RE)?.[1] ?? null;
return null;
}
@@ -457,7 +458,7 @@ export function parseExternalPanes(output: string): ExternalPane[] {
command: pane.command,
...(pane.codexThreadId ? { codexThreadId: pane.codexThreadId } : {}),
...(pane.cwd ? { cwd: pane.cwd } : {}),
- ...(pane.taggedKind && ['claude', 'codex', 'cursor', 'opencode', 'omp'].includes(pane.taggedKind)
+ ...(pane.taggedKind && ['claude', 'codex', 'cursor', 'opencode', 'omp', 'omo'].includes(pane.taggedKind)
? { taggedKind: pane.taggedKind as ExternalLocalCliKind }
: {}),
...(pane.taggedSessionId ? { taggedSessionId: pane.taggedSessionId } : {}),
@@ -500,6 +501,9 @@ function processCliKind(proc: Pick): External
if (isCursorCliProcess(proc)) return 'cursor';
if (executable('opencode')) return 'opencode';
if (executable('omp')) return 'omp';
+ // omo is a node script reached through a PATH shim, so argv carries
+ // ` /…/bin/omo` (no extension) and `executable` matches that token.
+ if (executable('omo')) return 'omo';
if (executable('ssh')) return 'ssh';
return null;
}
@@ -540,7 +544,7 @@ export function classifyExternalSessions(args: {
children.set(proc.ppid, siblings);
}
- const priority: Array> = ['claude', 'codex', 'cursor', 'opencode', 'omp', 'ssh'];
+ const priority: Array> = ['claude', 'codex', 'cursor', 'opencode', 'omp', 'omo', 'ssh'];
const result: ExternalCliSession[] = [];
for (const pane of args.panes) {
@@ -571,14 +575,17 @@ export function classifyExternalSessions(args: {
}
if (subtreeKinds.some(({ kind }) => kind === 'gjc')) continue;
- // Bun-launched Oh My Pi keeps the shell as tmux's pane PID and the `omp`
- // executable as its direct child, while pane_current_command is only `bun`.
- // Accept that exact shell-owned wrapper shape; an OMP worker nested under
- // an app process must remain an unclassified terminal row.
- const directShellOmp = isInteractiveShellProcess(procByPid.get(pane.pid))
- && subtreeKinds.some(({ kind, proc }) => kind === 'omp' && proc.ppid === pane.pid);
- if (directShellOmp) {
- kinds.add('omp');
+ // Bun-launched Oh My Pi and node-launched omo keep the shell as tmux's pane
+ // PID and the CLI as its direct child, while pane_current_command reads only
+ // `bun` or `node`. Accept that exact shell-owned wrapper shape; a worker
+ // nested under an app process must remain an unclassified terminal row.
+ const paneIsInteractiveShell = isInteractiveShellProcess(procByPid.get(pane.pid));
+ for (const wrappedKind of ['omp', 'omo'] as const) {
+ const directShellPi = paneIsInteractiveShell
+ && subtreeKinds.some(({ kind, proc }) => kind === wrappedKind && proc.ppid === pane.pid);
+ if (directShellPi) {
+ kinds.add(wrappedKind);
+ }
}
// The documented Cursor `agent` launcher execs a Node process, so tmux may
// report `agent`, `node`, or `MainThread` while the shell-owned child argv
@@ -986,38 +993,48 @@ export function extractContainedTranscriptSessionId(
* Resolve that file through /proc so an already-running, untagged tmux pane can
* attach to structured history without relying on filesystem creation times.
*/
-async function inferOpenOmpSessionIds(
+const PI_TRANSCRIPT_HOME_DIRS = {
+ omp: '.omp',
+ omo: '.omo',
+} as const;
+
+async function inferOpenPiSessionIds(
sessions: ExternalCliSession[],
): Promise
diff --git a/src/components/llm-logo-provider/OmoLogo.tsx b/src/components/llm-logo-provider/OmoLogo.tsx
new file mode 100644
index 0000000..bdc0f70
--- /dev/null
+++ b/src/components/llm-logo-provider/OmoLogo.tsx
@@ -0,0 +1,34 @@
+import { useId } from 'react';
+
+type OmoLogoProps = {
+ className?: string;
+};
+
+/**
+ * Placeholder mark: the omo package ships no brand asset, so this is a plain
+ * geometric "o" rather than an official logo. Swap it when one exists.
+ */
+const OmoLogo = ({ className = 'w-5 h-5' }: OmoLogoProps) => {
+ const gradientId = useId();
+
+ return (
+
+ );
+};
+
+export default OmoLogo;
diff --git a/src/components/llm-logo-provider/SessionProviderLogo.tsx b/src/components/llm-logo-provider/SessionProviderLogo.tsx
index e0fe599..53b4236 100644
--- a/src/components/llm-logo-provider/SessionProviderLogo.tsx
+++ b/src/components/llm-logo-provider/SessionProviderLogo.tsx
@@ -5,6 +5,7 @@ import CodexLogo from './CodexLogo';
import CursorLogo from './CursorLogo';
import OpenCodeLogo from './OpenCodeLogo';
import GjcLogo from './GjcLogo';
+import OmoLogo from './OmoLogo';
import OmpLogo from './OmpLogo';
type SessionProviderLogoProps = {
@@ -36,5 +37,9 @@ export default function SessionProviderLogo({
return ;
}
+ if (provider === 'omo') {
+ return ;
+ }
+
return ;
}
diff --git a/src/components/main-content/types/types.ts b/src/components/main-content/types/types.ts
index 7313093..c94201f 100644
--- a/src/components/main-content/types/types.ts
+++ b/src/components/main-content/types/types.ts
@@ -26,7 +26,7 @@ export type MainContentProps = {
liveSessionModel: string | null;
liveSessionEffort: string | null;
liveSessionName: string | null;
- liveSessionKind: 'gjc' | 'codex' | 'claude' | 'cursor' | 'opencode' | 'omp' | null;
+ liveSessionKind: 'gjc' | 'codex' | 'claude' | 'cursor' | 'opencode' | 'omp' | 'omo' | null;
/** True while the viewed live/external session is running a turn. */
liveSessionProcessing?: boolean;
activeTab: AppTab;
diff --git a/src/components/main-content/view/MainContent.tsx b/src/components/main-content/view/MainContent.tsx
index dd029d6..1361485 100644
--- a/src/components/main-content/view/MainContent.tsx
+++ b/src/components/main-content/view/MainContent.tsx
@@ -328,6 +328,7 @@ function MainContent({
cursor: 'Cursor',
opencode: 'OpenCode',
omp: 'Oh My Pi',
+ omo: 'omo',
}[externalTerminal.cliKind];
const pendingCliAttachTarget = buildTranscriptCliAttachTarget({
tmux: externalTerminal.tmux,
diff --git a/src/components/provider-auth/types.ts b/src/components/provider-auth/types.ts
index 5877050..8ee36bf 100644
--- a/src/components/provider-auth/types.ts
+++ b/src/components/provider-auth/types.ts
@@ -10,7 +10,7 @@ export type ProviderAuthStatus = {
export type ProviderAuthStatusMap = Record;
-export const CLI_PROVIDERS: LLMProvider[] = ['claude', 'cursor', 'codex', 'opencode', 'omp'];
+export const CLI_PROVIDERS: LLMProvider[] = ['claude', 'cursor', 'codex', 'opencode', 'omp', 'omo'];
export const PROVIDER_AUTH_STATUS_ENDPOINTS: Record = {
claude: '/api/providers/claude/auth/status',
@@ -19,6 +19,7 @@ export const PROVIDER_AUTH_STATUS_ENDPOINTS: Record = {
opencode: '/api/providers/opencode/auth/status',
gjc: '/api/providers/gjc/auth/status',
omp: '/api/providers/omp/auth/status',
+ omo: '/api/providers/omo/auth/status',
};
export const createInitialProviderAuthStatusMap = (loading = true): ProviderAuthStatusMap => ({
@@ -28,4 +29,5 @@ export const createInitialProviderAuthStatusMap = (loading = true): ProviderAuth
opencode: { authenticated: false, email: null, method: null, error: null, loading },
gjc: { authenticated: false, email: null, method: null, error: null, loading },
omp: { authenticated: false, email: null, method: null, error: null, loading },
+ omo: { authenticated: false, email: null, method: null, error: null, loading },
});
diff --git a/src/components/settings/view/tabs/agents-settings/AgentListItem.tsx b/src/components/settings/view/tabs/agents-settings/AgentListItem.tsx
index 0727571..c86b731 100644
--- a/src/components/settings/view/tabs/agents-settings/AgentListItem.tsx
+++ b/src/components/settings/view/tabs/agents-settings/AgentListItem.tsx
@@ -40,6 +40,10 @@ const agentConfig: Record = {
name: 'Oh My Pi',
color: 'gray',
},
+ omo: {
+ name: 'omo',
+ color: 'gray',
+ },
};
const colorClasses = {
diff --git a/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx b/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx
index 8403a3f..4b0e942 100644
--- a/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx
+++ b/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx
@@ -21,12 +21,13 @@ export default function AgentsSettingsTab({
const [selectedAgent, setSelectedAgent] = useState('claude');
const [selectedCategory, setSelectedCategory] = useState('account');
const visibleCategories = useMemo(() => {
- if (selectedAgent === 'omp') return ['account'];
+ // Neither pi-derived CLI exposes a ChatMux-managed permission surface.
+ if (selectedAgent === 'omp' || selectedAgent === 'omo') return ['account'];
return ['account', 'permissions'];
}, [selectedAgent]);
const visibleAgents = useMemo(() => {
- return ['claude', 'cursor', 'codex', 'opencode', 'omp'];
+ return ['claude', 'cursor', 'codex', 'opencode', 'omp', 'omo'];
}, []);
const agentContextById = useMemo>(() => ({
@@ -54,6 +55,10 @@ export default function AgentsSettingsTab({
authStatus: providerAuthStatus.omp,
onLogin: () => onProviderLogin('omp'),
},
+ omo: {
+ authStatus: providerAuthStatus.omo,
+ onLogin: () => onProviderLogin('omo'),
+ },
}), [
onProviderLogin,
providerAuthStatus.claude,
@@ -62,6 +67,7 @@ export default function AgentsSettingsTab({
providerAuthStatus.gjc,
providerAuthStatus.opencode,
providerAuthStatus.omp,
+ providerAuthStatus.omo,
]);
useEffect(() => {
diff --git a/src/components/settings/view/tabs/agents-settings/sections/AgentSelectorSection.tsx b/src/components/settings/view/tabs/agents-settings/sections/AgentSelectorSection.tsx
index f2d492e..3f7fff7 100644
--- a/src/components/settings/view/tabs/agents-settings/sections/AgentSelectorSection.tsx
+++ b/src/components/settings/view/tabs/agents-settings/sections/AgentSelectorSection.tsx
@@ -10,6 +10,7 @@ const AGENT_NAMES: Record = {
opencode: 'OpenCode',
gjc: 'Gajae Code',
omp: 'Oh My Pi',
+ omo: 'omo',
};
export default function AgentSelectorSection({
diff --git a/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx b/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx
index 49a1a31..973eec2 100644
--- a/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx
+++ b/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx
@@ -64,6 +64,15 @@ const agentConfig: Record = {
subtextClass: 'text-zinc-700 dark:text-zinc-300',
buttonClass: 'bg-zinc-900 hover:bg-zinc-800 active:bg-zinc-950 dark:bg-zinc-700 dark:hover:bg-zinc-600',
},
+ omo: {
+ name: 'omo',
+ description: 'omo coding agent',
+ bgClass: 'bg-zinc-50 dark:bg-zinc-900/20',
+ borderClass: 'border-zinc-200 dark:border-zinc-700',
+ textClass: 'text-zinc-900 dark:text-zinc-100',
+ subtextClass: 'text-zinc-700 dark:text-zinc-300',
+ buttonClass: 'bg-zinc-900 hover:bg-zinc-800 active:bg-zinc-950 dark:bg-zinc-700 dark:hover:bg-zinc-600',
+ },
omp: {
name: 'Oh My Pi',
description: 'Oh My Pi coding agent',
diff --git a/src/components/sidebar/hooks/useExternalCliSessions.ts b/src/components/sidebar/hooks/useExternalCliSessions.ts
index 09a63d0..7fa5d78 100644
--- a/src/components/sidebar/hooks/useExternalCliSessions.ts
+++ b/src/components/sidebar/hooks/useExternalCliSessions.ts
@@ -13,7 +13,7 @@ export type ExternalCliSession = {
tmuxName: string;
tmux: TmuxPaneIdentity;
process: TmuxProcessGeneration | null;
- kind: 'claude' | 'codex' | 'cursor' | 'opencode' | 'omp' | 'ssh' | 'shell';
+ kind: 'claude' | 'codex' | 'cursor' | 'opencode' | 'omp' | 'omo' | 'ssh' | 'shell';
projectPath?: string;
transcriptSessionId?: string;
sessionName?: string;
diff --git a/src/components/sidebar/view/subcomponents/SidebarExternalSection.tsx b/src/components/sidebar/view/subcomponents/SidebarExternalSection.tsx
index 855c267..e9a643d 100644
--- a/src/components/sidebar/view/subcomponents/SidebarExternalSection.tsx
+++ b/src/components/sidebar/view/subcomponents/SidebarExternalSection.tsx
@@ -19,6 +19,7 @@ const KIND_LABEL: Record = {
cursor: 'Cursor',
opencode: 'OpenCode',
omp: 'Oh My Pi',
+ omo: 'omo',
ssh: 'ssh (remote)',
shell: 'terminal',
};
diff --git a/src/components/sidebar/view/subcomponents/SidebarNewSession.tsx b/src/components/sidebar/view/subcomponents/SidebarNewSession.tsx
index a453292..11c3e95 100644
--- a/src/components/sidebar/view/subcomponents/SidebarNewSession.tsx
+++ b/src/components/sidebar/view/subcomponents/SidebarNewSession.tsx
@@ -6,7 +6,7 @@ import { api } from '../../../../utils/api';
import HomeDirInput from '../../../../shared/view/HomeDirInput';
import { cn } from '../../../../lib/utils';
-type SpawnProvider = 'gjc' | 'codex' | 'claude' | 'cursor' | 'opencode' | 'omp';
+type SpawnProvider = 'gjc' | 'codex' | 'claude' | 'cursor' | 'opencode' | 'omp' | 'omo';
type SpawnStatus =
| { kind: 'idle' }
@@ -20,6 +20,7 @@ const PROVIDERS: { id: SpawnProvider; label: string }[] = [
{ id: 'cursor', label: 'Cursor' },
{ id: 'opencode', label: 'OpenCode' },
{ id: 'omp', label: 'Oh My Pi' },
+ { id: 'omo', label: 'omo' },
];
// Working directories of successful spawns, most recent first. Typing an
diff --git a/src/types/app.ts b/src/types/app.ts
index d1f0df7..1b07459 100644
--- a/src/types/app.ts
+++ b/src/types/app.ts
@@ -1,6 +1,6 @@
import type { TmuxPaneIdentity, TmuxProcessGeneration } from '../../shared/tmux';
-export type LLMProvider = 'claude' | 'cursor' | 'codex' | 'opencode' | 'gjc' | 'omp';
+export type LLMProvider = 'claude' | 'cursor' | 'codex' | 'opencode' | 'gjc' | 'omp' | 'omo';
export type ProviderModelOption = {
value: string;
@@ -41,7 +41,7 @@ export type ExternalTerminalTarget = {
tmux: TmuxPaneIdentity;
process: TmuxProcessGeneration | null;
kind: string;
- cliKind: 'claude' | 'codex' | 'cursor' | 'opencode' | 'omp' | 'ssh' | 'shell';
+ cliKind: 'claude' | 'codex' | 'cursor' | 'opencode' | 'omp' | 'omo' | 'ssh' | 'shell';
project: Project | null;
projectPath?: string;
/** Opens the structured transcript instead of attaching a terminal. */