mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-27 19:52:49 +00:00
The Command Center tested the whole trimmed query as one substring against one concatenated haystack, so a query only matched when its words appeared consecutively, in that order, with that spacing. Typing `shortcuts` found "Keyboard shortcuts"; typing `short key` found nothing. Survivors were then ordered by a fixed group rank, so the best match was rarely first. Route both through `scoreTextFields`, the matcher the combobox, provider selection, directory suggestions and the daemon's history search already share. It splits on whitespace, scores each token against each field on its own, and requires every token to land somewhere. `short key`, `key short`, `workspace new` and `project add` all find their row now. The score is the filter: `scoreSearchFields` returns null when a token finds no home, so filtering and ranking are one pass rather than a boolean predicate that throws the scan away. Workspaces and agents are scored too, not just filtered, with each row's own ordering kept as the tiebreak. Fields are split into visible and hidden, so a match on an agent's `cwd` — which is never rendered — cannot outrank a match on its title. Subsequence matching is deliberately off here, unlike the composer's slash-command list where it makes `/pasbab` find `/paseo-babysit`. It widens far enough that three-letter typos hit several rows each, and this list preselects its first row, so a query that should find nothing would put an unrelated action under Enter. `MatchOptions.subsequence` defaults to on, leaving every other caller unchanged. The tier has to be suppressed during scoring rather than filtered afterwards, because a two-token total of tier 5 is either 0+5 or 4+1. Sections carry a band so the ones that reorder by relevance are compared before the pinned ones, which keeps the comparator total: file rows are matched by the daemon and carry no client-side score, so they cannot be relevance-compared against rows that do. Contiguous per-token matching is a strict widening of the old test. Tokens hold no whitespace and the old haystack joined fields with a space, so any token found there lay entirely within one field and is still found. Verified across a query set at zero rows lost, and pinned by a test.
214 lines
7.5 KiB
TypeScript
214 lines
7.5 KiB
TypeScript
import { test } from "../support/fixtures";
|
||
import type { FormPreferences } from "@/create-agent-preferences/preferences";
|
||
import { closeCommandCenter, openCommandCenter } from "../support/helpers/command-center";
|
||
import {
|
||
applyCommandCenterAgentControls,
|
||
chooseCommandCenterAgentControl,
|
||
openCommandCenterForAgent,
|
||
expectCommandCenterAgentControlSelected,
|
||
expectFocusedAgentControls,
|
||
expectWorkspaceAgentConfiguration,
|
||
submitDraftAgent,
|
||
waitForDraftComposer,
|
||
} from "../support/helpers/command-center-agent-controls";
|
||
import { clickNewChat, gotoWorkspace } from "../support/helpers/launcher";
|
||
import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent";
|
||
import { expectAppRoute } from "../support/helpers/route-assertions";
|
||
import { seedWorkspace } from "../support/helpers/seed-client";
|
||
import { buildSchedulesRoute } from "@/utils/host-routes";
|
||
|
||
const CREATE_AGENT_PREFERENCES_KEY = "@paseo:create-agent-preferences";
|
||
|
||
async function seedMockDraftPreferences(page: import("@playwright/test").Page): Promise<void> {
|
||
await page.addInitScript(
|
||
({ preferencesKey }) => {
|
||
localStorage.setItem(
|
||
preferencesKey,
|
||
JSON.stringify({
|
||
provider: "mock",
|
||
providerPreferences: {
|
||
mock: {
|
||
model: "five-minute-stream",
|
||
mode: "load-test",
|
||
},
|
||
},
|
||
} satisfies FormPreferences),
|
||
);
|
||
},
|
||
{ preferencesKey: CREATE_AGENT_PREFERENCES_KEY },
|
||
);
|
||
}
|
||
|
||
test.describe("Command Center agent controls", () => {
|
||
test.describe.configure({ timeout: 180_000 });
|
||
|
||
test("changes a running agent setting and preserves its selected row", async ({ page }) => {
|
||
const workspace = await seedMockAgentWorkspace({
|
||
repoPrefix: "command-center-live-controls-",
|
||
title: "Command Center live controls",
|
||
model: "five-minute-stream",
|
||
});
|
||
try {
|
||
// A stale runtime mode makes selecting the provider's supported mode
|
||
// exercise the live setting RPC instead of the selected-row no-op.
|
||
await workspace.client.setAgentMode(workspace.agentId, "legacy-mode");
|
||
await openAgentRoute(page, {
|
||
workspaceId: workspace.workspaceId,
|
||
agentId: workspace.agentId,
|
||
});
|
||
await openCommandCenter(page);
|
||
await chooseCommandCenterAgentControl({
|
||
page,
|
||
query: "load test",
|
||
choice: "Mode › Load test",
|
||
});
|
||
await expectWorkspaceAgentConfiguration(workspace, {
|
||
id: workspace.agentId,
|
||
provider: "mock",
|
||
model: "five-minute-stream",
|
||
modeId: "load-test",
|
||
});
|
||
await openCommandCenter(page);
|
||
await expectCommandCenterAgentControlSelected({
|
||
page,
|
||
query: "load test",
|
||
choice: "Mode › Load test",
|
||
});
|
||
} finally {
|
||
await workspace.cleanup();
|
||
}
|
||
});
|
||
|
||
test("activates the best-matching row, not the highest-ranked group", async ({ page }) => {
|
||
// "Mode › Load Test" matches "load" at offset 0; every "Model › Mock Load Test › …" row
|
||
// matches at offset 5. Models used to outrank modes by fixed group rank, so Enter picked a
|
||
// model. Asserting both model and mode catches either row winning.
|
||
const workspace = await seedMockAgentWorkspace({
|
||
repoPrefix: "command-center-relevance-",
|
||
title: "Command Center relevance",
|
||
model: "five-minute-stream",
|
||
});
|
||
try {
|
||
await workspace.client.setAgentMode(workspace.agentId, "approval-test");
|
||
const input = await openCommandCenterForAgent(page, workspace);
|
||
await input.fill("load");
|
||
await page.keyboard.press("Enter");
|
||
|
||
await expectWorkspaceAgentConfiguration(workspace, {
|
||
id: workspace.agentId,
|
||
provider: "mock",
|
||
model: "five-minute-stream",
|
||
modeId: "load-test",
|
||
});
|
||
} finally {
|
||
await workspace.cleanup();
|
||
}
|
||
});
|
||
|
||
test("matches query tokens in any order", async ({ page }) => {
|
||
// "Mode › Load test" flattens to "mode load test", so the reversed query "test load" is never
|
||
// a substring of it. Only per-token matching finds this row.
|
||
const workspace = await seedMockAgentWorkspace({
|
||
repoPrefix: "command-center-tokenized-",
|
||
title: "Command Center tokenized search",
|
||
model: "five-minute-stream",
|
||
});
|
||
try {
|
||
await workspace.client.setAgentMode(workspace.agentId, "legacy-mode");
|
||
await openAgentRoute(page, {
|
||
workspaceId: workspace.workspaceId,
|
||
agentId: workspace.agentId,
|
||
});
|
||
await openCommandCenter(page);
|
||
await chooseCommandCenterAgentControl({
|
||
page,
|
||
query: "test load",
|
||
choice: "Mode › Load test",
|
||
});
|
||
|
||
await expectWorkspaceAgentConfiguration(workspace, {
|
||
id: workspace.agentId,
|
||
provider: "mock",
|
||
model: "five-minute-stream",
|
||
modeId: "load-test",
|
||
});
|
||
} finally {
|
||
await workspace.cleanup();
|
||
}
|
||
});
|
||
|
||
test("keeps an action first when the action is the best match", async ({ page }) => {
|
||
const workspace = await seedMockAgentWorkspace({
|
||
repoPrefix: "command-center-action-priority-",
|
||
title: "Command Center action priority",
|
||
model: "five-minute-stream",
|
||
});
|
||
try {
|
||
const input = await openCommandCenterForAgent(page, workspace);
|
||
await input.fill("sched");
|
||
await page.keyboard.press("Enter");
|
||
|
||
await expectAppRoute(page, buildSchedulesRoute(), { timeout: 30_000 });
|
||
} finally {
|
||
await workspace.cleanup();
|
||
}
|
||
});
|
||
|
||
test("drops an arrow-key selection once the query changes", async ({ page }) => {
|
||
const workspace = await seedMockAgentWorkspace({
|
||
repoPrefix: "command-center-active-reset-",
|
||
title: "Command Center active reset",
|
||
model: "five-minute-stream",
|
||
});
|
||
try {
|
||
await workspace.client.setAgentMode(workspace.agentId, "approval-test");
|
||
const input = await openCommandCenterForAgent(page, workspace);
|
||
await input.fill("loa");
|
||
// Moves the highlight onto a model row, which still matches after the next keystroke.
|
||
await page.keyboard.press("ArrowDown");
|
||
await input.fill("load");
|
||
await page.keyboard.press("Enter");
|
||
|
||
await expectWorkspaceAgentConfiguration(workspace, {
|
||
id: workspace.agentId,
|
||
provider: "mock",
|
||
model: "five-minute-stream",
|
||
modeId: "load-test",
|
||
});
|
||
} finally {
|
||
await workspace.cleanup();
|
||
}
|
||
});
|
||
|
||
test("applies draft model and setting choices to the created agent", async ({ page }) => {
|
||
const workspace = await seedWorkspace({ repoPrefix: "command-center-draft-controls-" });
|
||
await seedMockDraftPreferences(page);
|
||
|
||
try {
|
||
await openDraftComposer(page, workspace.workspaceId);
|
||
await applyCommandCenterAgentControls(page, DRAFT_AGENT_CONTROL_CHOICES);
|
||
await closeCommandCenter(page);
|
||
await submitDraftAgent(page, "Create an agent with Command Center draft choices");
|
||
await expectFocusedAgentControls(page, DRAFT_AGENT_CONTROL_CHOICES);
|
||
await expectWorkspaceAgentConfiguration(workspace, {
|
||
provider: "mock",
|
||
model: "ten-second-stream",
|
||
modeId: "load-test",
|
||
});
|
||
} finally {
|
||
await workspace.cleanup();
|
||
}
|
||
});
|
||
});
|
||
|
||
const DRAFT_AGENT_CONTROL_CHOICES = [
|
||
{ query: "ten second stream", choice: "Model › Mock Load Test › Ten second stream" },
|
||
{ query: "load test", choice: "Mode › Load test" },
|
||
] as const;
|
||
|
||
async function openDraftComposer(page: import("@playwright/test").Page, workspaceId: string) {
|
||
await gotoWorkspace(page, workspaceId);
|
||
await clickNewChat(page);
|
||
await waitForDraftComposer(page);
|
||
}
|