Cache available editor discovery
This commit is contained in:
Generated
+41
@@ -28729,6 +28729,18 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mimic-function": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mimic-response": {
|
"node_modules/mimic-response": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
|
||||||
@@ -30117,6 +30129,34 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/p-memoize": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-memoize/-/p-memoize-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-jdZ10MCxavHoIHwJ5oweOtYy6ElPixEHaMkz0AuaEMovR1MRpVvYFzIEHRxgMEpXYzNpRVByFAniAzwmd1/uug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mimic-function": "^5.0.1",
|
||||||
|
"type-fest": "^4.41.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sindresorhus/p-memoize?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/p-memoize/node_modules/type-fest": {
|
||||||
|
"version": "4.41.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
|
||||||
|
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
|
||||||
|
"license": "(MIT OR CC0-1.0)",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/p-retry": {
|
"node_modules/p-retry": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
|
||||||
@@ -37859,6 +37899,7 @@
|
|||||||
"onnxruntime-node": "^1.23.0",
|
"onnxruntime-node": "^1.23.0",
|
||||||
"openai": "^4.20.0",
|
"openai": "^4.20.0",
|
||||||
"p-limit": "^7.3.0",
|
"p-limit": "^7.3.0",
|
||||||
|
"p-memoize": "^8.0.0",
|
||||||
"pino": "^10.2.0",
|
"pino": "^10.2.0",
|
||||||
"pino-pretty": "^13.1.3",
|
"pino-pretty": "^13.1.3",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
|
|||||||
@@ -63,8 +63,8 @@
|
|||||||
"@getpaseo/highlight": "0.1.60-beta.1",
|
"@getpaseo/highlight": "0.1.60-beta.1",
|
||||||
"@getpaseo/relay": "0.1.60-beta.1",
|
"@getpaseo/relay": "0.1.60-beta.1",
|
||||||
"@isaacs/ttlcache": "^2.1.4",
|
"@isaacs/ttlcache": "^2.1.4",
|
||||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
|
||||||
"@mariozechner/pi-coding-agent": "^0.67.68",
|
"@mariozechner/pi-coding-agent": "^0.67.68",
|
||||||
|
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||||
"@opencode-ai/sdk": "1.2.6",
|
"@opencode-ai/sdk": "1.2.6",
|
||||||
"@sctg/sentencepiece-js": "^1.1.0",
|
"@sctg/sentencepiece-js": "^1.1.0",
|
||||||
"@xterm/headless": "^6.0.0",
|
"@xterm/headless": "^6.0.0",
|
||||||
@@ -80,6 +80,7 @@
|
|||||||
"onnxruntime-node": "^1.23.0",
|
"onnxruntime-node": "^1.23.0",
|
||||||
"openai": "^4.20.0",
|
"openai": "^4.20.0",
|
||||||
"p-limit": "^7.3.0",
|
"p-limit": "^7.3.0",
|
||||||
|
"p-memoize": "^8.0.0",
|
||||||
"pino": "^10.2.0",
|
"pino": "^10.2.0",
|
||||||
"pino-pretty": "^13.1.3",
|
"pino-pretty": "^13.1.3",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import equal from "fast-deep-equal";
|
import equal from "fast-deep-equal";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { TTLCache } from "@isaacs/ttlcache";
|
||||||
|
import pMemoize from "p-memoize";
|
||||||
import type { FSWatcher } from "node:fs";
|
import type { FSWatcher } from "node:fs";
|
||||||
import { resolve, sep } from "path";
|
import { resolve, sep } from "path";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
@@ -448,6 +450,8 @@ const MIN_STREAMING_SEGMENT_BYTES = Math.round(
|
|||||||
);
|
);
|
||||||
const AgentIdSchema = z.string().uuid();
|
const AgentIdSchema = z.string().uuid();
|
||||||
const VOICE_INTERRUPT_CONFIRMATION_MS = 500;
|
const VOICE_INTERRUPT_CONFIRMATION_MS = 500;
|
||||||
|
const AVAILABLE_EDITOR_TARGETS_CACHE_TTL_MS = 60_000;
|
||||||
|
const AVAILABLE_EDITOR_TARGETS_CACHE_KEY = "available";
|
||||||
|
|
||||||
type VoiceModeBaseConfig = {
|
type VoiceModeBaseConfig = {
|
||||||
systemPrompt?: string;
|
systemPrompt?: string;
|
||||||
@@ -705,6 +709,21 @@ export class Session {
|
|||||||
private nextTerminalSlot = 0;
|
private nextTerminalSlot = 0;
|
||||||
private inflightRequests = 0;
|
private inflightRequests = 0;
|
||||||
private peakInflightRequests = 0;
|
private peakInflightRequests = 0;
|
||||||
|
private readonly availableEditorTargetsCache = new TTLCache<
|
||||||
|
string,
|
||||||
|
EditorTargetDescriptorPayload[]
|
||||||
|
>({
|
||||||
|
ttl: AVAILABLE_EDITOR_TARGETS_CACHE_TTL_MS,
|
||||||
|
max: 1,
|
||||||
|
checkAgeOnGet: true,
|
||||||
|
});
|
||||||
|
private readonly getMemoizedAvailableEditorTargets = pMemoize(
|
||||||
|
async () => this.resolveAvailableEditorTargets(),
|
||||||
|
{
|
||||||
|
cache: this.availableEditorTargetsCache,
|
||||||
|
cacheKey: () => AVAILABLE_EDITOR_TARGETS_CACHE_KEY,
|
||||||
|
},
|
||||||
|
);
|
||||||
private readonly checkoutDiffSubscriptions = new Map<string, () => void>();
|
private readonly checkoutDiffSubscriptions = new Map<string, () => void>();
|
||||||
private readonly workspaceGitWatchTargets = new Map<string, WorkspaceGitWatchTarget>();
|
private readonly workspaceGitWatchTargets = new Map<string, WorkspaceGitWatchTarget>();
|
||||||
private readonly workspaceSetupSnapshots = new Map<string, WorkspaceSetupSnapshot>();
|
private readonly workspaceSetupSnapshots = new Map<string, WorkspaceSetupSnapshot>();
|
||||||
@@ -6590,8 +6609,12 @@ export class Session {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async resolveAvailableEditorTargets(): Promise<EditorTargetDescriptorPayload[]> {
|
||||||
|
return listAvailableEditorTargets();
|
||||||
|
}
|
||||||
|
|
||||||
async getAvailableEditorTargets() {
|
async getAvailableEditorTargets() {
|
||||||
return this.filterEditorsForClient(await listAvailableEditorTargets());
|
return this.filterEditorsForClient(await this.getMemoizedAvailableEditorTargets());
|
||||||
}
|
}
|
||||||
|
|
||||||
async openEditorTarget(options: { editorId: EditorTargetId; path: string }): Promise<void> {
|
async openEditorTarget(options: { editorId: EditorTargetId; path: string }): Promise<void> {
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import { tmpdir } from "node:os";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { describe, expect, test, vi } from "vitest";
|
import { describe, expect, test, vi } from "vitest";
|
||||||
import { Session } from "./session.js";
|
import { Session } from "./session.js";
|
||||||
import type { AgentSnapshotPayload } from "../shared/messages.js";
|
import type {
|
||||||
|
AgentSnapshotPayload,
|
||||||
|
EditorTargetDescriptorPayload,
|
||||||
|
SessionOutboundMessage,
|
||||||
|
} from "../shared/messages.js";
|
||||||
import type { WorkspaceGitRuntimeSnapshot } from "./workspace-git-service.js";
|
import type { WorkspaceGitRuntimeSnapshot } from "./workspace-git-service.js";
|
||||||
import {
|
import {
|
||||||
createPersistedProjectRecord,
|
createPersistedProjectRecord,
|
||||||
@@ -188,6 +192,7 @@ function createWorkspaceRuntimeSnapshot(
|
|||||||
function createSessionForWorkspaceTests(
|
function createSessionForWorkspaceTests(
|
||||||
options: {
|
options: {
|
||||||
appVersion?: string | null;
|
appVersion?: string | null;
|
||||||
|
onMessage?: (message: SessionOutboundMessage) => void;
|
||||||
workspaceGitService?: ReturnType<typeof createNoopWorkspaceGitService>;
|
workspaceGitService?: ReturnType<typeof createNoopWorkspaceGitService>;
|
||||||
} = {},
|
} = {},
|
||||||
): Session {
|
): Session {
|
||||||
@@ -203,7 +208,7 @@ function createSessionForWorkspaceTests(
|
|||||||
const session = new Session({
|
const session = new Session({
|
||||||
clientId: "test-client",
|
clientId: "test-client",
|
||||||
appVersion: options.appVersion ?? null,
|
appVersion: options.appVersion ?? null,
|
||||||
onMessage: vi.fn(),
|
onMessage: options.onMessage ?? vi.fn(),
|
||||||
logger: logger as any,
|
logger: logger as any,
|
||||||
downloadTokenStore: {} as any,
|
downloadTokenStore: {} as any,
|
||||||
pushTokenStore: {} as any,
|
pushTokenStore: {} as any,
|
||||||
@@ -1555,6 +1560,61 @@ describe("workspace aggregation", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("list_available_editors_request coalesces concurrent discovery", async () => {
|
||||||
|
const emitted: SessionOutboundMessage[] = [];
|
||||||
|
const session = createSessionForWorkspaceTests({
|
||||||
|
appVersion: "0.1.50",
|
||||||
|
onMessage: (message) => emitted.push(message),
|
||||||
|
});
|
||||||
|
let resolveDiscovery: (editors: EditorTargetDescriptorPayload[]) => void = () => {};
|
||||||
|
let discoveryCalls = 0;
|
||||||
|
|
||||||
|
vi.spyOn(session, "resolveAvailableEditorTargets").mockImplementation(async () => {
|
||||||
|
discoveryCalls += 1;
|
||||||
|
return await new Promise<EditorTargetDescriptorPayload[]>((resolve) => {
|
||||||
|
resolveDiscovery = resolve;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const first = session.handleMessage({
|
||||||
|
type: "list_available_editors_request",
|
||||||
|
requestId: "req-editors-1",
|
||||||
|
});
|
||||||
|
const second = session.handleMessage({
|
||||||
|
type: "list_available_editors_request",
|
||||||
|
requestId: "req-editors-2",
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(discoveryCalls).toBe(1);
|
||||||
|
|
||||||
|
resolveDiscovery([{ id: "cursor", label: "Cursor" }]);
|
||||||
|
await Promise.all([first, second]);
|
||||||
|
|
||||||
|
const responses = emitted.filter(
|
||||||
|
(
|
||||||
|
message,
|
||||||
|
): message is Extract<SessionOutboundMessage, { type: "list_available_editors_response" }> =>
|
||||||
|
message.type === "list_available_editors_response",
|
||||||
|
);
|
||||||
|
expect(responses).toHaveLength(2);
|
||||||
|
expect(responses.map((response) => response.payload.requestId)).toEqual([
|
||||||
|
"req-editors-1",
|
||||||
|
"req-editors-2",
|
||||||
|
]);
|
||||||
|
expect(responses.map((response) => response.payload.editors)).toEqual([
|
||||||
|
[{ id: "cursor", label: "Cursor" }],
|
||||||
|
[{ id: "cursor", label: "Cursor" }],
|
||||||
|
]);
|
||||||
|
|
||||||
|
await session.handleMessage({
|
||||||
|
type: "list_available_editors_request",
|
||||||
|
requestId: "req-editors-3",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(discoveryCalls).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
test("list_available_editors_request filters unsupported ids for legacy clients", async () => {
|
test("list_available_editors_request filters unsupported ids for legacy clients", async () => {
|
||||||
const emitted: Array<{ type: string; payload: unknown }> = [];
|
const emitted: Array<{ type: string; payload: unknown }> = [];
|
||||||
const session = createSessionForWorkspaceTests({ appVersion: "0.1.49" }) as any;
|
const session = createSessionForWorkspaceTests({ appVersion: "0.1.49" }) as any;
|
||||||
|
|||||||
Reference in New Issue
Block a user