← Files Biological Sequence & Alignment ViewerARCHIVED FILE
src/views/workspace-tracks.ts
4.58 KB · Sep 30, 2026 · 23:01 UTC
import { App } from "@modelcontextprotocol/ext-apps";
import {
SEQUENCE_LIST_WORKSPACE_TRACK_DIRECTORY_TOOL_NAME,
SEQUENCE_LOAD_WORKSPACE_TRACK_TOOL_NAME,
SEQUENCE_RESOLVE_WORKSPACE_TRACK_BUNDLE_TOOL_NAME,
sequenceListWorkspaceTrackDirectoryInputSchema,
sequenceListWorkspaceTrackDirectoryResultSchema,
sequenceLoadWorkspaceTrackInputSchema,
sequenceLoadWorkspaceTrackResultSchema,
sequenceResolveWorkspaceTrackBundleInputSchema,
sequenceResolveWorkspaceTrackBundleResultSchema,
type SequenceListWorkspaceTrackDirectoryInput,
type SequenceListWorkspaceTrackDirectoryResult,
type SequenceLoadWorkspaceTrackInput,
type SequenceLoadWorkspaceTrackResult,
type SequenceResolveWorkspaceTrackBundleInput,
type SequenceResolveWorkspaceTrackBundleResult,
} from "../workspace-track-protocol";
import { isRetryableSequencePersistenceError } from "./workbench-persistence";
type WithSignal<T> = Omit<T, "sessionId"> & { signal?: AbortSignal };
export type SequenceWorkspaceTrackBrowserClient = {
listDirectory: (
input: WithSignal<SequenceListWorkspaceTrackDirectoryInput>,
) => Promise<SequenceListWorkspaceTrackDirectoryResult>;
loadBundle: (
input: WithSignal<SequenceLoadWorkspaceTrackInput>,
) => Promise<SequenceLoadWorkspaceTrackResult>;
resolveBundle: (
input: WithSignal<SequenceResolveWorkspaceTrackBundleInput>,
) => Promise<SequenceResolveWorkspaceTrackBundleResult>;
};
export function createSequenceWorkspaceTrackBrowserClient(
app: Pick<App, "callServerTool">,
sessionId: string,
): SequenceWorkspaceTrackBrowserClient {
return {
listDirectory: async ({ signal, ...input }) => {
const parsed = sequenceListWorkspaceTrackDirectoryInputSchema.parse({
...input,
sessionId,
});
const result = await callReadOnlyToolWithRetry(
app,
{
arguments: parsed,
name: SEQUENCE_LIST_WORKSPACE_TRACK_DIRECTORY_TOOL_NAME,
},
signal,
);
return sequenceListWorkspaceTrackDirectoryResultSchema.parse(
result.structuredContent,
);
},
loadBundle: async ({ signal, ...input }) => {
const parsed = sequenceLoadWorkspaceTrackInputSchema.parse({
...input,
sessionId,
});
throwIfAborted(signal);
const result =
signal == null
? await app.callServerTool({
arguments: parsed,
name: SEQUENCE_LOAD_WORKSPACE_TRACK_TOOL_NAME,
})
: await app.callServerTool(
{
arguments: parsed,
name: SEQUENCE_LOAD_WORKSPACE_TRACK_TOOL_NAME,
},
{ signal },
);
return sequenceLoadWorkspaceTrackResultSchema.parse(
result.structuredContent,
);
},
resolveBundle: async ({ signal, ...input }) => {
const parsed = sequenceResolveWorkspaceTrackBundleInputSchema.parse({
...input,
sessionId,
});
const result = await callReadOnlyToolWithRetry(
app,
{
arguments: parsed,
name: SEQUENCE_RESOLVE_WORKSPACE_TRACK_BUNDLE_TOOL_NAME,
},
signal,
);
return sequenceResolveWorkspaceTrackBundleResultSchema.parse(
result.structuredContent,
);
},
};
}
async function callReadOnlyToolWithRetry(
app: Pick<App, "callServerTool">,
request: Parameters<App["callServerTool"]>[0],
signal?: AbortSignal,
): Promise<Awaited<ReturnType<App["callServerTool"]>>> {
let lastError: unknown;
for (let attempt = 0; attempt < 3; attempt += 1) {
try {
throwIfAborted(signal);
return signal == null
? await app.callServerTool(request)
: await app.callServerTool(request, { signal });
} catch (error) {
lastError = error;
if (!isRetryableSequencePersistenceError(error) || attempt === 2) {
throw error;
}
await abortableDelay(100 * 2 ** attempt, signal);
}
}
throw lastError;
}
function throwIfAborted(signal?: AbortSignal): void {
if (!signal?.aborted) return;
throw (
signal.reason ??
new DOMException("Workspace browsing cancelled.", "AbortError")
);
}
async function abortableDelay(
milliseconds: number,
signal?: AbortSignal,
): Promise<void> {
await new Promise<void>((resolve, reject) => {
const timeout = window.setTimeout(resolve, milliseconds);
signal?.addEventListener(
"abort",
() => {
window.clearTimeout(timeout);
reject(
signal.reason ??
new DOMException("Workspace browsing cancelled.", "AbortError"),
);
},
{ once: true },
);
});
}
SHA-256: 6ca2750696fe73cab21a82981e8bfe9a6000e2b01c94b717c5c8a574f2463241