Skip to content

Commit 14ed34f

Browse files
author
pedro
committed
fix(omp): review follow-ups — OSC tails, argument range, cancelled generation, cache freshness, docs
1 parent 297761d commit 14ed34f

12 files changed

Lines changed: 125 additions & 24 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ We wanted something performant, remote-ready, and truly open. If we ever go the
1919
> - Claude: install [Claude Code](https://claude.com/product/claude-code) and run `claude auth login`
2020
> - Cursor: install [Cursor CLI](https://cursor.com/cli) and run `agent login`
2121
> - Grok Build: install [Grok Build CLI](https://x.ai/cli) and run `grok login`
22+
> - Oh My Pi (omp): install [Oh My Pi](https://github.com/can1357/oh-my-pi) and run `omp setup`
2223
> - OpenCode: install [OpenCode](https://opencode.ai) and run `opencode auth login`
23-
> - Oh My Pi (omp): install [Oh My Pi](https://github.com/can1357/oh-my-pi) and run `omp`
2424
> - Antigravity: enable it in Settings, then use **Install Antigravity** and **Sign in with Google**. No CLI is required.
2525
2626
### Try it out (install-free)

‎apps/server/src/provider/Drivers/OmpDriver.test.ts‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,50 @@ it.layer(testLayer)("OmpDriver", (it) => {
350350
}).pipe(Effect.scoped),
351351
);
352352

353+
// A cache hit re-records the cwd so the LRU keeps it, which must not also
354+
// restart its freshness window: a polled cwd would then never re-probe and
355+
// a skill installed out of band would stay invisible for the session.
356+
it.effect("re-probes after the freshness window even while the cwd is polled", () =>
357+
Effect.gen(function* () {
358+
const fs = yield* FileSystem.FileSystem;
359+
const path = yield* Path.Path;
360+
const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-omp-driver-stale-" });
361+
const probeLogPath = path.join(root, "probes.log");
362+
const fakePath = yield* makeFakeOmp({
363+
prefix: "t3-omp-driver-stale-bin-",
364+
checkOutput: "Current version: 18.1.18\n",
365+
probeLogPath,
366+
});
367+
const instance = yield* createTestInstance("omp-catalog-stale", {
368+
binaryPath: fakePath,
369+
enabled: true,
370+
});
371+
const snapshotForCwd = instance.snapshotForCwd;
372+
if (!snapshotForCwd)
373+
return yield* Effect.die("OmpDriver does not expose workspace snapshots.");
374+
const workspace = yield* fs.makeTempDirectoryScoped({ prefix: "t3-omp-stale-ws-" });
375+
376+
const realNow = Date.now;
377+
let clockOffsetMillis = 0;
378+
const nowSpy = vi.spyOn(Date, "now").mockImplementation(() => realNow() + clockOffsetMillis);
379+
try {
380+
yield* snapshotForCwd(workspace);
381+
// Polls inside the window are served from cache.
382+
clockOffsetMillis = 20_000;
383+
yield* snapshotForCwd(workspace);
384+
clockOffsetMillis = 29_000;
385+
yield* snapshotForCwd(workspace);
386+
expect(yield* readProbeCount(probeLogPath, workspace)).toBe(1);
387+
388+
clockOffsetMillis = 31_000;
389+
yield* snapshotForCwd(workspace);
390+
expect(yield* readProbeCount(probeLogPath, workspace)).toBe(2);
391+
} finally {
392+
nowSpy.mockRestore();
393+
}
394+
}).pipe(Effect.scoped),
395+
);
396+
353397
it.effect("applies a live available_commands_update without a second probe", () =>
354398
Effect.gen(function* () {
355399
const fs = yield* FileSystem.FileSystem;

‎apps/server/src/provider/Drivers/OmpDriver.ts‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,16 @@ export const OmpDriver: ProviderDriver<OmpSettings, OmpDriverEnv> = {
180180
workspaceCwd: string,
181181
catalog: OmpCommandCatalog,
182182
checkedAt: string,
183+
probedAtMillis?: number,
183184
): void => {
184185
skillNamesByCwd.set(workspaceCwd, new Set(catalog.skills.map((skill) => skill.name)));
186+
// The stamp is the probe's own time: re-recording a cached catalog
187+
// keeps the cwd hot in the LRU without extending its freshness
188+
// window, or a cwd polled faster than the window would never
189+
// re-probe and an out-of-band skill install would stay invisible.
185190
// @effect-diagnostics-next-line globalDate:off - cache stamp shares Date.now with the freshness read below; Effect Clock is unavailable in the sync callback path.
186-
catalogCacheByCwd.set(workspaceCwd, { catalog, cachedAt: Date.now() });
191+
const cachedAt = probedAtMillis ?? Date.now();
192+
catalogCacheByCwd.set(workspaceCwd, { catalog, cachedAt });
187193
retainedWorkspaceSnapshots = appendOmpWorkspaceSnapshot(retainedWorkspaceSnapshots, {
188194
cwd: workspaceCwd,
189195
checkedAt,
@@ -264,8 +270,14 @@ export const OmpDriver: ProviderDriver<OmpSettings, OmpDriverEnv> = {
264270
Effect.flatMap((machineSnapshot) =>
265271
Effect.map(DateTime.now, (now) => {
266272
// Re-record so a revisited cwd moves last and an evicted one
267-
// comes back; the timestamp slides while the cwd stays hot.
268-
rememberCatalog(workspaceCwd, cached.catalog, DateTime.formatIso(now));
273+
// comes back; the probe's own timestamp carries over so the
274+
// freshness window still expires on schedule.
275+
rememberCatalog(
276+
workspaceCwd,
277+
cached.catalog,
278+
DateTime.formatIso(now),
279+
cached.cachedAt,
280+
);
269281
return {
270282
...machineSnapshot,
271283
skills: cached.catalog.skills,

‎apps/server/src/provider/Drivers/OmpMaintenance.ts‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
* OmpMaintenance — update capabilities and workspace-snapshot helpers for the
33
* Oh My Pi (`omp`) driver.
44
*
5-
* omp is its own updater: it ships outside npm/homebrew, so no package
6-
* manager owns its install. `omp update --check` prints the installed
7-
* version (`Current version: X`) and, when behind, the version it would
8-
* install (`New version available: Y`); `omp update` performs the install.
9-
* The maintenance resolver therefore advertises the resolved `omp` binary
10-
* itself as the updater and bakes the `--check` output into `latestVersion`,
11-
* instead of guessing a registry the way npm/homebrew-backed drivers do.
5+
* omp is its own updater: `omp update` detects how this copy was installed
6+
* (Homebrew, mise, Bun, npm, or a direct binary) and delegates to it, so no
7+
* single registry describes the install. `omp update --check` prints the
8+
* installed version (`Current version: X`) and, when behind, the version it
9+
* would install (`New version available: Y`). The maintenance resolver
10+
* therefore advertises the resolved `omp` binary itself as the updater and
11+
* bakes the `--check` output into `latestVersion`, instead of guessing a
12+
* registry the way npm/homebrew-backed drivers do.
1213
*
1314
* @module provider/Drivers/OmpMaintenance
1415
*/

‎apps/server/src/provider/acp/OmpAnsi.test.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,20 @@ describe("makeAnsiFilter", () => {
4040
expect(filter.flush()).toBe("");
4141
});
4242

43+
it("keeps text printed after a terminated hyperlink in the same chunk", () => {
44+
const filter = makeAnsiFilter();
45+
46+
expect(filter.push("\u001B]8;;http://x\u0007label after")).toBe("label after");
47+
expect(filter.flush()).toBe("");
48+
});
49+
50+
it("still holds an unterminated hyperlink until it closes", () => {
51+
const filter = makeAnsiFilter();
52+
53+
expect(filter.push("start \u001B]8;;http://x")).toBe("start ");
54+
expect(filter.push("\u0007label")).toBe("label");
55+
});
56+
4357
it("discards a partial escape that the stream never completed", () => {
4458
const filter = makeAnsiFilter();
4559

‎apps/server/src/provider/acp/OmpAnsi.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,15 @@ const ANSI_PATTERN =
2121
// eslint-disable-next-line no-control-regex
2222
/\u001B\[[0-9;:?]*[ -/]*[@-~]|\u001B\][\s\S]*?(?:\u0007|\u001B\\)|\u001B[ -/]+[0-~]|\u001B[@-Z\\-_]/g;
2323

24-
/** A tail that could still become a complete sequence once more text arrives. */
24+
/**
25+
* A tail that could still become a complete sequence once more text arrives.
26+
* The OSC branch requires the sequence to be unterminated: a `ESC ]…BEL`
27+
* that already closed is a complete escape, and treating it as a tail would
28+
* withhold every character printed after it.
29+
*/
2530
const PARTIAL_ANSI_TAIL_PATTERN =
2631
// eslint-disable-next-line no-control-regex
27-
/\u001B(?:\[[0-9;:?]*[ -/]*|\][\s\S]*)?$/;
32+
/\u001B(?:\[[0-9;:?]*[ -/]*|\](?:(?!\u0007|\u001B\\)[\s\S])*)?$/;
2833

2934
/** Remove every terminal escape sequence from a complete string. */
3035
export function stripAnsi(text: string): string {

‎apps/server/src/textGeneration/OmpTextGeneration.ts‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,14 +133,20 @@ export const makeOmpTextGeneration = Effect.fn("makeOmpTextGeneration")(function
133133
),
134134
);
135135

136+
// A cancelled turn is a failure even when omp already streamed
137+
// parseable output: the text is a fragment of an answer nobody
138+
// finished, not a result.
139+
if (promptResult.stopReason === "cancelled") {
140+
return yield* new TextGenerationError({
141+
operation,
142+
detail: "Oh My Pi ACP request was cancelled.",
143+
});
144+
}
136145
const rawResult = (yield* Ref.get(outputRef)).trim();
137146
if (!rawResult) {
138147
return yield* new TextGenerationError({
139148
operation,
140-
detail:
141-
promptResult.stopReason === "cancelled"
142-
? "Oh My Pi ACP request was cancelled."
143-
: "Oh My Pi returned empty output.",
149+
detail: "Oh My Pi returned empty output.",
144150
});
145151
}
146152

‎apps/web/src/components/ComposerPromptEditor.test.ts‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -873,9 +873,7 @@ describe("isOpenableSkillPath", () => {
873873
});
874874

875875
it("refuses internal URLs and blank paths", () => {
876-
expect(
877-
isOpenableSkillPath("C:\Storage\AI\Agents\.omp\curated-skills\matt\matt-tdd\SKILL.md"),
878-
).toBe(false);
876+
expect(isOpenableSkillPath("skill://tdd/SKILL.md")).toBe(false);
879877
expect(isOpenableSkillPath("https://example.com/SKILL.md")).toBe(false);
880878
expect(isOpenableSkillPath(" ")).toBe(false);
881879
});

‎apps/web/src/composer-logic.test.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,18 @@ describe("detectComposerTrigger", () => {
167167
});
168168
});
169169

170+
it("covers the whole argument token when the caret sits inside it", () => {
171+
const text = "/compact remx";
172+
173+
expect(detectComposerTrigger(text, "/compact rem".length)).toEqual({
174+
kind: "slash-argument",
175+
command: "compact",
176+
query: "rem",
177+
rangeStart: "/compact ".length,
178+
rangeEnd: text.length,
179+
});
180+
});
181+
170182
it("stops triggering once a second argument word is typed", () => {
171183
const text = "/compact remote focus here";
172184

‎apps/web/src/composer-logic.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,12 +235,18 @@ export function detectComposerTrigger(text: string, cursorInput: number): Compos
235235
const argumentMatch = /^\/(\S+)[ \t]+(\S*)$/.exec(linePrefix);
236236
if (argumentMatch) {
237237
const query = argumentMatch[2] ?? "";
238+
// The caret may sit inside the argument (`/compact rem|x`). Selecting a
239+
// choice replaces the whole token, not just the part before the caret,
240+
// or the leftover would trail the inserted value.
241+
const lineEnd = text.indexOf("\n", cursor);
242+
const restOfLine = text.slice(cursor, lineEnd === -1 ? text.length : lineEnd);
243+
const tokenRest = /^\S*/.exec(restOfLine)?.[0] ?? "";
238244
return {
239245
kind: "slash-argument",
240246
command: argumentMatch[1] ?? "",
241247
query,
242248
rangeStart: cursor - query.length,
243-
rangeEnd: cursor,
249+
rangeEnd: cursor + tokenRest.length,
244250
};
245251
}
246252
}

0 commit comments

Comments
 (0)