chore: enforce erasable TypeScript syntax

This commit is contained in:
Mario Zechner
2026-05-19 23:15:39 +02:00
parent 48b6510c18
commit 06c6c324d7
17 changed files with 122 additions and 74 deletions

View File

@@ -16,6 +16,7 @@
- Check node_modules for external API type definitions instead of guessing - Check node_modules for external API type definitions instead of guessing
- **NEVER use inline imports** - no `await import("./foo.js")`, no `import("pkg").Type` in type positions, no dynamic imports for types. Always use standard top-level imports. - **NEVER use inline imports** - no `await import("./foo.js")`, no `import("pkg").Type` in type positions, no dynamic imports for types. Always use standard top-level imports.
- NEVER remove or downgrade code to fix type errors from outdated dependencies; upgrade the dependency instead - NEVER remove or downgrade code to fix type errors from outdated dependencies; upgrade the dependency instead
- In core source packages (`packages/ai/src`, `packages/agent/src`, `packages/coding-agent/src`), use only erasable TypeScript syntax compatible with Node strip-only mode. Do not use constructor parameter properties, `enum`, `namespace`/`module`, `import =`, `export =`, or other TypeScript constructs that require JavaScript emit. Use explicit fields and constructor assignments instead of parameter properties.
- Always ask before removing functionality or code that appears to be intentional - Always ask before removing functionality or code that appears to be intentional
- Do not preserve backward compatibility unless the user explicitly asks for it - Do not preserve backward compatibility unless the user explicitly asks for it
- Never hardcode key checks with, eg. `matchesKey(keyData, "ctrl+x")`. All keybindings must be configurable. Add default to matching object (`DEFAULT_EDITOR_KEYBINDINGS` or `DEFAULT_APP_KEYBINDINGS`) - Never hardcode key checks with, eg. `matchesKey(keyData, "ctrl+x")`. All keybindings must be configurable. Add default to matching object (`DEFAULT_EDITOR_KEYBINDINGS` or `DEFAULT_APP_KEYBINDINGS`)

View File

@@ -15,7 +15,8 @@
"build": "cd packages/tui && npm run build && cd ../ai && npm run build && cd ../agent && npm run build && cd ../coding-agent && npm run build && cd ../web-ui && npm run build", "build": "cd packages/tui && npm run build && cd ../ai && npm run build && cd ../agent && npm run build && cd ../coding-agent && npm run build && cd ../web-ui && npm run build",
"dev": "concurrently --names \"ai,agent,coding-agent,web-ui,tui\" --prefix-colors \"cyan,yellow,red,green,magenta\" \"cd packages/ai && npm run dev\" \"cd packages/agent && npm run dev\" \"cd packages/coding-agent && npm run dev\" \"cd packages/web-ui && npm run dev\" \"cd packages/tui && npm run dev\"", "dev": "concurrently --names \"ai,agent,coding-agent,web-ui,tui\" --prefix-colors \"cyan,yellow,red,green,magenta\" \"cd packages/ai && npm run dev\" \"cd packages/agent && npm run dev\" \"cd packages/coding-agent && npm run dev\" \"cd packages/web-ui && npm run dev\" \"cd packages/tui && npm run dev\"",
"dev:tsc": "concurrently --names \"ai,web-ui\" --prefix-colors \"cyan,green\" \"cd packages/ai && npm run dev:tsc\" \"cd packages/web-ui && npm run dev:tsc\"", "dev:tsc": "concurrently --names \"ai,web-ui\" --prefix-colors \"cyan,green\" \"cd packages/ai && npm run dev:tsc\" \"cd packages/web-ui && npm run dev:tsc\"",
"check": "biome check --write --error-on-warnings . && tsgo --noEmit && npm run check:browser-smoke && cd packages/web-ui && npm run check", "check": "biome check --write --error-on-warnings . && tsgo --noEmit && npm run check:erasable-types && npm run check:browser-smoke && cd packages/web-ui && npm run check",
"check:erasable-types": "tsgo -p packages/ai/tsconfig.build.json --noEmit --erasableSyntaxOnly && tsgo -p packages/agent/tsconfig.build.json --noEmit --erasableSyntaxOnly && tsgo -p packages/coding-agent/tsconfig.build.json --noEmit --erasableSyntaxOnly",
"check:browser-smoke": "node scripts/check-browser-smoke.mjs", "check:browser-smoke": "node scripts/check-browser-smoke.mjs",
"profile:tui": "node scripts/profile-coding-agent-node.mjs --mode tui", "profile:tui": "node scripts/profile-coding-agent-node.mjs --mode tui",
"profile:rpc": "node scripts/profile-coding-agent-node.mjs --mode rpc", "profile:rpc": "node scripts/profile-coding-agent-node.mjs --mode rpc",

