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
10 changes: 2 additions & 8 deletions packages/harness-adapter/src/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,18 +113,12 @@ export function parseHostUsage(value: unknown): HostUsage {
if (hasContextWindow && value.contextWindowTokens === 0) {
throw new Error("Harness Usage 'contextWindowTokens' must be greater than zero");
}
if (
value.planFiveHourResetsAtUnix !== undefined &&
value.planFiveHourUsedPercent === undefined
) {
if (value.planFiveHourResetsAtUnix !== undefined && value.planFiveHourUsedPercent === undefined) {
throw new Error(
"Harness Usage 'planFiveHourResetsAtUnix' must be provided with 'planFiveHourUsedPercent'",
);
}
if (
value.planSevenDayResetsAtUnix !== undefined &&
value.planSevenDayUsedPercent === undefined
) {
if (value.planSevenDayResetsAtUnix !== undefined && value.planSevenDayUsedPercent === undefined) {
throw new Error(
"Harness Usage 'planSevenDayResetsAtUnix' must be provided with 'planSevenDayUsedPercent'",
);
Expand Down
4 changes: 1 addition & 3 deletions packages/host-runtime/test/app-server-host.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2208,9 +2208,7 @@ describe("AppServerHost HarnessAdapter projection", () => {
method: "codexhost/thread/usage/inspect",
params: { threadId: claudeThreadId },
});
await expect(
fixture.collector.waitFor((message) => requestId(message, 72)),
).resolves.toEqual({
await expect(fixture.collector.waitFor((message) => requestId(message, 72))).resolves.toEqual({
id: 72,
result: {
threadId: claudeThreadId,
Expand Down
60 changes: 50 additions & 10 deletions packages/renderer-extension/src/renderer-binding-probe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,43 @@ const externalAgents: readonly ExternalRendererAgent[] = [
"grok",
];
type HarnessAvailability = Partial<Record<ExternalRendererAgent, RendererAgentAvailability>>;
type HarnessAvailabilityErrors = Record<ExternalRendererAgent, CodexhostError | undefined>;

function isRetryableHarnessAvailability(
availability: RendererAgentAvailability | undefined,
error: CodexhostError | undefined,
): boolean {
return (
availability !== undefined &&
availability !== "ready" &&
availability !== "notInstalled" &&
error?.retryable === true
);
}

export function retryableHarnessAvailabilityAgents(
availability: HarnessAvailability,
errors: HarnessAvailabilityErrors,
): ExternalRendererAgent[] {
return externalAgents.filter((agent) =>
isRetryableHarnessAvailability(availability[agent], errors[agent]),
);
}

export function passiveHarnessAvailabilityAgents(
availability: HarnessAvailability,
errors: HarnessAvailabilityErrors,
): ExternalRendererAgent[] {
return externalAgents.filter(
(agent) =>
availability[agent] === "checking" ||
isRetryableHarnessAvailability(availability[agent], errors[agent]),
);
}

interface HostHarnessAvailabilityState {
availability: HarnessAvailability;
errors: Record<ExternalRendererAgent, CodexhostError | undefined>;
errors: HarnessAvailabilityErrors;
requestGeneration: number;
request: { client: RendererModelClient; promise: Promise<void> } | null;
retryTimer: number | null;
Expand Down Expand Up @@ -1581,6 +1614,7 @@ export function installRendererBindingProbe(
hostId: string,
refresh = false,
retry = false,
force = false,
): Promise<void> {
const state = hostHarnessAvailabilityState(hostId);
if (!retry) resetHarnessAvailabilityRetry(hostId);
Expand All @@ -1590,20 +1624,26 @@ export function installRendererBindingProbe(
return Promise.resolve();
}
if (state.request?.client === client) return state.request.promise;
state.availability = Object.fromEntries(
externalAgents.map((agent) => [
agent,
state.availability[agent] === "ready" ? "ready" : "checking",
]),
) as HarnessAvailability;
const agentsToInspect = force
? externalAgents
: passiveHarnessAvailabilityAgents(state.availability, state.errors);
if (agentsToInspect.length === 0) {
resetHarnessAvailabilityRetry(hostId);
return Promise.resolve();
}
const nextAvailability = { ...state.availability };
for (const agent of agentsToInspect) {
if (nextAvailability[agent] !== "ready") nextAvailability[agent] = "checking";
}
state.availability = nextAvailability;
if (hostId === activeAvailabilityHostId) {
publishConnectionStatus();
for (const mounted of mountedByComposer.values()) renderMounted(mounted);
}
const generation = ++state.requestGeneration;
const promise = (async () => {
await Promise.all(
externalAgents.map(async (agent) => {
agentsToInspect.map(async (agent) => {
let status: RendererAgentAvailability = "error";
let nextError: CodexhostError | undefined;
try {
Expand Down Expand Up @@ -1670,7 +1710,7 @@ export function installRendererBindingProbe(
}),
);
if (generation !== state.requestGeneration || disposed) return;
if (externalAgents.every((agent) => state.availability[agent] === "ready")) {
if (retryableHarnessAvailabilityAgents(state.availability, state.errors).length === 0) {
resetHarnessAvailabilityRetry(hostId);
} else {
scheduleHarnessAvailabilityRetry(hostId);
Expand Down Expand Up @@ -1761,7 +1801,7 @@ export function installRendererBindingProbe(
},
refresh(): Promise<void> {
for (const hostId of harnessAvailabilityByHost.keys()) {
void refreshHarnessAvailabilityForHost(hostId, true);
void refreshHarnessAvailabilityForHost(hostId, true, false, true);
}
return Promise.resolve();
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ export function installRendererSettingsLifecycle(
clearUpdateRetry();
retryUpdateClient = client;
updateRetryAttempt = 0;
} else if (updateRetryTimer !== null) {
return;
}
checkedUpdateClient = client;
const generation = ++updateCheckGeneration;
Expand Down
3 changes: 1 addition & 2 deletions packages/renderer-extension/src/renderer-usage-control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,7 @@ export function applyRendererPopoverChrome(popover: HTMLElement): void {
popover.style.borderRadius = "14px";
popover.style.backgroundColor = "color-mix(in srgb, Canvas 92%, CanvasText 8%)";
popover.style.color = "CanvasText";
popover.style.boxShadow =
"0 20px 45px rgba(0, 0, 0, 0.35), 0 2px 10px rgba(0, 0, 0, 0.22)";
popover.style.boxShadow = "0 20px 45px rgba(0, 0, 0, 0.35), 0 2px 10px rgba(0, 0, 0, 0.22)";
}

function addDetailRow(parent: HTMLElement, label: string, value: string): void {
Expand Down
128 changes: 128 additions & 0 deletions packages/renderer-extension/test/renderer-binding-probe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import {
isComposerModelWriteAllowed,
isOwnershipSubmissionBlocked,
lateConversationTargetResolution,
passiveHarnessAvailabilityAgents,
restoredThreadOwnership,
retryableHarnessAvailabilityAgents,
rendererUsageRefreshDelay,
shouldApplyDraftAgentCarrier,
shouldPersistNewThreadConfigurationSelection,
Expand Down Expand Up @@ -44,6 +46,132 @@ import {
} from "../src/renderer-usage-control.js";

describe("Renderer Composer DOM behavior", () => {
it("does not re-probe ready Agents when an optional Harness is not installed", () => {
expect(
retryableHarnessAvailabilityAgents(
{
pi: "ready",
"claude-code": "ready",
"deepseek-harness": "notInstalled",
grok: "ready",
},
{
pi: undefined,
"claude-code": undefined,
"deepseek-harness": {
code: "notInstalled",
message: "DeepSeek Harness is not installed",
retryable: false,
},
grok: undefined,
},
),
).toEqual([]);

expect(
retryableHarnessAvailabilityAgents(
{
pi: "ready",
"claude-code": "ready",
"deepseek-harness": "error",
grok: "ready",
},
{
pi: undefined,
"claude-code": undefined,
"deepseek-harness": {
code: "internalError",
message: "Remote request manager is temporarily unavailable",
retryable: true,
},
grok: undefined,
},
),
).toEqual(["deepseek-harness"]);

expect(
retryableHarnessAvailabilityAgents(
{
pi: "ready",
"claude-code": "ready",
"deepseek-harness": "unavailable",
grok: "ready",
},
{
pi: undefined,
"claude-code": undefined,
"deepseek-harness": {
code: "unavailable",
message: "DeepSeek Harness is temporarily unavailable",
retryable: true,
},
grok: undefined,
},
),
).toEqual(["deepseek-harness"]);
});

it("keeps terminal Harness availability stable across passive focus refreshes", () => {
expect(
passiveHarnessAvailabilityAgents(
{
pi: "checking",
"claude-code": "checking",
"deepseek-harness": "checking",
grok: "checking",
},
{
pi: undefined,
"claude-code": undefined,
"deepseek-harness": undefined,
grok: undefined,
},
),
).toEqual(["pi", "claude-code", "deepseek-harness", "grok"]);

expect(
passiveHarnessAvailabilityAgents(
{
pi: "ready",
"claude-code": "ready",
"deepseek-harness": "notInstalled",
grok: "ready",
},
{
pi: undefined,
"claude-code": undefined,
"deepseek-harness": {
code: "notInstalled",
message: "DeepSeek Harness is not installed",
retryable: false,
},
grok: undefined,
},
),
).toEqual([]);

expect(
passiveHarnessAvailabilityAgents(
{
pi: "ready",
"claude-code": "ready",
"deepseek-harness": "unavailable",
grok: "ready",
},
{
pi: undefined,
"claude-code": undefined,
"deepseek-harness": {
code: "unavailable",
message: "DeepSeek Harness is temporarily unavailable",
retryable: true,
},
grok: undefined,
},
),
).toEqual(["deepseek-harness"]);
});

it("keeps a ready external Model catalog stable during repeated availability checks", () => {
expect(shouldReloadExternalCatalogAfterAvailabilityRefresh("ready", "ready", true)).toBe(false);
expect(shouldReloadExternalCatalogAfterAvailabilityRefresh("ready", "ready", false)).toBe(true);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import type { UpdateCheckResult } from "@codexhost/shared-contracts";
import { afterEach, describe, expect, it, vi } from "vitest";

const triggerRefresh = vi.fn(() => true);
const triggerSetUpdateAvailable = vi.fn();

vi.mock("../src/codex-locale-adapter.js", () => ({
readCodexLocaleSettings: vi.fn(async () => ({ preferredLocale: "en" })),
}));

vi.mock("../src/settings/localization.js", () => ({
rendererSettingsMessages: vi.fn(() => ({})),
resolveRendererSettingsLocale: vi.fn(() => "en"),
}));

vi.mock("../src/settings/pages.js", () => ({
createDefaultRendererSettingsPages: vi.fn(() => []),
}));

vi.mock("../src/settings/shell.js", () => ({
installRendererSettingsShell: vi.fn(() => ({
supported: true,
open: false,
activePageId: undefined,
openSettings: vi.fn(),
dispose: vi.fn(),
})),
}));

vi.mock("../src/settings/trigger.js", () => ({
installRendererSettingsHeaderTrigger: vi.fn(() => ({
root: null,
refresh: triggerRefresh,
setUpdateAvailable: triggerSetUpdateAvailable,
dispose: vi.fn(),
})),
}));

import { installRendererSettingsLifecycle } from "../src/renderer-settings-lifecycle.js";

function failedUpdateCheck(): UpdateCheckResult {
return {
currentVersion: "0.3.2",
installation: "npm",
latestVersion: null,
updateAvailable: false,
installationAvailable: false,
releaseNotes: null,
releaseNotesUrl: null,
status: null,
error: "Update metadata is temporarily unavailable",
};
}

describe("Renderer Settings lifecycle", () => {
afterEach(() => {
vi.useRealTimers();
vi.clearAllMocks();
});

it("does not bypass update backoff when DOM reconciliation refreshes repeatedly", async () => {
vi.useFakeTimers();
const checkUpdate = vi.fn(async () => failedUpdateCheck());
const client = {
checkUpdate,
startUpdate: vi.fn(),
readUpdateStatus: vi.fn(),
};
const ownerWindow = {
navigator: { languages: ["en"] },
document: {},
setTimeout,
clearTimeout,
} as unknown as Window;

const lifecycle = installRendererSettingsLifecycle(ownerWindow, {
getUpdateClient: () => client,
});
await Promise.resolve();
await Promise.resolve();
expect(checkUpdate).toHaveBeenCalledTimes(1);

for (let index = 0; index < 50; index += 1) lifecycle.refresh();
await Promise.resolve();
await Promise.resolve();
expect(checkUpdate).toHaveBeenCalledTimes(1);

await vi.advanceTimersByTimeAsync(999);
expect(checkUpdate).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(1);
expect(checkUpdate).toHaveBeenCalledTimes(2);

lifecycle.dispose();
});
});
Loading
Loading