Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c2eb85340 | ||
|
|
7c7b6555f1 |
@@ -0,0 +1,51 @@
|
||||
import { ACP_PROVIDER_ICON_SVGS } from "../../src/assets/acp-provider-icons";
|
||||
import { expect, test, type Page } from "../support/fixtures";
|
||||
import type { TestInfo } from "@playwright/test";
|
||||
import { gotoAppShell, openSettings } from "../support/helpers/app";
|
||||
import { getServerId } from "../support/helpers/server-id";
|
||||
import {
|
||||
findAcpCatalogProvider,
|
||||
openAddProviderArea,
|
||||
openSettingsHost,
|
||||
openSettingsHostSection,
|
||||
} from "../support/helpers/settings";
|
||||
|
||||
async function openProviderCatalog(page: Page) {
|
||||
await gotoAppShell(page);
|
||||
await openSettings(page);
|
||||
await openSettingsHost(page, getServerId());
|
||||
await openSettingsHostSection(page, getServerId(), "providers");
|
||||
await openAddProviderArea(page);
|
||||
}
|
||||
|
||||
async function expectProviderArtwork(
|
||||
page: Page,
|
||||
testInfo: TestInfo,
|
||||
id: "devin" | "hermes" | "kiro",
|
||||
name: string,
|
||||
) {
|
||||
await findAcpCatalogProvider(page, name);
|
||||
// Catalog tiles have no semantic role; scope artwork to the named provider's tile.
|
||||
const icon = page.getByTestId(`provider-catalog-tile-${id}`).locator("svg").first();
|
||||
await expect(icon).toBeVisible();
|
||||
// Provider artwork is 24px; the generic PackagePlus fallback is 20px.
|
||||
await expect.soft(icon).toHaveAttribute("width", "24");
|
||||
const expectedPaths = await page.evaluate((xml) => {
|
||||
const document = new DOMParser().parseFromString(xml, "image/svg+xml");
|
||||
return Array.from(document.querySelectorAll("path"), (path) => path.getAttribute("d")!);
|
||||
}, ACP_PROVIDER_ICON_SVGS[id]);
|
||||
await expect(icon.locator("path")).toHaveCount(expectedPaths.length);
|
||||
for (const [index, path] of expectedPaths.entries()) {
|
||||
await expect(icon.locator("path").nth(index)).toHaveAttribute("d", path);
|
||||
}
|
||||
await page.screenshot({ path: testInfo.outputPath(`${id}-catalog.png`) });
|
||||
}
|
||||
|
||||
test("#7087: the provider catalog shows Devin, Hermes, and Kiro artwork", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
await openProviderCatalog(page);
|
||||
await expectProviderArtwork(page, testInfo, "devin", "Devin CLI");
|
||||
await expectProviderArtwork(page, testInfo, "hermes", "Hermes");
|
||||
await expectProviderArtwork(page, testInfo, "kiro", "Kiro CLI");
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
# Android input delay diagnostic
|
||||
|
||||
Diagnostic evidence for https://github.com/getpaseo/paseo/issues/4408, not a fix or a CI test.
|
||||
|
||||
Observed with the published Android 0.11.4 APK on an Android 16 emulator and a main
|
||||
daemon at 7c7b6555f17a88cffddf484129cd04ad79704f59. The fixture has 400 active
|
||||
workspaces sharing one checkout, plus a synthetic 110-turn conversation and a
|
||||
five-minute mock-provider stream. Clean/dirty transitions emitted 400 workspace
|
||||
updates each. The JavaScript thread reached 96–100% CPU, and tapping Open menu
|
||||
did not expose Close sidebar within five seconds. The same process stayed alive.
|
||||
|
||||
This combines the many-workspace and streaming reports. It does not isolate which
|
||||
work causes the delay, reproduce an allocation crash, or establish that the
|
||||
contributor's byte-counting optimization fixes it. An emulator is not the affected
|
||||
physical phone. No forced memory pressure, forced garbage collection, or fake
|
||||
client responses are used. The daemon's existing mock provider supplies synthetic
|
||||
agent output through the normal transport.
|
||||
|
||||
## Run
|
||||
|
||||
Use a disposable emulator with the published app installed and agent-device
|
||||
0.20.3 or later. Preserve its previous state before changing its app installation.
|
||||
Build the repository's server dependencies first. All commands below run from
|
||||
the repository root in a disposable worktree.
|
||||
|
||||
1. Start `npx tsx packages/app/e2e/mobile/workspace-update-freeze/fixture.ts`.
|
||||
It writes an isolated home under `.dev/workspace-update-freeze`, starts an
|
||||
isolated daemon, and exposes it on local TCP port 45151. Do not use an existing
|
||||
daemon or point this script at real data.
|
||||
2. Forward that port to the emulator, open the app with an agent-device session,
|
||||
and add a direct connection to that isolated daemon. Open the sidebar and
|
||||
verify that the Android reproduction project and workspace rows are visible.
|
||||
3. Start `npx tsx packages/app/e2e/mobile/workspace-update-freeze/stream.ts`.
|
||||
Wait for `HISTORY_READY`. The script writes the agent ID to
|
||||
`.dev/workspace-update-freeze/history-ready` and starts the five-minute stream.
|
||||
4. Open `paseo://h/diag515a/agent/<agent-id>` with agent-device, using the generated
|
||||
ID. Dismiss the notification prompt if present. Verify that the agent's text
|
||||
and Stop agent button are visible, then scroll the conversation.
|
||||
5. Start `node packages/app/e2e/mobile/workspace-update-freeze/toggle.mjs` in a
|
||||
separate terminal. It toggles the fixture's tracked file every three seconds
|
||||
for 90 seconds. `events.json` records workspace updates observed by an actual
|
||||
subscribed daemon client; initial subscription updates are included.
|
||||
6. While the toggles and stream are active, run:
|
||||
|
||||
```sh
|
||||
node packages/app/e2e/mobile/workspace-update-freeze/android.mjs <device-session>
|
||||
```
|
||||
|
||||
It taps Open menu and requires Close sidebar within five seconds. The observed
|
||||
failure was a timeout for Close sidebar while Open menu remained visible.
|
||||
This asserts an input delay, not an application crash or its root cause.
|
||||
|
||||
Stop the fixture with SIGTERM to close its client, proxy, and isolated daemon.
|
||||
Close the agent-device session and restore the emulator's prior state afterward.
|
||||
Do not interpret a passing run as disproving the physical-device reports.
|
||||
@@ -0,0 +1,16 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
|
||||
const session = process.argv[2];
|
||||
if (!session) throw new Error("Pass the agent-device session connected to the Android fixture");
|
||||
|
||||
function device(...args) {
|
||||
return execFileSync("agent-device", [...args, "--session", session], {
|
||||
encoding: "utf8",
|
||||
stdio: "inherit",
|
||||
timeout: 30_000,
|
||||
});
|
||||
}
|
||||
|
||||
device("wait", 'label="Open menu"', "5000");
|
||||
device("press", 'label="Open menu"', "--settle");
|
||||
device("wait", 'label="Close sidebar"', "5000");
|
||||
@@ -0,0 +1,107 @@
|
||||
import type { SessionOutboundMessage } from "@getpaseo/protocol/messages";
|
||||
import { startIsolatedHostDaemon } from "../../support/helpers/isolated-host-daemon";
|
||||
import { connectDaemonClient } from "../../support/helpers/daemon-client-loader";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import net from "node:net";
|
||||
|
||||
interface FixtureClient {
|
||||
connect(): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
fetchWorkspaces(options: {
|
||||
page: { limit: number };
|
||||
subscribe: Record<string, never>;
|
||||
}): Promise<{ entries: unknown[] }>;
|
||||
subscribeRawMessages(handler: (message: SessionOutboundMessage) => void): () => void;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
process.env.E2E_METRO_PORT = "8098";
|
||||
const root = path.resolve(".dev/workspace-update-freeze");
|
||||
const cwd = path.join(root, "repo");
|
||||
const home = path.join(root, "home");
|
||||
await mkdir(cwd, { recursive: true });
|
||||
await mkdir(home + "/projects", { recursive: true });
|
||||
for (const args of [
|
||||
["init", "-b", "main"],
|
||||
["config", "user.name", "Repro"],
|
||||
["config", "user.email", "repro@example.com"],
|
||||
])
|
||||
execFileSync("git", args, { cwd });
|
||||
await writeFile(cwd + "/README.md", "initial\n");
|
||||
execFileSync("git", ["add", "README.md"], { cwd });
|
||||
execFileSync("git", ["commit", "--allow-empty", "-m", "initial"], { cwd });
|
||||
const date = "2026-03-01T00:00:00.000Z";
|
||||
const project = {
|
||||
projectId: "prj_diag515a",
|
||||
rootPath: cwd,
|
||||
kind: "git",
|
||||
displayName: "Android reproduction",
|
||||
projectKey: null,
|
||||
customName: null,
|
||||
customIconRevision: null,
|
||||
createdAt: date,
|
||||
updatedAt: date,
|
||||
archivedAt: null,
|
||||
};
|
||||
const workspaces = Array.from({ length: 400 }, (_, i) => ({
|
||||
workspaceId: `wks_diag515a_${i}`,
|
||||
projectId: project.projectId,
|
||||
cwd,
|
||||
kind: "directory",
|
||||
displayName: `Workspace ${String(i).padStart(3, "0")}`,
|
||||
title: null,
|
||||
branch: null,
|
||||
worktreeRoot: null,
|
||||
baseBranch: "main",
|
||||
isPaseoOwnedWorktree: false,
|
||||
mainRepoRoot: cwd,
|
||||
createdAt: date,
|
||||
updatedAt: date,
|
||||
archivedAt: null,
|
||||
autoArchivedChangeRequestUrl: null,
|
||||
pinnedAt: null,
|
||||
}));
|
||||
await writeFile(home + "/projects/projects.json", JSON.stringify([project]));
|
||||
await writeFile(home + "/projects/workspaces.json", JSON.stringify(workspaces));
|
||||
const daemon = await startIsolatedHostDaemon("diag515a", { paseoHome: home, preserveHome: true });
|
||||
const client = await connectDaemonClient<FixtureClient>({
|
||||
clientIdPrefix: "diag515a",
|
||||
port: daemon.port,
|
||||
});
|
||||
const events: { at: number; payload: unknown }[] = [];
|
||||
client.subscribeRawMessages((m) => {
|
||||
if (m.type === "workspace_update") events.push({ at: Date.now(), payload: m.payload });
|
||||
});
|
||||
const initial = await client.fetchWorkspaces({ page: { limit: 200 }, subscribe: {} });
|
||||
console.log("INITIAL", initial.entries.length);
|
||||
const proxy = net.createServer((socket) => {
|
||||
const up = net.connect(daemon.port, "127.0.0.1");
|
||||
socket.pipe(up);
|
||||
up.pipe(socket);
|
||||
socket.on("error", () => up.destroy());
|
||||
up.on("error", () => socket.destroy());
|
||||
});
|
||||
await new Promise<void>((resolve) => proxy.listen(45151, "127.0.0.1", resolve));
|
||||
await writeFile(
|
||||
root + "/ready.json",
|
||||
JSON.stringify({ port: daemon.port, cwd, pid: daemon.getPid() }),
|
||||
);
|
||||
console.log("READY", daemon.port);
|
||||
const timer = setInterval(async () => {
|
||||
await writeFile(root + "/events.json", JSON.stringify(events));
|
||||
}, 1000);
|
||||
process.on("SIGTERM", async () => {
|
||||
clearInterval(timer);
|
||||
proxy.close();
|
||||
await client.close();
|
||||
await daemon.close();
|
||||
process.exit(0);
|
||||
});
|
||||
await new Promise(() => {});
|
||||
}
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { SeedDaemonClient } from "../../support/helpers/seed-client";
|
||||
import { connectDaemonClient } from "../../support/helpers/daemon-client-loader";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
async function main() {
|
||||
const { port, cwd } = JSON.parse(
|
||||
await readFile(".dev/workspace-update-freeze/ready.json", "utf8"),
|
||||
);
|
||||
const client = await connectDaemonClient<SeedDaemonClient>({
|
||||
clientIdPrefix: "stream515a",
|
||||
port,
|
||||
});
|
||||
try {
|
||||
const agent = await client.createAgent({
|
||||
provider: "mock",
|
||||
cwd,
|
||||
workspaceId: "wks_diag515a_0",
|
||||
title: "Android stream reproduction",
|
||||
modeId: "load-test",
|
||||
model: "five-minute-stream",
|
||||
});
|
||||
await writeFile(".dev/workspace-update-freeze/agent.json", JSON.stringify(agent));
|
||||
for (let i = 0; i < 110; i++) {
|
||||
await client.sendAgentMessage(
|
||||
agent.id,
|
||||
`history ${i}: emit 1 coalesced agent stream updates`,
|
||||
);
|
||||
await client.waitForFinish(agent.id, 15000);
|
||||
}
|
||||
console.log("HISTORY_READY", agent.id);
|
||||
await writeFile(".dev/workspace-update-freeze/history-ready", agent.id);
|
||||
await client.sendAgentMessage(agent.id, "Stream a realistic long answer with code blocks.");
|
||||
await client.waitForFinish(agent.id, 360000);
|
||||
console.log("FINISHED");
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
const root = path.resolve(".dev/workspace-update-freeze");
|
||||
const { cwd } = JSON.parse(await readFile(path.join(root, "ready.json"), "utf8"));
|
||||
for (let index = 0; index < 30; index += 1) {
|
||||
await writeFile(path.join(cwd, "README.md"), index % 2 ? "changed first\n" : "initial\n");
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
|
||||
<path d="M2.033 9.867l2.554 1.483a.589.589 0 00.592 0l2.554-1.483.01-.008a.608.608 0 00.11-.084l.013-.015a.631.631 0 00.076-.1c.003-.005.008-.01.01-.016a.558.558 0 00.052-.125l.007-.028a.611.611 0 00.019-.14V7.868c0-.572.307-1.105.8-1.392a1.595 1.595 0 011.598 0l1.277.742a.54.54 0 00.129.053l.028.01c.044.01.088.015.133.016h.006l.013-.002a.587.587 0 00.27-.074l.011-.004 2.554-1.483a.596.596 0 00.297-.516V2.253a.595.595 0 00-.297-.516L12.293.257a.587.587 0 00-.591 0L9.148 1.737l-.01.01a.609.609 0 00-.109.083l-.014.015a.632.632 0 00-.076.1c-.003.005-.008.01-.01.016a.57.57 0 00-.052.124l-.007.028a.612.612 0 00-.018.14v1.483c0 .572-.307 1.105-.8 1.393a1.597 1.597 0 01-1.599 0l-1.276-.742a.603.603 0 00-.13-.053l-.028-.008a.658.658 0 00-.133-.018h-.02a.57.57 0 00-.269.074c-.003.002-.008.002-.012.005L2.033 5.872a.596.596 0 00-.297.515v2.966c0 .213.113.41.297.515z" fill="currentColor"></path><path d="M15.943 10.607a1.596 1.596 0 011.599 0l1.276.74c.041.025.085.04.13.055l.028.008c.043.01.088.016.133.018h.005c.005 0 .01-.002.014-.003a.474.474 0 00.122-.016l.021-.005a.616.616 0 00.126-.052c.004-.002.009-.002.013-.005l2.554-1.482a.597.597 0 00.297-.516V6.383a.596.596 0 00-.297-.515l-2.552-1.483a.587.587 0 00-.592 0l-2.553 1.482-.011.008a.61.61 0 00-.108.084l-.014.016a.637.637 0 00-.076.1c-.003.005-.008.01-.01.016a.57.57 0 00-.052.124l-.007.029a.612.612 0 00-.018.14v1.482c0 .572-.307 1.105-.8 1.393a1.597 1.597 0 01-1.599 0l-1.276-.742a.584.584 0 00-.13-.053l-.028-.008a.62.62 0 00-.133-.018h-.02a.587.587 0 00-.269.074l-.012.004L9.15 10a.596.596 0 00-.296.516v2.966c0 .212.112.409.296.515l2.554 1.483s.008.002.012.005c.04.022.082.04.126.052l.02.004a.57.57 0 00.123.017l.014.002h.006c.054 0 .108-.01.16-.025a.587.587 0 00.13-.054l1.277-.741a1.597 1.597 0 012.398 1.392v1.482c0 .049.007.095.019.14l.007.028a.619.619 0 00.051.125c.004.006.008.01.01.016a.6.6 0 00.076.1l.014.015c.033.032.069.06.108.084.004.002.006.006.011.008l2.554 1.483a.59.59 0 00.593 0l2.554-1.483a.597.597 0 00.296-.516v-2.965a.595.595 0 00-.296-.516l-2.554-1.483s-.008-.002-.012-.005a.54.54 0 00-.126-.051c-.007-.003-.013-.003-.02-.005a.635.635 0 00-.125-.017h-.018a.557.557 0 00-.16.026.588.588 0 00-.13.053l-1.276.742a1.595 1.595 0 01-1.598 0 1.615 1.615 0 010-2.785l-.005-.001z" fill="currentColor"></path><path d="M14.848 18.265l-2.554-1.482-.012-.005a.526.526 0 00-.126-.052c-.007-.002-.014-.002-.02-.005a.64.64 0 00-.124-.017h-.02a.56.56 0 00-.16.026.588.588 0 00-.13.053l-1.276.742a1.594 1.594 0 01-1.598 0c-.493-.286-.8-.82-.8-1.393V14.65a.563.563 0 00-.018-.14l-.008-.028a.604.604 0 00-.051-.124l-.01-.017a.603.603 0 00-.076-.1l-.014-.015a.596.596 0 00-.109-.084c-.003-.002-.005-.006-.01-.008L5.178 12.65a.587.587 0 00-.591 0l-2.554 1.483a.596.596 0 00-.297.516v2.965c0 .213.113.41.297.516l2.554 1.483.012.004a.618.618 0 00.267.074l.016.002h.007a.55.55 0 00.16-.026.584.584 0 00.129-.053l1.277-.742a1.597 1.597 0 012.398 1.393v1.482c0 .05.007.095.019.14l.007.028c.013.044.03.085.051.125l.01.016c.022.036.047.07.076.1l.014.015c.032.032.069.06.109.084l.01.008 2.554 1.483a.587.587 0 00.593 0l2.554-1.483a.596.596 0 00.296-.515v-2.966a.596.596 0 00-.296-.516h-.002z" fill="currentColor"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 1024 1024" fill="none">
|
||||
<path fill="currentColor" d="M395.479 633.828L735.91 381.105C752.599 368.715 776.454 373.548 784.406 392.792C826.26 494.285 807.561 616.253 724.288 699.996C641.016 783.739 525.151 802.104 419.247 760.277L303.556 814.143C469.49 928.202 670.987 899.995 796.901 773.282C896.776 672.843 927.708 535.937 898.785 412.476L899.047 412.739C857.105 231.37 909.358 158.874 1016.4 10.6326C1018.93 7.11771 1021.47 3.60279 1024 0L883.144 141.651V141.212L395.392 633.916"/>
|
||||
<path fill="currentColor" d="M325.226 695.251C206.128 580.84 226.662 403.776 328.285 301.668C403.431 226.097 526.549 195.254 634.026 240.596L749.454 186.994C728.657 171.88 702.007 155.623 671.424 144.2C533.19 86.9942 367.693 115.465 255.323 228.382C147.234 337.081 113.244 504.215 171.613 646.833C215.216 753.423 143.739 828.818 71.7385 904.916C46.2237 931.893 20.6216 958.87 0 987.429L325.139 695.339"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 975 B |
|
After Width: | Height: | Size: 19 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M18.8 0H5.2A5.2 5.2 0 000 5.2v13.6A5.2 5.2 0 005.2 24h13.6a5.2 5.2 0 005.2-5.2V5.2A5.2 5.2 0 0018.8 0z" fill="#9046FF" /> <path d="M7.97 16.376c-1.644 3.642 1.86 4.556 4.443 2.424.76 2.39 3.608.607 4.631-1.247 2.251-4.084 1.342-8.249 1.108-9.108-1.6-5.859-9.6-5.869-10.976.03-.323 1.033-.328 2.206-.507 3.423-.09.617-.16 1.009-.393 1.655-.139.373-.323.7-.62 1.257-.458.865-.264 2.53 2.101 1.665l.224-.1h-.01l-.001.001z" fill="#fff" /> <path d="M12.722 10.985c-.656 0-.755-.785-.755-1.252 0-.423.074-.756.218-.97a.61.61 0 01.537-.283c.229 0 .428.095.567.289.159.218.243.55.243.964 0 .785-.303 1.252-.805 1.252h-.005zm2.703 0c-.656 0-.755-.785-.755-1.252 0-.423.074-.756.219-.97a.61.61 0 01.536-.283c.229 0 .428.095.567.289.159.218.243.55.243.964 0 .785-.303 1.252-.805 1.252h-.005z" fill="#000" /></svg>
|
||||
|
After Width: | Height: | Size: 895 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
|
||||
<path d="M16.278 2c1.156 0 2.093.927 2.093 2.07v12.501a.74.74 0 00.744.709.74.74 0 00.743-.709V9.099a2.06 2.06 0 012.071-2.049A2.06 2.06 0 0124 9.1v6.561a.649.649 0 01-.652.645.649.649 0 01-.653-.645V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v7.472a2.037 2.037 0 01-2.048 2.026 2.037 2.037 0 01-2.048-2.026v-12.5a.785.785 0 00-.788-.753.785.785 0 00-.789.752l-.001 15.904A2.037 2.037 0 0113.441 22a2.037 2.037 0 01-2.048-2.026V18.04c0-.356.292-.645.652-.645.36 0 .652.289.652.645v1.934c0 .263.142.506.372.638.23.131.514.131.744 0a.734.734 0 00.372-.638V4.07c0-1.143.937-2.07 2.093-2.07zm-5.674 0c1.156 0 2.093.927 2.093 2.07v11.523a.648.648 0 01-.652.645.648.648 0 01-.652-.645V4.07a.785.785 0 00-.789-.78.785.785 0 00-.789.78v14.013a2.06 2.06 0 01-2.07 2.048 2.06 2.06 0 01-2.071-2.048V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v3.8a2.06 2.06 0 01-2.071 2.049A2.06 2.06 0 010 12.9v-1.378c0-.357.292-.646.652-.646.36 0 .653.29.653.646V12.9c0 .418.343.757.766.757s.766-.339.766-.757V9.099a2.06 2.06 0 012.07-2.048 2.06 2.06 0 012.071 2.048v8.984c0 .419.343.758.767.758.423 0 .766-.339.766-.758V4.07c0-1.143.937-2.07 2.093-2.07z" fill="currentColor" fill-rule="nonzero"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
|
||||
<path d="M24 20.541H3.428v-3.426H0V3.4h24V20.54zM3.428 17.115h17.144V6.827H3.428v10.288zm8.573-5.196l-2.425 2.424-2.424-2.424 2.424-2.424 2.425 2.424zm6.857-.001l-2.424 2.423-2.425-2.423 2.425-2.425 2.424 2.425z" fill="currentColor"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 334 B |
@@ -0,0 +1,17 @@
|
||||
import Svg, { Path } from "react-native-svg";
|
||||
|
||||
interface AntigravityIconProps {
|
||||
size?: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export function AntigravityIcon({ size = 16, color = "currentColor" }: AntigravityIconProps) {
|
||||
return (
|
||||
<Svg width={size} height={size} viewBox="0 0 24 24" fill={color}>
|
||||
<Path
|
||||
transform="translate(12 12) scale(0.24) translate(-55.738 -56.945)"
|
||||
d="M89.6992 93.695C94.3659 97.195 101.366 94.8617 94.9492 88.445C75.6992 69.7783 79.7825 18.445 55.8659 18.445C31.9492 18.445 36.0325 69.7783 16.7825 88.445C9.78251 95.445 17.3658 97.195 22.0325 93.695C40.1159 81.445 38.9492 59.8617 55.8659 59.8617C72.7825 59.8617 71.6159 81.445 89.6992 93.695Z"
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import Svg, { Path } from "react-native-svg";
|
||||
|
||||
interface KiroIconProps {
|
||||
size?: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export function KiroIcon({ size = 16, color = "currentColor" }: KiroIconProps) {
|
||||
return (
|
||||
<Svg width={size} height={size} viewBox="0 0 24 24" fill={color}>
|
||||
<Path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M7.97 16.376c-1.644 3.642 1.86 4.556 4.443 2.424.76 2.39 3.608.607 4.631-1.247 2.251-4.084 1.342-8.249 1.108-9.108-1.6-5.859-9.6-5.869-10.976.03-.323 1.033-.328 2.206-.507 3.423-.09.617-.16 1.009-.393 1.655-.139.373-.323.7-.62 1.257-.458.865-.264 2.53 2.101 1.665l.224-.1h-.01l-.001.001zM12.722 10.985c-.656 0-.755-.785-.755-1.252 0-.423.074-.756.218-.97a.61.61 0 01.537-.283c.229 0 .428.095.567.289.159.218.243.55.243.964 0 .785-.303 1.252-.805 1.252h-.005zm2.703 0c-.656 0-.755-.785-.755-1.252 0-.423.074-.756.219-.97a.61.61 0 01.536-.283c.229 0 .428.095.567.289.159.218.243.55.243.964 0 .785-.303 1.252-.805 1.252h-.005z"
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
@@ -17,8 +17,10 @@ describe("resolveProviderIconName", () => {
|
||||
|
||||
it("returns the catalog identifier for ACP catalog provider ids that ship an icon", () => {
|
||||
expect(resolveProviderIconName("amp-acp")).toEqual({ kind: "catalog", id: "amp-acp" });
|
||||
expect(resolveProviderIconName("devin")).toEqual({ kind: "catalog", id: "devin" });
|
||||
expect(resolveProviderIconName("gemini")).toEqual({ kind: "catalog", id: "gemini" });
|
||||
expect(resolveProviderIconName("gjc")).toEqual({ kind: "catalog", id: "gjc" });
|
||||
expect(resolveProviderIconName("hermes")).toEqual({ kind: "catalog", id: "hermes" });
|
||||
expect(resolveProviderIconName("traecli")).toEqual({ kind: "catalog", id: "traecli" });
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @vitest-environment jsdom
|
||||
import { act, cleanup, renderHook } from "@testing-library/react";
|
||||
import { Bot } from "lucide-react-native";
|
||||
import { Bot, PackagePlus } from "lucide-react-native";
|
||||
import { SvgXml } from "react-native-svg";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { replaceProviderSnapshotIcons } from "@/data/provider-icons";
|
||||
@@ -17,6 +17,16 @@ function renderIcon(Component: ProviderIconComponent) {
|
||||
}
|
||||
|
||||
describe("useProviderIcon", () => {
|
||||
it.each(["devin", "hermes", "kiro", "antigravity"])(
|
||||
"#7087: %s has a provider mark before a host snapshot arrives",
|
||||
(provider) => {
|
||||
const Icon = renderHook(() => useProviderIcon(provider)).result.current;
|
||||
expect(Icon).not.toBe(Bot);
|
||||
expect(Icon).not.toBe(PackagePlus);
|
||||
expect(typeof Icon).toBe("function");
|
||||
},
|
||||
);
|
||||
|
||||
it("renders registered snapshot SVG metadata with the requested size and color", () => {
|
||||
const svg = '<svg viewBox="0 0 24 24"><path d="M4 4h16v16H4z" /></svg>';
|
||||
replaceProviderSnapshotIcons("server-1", [{ provider: "rendered-provider", iconSvg: svg }]);
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { useStore } from "zustand";
|
||||
import { providerSnapshotIcons } from "@/data/provider-icons";
|
||||
import { Bot, PackagePlus } from "lucide-react-native";
|
||||
import { Bot } from "lucide-react-native";
|
||||
import { createElement, useCallback, type ComponentType } from "react";
|
||||
import { SvgXml } from "react-native-svg";
|
||||
import { TERMINAL_PROFILE_ICON_NAMES } from "@getpaseo/protocol/provider-icon-names";
|
||||
import { AntigravityIcon } from "@/components/icons/antigravity-icon";
|
||||
import { ClaudeIcon } from "@/components/icons/claude-icon";
|
||||
import { CodexIcon } from "@/components/icons/codex-icon";
|
||||
import { CopilotIcon } from "@/components/icons/copilot-icon";
|
||||
import { KiroIcon } from "@/components/icons/kiro-icon";
|
||||
import { MiniMaxIcon } from "@/components/icons/minimax-icon";
|
||||
import { OpenCodeIcon } from "@/components/icons/opencode-icon";
|
||||
import { OmpIcon } from "@/components/icons/omp-icon";
|
||||
@@ -23,10 +25,11 @@ export interface ProviderIconProps {
|
||||
export type ProviderIconComponent = ComponentType<ProviderIconProps>;
|
||||
|
||||
const BUILTIN_PROVIDER_ICONS: Record<string, ProviderIconComponent> = {
|
||||
antigravity: AntigravityIcon as unknown as ProviderIconComponent,
|
||||
claude: ClaudeIcon as unknown as ProviderIconComponent,
|
||||
codex: CodexIcon as unknown as ProviderIconComponent,
|
||||
copilot: CopilotIcon as unknown as ProviderIconComponent,
|
||||
kiro: PackagePlus,
|
||||
kiro: KiroIcon as unknown as ProviderIconComponent,
|
||||
minimax: MiniMaxIcon as unknown as ProviderIconComponent,
|
||||
omp: OmpIcon as unknown as ProviderIconComponent,
|
||||
opencode: OpenCodeIcon as unknown as ProviderIconComponent,
|
||||
|
||||
@@ -133,7 +133,7 @@ const CATALOG_DATA = [
|
||||
title: "Devin CLI",
|
||||
description: "Cognition's Devin for Terminal via Agent Client Protocol",
|
||||
version: "manual",
|
||||
iconId: null,
|
||||
iconId: "devin",
|
||||
installLink: "https://cli.devin.ai/docs",
|
||||
command: ["devin", "acp"],
|
||||
},
|
||||
@@ -235,7 +235,7 @@ const CATALOG_DATA = [
|
||||
title: "Hermes",
|
||||
description: "Nous Research self-improving AI agent",
|
||||
version: "manual",
|
||||
iconId: null,
|
||||
iconId: "hermes",
|
||||
installLink: "https://hermes-agent.nousresearch.com/docs/user-guide/features/acp",
|
||||
command: ["hermes", "acp"],
|
||||
},
|
||||
@@ -272,7 +272,7 @@ const CATALOG_DATA = [
|
||||
title: "Kiro CLI",
|
||||
description: "Amazon's AI coding agent with native ACP support",
|
||||
version: "manual",
|
||||
iconId: null,
|
||||
iconId: "kiro",
|
||||
installLink: "https://kiro.dev/docs/cli/acp/",
|
||||
command: ["kiro-cli", "acp"],
|
||||
},
|
||||
|
||||
@@ -11,6 +11,13 @@ function findProvider(id: string) {
|
||||
}
|
||||
|
||||
describe("ACP provider catalog", () => {
|
||||
it.each(["devin", "hermes", "kiro"])(
|
||||
"#7087: %s supplies its provider mark to the catalog picker",
|
||||
(provider) => {
|
||||
expect(findProvider(provider).iconSvg).toContain("<svg");
|
||||
},
|
||||
);
|
||||
|
||||
it("vendors provider entries with unique ids and concrete commands", () => {
|
||||
const ids = new Set<string>();
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export const BUILTIN_PROVIDER_ICON_NAMES = [
|
||||
"antigravity",
|
||||
"claude",
|
||||
"codex",
|
||||
"copilot",
|
||||
@@ -22,6 +23,7 @@ export const ACP_PROVIDER_ICON_NAMES = [
|
||||
"crow-cli",
|
||||
"cursor",
|
||||
"deepagents",
|
||||
"devin",
|
||||
"dimcode",
|
||||
"dirac",
|
||||
"factory-droid",
|
||||
@@ -31,6 +33,7 @@ export const ACP_PROVIDER_ICON_NAMES = [
|
||||
"glm-acp-agent",
|
||||
"goose",
|
||||
"grok",
|
||||
"hermes",
|
||||
"hoplite",
|
||||
"junie",
|
||||
"kilo",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { expect, test, type Page } from "playwright/test";
|
||||
|
||||
test("keeps unrelated agent artwork unchanged while adding the Hermes mark", async ({ page }) => {
|
||||
await openSupportedAgents(page);
|
||||
await expectGenericAgentMark(page, "CodeWhale");
|
||||
await expectVendoredAgentMark(page, "Hermes Agent");
|
||||
});
|
||||
|
||||
async function openSupportedAgents(page: Page) {
|
||||
await page.goto("/agents");
|
||||
await expect(page.getByRole("heading", { level: 1, name: "Supported agents" })).toBeVisible();
|
||||
}
|
||||
|
||||
async function expectGenericAgentMark(page: Page, name: string) {
|
||||
const card = page.getByRole("link", { name, exact: true });
|
||||
await expect(card).toBeVisible();
|
||||
// Decorative SVGs have no accessible role; the fallback is Lucide's bot mark.
|
||||
await expect(card.locator("svg.lucide-bot")).toBeVisible();
|
||||
}
|
||||
|
||||
async function expectVendoredAgentMark(page: Page, name: string) {
|
||||
const card = page.getByRole("link", { name, exact: true });
|
||||
await expect(card).toBeVisible();
|
||||
await expect(card.locator("svg")).toBeVisible();
|
||||
await expect(card.locator("svg.lucide-bot")).toHaveCount(0);
|
||||
}
|
||||
|
After Width: | Height: | Size: 19 KiB |