View File

@@ -2,6 +2,10 @@
## [Unreleased] ## [Unreleased]
### Changed
- Changed source syntax to avoid TypeScript constructs that require JavaScript emit, keeping the package compatible with Node.js strip-only TypeScript checks.
### Fixed ### Fixed
- Fixed tool-call preflight to stop preparing sibling tool calls after the run is aborted ([#4276](https://github.com/earendil-works/pi/issues/4276)). - Fixed tool-call preflight to stop preparing sibling tool calls after the run is aborted ([#4276](https://github.com/earendil-works/pi/issues/4276)).

View File

@@ -117,8 +117,11 @@ export interface AgentOptions {
class PendingMessageQueue { class PendingMessageQueue {
private messages: AgentMessage[] = []; private messages: AgentMessage[] = [];
public mode: QueueMode;
constructor(public mode: QueueMode) {} constructor(mode: QueueMode) {
this.mode = mode;
}
enqueue(message: AgentMessage): void { enqueue(message: AgentMessage): void {
this.messages.push(message); this.messages.push(message);

View File

@@ -120,16 +120,16 @@ export type FileErrorCode =
/** Error returned by {@link FileSystem} file operations. */ /** Error returned by {@link FileSystem} file operations. */
export class FileError extends Error { export class FileError extends Error {
constructor( /** Backend-independent error code. */
/** Backend-independent error code. */ public code: FileErrorCode;
public code: FileErrorCode, /** Absolute addressed path associated with the failure, when available. */
message: string, public path?: string;
/** Absolute addressed path associated with the failure, when available. */
public path?: string, constructor(code: FileErrorCode, message: string, path?: string, cause?: Error) {
cause?: Error,
) {
super(message, cause === undefined ? undefined : { cause }); super(message, cause === undefined ? undefined : { cause });
this.name = "FileError"; this.name = "FileError";
this.code = code;
this.path = path;
} }
} }
@@ -144,14 +144,13 @@ export type ExecutionErrorCode =
/** Error returned by {@link ExecutionEnv.exec}. */ /** Error returned by {@link ExecutionEnv.exec}. */
export class ExecutionError extends Error { export class ExecutionError extends Error {
constructor( /** Backend-independent error code. */
/** Backend-independent error code. */ public code: ExecutionErrorCode;
public code: ExecutionErrorCode,
message: string, constructor(code: ExecutionErrorCode, message: string, cause?: Error) {
cause?: Error,
) {
super(message, cause === undefined ? undefined : { cause }); super(message, cause === undefined ? undefined : { cause });
this.name = "ExecutionError"; this.name = "ExecutionError";
this.code = code;
} }
} }
@@ -160,14 +159,13 @@ export type CompactionErrorCode = "aborted" | "summarization_failed" | "invalid_
/** Error returned by compaction helpers. */ /** Error returned by compaction helpers. */
export class CompactionError extends Error { export class CompactionError extends Error {
constructor( /** Backend-independent error code. */
/** Backend-independent error code. */ public code: CompactionErrorCode;
public code: CompactionErrorCode,
message: string, constructor(code: CompactionErrorCode, message: string, cause?: Error) {
cause?: Error,
) {
super(message, cause === undefined ? undefined : { cause }); super(message, cause === undefined ? undefined : { cause });
this.name = "CompactionError"; this.name = "CompactionError";
this.code = code;
} }
} }
@@ -176,14 +174,13 @@ export type BranchSummaryErrorCode = "aborted" | "summarization_failed" | "inval
/** Error returned by branch summarization helpers. */ /** Error returned by branch summarization helpers. */
export class BranchSummaryError extends Error { export class BranchSummaryError extends Error {
constructor( /** Backend-independent error code. */
/** Backend-independent error code. */ public code: BranchSummaryErrorCode;
public code: BranchSummaryErrorCode,
message: string, constructor(code: BranchSummaryErrorCode, message: string, cause?: Error) {
cause?: Error,
) {
super(message, cause === undefined ? undefined : { cause }); super(message, cause === undefined ? undefined : { cause });
this.name = "BranchSummaryError"; this.name = "BranchSummaryError";
this.code = code;
} }
} }
@@ -197,14 +194,13 @@ export type SessionErrorCode =
/** Error thrown by session storage, repositories, and session tree operations. */ /** Error thrown by session storage, repositories, and session tree operations. */
export class SessionError extends Error { export class SessionError extends Error {
constructor( /** Session subsystem error code. */
/** Session subsystem error code. */ public code: SessionErrorCode;
public code: SessionErrorCode,
message: string, constructor(code: SessionErrorCode, message: string, cause?: Error) {
cause?: Error,
) {
super(message, cause === undefined ? undefined : { cause }); super(message, cause === undefined ? undefined : { cause });
this.name = "SessionError"; this.name = "SessionError";
this.code = code;
} }
} }
@@ -221,13 +217,12 @@ export type AgentHarnessErrorCode =
/** Public AgentHarness failure with a stable top-level classification. */ /** Public AgentHarness failure with a stable top-level classification. */
export class AgentHarnessError extends Error { export class AgentHarnessError extends Error {
constructor( public code: AgentHarnessErrorCode;
public code: AgentHarnessErrorCode,
message: string, constructor(code: AgentHarnessErrorCode, message: string, cause?: Error) {
cause?: Error,
) {
super(message, cause === undefined ? undefined : { cause }); super(message, cause === undefined ? undefined : { cause });
this.name = "AgentHarnessError"; this.name = "AgentHarnessError";
this.code = code;
} }
} }

View File

@@ -2,6 +2,10 @@
## [Unreleased] ## [Unreleased]
### Changed
- Changed source syntax to avoid TypeScript constructs that require JavaScript emit, keeping the package compatible with Node.js strip-only TypeScript checks.
### Fixed ### Fixed
- Fixed OpenAI-compatible `streamSimple()` requests to stop sending model-derived default output token caps, avoiding context-window reservation failures on servers such as vLLM while preserving explicit `maxTokens` and required Anthropic `max_tokens` handling ([#4675](https://github.com/earendil-works/pi/issues/4675)). - Fixed OpenAI-compatible `streamSimple()` requests to stop sending model-derived default output token caps, avoiding context-window reservation failures on servers such as vLLM while preserving explicit `maxTokens` and required Anthropic `max_tokens` handling ([#4675](https://github.com/earendil-works/pi/issues/4675)).

View File

@@ -7,11 +7,12 @@ export class EventStream<T, R = T> implements AsyncIterable<T> {
private done = false; private done = false;
private finalResultPromise: Promise<R>; private finalResultPromise: Promise<R>;
private resolveFinalResult!: (result: R) => void; private resolveFinalResult!: (result: R) => void;
private isComplete: (event: T) => boolean;
private extractResult: (event: T) => R;
constructor( constructor(isComplete: (event: T) => boolean, extractResult: (event: T) => R) {
private isComplete: (event: T) => boolean, this.isComplete = isComplete;
private extractResult: (event: T) => R, this.extractResult = extractResult;
) {
this.finalResultPromise = new Promise((resolve) => { this.finalResultPromise = new Promise((resolve) => {
this.resolveFinalResult = resolve; this.resolveFinalResult = resolve;
}); });

View File

@@ -2,6 +2,10 @@
## [Unreleased] ## [Unreleased]
### Changed
- Changed source syntax to avoid TypeScript constructs that require JavaScript emit, keeping core sources compatible with Node.js strip-only TypeScript checks.
### Fixed ### Fixed
- Fixed the system prompt to tell models to resolve pi docs and examples under the absolute package paths before reading topic-specific relative references ([#4752](https://github.com/earendil-works/pi/issues/4752)). - Fixed the system prompt to tell models to resolve pi docs and examples under the absolute package paths before reading topic-specific relative references ([#4752](https://github.com/earendil-works/pi/issues/4752)).

View File

@@ -67,14 +67,25 @@ function extractUserMessageText(content: string | Array<{ type: string; text?: s
export class AgentSessionRuntime { export class AgentSessionRuntime {
private rebindSession?: (session: AgentSession) => Promise<void>; private rebindSession?: (session: AgentSession) => Promise<void>;
private beforeSessionInvalidate?: () => void; private beforeSessionInvalidate?: () => void;
private _session: AgentSession;
private _services: AgentSessionServices;
private readonly createRuntime: CreateAgentSessionRuntimeFactory;
private _diagnostics: AgentSessionRuntimeDiagnostic[];
private _modelFallbackMessage?: string;
constructor( constructor(
private _session: AgentSession, _session: AgentSession,
private _services: AgentSessionServices, _services: AgentSessionServices,
private readonly createRuntime: CreateAgentSessionRuntimeFactory, createRuntime: CreateAgentSessionRuntimeFactory,
private _diagnostics: AgentSessionRuntimeDiagnostic[] = [], _diagnostics: AgentSessionRuntimeDiagnostic[] = [],
private _modelFallbackMessage?: string, _modelFallbackMessage?: string,
) {} ) {
this._session = _session;
this._services = _services;
this.createRuntime = createRuntime;
this._diagnostics = _diagnostics;
this._modelFallbackMessage = _modelFallbackMessage;
}
get services(): AgentSessionServices { get services(): AgentSessionServices {
return this._services; return this._services;

View File

@@ -50,7 +50,11 @@ export interface AuthStorageBackend {
} }
export class FileAuthStorageBackend implements AuthStorageBackend { export class FileAuthStorageBackend implements AuthStorageBackend {
constructor(private authPath: string = join(getAgentDir(), "auth.json")) {} private authPath: string;
constructor(authPath: string = join(getAgentDir(), "auth.json")) {
this.authPath = authPath;
}
private ensureParentDir(): void { private ensureParentDir(): void {
const dir = dirname(this.authPath); const dir = dirname(this.authPath);
@@ -194,8 +198,10 @@ export class AuthStorage {
private fallbackResolver?: (provider: string) => string | undefined; private fallbackResolver?: (provider: string) => string | undefined;
private loadError: Error | null = null; private loadError: Error | null = null;
private errors: Error[] = []; private errors: Error[] = [];
private storage: AuthStorageBackend;
private constructor(private storage: AuthStorageBackend) { private constructor(storage: AuthStorageBackend) {
this.storage = storage;
this.reload(); this.reload();
} }

View File

@@ -334,11 +334,12 @@ export class ModelRegistry {
private modelRequestHeaders: Map<string, Record<string, string>> = new Map(); private modelRequestHeaders: Map<string, Record<string, string>> = new Map();
private registeredProviders: Map<string, ProviderConfigInput> = new Map(); private registeredProviders: Map<string, ProviderConfigInput> = new Map();
private loadError: string | undefined = undefined; private loadError: string | undefined = undefined;
readonly authStorage: AuthStorage;
private modelsJsonPath: string | undefined;
private constructor( private constructor(authStorage: AuthStorage, modelsJsonPath: string | undefined) {
readonly authStorage: AuthStorage, this.authStorage = authStorage;
private modelsJsonPath: string | undefined, this.modelsJsonPath = modelsJsonPath;
) {
this.loadModels(); this.loadModels();
} }

View File

@@ -7,13 +7,14 @@ import type { TUI } from "@earendil-works/pi-tui";
export class CountdownTimer { export class CountdownTimer {
private intervalId: ReturnType<typeof setInterval> | undefined; private intervalId: ReturnType<typeof setInterval> | undefined;
private remainingSeconds: number; private remainingSeconds: number;
private tui: TUI | undefined;
private onTick: (seconds: number) => void;
private onExpire: () => void;
constructor( constructor(timeoutMs: number, tui: TUI | undefined, onTick: (seconds: number) => void, onExpire: () => void) {
timeoutMs: number, this.tui = tui;
private tui: TUI | undefined, this.onTick = onTick;
private onTick: (seconds: number) => void, this.onExpire = onExpire;
private onExpire: () => void,
) {
this.remainingSeconds = Math.ceil(timeoutMs / 1000); this.remainingSeconds = Math.ceil(timeoutMs / 1000);
this.onTick(this.remainingSeconds); this.onTick(this.remainingSeconds);

View File

@@ -32,11 +32,13 @@ function formatTokens(count: number): string {
*/ */
export class FooterComponent implements Component { export class FooterComponent implements Component {
private autoCompactEnabled = true; private autoCompactEnabled = true;
private session: AgentSession;
private footerData: ReadonlyFooterDataProvider;
constructor( constructor(session: AgentSession, footerData: ReadonlyFooterDataProvider) {
private session: AgentSession, this.session = session;
private footerData: ReadonlyFooterDataProvider, this.footerData = footerData;
) {} }
setSession(session: AgentSession): void { setSession(session: AgentSession): void {
this.session = session; this.session = session;

View File

@@ -15,6 +15,7 @@ export class LoginDialogComponent extends Container implements Focusable {
private abortController = new AbortController(); private abortController = new AbortController();
private inputResolver?: (value: string) => void; private inputResolver?: (value: string) => void;
private inputRejecter?: (error: Error) => void; private inputRejecter?: (error: Error) => void;
private onComplete: (success: boolean, message?: string) => void;
// Focusable implementation - propagate to input for IME cursor positioning // Focusable implementation - propagate to input for IME cursor positioning
private _focused = false; private _focused = false;
@@ -29,12 +30,13 @@ export class LoginDialogComponent extends Container implements Focusable {
constructor( constructor(
tui: TUI, tui: TUI,
providerId: string, providerId: string,
private onComplete: (success: boolean, message?: string) => void, onComplete: (success: boolean, message?: string) => void,
providerNameOverride?: string, providerNameOverride?: string,
titleOverride?: string, titleOverride?: string,
) { ) {
super(); super();
this.tui = tui; this.tui = tui;
this.onComplete = onComplete;
const providerInfo = getOAuthProviders().find((p) => p.id === providerId); const providerInfo = getOAuthProviders().find((p) => p.id === providerId);
const providerName = providerNameOverride || providerInfo?.name || providerId; const providerName = providerNameOverride || providerInfo?.name || providerId;

View File

@@ -1056,7 +1056,11 @@ class TreeList implements Component {
/** Component that displays the current search query */ /** Component that displays the current search query */
class SearchLine implements Component { class SearchLine implements Component {
constructor(private treeList: TreeList) {} private treeList: TreeList;
constructor(treeList: TreeList) {
this.treeList = treeList;
}
invalidate(): void {} invalidate(): void {}

View File

@@ -147,14 +147,19 @@ function isExpandable(obj: unknown): obj is Expandable {
} }
class ExpandableText extends Text implements Expandable { class ExpandableText extends Text implements Expandable {
private readonly getCollapsedText: () => string;
private readonly getExpandedText: () => string;
constructor( constructor(
private readonly getCollapsedText: () => string, getCollapsedText: () => string,
private readonly getExpandedText: () => string, getExpandedText: () => string,
expanded = false, expanded = false,
paddingX = 0, paddingX = 0,
paddingY = 0, paddingY = 0,
) { ) {
super(expanded ? getExpandedText() : getCollapsedText(), paddingX, paddingY); super(expanded ? getExpandedText() : getCollapsedText(), paddingX, paddingY);
this.getCollapsedText = getCollapsedText;
this.getExpandedText = getExpandedText;
} }
setExpanded(expanded: boolean): void { setExpanded(expanded: boolean): void {
@@ -334,6 +339,8 @@ export class InteractiveMode {
// Custom header from extension (undefined = use built-in header) // Custom header from extension (undefined = use built-in header)
private customHeader: (Component & { dispose?(): void }) | undefined = undefined; private customHeader: (Component & { dispose?(): void }) | undefined = undefined;
private options: InteractiveModeOptions;
// Convenience accessors // Convenience accessors
private get session(): AgentSession { private get session(): AgentSession {
return this.runtimeHost.session; return this.runtimeHost.session;
@@ -348,11 +355,9 @@ export class InteractiveMode {
return this.session.settingsManager; return this.session.settingsManager;
} }
constructor( constructor(runtimeHost: AgentSessionRuntime, options: InteractiveModeOptions = {}) {
runtimeHost: AgentSessionRuntime,
private options: InteractiveModeOptions = {},
) {
this.runtimeHost = runtimeHost; this.runtimeHost = runtimeHost;
this.options = options;
this.runtimeHost.setBeforeSessionInvalidate(() => { this.runtimeHost.setBeforeSessionInvalidate(() => {
this.resetExtensionUI(); this.resetExtensionUI();
}); });

View File

@@ -59,8 +59,11 @@ export class RpcClient {
new Map(); new Map();
private requestId = 0; private requestId = 0;
private stderr = ""; private stderr = "";
private options: RpcClientOptions;
constructor(private options: RpcClientOptions = {}) {} constructor(options: RpcClientOptions = {}) {
this.options = options;
}
/** /**
* Start the RPC agent process. * Start the RPC agent process.