Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ All notable changes to **omp-web** (`@kahme247/ompweb`) are documented in this f

### Fixes & Improvements

- **Delivered screenshots no longer stay pinned as “Delivering…” prompts.** The bundled runner now advertises its message-correlation support, so image-only prompts settle by their message ID instead of failing a text match. State snapshots also retain queued message IDs after reconnects.
- The repository moved to ompweb-dev/ompweb; old links redirect.
- **omp updates with ompweb: the omp row no longer runs `omp update`.** Each ompweb release ships its own omp runner, so Settings → Hosts shows the host's omp version with "Updated with ompweb" and no update button. Hosts refuse an omp update request, and `/api/omp-update` keeps only `restart`.
- **Sorting, pinning and reordering work in the cluster's sidebar group.** The group listed the cluster's shared projects first whatever the sort, and Move up/down, drag and pins only worked among projects of the same machine, so a pinned or alphabetically earlier project of a member could not move above a shared one. The group is now one list: the chosen sort, pins and manual order apply across shared projects and every member's own projects.
Expand Down
2 changes: 1 addition & 1 deletion graphify-out/graph.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions runner/src/commands/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import { isSessionSettled } from "@oh-my-pi/pi-coding-agent/session/session-sett
import { buildAvailableSlashCommands } from "@oh-my-pi/pi-coding-agent/slash-commands/available-commands";
import { calculateTokensPerSecond } from "@oh-my-pi/pi-coding-agent/utils/token-rate";
import { parseConfiguredThinkingLevel } from "@oh-my-pi/pi-tui/thinking";
import { queuedClientMessageIds } from "../correlation";
import type { CommandTable, FrameTable } from "./types";

// shortcut: RPC's get_state first awaits its goal controller and counts a scheduled goal turn as unsettled;
// that controller is RPC transport state, so add it here when the runner ports goals.
export function sessionState(session: AgentSession) {
const queued = session.getQueuedMessages();
const clientMessageIds = queuedClientMessageIds(session);
return {
model: session.model,
thinkingLevel: session.thinkingLevel,
Expand All @@ -27,10 +29,12 @@ export function sessionState(session: AgentSession) {
queuedMessageCount: session.queuedMessageCount,
hasPendingAsyncWork: session.hasPendingAsyncWork(),
isSettled: isSessionSettled(session),
supportsClientMessageIds: true,
queuedMessages: {
steering: [...queued.steering],
followUp: [...queued.followUp],
liveSteered: queued.liveSteered,
...(clientMessageIds ? { clientMessageIds } : {}),
},
todoPhases: session.getTodoPhases(),
fastModeEnabled: session.isFastModeEnabled(),
Expand Down
42 changes: 42 additions & 0 deletions runner/test/channel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { afterEach, describe, expect, it } from "bun:test";
import type { ExtensionUIContext } from "@oh-my-pi/pi-coding-agent/extensibility/extensions/types";
import type { AgentSession } from "@oh-my-pi/pi-coding-agent/session/agent-session";
import { SessionManager, type SessionPersistenceNotice } from "@oh-my-pi/pi-coding-agent/session/session-manager";
import { isRecord } from "@oh-my-pi/pi-utils";
import { HostChannel } from "../src/channel";
import { createTestSession, type Frame, startHost, type TestHost, type TestSession } from "./harness";

Expand Down Expand Up @@ -74,6 +75,47 @@ describe("ompweb channel", () => {
expect(JSON.stringify(end)).toContain("scripted reply");
});

it("identifies an image-only prompt using the capability negotiated by get_state", async () => {
host = await startHost();
test = await createTestSession({ responses: [{ content: ["received"] }] });
await host.connected;
const state = await host.command("get_state");
const supportsIds = isRecord(state.data) && state.data.supportsClientMessageIds === true;
expect(await host.command("prompt", {
message: "",
images: [{
type: "image",
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a5S8AAAAASUVORK5CYII=",
mimeType: "image/png",
}],
...(supportsIds ? { clientMessageId: "screenshot-intent" } : {}),
})).toMatchObject({ ok: true });
const echo = await host.waitFor(frame => eventType(frame) === "message_end"
&& isRecord(frame.event) && isRecord(frame.event.message) && frame.event.message.role === "user");
expect(echo.event).toMatchObject({ clientMessageId: "screenshot-intent" });
await host.waitFor(frame => eventType(frame) === "agent_end");
expect(await host.command("get_client_message_status", { clientMessageIds: ["screenshot-intent"] }))
.toMatchObject({ ok: true, data: { persistedClientMessageIds: ["screenshot-intent"], unresolvedClientMessageIds: [] } });
});

it("keeps correlated queue IDs in get_state for a reconnecting host", async () => {
host = await startHost();
test = await askSession();
await host.connected;
await host.command("prompt", { message: "start" });
const request = await host.waitFor(frame => frame.t === "ui_request");
await host.command("follow_up", { message: "later", clientMessageId: "queued-intent" });
// The mock model records circular call contexts; real model descriptors have no call log.
test.mock.calls.length = 0;
expect(await host.command("get_state")).toMatchObject({
ok: true,
data: { queuedMessages: { followUp: ["later"], clientMessageIds: { steering: [], followUp: ["queued-intent"] } } },
});
await host.command("remove_queued_message", { message: "later", queue: "followUp" });
host.send({ t: "ui_response", id: request.id, value: { data: { q: "continue" } } });
await test.session.waitForIdle();
});

it("lets the host answer the ask tool's formDialog", async () => {
const html = '<form><label>Name <input name="q"></label></form>';
host = await startHost();
Expand Down
Loading