Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ between releases do not reach them. Remote-PC runtime bundles are versioned sepa

## [Unreleased]

### Fixed
- A pane running a command that is only given an omo path (`grep -q …/omo-ai/x`, `cat …/bin/omo`) is
no longer shown as OmO or counted as a second OmO in its folder. Only the program a pane runs
counts, or the script that node or bun runs
([#249](https://github.com/devswha/herdr-web-ui/pull/249)).

## [0.3.39] - 2026-10-01

### Fixed
Expand Down
25 changes: 24 additions & 1 deletion server/omo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,37 @@ import { expect, it } from "bun:test";
import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { heldSessionIds, omoCandidates, selectOmoTranscript, type OmoRuntime } from "./omo.ts";
import { heldSessionIds, isOmoProcess, omoCandidates, selectOmoTranscript, type OmoRuntime } from "./omo.ts";

const runtime = (paneId: string, startedAt: number | null = 10_000, paths: string[] = [], ids: string[] = []): OmoRuntime => ({ paneId, startedAt, paths, ids });
const files = [
{ path: "/old.jsonl", id: "old-session", createdAt: 100 },
{ path: "/fresh.jsonl", id: "fresh-session", createdAt: 10_000 },
];

const OMO_AI = "/home/u/.nvm/versions/node/v24.18.0/lib/node_modules/omo-ai";

it("takes omo from the program a process runs: its own binary, or the script of node or bun", () => {
// argv as herdr 0.9.0's pane.process_info reported them for omo 5.1.7 and its MCP child
expect(isOmoProcess(["/home/u/.bun/bin/bun", `${OMO_AI}/node_modules/@code-yeongyu/senpi/dist/bundle/cli.js`, "--extension", `${OMO_AI}/plugin`])).toBeTrue();
expect(isOmoProcess(["/home/u/.bun/bin/bun", `${OMO_AI}/plugin/runtime/ast-grep-mcp/cli.js`, "mcp"])).toBeTrue();
expect(isOmoProcess(["node", "/home/u/.nvm/versions/node/v24.18.0/bin/omo"])).toBeTrue();
expect(isOmoProcess(["node", "--enable-source-maps", `${OMO_AI}/bin/omo.js`, "--session-id", "abcdefgh"])).toBeTrue();
expect(isOmoProcess(["omo"])).toBeTrue();
expect(isOmoProcess(["/home/u/.local/bin/omo", "--session-id", "abcdefgh"])).toBeTrue();
expect(isOmoProcess([`${OMO_AI}/node_modules/@anthropic-ai/claude-agent-sdk-linux-x64/claude`, "--output-format", "stream-json"])).toBeTrue();
});

it("does not take an omo path given to another program for omo", () => {
expect(isOmoProcess(["grep", "-q", `${OMO_AI}/x`])).toBeFalse();
expect(isOmoProcess(["cat", "/home/u/.nvm/versions/node/v24.18.0/bin/omo"])).toBeFalse();
expect(isOmoProcess(["ls", "omo"])).toBeFalse();
expect(isOmoProcess(["node", "/home/u/tools/watch.js", `${OMO_AI}/plugin`])).toBeFalse();
expect(isOmoProcess(["bun", "--version"])).toBeFalse();
expect(isOmoProcess(["/home/u/omo-ai-tools/bun", "/home/u/x.js"])).toBeFalse();
expect(isOmoProcess([])).toBeFalse();
});

it("uses only a unique session created during the sole runtime, never cwd recency", () => {
expect(selectOmoTranscript("a", files, [runtime("a")], 20_000)).toBe("/fresh.jsonl");
expect(selectOmoTranscript("a", files, [runtime("a"), runtime("b", 11_000)], 20_000)).toBeNull();
Expand Down
15 changes: 11 additions & 4 deletions server/omo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@ import { herdrRpc } from "./herdr/client.ts";
import { processStartedAt } from "./process-start.ts";

const OMO_PROCESS = /(^|\/)omo(\.js)?$|\/omo-ai\//;
/** node and bun run a script: the program is then the script, the first word that is not a flag */
const JS_RUNTIME = /(^|\/)(node|nodejs|bun)$/;
/**
* A word counts only as one path: a PATH list that names omo-ai's bin directory is an argument
* of the shell's startup (`printf %s\n $PATH` in an rc file), not omo, and it made a fresh
* shell pass for omo for a moment.
* Only the program counts: argv[0] (omo's native binary, its SDK's claude), or the script a
* JS runtime runs (`bun …/omo-ai/…/cli.js`, `node …/bin/omo`). An omo-ai path handed to another
* program (`grep -q …/omo-ai/x`, `cat …/bin/omo`) is that program's argument, not omo. The word
* is one path: a PATH list that names omo-ai's bin directory (`printf %s\n $PATH` in an rc
* file) made a fresh shell pass for omo for a moment.
*/
export function isOmoProcess(argv: readonly string[]): boolean { return argv.some((word) => !word.includes(":") && OMO_PROCESS.test(word)); }
export function isOmoProcess(argv: readonly string[]): boolean {
const program = JS_RUNTIME.test(argv[0] ?? "") ? argv.slice(1).find((word) => !word.startsWith("-")) : argv[0];
return program !== undefined && !program.includes(":") && OMO_PROCESS.test(program);
}

export interface OmoCandidate { path: string; id: string; createdAt: number | null }
export interface OmoRuntime {
Expand Down
Loading