Compare commits
8
Commits
v1.3.3
..
20f6087340
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20f6087340 | ||
|
|
c401290080 | ||
|
|
9bd92ba53c | ||
|
|
f6cae21bfa | ||
|
|
1bc5c329a8 | ||
|
|
c973738b60 | ||
|
|
c24e38bbf2 | ||
|
|
15650d9bb7 |
@@ -12,6 +12,12 @@ Run the installer without sudo:
|
|||||||
curl -fsSL https://git.krtss.de/StanPonomarev/stanp-opencode/raw/branch/main/install.sh | bash
|
curl -fsSL https://git.krtss.de/StanPonomarev/stanp-opencode/raw/branch/main/install.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Pass `--playwright` to let the plugin automatically configure a Playwright MCP server:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -fsSL https://git.krtss.de/StanPonomarev/stanp-opencode/raw/branch/main/install.sh | bash -s -- --playwright
|
||||||
|
```
|
||||||
|
|
||||||
Rerun the command to upgrade to the latest published release.
|
Rerun the command to upgrade to the latest published release.
|
||||||
|
|
||||||
## Install from a local checkout
|
## Install from a local checkout
|
||||||
@@ -22,6 +28,8 @@ This requires Bash, jq, and Bun 1.3.14+:
|
|||||||
bash /path/to/stanp-opencode/install-local.sh
|
bash /path/to/stanp-opencode/install-local.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Add `--playwright` to this command to enable automatic Playwright MCP configuration.
|
||||||
|
|
||||||
The script installs dependencies and builds the checkout before installing it.
|
The script installs dependencies and builds the checkout before installing it.
|
||||||
|
|
||||||
After either installation, restart `opencode2` or run `opencode2 service restart` for an existing service. Connect OpenAI and select `coordinate`. The required models must be available for the workers to run.
|
After either installation, restart `opencode2` or run `opencode2 service restart` for an existing service. Connect OpenAI and select `coordinate`. The required models must be available for the workers to run.
|
||||||
@@ -36,7 +44,7 @@ For an installed plugin, use `./plugins/stanp-opencode` as the plugin package pa
|
|||||||
|
|
||||||
## Playwright
|
## Playwright
|
||||||
|
|
||||||
The plugin prefers a local Playwright MCP installation. If ordinary Playwright is installed, it can download and run the MCP package with `npx`; otherwise Docker is required for the automatic fallback. Automatically configured servers allow five minutes for startup because the first launch may need to download a package or container image. Docker reuses its cached image on later launches. Existing `playwright` configuration, including its timeout, is preserved.
|
When installed with `--playwright`, the plugin prefers a local Playwright MCP installation. If ordinary Playwright is installed, it can download and run the MCP package with `npx`; otherwise Docker is required for the automatic fallback. Automatically configured servers allow five minutes for startup because the first launch may need to download a package or container image. Docker reuses its cached image on later launches. Existing `playwright` configuration, including its timeout, is preserved.
|
||||||
|
|
||||||
If `opencode mcp list` reports `MCP error -32001: Request timed out`, prefetch the Docker fallback with `docker pull mcr.microsoft.com/playwright/mcp`, or install `@playwright/mcp` in the project so the plugin can launch it directly. Then reconnect the server from `/mcps` or restart OpenCode. A custom `playwright` entry under `mcp.servers` can also set its own `timeout.startup` in milliseconds.
|
If `opencode mcp list` reports `MCP error -32001: Request timed out`, prefetch the Docker fallback with `docker pull mcr.microsoft.com/playwright/mcp`, or install `@playwright/mcp` in the project so the plugin can launch it directly. Then reconnect the server from `/mcps` or restart OpenCode. A custom `playwright` entry under `mcp.servers` can also set its own `timeout.startup` in milliseconds.
|
||||||
|
|
||||||
|
|||||||
+16
-3
@@ -1,6 +1,15 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
playwright=false
|
||||||
|
if (($#)); then
|
||||||
|
if [[ "$#" -ne 1 || "$1" != "--playwright" ]]; then
|
||||||
|
printf 'Usage: %s [--playwright]\n' "$0" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
playwright=true
|
||||||
|
fi
|
||||||
|
|
||||||
for command in bun jq; do
|
for command in bun jq; do
|
||||||
command -v "$command" >/dev/null || { printf 'Required command not found: %s\n' "$command" >&2; exit 1; }
|
command -v "$command" >/dev/null || { printf 'Required command not found: %s\n' "$command" >&2; exit 1; }
|
||||||
done
|
done
|
||||||
@@ -43,15 +52,19 @@ if [[ ! -e "$config_source" ]]; then
|
|||||||
config_source="$install/empty-config.json"
|
config_source="$install/empty-config.json"
|
||||||
printf '{}\n' > "$config_source"
|
printf '{}\n' > "$config_source"
|
||||||
fi
|
fi
|
||||||
jq -e '
|
jq -e --argjson playwright "$playwright" '
|
||||||
if type != "object" then error("opencode.json must contain an object") else . end
|
if type != "object" then error("opencode.json must contain an object") else . end
|
||||||
| if (.plugins // []) | type != "array" then error("opencode.json plugins must be an array") else . end
|
| if (.plugins // []) | type != "array" then error("opencode.json plugins must be an array") else . end
|
||||||
| .["$schema"] = (.["$schema"] // "https://opencode.ai/config.json")
|
| .["$schema"] = (.["$schema"] // "https://opencode.ai/config.json")
|
||||||
| .plugins = (.plugins // [])
|
| .plugins = (.plugins // [])
|
||||||
| if any(.plugins[]?; .package == "./plugins/stanp-opencode") then . else
|
| if any(.plugins[]?; .package == "./plugins/stanp-opencode") then
|
||||||
|
if $playwright then
|
||||||
|
.plugins |= map(if .package == "./plugins/stanp-opencode" then .options = ((.options // {}) + { playwright: true }) else . end)
|
||||||
|
else . end
|
||||||
|
else
|
||||||
.plugins += [{
|
.plugins += [{
|
||||||
package: "./plugins/stanp-opencode",
|
package: "./plugins/stanp-opencode",
|
||||||
options: { mode: "gpt", provider: "openai", lunaPriority: true }
|
options: ({ mode: "gpt", provider: "openai", lunaPriority: true } + if $playwright then { playwright: true } else {} end)
|
||||||
}]
|
}]
|
||||||
end
|
end
|
||||||
' "$config_source" > "$install/opencode.json"
|
' "$config_source" > "$install/opencode.json"
|
||||||
|
|||||||
+16
-3
@@ -1,6 +1,15 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
playwright=false
|
||||||
|
if (($#)); then
|
||||||
|
if [[ "$#" -ne 1 || "$1" != "--playwright" ]]; then
|
||||||
|
printf 'Usage: %s [--playwright]\n' "$0" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
playwright=true
|
||||||
|
fi
|
||||||
|
|
||||||
for command in bun curl jq sha256sum tar; do
|
for command in bun curl jq sha256sum tar; do
|
||||||
command -v "$command" >/dev/null || { printf 'Required command not found: %s\n' "$command" >&2; exit 1; }
|
command -v "$command" >/dev/null || { printf 'Required command not found: %s\n' "$command" >&2; exit 1; }
|
||||||
done
|
done
|
||||||
@@ -57,15 +66,19 @@ if [[ ! -e "$config_source" ]]; then
|
|||||||
config_source="$install/empty-config.json"
|
config_source="$install/empty-config.json"
|
||||||
printf '{}\n' > "$config_source"
|
printf '{}\n' > "$config_source"
|
||||||
fi
|
fi
|
||||||
jq -e '
|
jq -e --argjson playwright "$playwright" '
|
||||||
if type != "object" then error("opencode.json must contain an object") else . end
|
if type != "object" then error("opencode.json must contain an object") else . end
|
||||||
| if (.plugins // []) | type != "array" then error("opencode.json plugins must be an array") else . end
|
| if (.plugins // []) | type != "array" then error("opencode.json plugins must be an array") else . end
|
||||||
| .["$schema"] = (.["$schema"] // "https://opencode.ai/config.json")
|
| .["$schema"] = (.["$schema"] // "https://opencode.ai/config.json")
|
||||||
| .plugins = (.plugins // [])
|
| .plugins = (.plugins // [])
|
||||||
| if any(.plugins[]?; .package == "./plugins/stanp-opencode") then . else
|
| if any(.plugins[]?; .package == "./plugins/stanp-opencode") then
|
||||||
|
if $playwright then
|
||||||
|
.plugins |= map(if .package == "./plugins/stanp-opencode" then .options = ((.options // {}) + { playwright: true }) else . end)
|
||||||
|
else . end
|
||||||
|
else
|
||||||
.plugins += [{
|
.plugins += [{
|
||||||
package: "./plugins/stanp-opencode",
|
package: "./plugins/stanp-opencode",
|
||||||
options: { mode: "gpt", provider: "openai", lunaPriority: true }
|
options: ({ mode: "gpt", provider: "openai", lunaPriority: true } + if $playwright then { playwright: true } else {} end)
|
||||||
}]
|
}]
|
||||||
end
|
end
|
||||||
' "$config_source" > "$install/opencode.json"
|
' "$config_source" > "$install/opencode.json"
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const assignment = z.strictObject({
|
|||||||
const common = {
|
const common = {
|
||||||
provider: z.string().trim().min(1).default("openai"),
|
provider: z.string().trim().min(1).default("openai"),
|
||||||
lunaPriority: z.boolean().default(true),
|
lunaPriority: z.boolean().default(true),
|
||||||
|
playwright: z.boolean().default(false),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const settings = z.discriminatedUnion("mode", [
|
export const settings = z.discriminatedUnion("mode", [
|
||||||
|
|||||||
+14
-6
@@ -114,10 +114,12 @@ export default Plugin.define({
|
|||||||
const assignments = selected.mode === "gpt" ? gptAgents : selected.agents;
|
const assignments = selected.mode === "gpt" ? gptAgents : selected.agents;
|
||||||
let workers: (typeof workerNames)[number][] = [];
|
let workers: (typeof workerNames)[number][] = [];
|
||||||
const active = new Set<string>();
|
const active = new Set<string>();
|
||||||
const playwrightCommand = await playwrightMcpCommand(context.location.directory);
|
const playwrightCommand = selected.playwright
|
||||||
|
? await playwrightMcpCommand(context.location.directory)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
await context.mcp.transform((editor) => {
|
await context.mcp.transform((editor) => {
|
||||||
if (!editor.get("playwright"))
|
if (playwrightCommand && !editor.get("playwright"))
|
||||||
editor.set("playwright", {
|
editor.set("playwright", {
|
||||||
type: "local",
|
type: "local",
|
||||||
command: playwrightCommand,
|
command: playwrightCommand,
|
||||||
@@ -166,6 +168,9 @@ export default Plugin.define({
|
|||||||
const assignment = assignments[name];
|
const assignment = assignments[name];
|
||||||
const available = models.get(assignment.model)!;
|
const available = models.get(assignment.model)!;
|
||||||
editor.update(name, (agent) => {
|
editor.update(name, (agent) => {
|
||||||
|
const hasVariant = available.model.variants.some(
|
||||||
|
({ id }) => id === assignment.effort,
|
||||||
|
);
|
||||||
agent.name = Agent.Name.make(name);
|
agent.name = Agent.Name.make(name);
|
||||||
agent.mode = "subagent";
|
agent.mode = "subagent";
|
||||||
agent.hidden = false;
|
agent.hidden = false;
|
||||||
@@ -174,10 +179,9 @@ export default Plugin.define({
|
|||||||
providerID: Provider.ID.make(available.model.providerID),
|
providerID: Provider.ID.make(available.model.providerID),
|
||||||
id: Model.ID.make(available.model.id),
|
id: Model.ID.make(available.model.id),
|
||||||
};
|
};
|
||||||
|
if (hasVariant) agent.model.variant = Model.VariantID.make(assignment.effort);
|
||||||
agent.request = {
|
agent.request = {
|
||||||
settings: {
|
settings: hasVariant ? {} : { reasoningEffort: assignment.effort },
|
||||||
reasoningEffort: assignment.effort,
|
|
||||||
},
|
|
||||||
headers: {},
|
headers: {},
|
||||||
body: {},
|
body: {},
|
||||||
};
|
};
|
||||||
@@ -191,6 +195,9 @@ export default Plugin.define({
|
|||||||
}
|
}
|
||||||
editor.update("coordinate", (agent) => {
|
editor.update("coordinate", (agent) => {
|
||||||
const available = models.get(coordinate.model)!;
|
const available = models.get(coordinate.model)!;
|
||||||
|
const hasVariant = available.model.variants.some(
|
||||||
|
({ id }) => id === coordinate.effort,
|
||||||
|
);
|
||||||
agent.name = Agent.Name.make("coordinate");
|
agent.name = Agent.Name.make("coordinate");
|
||||||
agent.mode = "primary";
|
agent.mode = "primary";
|
||||||
agent.hidden = false;
|
agent.hidden = false;
|
||||||
@@ -200,8 +207,9 @@ export default Plugin.define({
|
|||||||
providerID: Provider.ID.make(available.model.providerID),
|
providerID: Provider.ID.make(available.model.providerID),
|
||||||
id: Model.ID.make(available.model.id),
|
id: Model.ID.make(available.model.id),
|
||||||
};
|
};
|
||||||
|
if (hasVariant) agent.model.variant = Model.VariantID.make(coordinate.effort);
|
||||||
agent.request = {
|
agent.request = {
|
||||||
settings: { reasoningEffort: coordinate.effort },
|
settings: hasVariant ? {} : { reasoningEffort: coordinate.effort },
|
||||||
headers: {},
|
headers: {},
|
||||||
body: {},
|
body: {},
|
||||||
};
|
};
|
||||||
|
|||||||
+15
-13
@@ -3,42 +3,44 @@ import { createResource, createSignal, For, Show } from "solid-js";
|
|||||||
import type { QuotaStatus, QuotaWindow } from "./quota.js";
|
import type { QuotaStatus, QuotaWindow } from "./quota.js";
|
||||||
import { QuotaRpc } from "./quota-rpc.js";
|
import { QuotaRpc } from "./quota-rpc.js";
|
||||||
|
|
||||||
|
function percent(value: number) {
|
||||||
|
return value < 0.1 && value > 0 ? "<0.1" : value.toFixed(value < 10 ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
function duration(seconds: number) {
|
function duration(seconds: number) {
|
||||||
if (seconds % 86_400 === 0) return `${seconds / 86_400}d`;
|
if (seconds % 86_400 === 0) return `${seconds / 86_400}d`;
|
||||||
if (seconds % 3_600 === 0) return `${seconds / 3_600}h`;
|
if (seconds % 3_600 === 0) return `${seconds / 3_600}h`;
|
||||||
return `${Math.round(seconds / 60)}m`;
|
return `${Math.round(seconds / 60)}m`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function percent(value: number) {
|
|
||||||
return value < 0.1 && value > 0 ? "<0.1" : value.toFixed(value < 10 ? 1 : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetTiming(resetsAt: number, now: number) {
|
function resetTiming(resetsAt: number, now: number) {
|
||||||
if (!Number.isFinite(resetsAt) || resetsAt <= 0 || !Number.isFinite(now))
|
if (!Number.isFinite(resetsAt) || resetsAt <= 0 || !Number.isFinite(now)) return "unavailable";
|
||||||
return "reset unavailable";
|
|
||||||
const remaining = resetsAt - now;
|
const remaining = resetsAt - now;
|
||||||
if (remaining <= 0) return "reset due";
|
if (remaining <= 0) return "due";
|
||||||
if (remaining < 60_000) return "resets in <1m";
|
if (remaining < 60_000) return "<1m";
|
||||||
|
|
||||||
const minutes = Math.floor(remaining / 60_000);
|
const minutes = Math.floor(remaining / 60_000);
|
||||||
const days = Math.floor(minutes / 1_440);
|
const days = Math.floor(minutes / 1_440);
|
||||||
if (days > 0) {
|
if (days > 0) {
|
||||||
const hours = Math.floor((minutes % 1_440) / 60);
|
const hours = Math.floor((minutes % 1_440) / 60);
|
||||||
return `resets in ${days}d${hours > 0 ? ` ${hours}h` : ""}`;
|
return `${days}d${hours > 0 ? ` ${hours}h` : ""}`;
|
||||||
}
|
}
|
||||||
const hours = Math.floor(minutes / 60);
|
const hours = Math.floor(minutes / 60);
|
||||||
if (hours > 0) {
|
if (hours > 0) {
|
||||||
const rest = minutes % 60;
|
const rest = minutes % 60;
|
||||||
return `resets in ${hours}h${rest > 0 ? ` ${rest}m` : ""}`;
|
return `${hours}h${rest > 0 ? ` ${rest}m` : ""}`;
|
||||||
}
|
}
|
||||||
return `resets in ${minutes}m`;
|
return `${minutes}m`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatQuotaWindow(window: QuotaWindow & { remaining: number }, now = Date.now()) {
|
export function formatQuotaWindow(
|
||||||
|
window: QuotaWindow & { remaining: number; sessionConsumed: number },
|
||||||
|
now = Date.now(),
|
||||||
|
) {
|
||||||
const label = window.label
|
const label = window.label
|
||||||
? `${window.label} ${duration(window.seconds)}`
|
? `${window.label} ${duration(window.seconds)}`
|
||||||
: duration(window.seconds);
|
: duration(window.seconds);
|
||||||
return `${label}: ${percent(window.remaining)}% left · ${resetTiming(window.resetsAt, now)}`;
|
return `${label}: ${percent(window.remaining)}% · ~${percent(window.sessionConsumed)}pp · ${resetTiming(window.resetsAt, now)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadQuotaStatus(load: () => Promise<QuotaStatus | null>) {
|
export async function loadQuotaStatus(load: () => Promise<QuotaStatus | null>) {
|
||||||
|
|||||||
@@ -199,6 +199,18 @@ test("reinstall replaces bundles while preserving existing settings and plugin o
|
|||||||
expect(await readFile(join(setup.installed, "index.js"), "utf8")).toContain('return "second"');
|
expect(await readFile(join(setup.installed, "index.js"), "utf8")).toContain('return "second"');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("enables Playwright only when requested", async () => {
|
||||||
|
const setup = await fixture();
|
||||||
|
const result = await run(
|
||||||
|
["bash", join(setup.checkout, "install-local.sh"), "--playwright"],
|
||||||
|
setup.cwd,
|
||||||
|
setup.env,
|
||||||
|
);
|
||||||
|
expect(result, result.stderr).toMatchObject({ exitCode: 0 });
|
||||||
|
const installed = JSON.parse(await readFile(join(setup.config, "opencode.json"), "utf8"));
|
||||||
|
expect(installed.plugins.at(-1).options.playwright).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
for (const mode of ["dependency", "build", "validation", "config"] as const) {
|
for (const mode of ["dependency", "build", "validation", "config"] as const) {
|
||||||
test(`${mode} failure leaves the previous installation and configuration untouched`, async () => {
|
test(`${mode} failure leaves the previous installation and configuration untouched`, async () => {
|
||||||
const invalidConfig = mode === "config" ? { plugins: "not-an-array" } : undefined;
|
const invalidConfig = mode === "config" ? { plugins: "not-an-array" } : undefined;
|
||||||
|
|||||||
+17
-2
@@ -44,7 +44,11 @@ async function run(command: string[], cwd: string, env: NodeJS.ProcessEnv) {
|
|||||||
return { exitCode, stdout, stderr };
|
return { exitCode, stdout, stderr };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function install(mode: "native" | "checksum" | "legacy", configured = false) {
|
async function install(
|
||||||
|
mode: "native" | "checksum" | "legacy",
|
||||||
|
configured = false,
|
||||||
|
playwright = false,
|
||||||
|
) {
|
||||||
const directory = await mkdtemp(join(tmpdir(), "stanp-install-"));
|
const directory = await mkdtemp(join(tmpdir(), "stanp-install-"));
|
||||||
directories.push(directory);
|
directories.push(directory);
|
||||||
const config =
|
const config =
|
||||||
@@ -187,7 +191,11 @@ exec sha256sum "$@"
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
await Promise.all([chmod(join(bin, "curl"), 0o755), chmod(join(bin, "sha256sum"), 0o755)]);
|
await Promise.all([chmod(join(bin, "curl"), 0o755), chmod(join(bin, "sha256sum"), 0o755)]);
|
||||||
const result = await run(["bash", installer], directory, env);
|
const result = await run(
|
||||||
|
["bash", installer, ...(playwright ? ["--playwright"] : [])],
|
||||||
|
directory,
|
||||||
|
env,
|
||||||
|
);
|
||||||
return { directory, config, env, result };
|
return { directory, config, env, result };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,6 +257,13 @@ test("preserves an existing stanp-opencode configuration", async () => {
|
|||||||
expect(installed.permissions).toEqual(jsonConfig.permissions);
|
expect(installed.permissions).toEqual(jsonConfig.permissions);
|
||||||
}, 60_000);
|
}, 60_000);
|
||||||
|
|
||||||
|
test("enables Playwright only when requested", async () => {
|
||||||
|
const { config, result } = await install("native", false, true);
|
||||||
|
expect(result, result.stderr).toMatchObject({ exitCode: 0 });
|
||||||
|
const installed = JSON.parse(await readFile(join(config, "opencode.json"), "utf8"));
|
||||||
|
expect(installed.plugins.at(-1).options.playwright).toBe(true);
|
||||||
|
}, 60_000);
|
||||||
|
|
||||||
for (const mode of ["checksum", "legacy"] as const) {
|
for (const mode of ["checksum", "legacy"] as const) {
|
||||||
test(`${mode} failure preserves the installed plugin in a custom path containing spaces`, async () => {
|
test(`${mode} failure preserves the installed plugin in a custom path containing spaces`, async () => {
|
||||||
const { directory, config, env, result } = await install(mode);
|
const { directory, config, env, result } = await install(mode);
|
||||||
|
|||||||
+60
-5
@@ -17,7 +17,16 @@ const providerID = Provider.ID.make("test");
|
|||||||
function model(id: string): ModelInfo {
|
function model(id: string): ModelInfo {
|
||||||
return Model.Info.default(providerID, Model.ID.make(id));
|
return Model.Info.default(providerID, Model.ID.make(id));
|
||||||
}
|
}
|
||||||
const allModels = ["gpt-5.6-luna", "gpt-5.6-sol", "gpt-6-astra"].map(model);
|
function reasoningModel(id: string): ModelInfo {
|
||||||
|
return {
|
||||||
|
...model(id),
|
||||||
|
variants: ["low", "medium", "high"].map((effort) => ({
|
||||||
|
id: Model.VariantID.make(effort),
|
||||||
|
settings: { reasoningEffort: effort },
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const allModels = ["gpt-5.6-luna", "gpt-5.6-sol", "gpt-6-astra"].map(reasoningModel);
|
||||||
const cleanups: Plugin.Cleanup[] = [];
|
const cleanups: Plugin.Cleanup[] = [];
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
for (const cleanup of cleanups.splice(0)) await cleanup();
|
for (const cleanup of cleanups.splice(0)) await cleanup();
|
||||||
@@ -442,7 +451,13 @@ test("playwright MCP prefers direct MCP, uses npx for Playwright, and preserves
|
|||||||
await writeFile(executable, "#!/usr/bin/env node\n");
|
await writeFile(executable, "#!/usr/bin/env node\n");
|
||||||
await chmod(executable, 0o755);
|
await chmod(executable, 0o755);
|
||||||
|
|
||||||
const local = await host(allModels, { provider: "test" }, {}, [], localProject);
|
const local = await host(
|
||||||
|
allModels,
|
||||||
|
{ provider: "test", playwright: true },
|
||||||
|
{},
|
||||||
|
[],
|
||||||
|
localProject,
|
||||||
|
);
|
||||||
expect(local.mcpServers.get("playwright")).toEqual({
|
expect(local.mcpServers.get("playwright")).toEqual({
|
||||||
type: "local",
|
type: "local",
|
||||||
command: [executable],
|
command: [executable],
|
||||||
@@ -475,7 +490,13 @@ test("playwright MCP prefers direct MCP, uses npx for Playwright, and preserves
|
|||||||
);
|
);
|
||||||
await writeFile(ordinaryExecutable, "#!/usr/bin/env node\n");
|
await writeFile(ordinaryExecutable, "#!/usr/bin/env node\n");
|
||||||
await chmod(ordinaryExecutable, 0o755);
|
await chmod(ordinaryExecutable, 0o755);
|
||||||
const ordinary = await host(allModels, { provider: "test" }, {}, [], ordinaryProject);
|
const ordinary = await host(
|
||||||
|
allModels,
|
||||||
|
{ provider: "test", playwright: true },
|
||||||
|
{},
|
||||||
|
[],
|
||||||
|
ordinaryProject,
|
||||||
|
);
|
||||||
expect(ordinary.mcpServers.get("playwright")?.command).toEqual([
|
expect(ordinary.mcpServers.get("playwright")?.command).toEqual([
|
||||||
"npx",
|
"npx",
|
||||||
"-y",
|
"-y",
|
||||||
@@ -534,7 +555,7 @@ test("playwright MCP prefers direct MCP, uses npx for Playwright, and preserves
|
|||||||
await chmod(fakePlaywright, 0o755);
|
await chmod(fakePlaywright, 0o755);
|
||||||
expect((await playwrightMcpCommand(globalProject, ordinaryBin))[0]).toBe("docker");
|
expect((await playwrightMcpCommand(globalProject, ordinaryBin))[0]).toBe("docker");
|
||||||
|
|
||||||
const fallback = await host();
|
const fallback = await host(allModels, { provider: "test", playwright: true });
|
||||||
expect(fallback.mcpServers.get("playwright")).toEqual({
|
expect(fallback.mcpServers.get("playwright")).toEqual({
|
||||||
type: "local",
|
type: "local",
|
||||||
command: ["docker", "run", "-i", "--rm", "--init", "mcr.microsoft.com/playwright/mcp"],
|
command: ["docker", "run", "-i", "--rm", "--init", "mcr.microsoft.com/playwright/mcp"],
|
||||||
@@ -561,6 +582,9 @@ test("playwright MCP prefers direct MCP, uses npx for Playwright, and preserves
|
|||||||
timeout: { startup: 12_345 },
|
timeout: { startup: 12_345 },
|
||||||
});
|
});
|
||||||
expect(existing.mcpServers.get("docs")?.codemode).toBe(true);
|
expect(existing.mcpServers.get("docs")?.codemode).toBe(true);
|
||||||
|
|
||||||
|
const disabled = await host();
|
||||||
|
expect(disabled.mcpServers.has("playwright")).toBe(false);
|
||||||
} finally {
|
} finally {
|
||||||
if (originalPath === undefined) delete process.env.PATH;
|
if (originalPath === undefined) delete process.env.PATH;
|
||||||
else process.env.PATH = originalPath;
|
else process.env.PATH = originalPath;
|
||||||
@@ -594,7 +618,8 @@ test("reasoning settings and Luna priority use native advertised or official sup
|
|||||||
settings: { baseURL: "https://api.openai.com/v1" },
|
settings: { baseURL: "https://api.openai.com/v1" },
|
||||||
};
|
};
|
||||||
const ordinary = await host();
|
const ordinary = await host();
|
||||||
expect(ordinary.agents.get("fast-lo")?.request.settings).toEqual({ reasoningEffort: "low" });
|
expect(ordinary.agents.get("fast-lo")?.request.settings).toEqual({});
|
||||||
|
expect(String(ordinary.agents.get("fast-lo")?.model?.variant)).toBe("low");
|
||||||
const official = await host([luna, ...allModels.slice(1)]);
|
const official = await host([luna, ...allModels.slice(1)]);
|
||||||
expect(official.agents.get("fast-hi")?.request.settings).toEqual({
|
expect(official.agents.get("fast-hi")?.request.settings).toEqual({
|
||||||
reasoningEffort: "high",
|
reasoningEffort: "high",
|
||||||
@@ -617,13 +642,43 @@ test("reasoning settings and Luna priority use native advertised or official sup
|
|||||||
expect(disabled.agents.get("fast-lo")?.request.settings).toEqual({ reasoningEffort: "low" });
|
expect(disabled.agents.get("fast-lo")?.request.settings).toEqual({ reasoningEffort: "low" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("coordinator defaults expose their effort variant without relying on it for requests", async () => {
|
||||||
|
for (const options of [{ provider: "test" }, { mode: "gpt", provider: "test" }] as const) {
|
||||||
|
const result = await host(allModels, options);
|
||||||
|
expect(result.agents.get("coordinate")?.model).toMatchObject({
|
||||||
|
id: "gpt-6-astra",
|
||||||
|
variant: "medium",
|
||||||
|
});
|
||||||
|
expect(result.agents.get("coordinate")?.request.settings.reasoningEffort).toBeUndefined();
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallback = await host(allModels.slice(0, 2));
|
||||||
|
expect(fallback.agents.get("coordinate")?.model).toMatchObject({
|
||||||
|
id: "gpt-5.6-sol",
|
||||||
|
variant: "medium",
|
||||||
|
});
|
||||||
|
expect(fallback.agents.get("coordinate")?.request.settings.reasoningEffort).toBeUndefined();
|
||||||
|
|
||||||
|
const custom = await host([...allModels, model("local")], {
|
||||||
|
mode: "custom",
|
||||||
|
provider: "test",
|
||||||
|
agents: { ...gptAgents, coordinate: { model: "local", effort: "xhigh" } },
|
||||||
|
});
|
||||||
|
expect(custom.agents.get("coordinate")?.model?.variant).toBeUndefined();
|
||||||
|
expect(custom.agents.get("coordinate")?.request.settings.reasoningEffort).toBe("xhigh");
|
||||||
|
});
|
||||||
|
|
||||||
test("catalog updates activate late models and remove unavailable agents", async () => {
|
test("catalog updates activate late models and remove unavailable agents", async () => {
|
||||||
const result = await host([]);
|
const result = await host([]);
|
||||||
await result.refresh(allModels);
|
await result.refresh(allModels);
|
||||||
expect(String(result.agents.get("coordinate")?.model?.id)).toBe("gpt-6-astra");
|
expect(String(result.agents.get("coordinate")?.model?.id)).toBe("gpt-6-astra");
|
||||||
|
expect(String(result.agents.get("coordinate")?.model?.variant)).toBe("medium");
|
||||||
|
expect(result.agents.get("coordinate")?.request.settings.reasoningEffort).toBeUndefined();
|
||||||
await result.refresh(allModels.slice(0, 2));
|
await result.refresh(allModels.slice(0, 2));
|
||||||
expect(result.agents.has("deep-lo")).toBe(false);
|
expect(result.agents.has("deep-lo")).toBe(false);
|
||||||
expect(String(result.agents.get("coordinate")?.model?.id)).toBe("gpt-5.6-sol");
|
expect(String(result.agents.get("coordinate")?.model?.id)).toBe("gpt-5.6-sol");
|
||||||
|
expect(String(result.agents.get("coordinate")?.model?.variant)).toBe("medium");
|
||||||
|
expect(result.agents.get("coordinate")?.request.settings.reasoningEffort).toBeUndefined();
|
||||||
await result.refresh([]);
|
await result.refresh([]);
|
||||||
expect(result.agents.has("coordinate")).toBe(false);
|
expect(result.agents.has("coordinate")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|||||||
+12
-9
@@ -424,11 +424,12 @@ test("sidebar text places reset timing beside latest remaining quota", () => {
|
|||||||
label: null,
|
label: null,
|
||||||
used: 25,
|
used: 25,
|
||||||
remaining: 75,
|
remaining: 75,
|
||||||
|
sessionConsumed: 1.5,
|
||||||
resetsAt: 100_000 + 2 * 86_400_000 + 5 * 3_600_000,
|
resetsAt: 100_000 + 2 * 86_400_000 + 5 * 3_600_000,
|
||||||
},
|
},
|
||||||
100_000,
|
100_000,
|
||||||
),
|
),
|
||||||
).toBe("7d: 75% left · resets in 2d 5h");
|
).toBe("7d: 75% · ~1.5pp · 2d 5h");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("sidebar reset timing uses compact future units and handles boundaries", () => {
|
test("sidebar reset timing uses compact future units and handles boundaries", () => {
|
||||||
@@ -441,31 +442,33 @@ test("sidebar reset timing uses compact future units and handles boundaries", ()
|
|||||||
label: null,
|
label: null,
|
||||||
used: 25,
|
used: 25,
|
||||||
remaining: 75,
|
remaining: 75,
|
||||||
|
sessionConsumed: 1.5,
|
||||||
resetsAt: now + resetIn,
|
resetsAt: now + resetIn,
|
||||||
},
|
},
|
||||||
now,
|
now,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(format(2 * 3_600_000 + 15 * 60_000)).toBe("5h: 75% left · resets in 2h 15m");
|
expect(format(2 * 3_600_000 + 15 * 60_000)).toBe("5h: 75% · ~1.5pp · 2h 15m");
|
||||||
expect(format(3_600_000)).toBe("5h: 75% left · resets in 1h");
|
expect(format(3_600_000)).toBe("5h: 75% · ~1.5pp · 1h");
|
||||||
expect(format(15 * 60_000)).toBe("5h: 75% left · resets in 15m");
|
expect(format(15 * 60_000)).toBe("5h: 75% · ~1.5pp · 15m");
|
||||||
expect(format(60_000)).toBe("5h: 75% left · resets in 1m");
|
expect(format(60_000)).toBe("5h: 75% · ~1.5pp · 1m");
|
||||||
expect(format(59_999)).toBe("5h: 75% left · resets in <1m");
|
expect(format(59_999)).toBe("5h: 75% · ~1.5pp · <1m");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("sidebar reset timing distinguishes stale and unavailable timestamps", () => {
|
test("sidebar reset timing preserves labels and distinguishes stale and unavailable timestamps", () => {
|
||||||
const window = {
|
const window = {
|
||||||
seconds: 18_000,
|
seconds: 18_000,
|
||||||
id: "codex:primary",
|
id: "codex:primary",
|
||||||
label: "GPT-5.6 Luna",
|
label: "GPT-5.6 Luna",
|
||||||
used: 99.95,
|
used: 99.95,
|
||||||
remaining: 0.05,
|
remaining: 0.05,
|
||||||
|
sessionConsumed: 1.5,
|
||||||
};
|
};
|
||||||
expect(formatQuotaWindow({ ...window, resetsAt: 999 }, 1_000)).toBe(
|
expect(formatQuotaWindow({ ...window, resetsAt: 999 }, 1_000)).toBe(
|
||||||
"GPT-5.6 Luna 5h: <0.1% left · reset due",
|
"GPT-5.6 Luna 5h: <0.1% · ~1.5pp · due",
|
||||||
);
|
);
|
||||||
expect(formatQuotaWindow({ ...window, resetsAt: Number.NaN }, 1_000)).toBe(
|
expect(formatQuotaWindow({ ...window, resetsAt: Number.NaN }, 1_000)).toBe(
|
||||||
"GPT-5.6 Luna 5h: <0.1% left · reset unavailable",
|
"GPT-5.6 Luna 5h: <0.1% · ~1.5pp · unavailable",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -84,7 +84,7 @@ test("built quota slot stays reactive across pending, empty, failure, update, an
|
|||||||
|
|
||||||
pending.resolve(status("plus", 75));
|
pending.resolve(status("plus", 75));
|
||||||
await setup.waitForFrame((frame) => frame.includes("ChatGPT quota (plus)"));
|
await setup.waitForFrame((frame) => frame.includes("ChatGPT quota (plus)"));
|
||||||
expect(setup.captureCharFrame()).toContain("5h: 75% left · reset due");
|
expect(setup.captureCharFrame()).toContain("5h: 75% · ~1.5pp · due");
|
||||||
|
|
||||||
responses.push(async () => null);
|
responses.push(async () => null);
|
||||||
notifyUpdated();
|
notifyUpdated();
|
||||||
|
|||||||
Reference in New Issue
Block a user