mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 16:25:51 +00:00
style: 修复所有 lint 错误,覆盖 @ant forked 代码
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
"useIgnoreFile": true
|
"useIgnoreFile": true
|
||||||
},
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"includes": ["**", "!!**/dist", "!!**/packages/@ant"]
|
"includes": ["**", "!!**/dist"]
|
||||||
},
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,327 +1,324 @@
|
|||||||
import { promises as fsPromises } from "fs";
|
import { promises as fsPromises } from 'fs'
|
||||||
import { createConnection } from "net";
|
import { createConnection } from 'net'
|
||||||
import type { Socket } from "net";
|
import type { Socket } from 'net'
|
||||||
import { platform } from "os";
|
import { platform } from 'os'
|
||||||
import { dirname } from "path";
|
import { dirname } from 'path'
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ClaudeForChromeContext,
|
ClaudeForChromeContext,
|
||||||
PermissionMode,
|
PermissionMode,
|
||||||
PermissionOverrides,
|
PermissionOverrides,
|
||||||
} from "./types.js";
|
} from './types.js'
|
||||||
|
|
||||||
export class SocketConnectionError extends Error {
|
export class SocketConnectionError extends Error {
|
||||||
constructor(message: string) {
|
constructor(message: string) {
|
||||||
super(message);
|
super(message)
|
||||||
this.name = "SocketConnectionError";
|
this.name = 'SocketConnectionError'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ToolRequest {
|
interface ToolRequest {
|
||||||
method: string; // "execute_tool"
|
method: string // "execute_tool"
|
||||||
params?: {
|
params?: {
|
||||||
client_id?: string; // "desktop" | "claude-code"
|
client_id?: string // "desktop" | "claude-code"
|
||||||
tool?: string;
|
tool?: string
|
||||||
args?: Record<string, unknown>;
|
args?: Record<string, unknown>
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ToolResponse {
|
interface ToolResponse {
|
||||||
result?: unknown;
|
result?: unknown
|
||||||
error?: string;
|
error?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
method: string;
|
method: string
|
||||||
params?: Record<string, unknown>;
|
params?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
type SocketMessage = ToolResponse | Notification;
|
type SocketMessage = ToolResponse | Notification
|
||||||
|
|
||||||
function isToolResponse(message: SocketMessage): message is ToolResponse {
|
function isToolResponse(message: SocketMessage): message is ToolResponse {
|
||||||
return "result" in message || "error" in message;
|
return 'result' in message || 'error' in message
|
||||||
}
|
}
|
||||||
|
|
||||||
function isNotification(message: SocketMessage): message is Notification {
|
function isNotification(message: SocketMessage): message is Notification {
|
||||||
return "method" in message && typeof message.method === "string";
|
return 'method' in message && typeof message.method === 'string'
|
||||||
}
|
}
|
||||||
|
|
||||||
class McpSocketClient {
|
class McpSocketClient {
|
||||||
private socket: Socket | null = null;
|
private socket: Socket | null = null
|
||||||
private connected = false;
|
private connected = false
|
||||||
private connecting = false;
|
private connecting = false
|
||||||
private responseCallback: ((response: ToolResponse) => void) | null = null;
|
private responseCallback: ((response: ToolResponse) => void) | null = null
|
||||||
private notificationHandler: ((notification: Notification) => void) | null =
|
private notificationHandler: ((notification: Notification) => void) | null =
|
||||||
null;
|
null
|
||||||
private responseBuffer = Buffer.alloc(0);
|
private responseBuffer = Buffer.alloc(0)
|
||||||
private reconnectAttempts = 0;
|
private reconnectAttempts = 0
|
||||||
private maxReconnectAttempts = 10;
|
private maxReconnectAttempts = 10
|
||||||
private reconnectDelay = 1000;
|
private reconnectDelay = 1000
|
||||||
private reconnectTimer: NodeJS.Timeout | null = null;
|
private reconnectTimer: NodeJS.Timeout | null = null
|
||||||
private context: ClaudeForChromeContext;
|
private context: ClaudeForChromeContext
|
||||||
// When true, disables automatic reconnection. Used by McpSocketPool which
|
// When true, disables automatic reconnection. Used by McpSocketPool which
|
||||||
// manages reconnection externally by rescanning available sockets.
|
// manages reconnection externally by rescanning available sockets.
|
||||||
public disableAutoReconnect = false;
|
public disableAutoReconnect = false
|
||||||
|
|
||||||
constructor(context: ClaudeForChromeContext) {
|
constructor(context: ClaudeForChromeContext) {
|
||||||
this.context = context;
|
this.context = context
|
||||||
}
|
}
|
||||||
|
|
||||||
private async connect(): Promise<void> {
|
private async connect(): Promise<void> {
|
||||||
const { serverName, logger } = this.context;
|
const { serverName, logger } = this.context
|
||||||
|
|
||||||
if (this.connecting) {
|
if (this.connecting) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`[${serverName}] Already connecting, skipping duplicate attempt`,
|
`[${serverName}] Already connecting, skipping duplicate attempt`,
|
||||||
);
|
)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.closeSocket();
|
this.closeSocket()
|
||||||
this.connecting = true;
|
this.connecting = true
|
||||||
|
|
||||||
const socketPath =
|
const socketPath = this.context.getSocketPath?.() ?? this.context.socketPath
|
||||||
this.context.getSocketPath?.() ?? this.context.socketPath;
|
logger.info(`[${serverName}] Attempting to connect to: ${socketPath}`)
|
||||||
logger.info(`[${serverName}] Attempting to connect to: ${socketPath}`);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.validateSocketSecurity(socketPath);
|
await this.validateSocketSecurity(socketPath)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.connecting = false;
|
this.connecting = false
|
||||||
logger.info(`[${serverName}] Security validation failed:`, error);
|
logger.info(`[${serverName}] Security validation failed:`, error)
|
||||||
// Don't retry on security failures (wrong perms/owner) - those won't
|
// Don't retry on security failures (wrong perms/owner) - those won't
|
||||||
// self-resolve. Only the error handler retries on transient errors.
|
// self-resolve. Only the error handler retries on transient errors.
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.socket = createConnection(socketPath);
|
this.socket = createConnection(socketPath)
|
||||||
|
|
||||||
// Timeout the initial connection attempt - if socket file exists but native
|
// Timeout the initial connection attempt - if socket file exists but native
|
||||||
// host is dead, the connect can hang indefinitely
|
// host is dead, the connect can hang indefinitely
|
||||||
const connectTimeout = setTimeout(() => {
|
const connectTimeout = setTimeout(() => {
|
||||||
if (!this.connected) {
|
if (!this.connected) {
|
||||||
logger.info(
|
logger.info(`[${serverName}] Connection attempt timed out after 5000ms`)
|
||||||
`[${serverName}] Connection attempt timed out after 5000ms`,
|
this.closeSocket()
|
||||||
);
|
this.scheduleReconnect()
|
||||||
this.closeSocket();
|
|
||||||
this.scheduleReconnect();
|
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000)
|
||||||
|
|
||||||
this.socket.on("connect", () => {
|
this.socket.on('connect', () => {
|
||||||
clearTimeout(connectTimeout);
|
clearTimeout(connectTimeout)
|
||||||
this.connected = true;
|
this.connected = true
|
||||||
this.connecting = false;
|
this.connecting = false
|
||||||
this.reconnectAttempts = 0;
|
this.reconnectAttempts = 0
|
||||||
logger.info(`[${serverName}] Successfully connected to bridge server`);
|
logger.info(`[${serverName}] Successfully connected to bridge server`)
|
||||||
});
|
})
|
||||||
|
|
||||||
this.socket.on("data", (data: Buffer) => {
|
this.socket.on('data', (data: Buffer) => {
|
||||||
this.responseBuffer = Buffer.concat([this.responseBuffer, data]);
|
this.responseBuffer = Buffer.concat([this.responseBuffer, data])
|
||||||
|
|
||||||
while (this.responseBuffer.length >= 4) {
|
while (this.responseBuffer.length >= 4) {
|
||||||
const length = this.responseBuffer.readUInt32LE(0);
|
const length = this.responseBuffer.readUInt32LE(0)
|
||||||
|
|
||||||
if (this.responseBuffer.length < 4 + length) {
|
if (this.responseBuffer.length < 4 + length) {
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageBytes = this.responseBuffer.slice(4, 4 + length);
|
const messageBytes = this.responseBuffer.slice(4, 4 + length)
|
||||||
this.responseBuffer = this.responseBuffer.slice(4 + length);
|
this.responseBuffer = this.responseBuffer.slice(4 + length)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const message = JSON.parse(
|
const message = JSON.parse(
|
||||||
messageBytes.toString("utf-8"),
|
messageBytes.toString('utf-8'),
|
||||||
) as SocketMessage;
|
) as SocketMessage
|
||||||
|
|
||||||
if (isNotification(message)) {
|
if (isNotification(message)) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`[${serverName}] Received notification: ${message.method}`,
|
`[${serverName}] Received notification: ${message.method}`,
|
||||||
);
|
)
|
||||||
if (this.notificationHandler) {
|
if (this.notificationHandler) {
|
||||||
this.notificationHandler(message);
|
this.notificationHandler(message)
|
||||||
}
|
}
|
||||||
} else if (isToolResponse(message)) {
|
} else if (isToolResponse(message)) {
|
||||||
logger.info(`[${serverName}] Received tool response: ${message}`);
|
logger.info(`[${serverName}] Received tool response: ${message}`)
|
||||||
this.handleResponse(message);
|
this.handleResponse(message)
|
||||||
} else {
|
} else {
|
||||||
logger.info(`[${serverName}] Received unknown message: ${message}`);
|
logger.info(`[${serverName}] Received unknown message: ${message}`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.info(`[${serverName}] Failed to parse message:`, error);
|
logger.info(`[${serverName}] Failed to parse message:`, error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
this.socket.on("error", (error: Error & { code?: string }) => {
|
this.socket.on('error', (error: Error & { code?: string }) => {
|
||||||
clearTimeout(connectTimeout);
|
clearTimeout(connectTimeout)
|
||||||
logger.info(`[${serverName}] Socket error (code: ${error.code}):`, error);
|
logger.info(`[${serverName}] Socket error (code: ${error.code}):`, error)
|
||||||
this.connected = false;
|
this.connected = false
|
||||||
this.connecting = false;
|
this.connecting = false
|
||||||
|
|
||||||
if (
|
if (
|
||||||
error.code &&
|
error.code &&
|
||||||
[
|
[
|
||||||
"ECONNREFUSED", // Native host not listening (stale socket)
|
'ECONNREFUSED', // Native host not listening (stale socket)
|
||||||
"ECONNRESET", // Connection reset by peer
|
'ECONNRESET', // Connection reset by peer
|
||||||
"EPIPE", // Broken pipe (native host died mid-write)
|
'EPIPE', // Broken pipe (native host died mid-write)
|
||||||
"ENOENT", // Socket file was deleted
|
'ENOENT', // Socket file was deleted
|
||||||
"EOPNOTSUPP", // Socket file exists but is not a valid socket
|
'EOPNOTSUPP', // Socket file exists but is not a valid socket
|
||||||
"ECONNABORTED", // Connection aborted
|
'ECONNABORTED', // Connection aborted
|
||||||
].includes(error.code)
|
].includes(error.code)
|
||||||
) {
|
) {
|
||||||
this.scheduleReconnect();
|
this.scheduleReconnect()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
this.socket.on("close", () => {
|
this.socket.on('close', () => {
|
||||||
clearTimeout(connectTimeout);
|
clearTimeout(connectTimeout)
|
||||||
this.connected = false;
|
this.connected = false
|
||||||
this.connecting = false;
|
this.connecting = false
|
||||||
this.scheduleReconnect();
|
this.scheduleReconnect()
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private scheduleReconnect(): void {
|
private scheduleReconnect(): void {
|
||||||
const { serverName, logger } = this.context;
|
const { serverName, logger } = this.context
|
||||||
|
|
||||||
if (this.disableAutoReconnect) {
|
if (this.disableAutoReconnect) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.reconnectTimer) {
|
if (this.reconnectTimer) {
|
||||||
logger.info(`[${serverName}] Reconnect already scheduled, skipping`);
|
logger.info(`[${serverName}] Reconnect already scheduled, skipping`)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.reconnectAttempts++;
|
this.reconnectAttempts++
|
||||||
|
|
||||||
// Give up after extended polling (~50 min). A new ensureConnected() call
|
// Give up after extended polling (~50 min). A new ensureConnected() call
|
||||||
// from a tool request will restart the cycle if needed.
|
// from a tool request will restart the cycle if needed.
|
||||||
const maxTotalAttempts = 100;
|
const maxTotalAttempts = 100
|
||||||
if (this.reconnectAttempts > maxTotalAttempts) {
|
if (this.reconnectAttempts > maxTotalAttempts) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`[${serverName}] Giving up after ${maxTotalAttempts} attempts. Will retry on next tool call.`,
|
`[${serverName}] Giving up after ${maxTotalAttempts} attempts. Will retry on next tool call.`,
|
||||||
);
|
)
|
||||||
this.reconnectAttempts = 0;
|
this.reconnectAttempts = 0
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use aggressive backoff for first 10 attempts, then slow poll every 30s.
|
// Use aggressive backoff for first 10 attempts, then slow poll every 30s.
|
||||||
const delay = Math.min(
|
const delay = Math.min(
|
||||||
this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1),
|
this.reconnectDelay * 1.5 ** (this.reconnectAttempts - 1),
|
||||||
30000,
|
30000,
|
||||||
);
|
)
|
||||||
|
|
||||||
if (this.reconnectAttempts <= this.maxReconnectAttempts) {
|
if (this.reconnectAttempts <= this.maxReconnectAttempts) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`[${serverName}] Reconnecting in ${Math.round(delay)}ms (attempt ${
|
`[${serverName}] Reconnecting in ${Math.round(delay)}ms (attempt ${
|
||||||
this.reconnectAttempts
|
this.reconnectAttempts
|
||||||
})`,
|
})`,
|
||||||
);
|
)
|
||||||
} else if (this.reconnectAttempts % 10 === 0) {
|
} else if (this.reconnectAttempts % 10 === 0) {
|
||||||
// Log every 10th slow-poll attempt to avoid log spam
|
// Log every 10th slow-poll attempt to avoid log spam
|
||||||
logger.info(
|
logger.info(
|
||||||
`[${serverName}] Still polling for native host (attempt ${this.reconnectAttempts})`,
|
`[${serverName}] Still polling for native host (attempt ${this.reconnectAttempts})`,
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.reconnectTimer = setTimeout(() => {
|
this.reconnectTimer = setTimeout(() => {
|
||||||
this.reconnectTimer = null;
|
this.reconnectTimer = null
|
||||||
void this.connect();
|
void this.connect()
|
||||||
}, delay);
|
}, delay)
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleResponse(response: ToolResponse): void {
|
private handleResponse(response: ToolResponse): void {
|
||||||
if (this.responseCallback) {
|
if (this.responseCallback) {
|
||||||
const callback = this.responseCallback;
|
const callback = this.responseCallback
|
||||||
this.responseCallback = null;
|
this.responseCallback = null
|
||||||
callback(response);
|
callback(response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public setNotificationHandler(
|
public setNotificationHandler(
|
||||||
handler: (notification: Notification) => void,
|
handler: (notification: Notification) => void,
|
||||||
): void {
|
): void {
|
||||||
this.notificationHandler = handler;
|
this.notificationHandler = handler
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ensureConnected(): Promise<boolean> {
|
public async ensureConnected(): Promise<boolean> {
|
||||||
const { serverName } = this.context;
|
const { serverName } = this.context
|
||||||
|
|
||||||
if (this.connected && this.socket) {
|
if (this.connected && this.socket) {
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.socket && !this.connecting) {
|
if (!this.socket && !this.connecting) {
|
||||||
await this.connect();
|
await this.connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for connection with timeout
|
// Wait for connection with timeout
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let checkTimeoutId: NodeJS.Timeout | null = null;
|
let checkTimeoutId: NodeJS.Timeout | null = null
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
if (checkTimeoutId) {
|
if (checkTimeoutId) {
|
||||||
clearTimeout(checkTimeoutId);
|
clearTimeout(checkTimeoutId)
|
||||||
}
|
}
|
||||||
reject(
|
reject(
|
||||||
new SocketConnectionError(
|
new SocketConnectionError(
|
||||||
`[${serverName}] Connection attempt timed out after 5000ms`,
|
`[${serverName}] Connection attempt timed out after 5000ms`,
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
}, 5000);
|
}, 5000)
|
||||||
|
|
||||||
const checkConnection = () => {
|
const checkConnection = () => {
|
||||||
if (this.connected) {
|
if (this.connected) {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout)
|
||||||
resolve(true);
|
resolve(true)
|
||||||
} else {
|
} else {
|
||||||
checkTimeoutId = setTimeout(checkConnection, 500);
|
checkTimeoutId = setTimeout(checkConnection, 500)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
checkConnection();
|
checkConnection()
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private async sendRequest(
|
private async sendRequest(
|
||||||
request: ToolRequest,
|
request: ToolRequest,
|
||||||
timeoutMs = 30000,
|
timeoutMs = 30000,
|
||||||
): Promise<ToolResponse> {
|
): Promise<ToolResponse> {
|
||||||
const { serverName } = this.context;
|
const { serverName } = this.context
|
||||||
|
|
||||||
if (!this.socket) {
|
if (!this.socket) {
|
||||||
throw new SocketConnectionError(
|
throw new SocketConnectionError(
|
||||||
`[${serverName}] Cannot send request: not connected`,
|
`[${serverName}] Cannot send request: not connected`,
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const socket = this.socket;
|
const socket = this.socket
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
this.responseCallback = null;
|
this.responseCallback = null
|
||||||
reject(
|
reject(
|
||||||
new SocketConnectionError(
|
new SocketConnectionError(
|
||||||
`[${serverName}] Tool request timed out after ${timeoutMs}ms`,
|
`[${serverName}] Tool request timed out after ${timeoutMs}ms`,
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
}, timeoutMs);
|
}, timeoutMs)
|
||||||
|
|
||||||
this.responseCallback = (response) => {
|
this.responseCallback = response => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout)
|
||||||
resolve(response);
|
resolve(response)
|
||||||
};
|
}
|
||||||
|
|
||||||
const requestJson = JSON.stringify(request);
|
const requestJson = JSON.stringify(request)
|
||||||
const requestBytes = Buffer.from(requestJson, "utf-8");
|
const requestBytes = Buffer.from(requestJson, 'utf-8')
|
||||||
|
|
||||||
const lengthPrefix = Buffer.allocUnsafe(4);
|
const lengthPrefix = Buffer.allocUnsafe(4)
|
||||||
lengthPrefix.writeUInt32LE(requestBytes.length, 0);
|
lengthPrefix.writeUInt32LE(requestBytes.length, 0)
|
||||||
|
|
||||||
const message = Buffer.concat([lengthPrefix, requestBytes]);
|
const message = Buffer.concat([lengthPrefix, requestBytes])
|
||||||
socket.write(message);
|
socket.write(message)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public async callTool(
|
public async callTool(
|
||||||
@@ -330,15 +327,15 @@ class McpSocketClient {
|
|||||||
_permissionOverrides?: PermissionOverrides,
|
_permissionOverrides?: PermissionOverrides,
|
||||||
): Promise<unknown> {
|
): Promise<unknown> {
|
||||||
const request: ToolRequest = {
|
const request: ToolRequest = {
|
||||||
method: "execute_tool",
|
method: 'execute_tool',
|
||||||
params: {
|
params: {
|
||||||
client_id: this.context.clientTypeId,
|
client_id: this.context.clientTypeId,
|
||||||
tool: name,
|
tool: name,
|
||||||
args,
|
args,
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
||||||
return this.sendRequestWithRetry(request);
|
return this.sendRequestWithRetry(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -349,23 +346,23 @@ class McpSocketClient {
|
|||||||
* and retry once.
|
* and retry once.
|
||||||
*/
|
*/
|
||||||
private async sendRequestWithRetry(request: ToolRequest): Promise<unknown> {
|
private async sendRequestWithRetry(request: ToolRequest): Promise<unknown> {
|
||||||
const { serverName, logger } = this.context;
|
const { serverName, logger } = this.context
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.sendRequest(request);
|
return await this.sendRequest(request)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!(error instanceof SocketConnectionError)) {
|
if (!(error instanceof SocketConnectionError)) {
|
||||||
throw error;
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`[${serverName}] Connection error, forcing reconnect and retrying: ${error.message}`,
|
`[${serverName}] Connection error, forcing reconnect and retrying: ${error.message}`,
|
||||||
);
|
)
|
||||||
|
|
||||||
this.closeSocket();
|
this.closeSocket()
|
||||||
await this.ensureConnected();
|
await this.ensureConnected()
|
||||||
|
|
||||||
return await this.sendRequest(request);
|
return await this.sendRequest(request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,109 +374,109 @@ class McpSocketClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public isConnected(): boolean {
|
public isConnected(): boolean {
|
||||||
return this.connected;
|
return this.connected
|
||||||
}
|
}
|
||||||
|
|
||||||
private closeSocket(): void {
|
private closeSocket(): void {
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
this.socket.removeAllListeners();
|
this.socket.removeAllListeners()
|
||||||
this.socket.end();
|
this.socket.end()
|
||||||
this.socket.destroy();
|
this.socket.destroy()
|
||||||
this.socket = null;
|
this.socket = null
|
||||||
}
|
}
|
||||||
this.connected = false;
|
this.connected = false
|
||||||
this.connecting = false;
|
this.connecting = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private cleanup(): void {
|
private cleanup(): void {
|
||||||
if (this.reconnectTimer) {
|
if (this.reconnectTimer) {
|
||||||
clearTimeout(this.reconnectTimer);
|
clearTimeout(this.reconnectTimer)
|
||||||
this.reconnectTimer = null;
|
this.reconnectTimer = null
|
||||||
}
|
}
|
||||||
|
|
||||||
this.closeSocket();
|
this.closeSocket()
|
||||||
this.reconnectAttempts = 0;
|
this.reconnectAttempts = 0
|
||||||
this.responseBuffer = Buffer.alloc(0);
|
this.responseBuffer = Buffer.alloc(0)
|
||||||
this.responseCallback = null;
|
this.responseCallback = null
|
||||||
}
|
}
|
||||||
|
|
||||||
public disconnect(): void {
|
public disconnect(): void {
|
||||||
this.cleanup();
|
this.cleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
private async validateSocketSecurity(socketPath: string): Promise<void> {
|
private async validateSocketSecurity(socketPath: string): Promise<void> {
|
||||||
const { serverName, logger } = this.context;
|
const { serverName, logger } = this.context
|
||||||
if (platform() === "win32") {
|
if (platform() === 'win32') {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// Validate the parent directory permissions if it's the socket directory
|
// Validate the parent directory permissions if it's the socket directory
|
||||||
// (not /tmp itself, which has mode 1777 for legacy single-socket paths)
|
// (not /tmp itself, which has mode 1777 for legacy single-socket paths)
|
||||||
const dirPath = dirname(socketPath);
|
const dirPath = dirname(socketPath)
|
||||||
const dirBasename = dirPath.split("/").pop() || "";
|
const dirBasename = dirPath.split('/').pop() || ''
|
||||||
const isSocketDir = dirBasename.startsWith("claude-mcp-browser-bridge-");
|
const isSocketDir = dirBasename.startsWith('claude-mcp-browser-bridge-')
|
||||||
if (isSocketDir) {
|
if (isSocketDir) {
|
||||||
try {
|
try {
|
||||||
const dirStats = await fsPromises.stat(dirPath);
|
const dirStats = await fsPromises.stat(dirPath)
|
||||||
if (dirStats.isDirectory()) {
|
if (dirStats.isDirectory()) {
|
||||||
const dirMode = dirStats.mode & 0o777;
|
const dirMode = dirStats.mode & 0o777
|
||||||
if (dirMode !== 0o700) {
|
if (dirMode !== 0o700) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`[${serverName}] Insecure socket directory permissions: ${dirMode.toString(
|
`[${serverName}] Insecure socket directory permissions: ${dirMode.toString(
|
||||||
8,
|
8,
|
||||||
)} (expected 0700). Directory may have been tampered with.`,
|
)} (expected 0700). Directory may have been tampered with.`,
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
const currentUid = process.getuid?.();
|
const currentUid = process.getuid?.()
|
||||||
if (currentUid !== undefined && dirStats.uid !== currentUid) {
|
if (currentUid !== undefined && dirStats.uid !== currentUid) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Socket directory not owned by current user (uid: ${currentUid}, dir uid: ${dirStats.uid}). ` +
|
`Socket directory not owned by current user (uid: ${currentUid}, dir uid: ${dirStats.uid}). ` +
|
||||||
`Potential security risk.`,
|
`Potential security risk.`,
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (dirError) {
|
} catch (dirError) {
|
||||||
if ((dirError as NodeJS.ErrnoException).code !== "ENOENT") {
|
if ((dirError as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||||
throw dirError;
|
throw dirError
|
||||||
}
|
}
|
||||||
// Directory doesn't exist yet - native host will create it
|
// Directory doesn't exist yet - native host will create it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const stats = await fsPromises.stat(socketPath);
|
const stats = await fsPromises.stat(socketPath)
|
||||||
|
|
||||||
if (!stats.isSocket()) {
|
if (!stats.isSocket()) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`[${serverName}] Path exists but it's not a socket: ${socketPath}`,
|
`[${serverName}] Path exists but it's not a socket: ${socketPath}`,
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const mode = stats.mode & 0o777;
|
const mode = stats.mode & 0o777
|
||||||
if (mode !== 0o600) {
|
if (mode !== 0o600) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`[${serverName}] Insecure socket permissions: ${mode.toString(
|
`[${serverName}] Insecure socket permissions: ${mode.toString(
|
||||||
8,
|
8,
|
||||||
)} (expected 0600). Socket may have been tampered with.`,
|
)} (expected 0600). Socket may have been tampered with.`,
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentUid = process.getuid?.();
|
const currentUid = process.getuid?.()
|
||||||
if (currentUid !== undefined && stats.uid !== currentUid) {
|
if (currentUid !== undefined && stats.uid !== currentUid) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Socket not owned by current user (uid: ${currentUid}, socket uid: ${stats.uid}). ` +
|
`Socket not owned by current user (uid: ${currentUid}, socket uid: ${stats.uid}). ` +
|
||||||
`Potential security risk.`,
|
`Potential security risk.`,
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`[${serverName}] Socket security validation passed`);
|
logger.info(`[${serverName}] Socket security validation passed`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||||
logger.info(
|
logger.info(
|
||||||
`[${serverName}] Socket not found, will be created by server`,
|
`[${serverName}] Socket not found, will be created by server`,
|
||||||
);
|
)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
throw error;
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -487,7 +484,7 @@ class McpSocketClient {
|
|||||||
export function createMcpSocketClient(
|
export function createMcpSocketClient(
|
||||||
context: ClaudeForChromeContext,
|
context: ClaudeForChromeContext,
|
||||||
): McpSocketClient {
|
): McpSocketClient {
|
||||||
return new McpSocketClient(context);
|
return new McpSocketClient(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { McpSocketClient };
|
export type { McpSocketClient }
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -67,7 +67,7 @@ export class LogUpdate {
|
|||||||
const { screen } = frame
|
const { screen } = frame
|
||||||
const lines: string[] = []
|
const lines: string[] = []
|
||||||
let currentStyles: AnsiCode[] = []
|
let currentStyles: AnsiCode[] = []
|
||||||
let currentHyperlink: Hyperlink = undefined
|
let currentHyperlink: Hyperlink
|
||||||
for (let y = 0; y < screen.height; y++) {
|
for (let y = 0; y < screen.height; y++) {
|
||||||
let line = ''
|
let line = ''
|
||||||
for (let x = 0; x < screen.width; x++) {
|
for (let x = 0; x < screen.width; x++) {
|
||||||
@@ -301,7 +301,7 @@ export class LogUpdate {
|
|||||||
cursorRestoreScroll
|
cursorRestoreScroll
|
||||||
|
|
||||||
let currentStyleId = stylePool.none
|
let currentStyleId = stylePool.none
|
||||||
let currentHyperlink: Hyperlink = undefined
|
let currentHyperlink: Hyperlink
|
||||||
|
|
||||||
// First pass: render changes to existing rows (rows < prev.screen.height)
|
// First pass: render changes to existing rows (rows < prev.screen.height)
|
||||||
let needsFullReset = false
|
let needsFullReset = false
|
||||||
@@ -533,7 +533,7 @@ function renderFrameSlice(
|
|||||||
stylePool: StylePool,
|
stylePool: StylePool,
|
||||||
): VirtualScreen {
|
): VirtualScreen {
|
||||||
let currentStyleId = stylePool.none
|
let currentStyleId = stylePool.none
|
||||||
let currentHyperlink: Hyperlink = undefined
|
let currentHyperlink: Hyperlink
|
||||||
// Track the styleId of the last rendered cell on this line (-1 if none).
|
// Track the styleId of the last rendered cell on this line (-1 if none).
|
||||||
// Passed to visibleCellAtIndex to enable fg-only space optimization.
|
// Passed to visibleCellAtIndex to enable fg-only space optimization.
|
||||||
let lastRenderedStyleId = -1
|
let lastRenderedStyleId = -1
|
||||||
|
|||||||
@@ -8,18 +8,17 @@ import { Buffer } from 'buffer'
|
|||||||
import { PASTE_END, PASTE_START } from './termio/csi.js'
|
import { PASTE_END, PASTE_START } from './termio/csi.js'
|
||||||
import { createTokenizer, type Tokenizer } from './termio/tokenize.js'
|
import { createTokenizer, type Tokenizer } from './termio/tokenize.js'
|
||||||
|
|
||||||
// eslint-disable-next-line no-control-regex
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: terminal escape sequence parsing
|
||||||
const META_KEY_CODE_RE = /^(?:\x1b)([a-zA-Z0-9])$/
|
const META_KEY_CODE_RE = /^(?:\x1b)([a-zA-Z0-9])$/
|
||||||
|
|
||||||
// eslint-disable-next-line no-control-regex
|
|
||||||
const FN_KEY_RE =
|
const FN_KEY_RE =
|
||||||
// eslint-disable-next-line no-control-regex
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: terminal escape sequence parsing
|
||||||
/^(?:\x1b+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/
|
/^(?:\x1b+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/
|
||||||
|
|
||||||
// CSI u (kitty keyboard protocol): ESC [ codepoint [; modifier] u
|
// CSI u (kitty keyboard protocol): ESC [ codepoint [; modifier] u
|
||||||
// Example: ESC[13;2u = Shift+Enter, ESC[27u = Escape (no modifiers)
|
// Example: ESC[13;2u = Shift+Enter, ESC[27u = Escape (no modifiers)
|
||||||
// Modifier is optional - when absent, defaults to 1 (no modifiers)
|
// Modifier is optional - when absent, defaults to 1 (no modifiers)
|
||||||
// eslint-disable-next-line no-control-regex
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: terminal escape sequence parsing
|
||||||
const CSI_U_RE = /^\x1b\[(\d+)(?:;(\d+))?u/
|
const CSI_U_RE = /^\x1b\[(\d+)(?:;(\d+))?u/
|
||||||
|
|
||||||
// xterm modifyOtherKeys: ESC [ 27 ; modifier ; keycode ~
|
// xterm modifyOtherKeys: ESC [ 27 ; modifier ; keycode ~
|
||||||
@@ -27,41 +26,41 @@ const CSI_U_RE = /^\x1b\[(\d+)(?:;(\d+))?u/
|
|||||||
// modifyOtherKeys=2 is active or via user keybinds, typically over SSH where
|
// modifyOtherKeys=2 is active or via user keybinds, typically over SSH where
|
||||||
// TERM sniffing misses Ghostty and we never push Kitty keyboard mode.
|
// TERM sniffing misses Ghostty and we never push Kitty keyboard mode.
|
||||||
// Note param order is reversed vs CSI u (modifier first, keycode second).
|
// Note param order is reversed vs CSI u (modifier first, keycode second).
|
||||||
// eslint-disable-next-line no-control-regex
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: terminal escape sequence parsing
|
||||||
const MODIFY_OTHER_KEYS_RE = /^\x1b\[27;(\d+);(\d+)~/
|
const MODIFY_OTHER_KEYS_RE = /^\x1b\[27;(\d+);(\d+)~/
|
||||||
|
|
||||||
// -- Terminal response patterns (inbound sequences from the terminal itself) --
|
// -- Terminal response patterns (inbound sequences from the terminal itself) --
|
||||||
// DECRPM: CSI ? Ps ; Pm $ y — response to DECRQM (request mode)
|
// DECRPM: CSI ? Ps ; Pm $ y — response to DECRQM (request mode)
|
||||||
// eslint-disable-next-line no-control-regex
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: terminal escape sequence parsing
|
||||||
const DECRPM_RE = /^\x1b\[\?(\d+);(\d+)\$y$/
|
const DECRPM_RE = /^\x1b\[\?(\d+);(\d+)\$y$/
|
||||||
// DA1: CSI ? Ps ; ... c — primary device attributes response
|
// DA1: CSI ? Ps ; ... c — primary device attributes response
|
||||||
// eslint-disable-next-line no-control-regex
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: terminal escape sequence parsing
|
||||||
const DA1_RE = /^\x1b\[\?([\d;]*)c$/
|
const DA1_RE = /^\x1b\[\?([\d;]*)c$/
|
||||||
// DA2: CSI > Ps ; ... c — secondary device attributes response
|
// DA2: CSI > Ps ; ... c — secondary device attributes response
|
||||||
// eslint-disable-next-line no-control-regex
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: terminal escape sequence parsing
|
||||||
const DA2_RE = /^\x1b\[>([\d;]*)c$/
|
const DA2_RE = /^\x1b\[>([\d;]*)c$/
|
||||||
// Kitty keyboard flags: CSI ? flags u — response to CSI ? u query
|
// Kitty keyboard flags: CSI ? flags u — response to CSI ? u query
|
||||||
// (private ? marker distinguishes from CSI u key events)
|
// (private ? marker distinguishes from CSI u key events)
|
||||||
// eslint-disable-next-line no-control-regex
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: terminal escape sequence parsing
|
||||||
const KITTY_FLAGS_RE = /^\x1b\[\?(\d+)u$/
|
const KITTY_FLAGS_RE = /^\x1b\[\?(\d+)u$/
|
||||||
// DECXCPR cursor position: CSI ? row ; col R
|
// DECXCPR cursor position: CSI ? row ; col R
|
||||||
// The ? marker disambiguates from modified F3 keys (Shift+F3 = CSI 1;2 R,
|
// The ? marker disambiguates from modified F3 keys (Shift+F3 = CSI 1;2 R,
|
||||||
// Ctrl+F3 = CSI 1;5 R, etc.) — plain CSI row;col R is genuinely ambiguous.
|
// Ctrl+F3 = CSI 1;5 R, etc.) — plain CSI row;col R is genuinely ambiguous.
|
||||||
// eslint-disable-next-line no-control-regex
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: terminal escape sequence parsing
|
||||||
const CURSOR_POSITION_RE = /^\x1b\[\?(\d+);(\d+)R$/
|
const CURSOR_POSITION_RE = /^\x1b\[\?(\d+);(\d+)R$/
|
||||||
// OSC response: OSC code ; data (BEL|ST)
|
// OSC response: OSC code ; data (BEL|ST)
|
||||||
// eslint-disable-next-line no-control-regex
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: terminal escape sequence parsing
|
||||||
const OSC_RESPONSE_RE = /^\x1b\](\d+);(.*?)(?:\x07|\x1b\\)$/s
|
const OSC_RESPONSE_RE = /^\x1b\](\d+);(.*?)(?:\x07|\x1b\\)$/s
|
||||||
// XTVERSION: DCS > | name ST — terminal name/version string (answer to CSI > 0 q).
|
// XTVERSION: DCS > | name ST — terminal name/version string (answer to CSI > 0 q).
|
||||||
// xterm.js replies "xterm.js(X.Y.Z)"; Ghostty, kitty, iTerm2, etc. reply with
|
// xterm.js replies "xterm.js(X.Y.Z)"; Ghostty, kitty, iTerm2, etc. reply with
|
||||||
// their own name. Unlike TERM_PROGRAM, this survives SSH since the query/reply
|
// their own name. Unlike TERM_PROGRAM, this survives SSH since the query/reply
|
||||||
// goes through the pty, not the environment.
|
// goes through the pty, not the environment.
|
||||||
// eslint-disable-next-line no-control-regex
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: terminal escape sequence parsing
|
||||||
const XTVERSION_RE = /^\x1bP>\|(.*?)(?:\x07|\x1b\\)$/s
|
const XTVERSION_RE = /^\x1bP>\|(.*?)(?:\x07|\x1b\\)$/s
|
||||||
// SGR mouse event: CSI < button ; col ; row M (press) or m (release)
|
// SGR mouse event: CSI < button ; col ; row M (press) or m (release)
|
||||||
// Button codes: 64=wheel-up, 65=wheel-down (0x40 | wheel-bit).
|
// Button codes: 64=wheel-up, 65=wheel-down (0x40 | wheel-bit).
|
||||||
// Button 32=left-drag (0x20 | motion-bit). Plain 0/1/2 = left/mid/right click.
|
// Button 32=left-drag (0x20 | motion-bit). Plain 0/1/2 = left/mid/right click.
|
||||||
// eslint-disable-next-line no-control-regex
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: terminal escape sequence parsing
|
||||||
const SGR_MOUSE_RE = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/
|
const SGR_MOUSE_RE = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/
|
||||||
|
|
||||||
function createPasteKey(content: string): ParsedKey {
|
function createPasteKey(content: string): ParsedKey {
|
||||||
|
|||||||
@@ -34,8 +34,10 @@ if (process.env.NODE_ENV === 'development') {
|
|||||||
void import('./devtools.js')
|
void import('./devtools.js')
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (error instanceof Error && (error as NodeJS.ErrnoException).code === 'ERR_MODULE_NOT_FOUND') {
|
if (
|
||||||
// biome-ignore lint/suspicious/noConsole: intentional warning
|
error instanceof Error &&
|
||||||
|
(error as NodeJS.ErrnoException).code === 'ERR_MODULE_NOT_FOUND'
|
||||||
|
) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`
|
`
|
||||||
The environment variable DEV is set to true, so Ink tried to import \`react-devtools-core\`,
|
The environment variable DEV is set to true, so Ink tried to import \`react-devtools-core\`,
|
||||||
@@ -197,7 +199,6 @@ let _prepareAt = 0
|
|||||||
|
|
||||||
/** Debug log helper — replaces fs.appendFileSync with console.warn. */
|
/** Debug log helper — replaces fs.appendFileSync with console.warn. */
|
||||||
function debugLog(message: string): void {
|
function debugLog(message: string): void {
|
||||||
// biome-ignore lint/suspicious/noConsole: debug instrumentation
|
|
||||||
console.warn(`[ink-commit] ${message}`)
|
console.warn(`[ink-commit] ${message}`)
|
||||||
}
|
}
|
||||||
// --- END ---
|
// --- END ---
|
||||||
@@ -304,9 +305,7 @@ const reconciler = createReconciler<
|
|||||||
if (COMMIT_LOG) {
|
if (COMMIT_LOG) {
|
||||||
const renderMs = performance.now() - _tr
|
const renderMs = performance.now() - _tr
|
||||||
if (renderMs > 10) {
|
if (renderMs > 10) {
|
||||||
debugLog(
|
debugLog(`${_tr.toFixed(1)} SLOW_PAINT ${renderMs.toFixed(1)}ms`)
|
||||||
`${_tr.toFixed(1)} SLOW_PAINT ${renderMs.toFixed(1)}ms`,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -114,7 +114,6 @@ const wrappedRender = async (
|
|||||||
await Promise.resolve()
|
await Promise.resolve()
|
||||||
const instance = renderSync(node, options)
|
const instance = renderSync(node, options)
|
||||||
if (process.env.CLAUDE_CODE_DEBUG_REPAINTS === '1') {
|
if (process.env.CLAUDE_CODE_DEBUG_REPAINTS === '1') {
|
||||||
// biome-ignore lint/suspicious/noConsole: debug instrumentation
|
|
||||||
console.warn(
|
console.warn(
|
||||||
`[render] first ink render: ${Math.round(process.uptime() * 1000)}ms since process start`,
|
`[render] first ink render: ${Math.round(process.uptime() * 1000)}ms since process start`,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ function hasVisibleSpaceEffect(styles: AnsiCode[]): boolean {
|
|||||||
* @see https://mitchellh.com/writing/grapheme-clusters-in-terminals
|
* @see https://mitchellh.com/writing/grapheme-clusters-in-terminals
|
||||||
*/
|
*/
|
||||||
// const enum is inlined at compile time - no runtime object, no property access
|
// const enum is inlined at compile time - no runtime object, no property access
|
||||||
export const enum CellWidth {
|
export enum CellWidth {
|
||||||
// Not a wide character, cell width 1
|
// Not a wide character, cell width 1
|
||||||
Narrow = 0,
|
Narrow = 0,
|
||||||
// Wide character, cell width 2. This cell contains the actual character.
|
// Wide character, cell width 2. This cell contains the actual character.
|
||||||
@@ -1144,7 +1144,7 @@ type DiffCallback = (
|
|||||||
y: number,
|
y: number,
|
||||||
removed: Cell | undefined,
|
removed: Cell | undefined,
|
||||||
added: Cell | undefined,
|
added: Cell | undefined,
|
||||||
) => boolean | void
|
) => boolean | undefined
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Like diff(), but calls a callback for each change instead of building an array.
|
* Like diff(), but calls a callback for each change instead of building an array.
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ function isDefined(n: number): boolean {
|
|||||||
|
|
||||||
// NaN-safe equality for layout-cache input comparison
|
// NaN-safe equality for layout-cache input comparison
|
||||||
function sameFloat(a: number, b: number): boolean {
|
function sameFloat(a: number, b: number): boolean {
|
||||||
|
// biome-ignore lint/suspicious/noSelfCompare: intentional NaN check (x !== x ↔ isNaN)
|
||||||
return a === b || (a !== a && b !== b)
|
return a === b || (a !== a && b !== b)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2372,12 +2373,14 @@ function boundAxis(
|
|||||||
if (v > maxV.value) v = maxV.value
|
if (v > maxV.value) v = maxV.value
|
||||||
} else if (maxU === 2) {
|
} else if (maxU === 2) {
|
||||||
const m = (maxV.value * owner) / 100
|
const m = (maxV.value * owner) / 100
|
||||||
|
// biome-ignore lint/suspicious/noSelfCompare: intentional NaN check
|
||||||
if (m === m && v > m) v = m
|
if (m === m && v > m) v = m
|
||||||
}
|
}
|
||||||
if (minU === 1) {
|
if (minU === 1) {
|
||||||
if (v < minV.value) v = minV.value
|
if (v < minV.value) v = minV.value
|
||||||
} else if (minU === 2) {
|
} else if (minU === 2) {
|
||||||
const m = (minV.value * owner) / 100
|
const m = (minV.value * owner) / 100
|
||||||
|
// biome-ignore lint/suspicious/noSelfCompare: intentional NaN check
|
||||||
if (m === m && v < m) v = m
|
if (m === m && v < m) v = m
|
||||||
}
|
}
|
||||||
return v
|
return v
|
||||||
|
|||||||
@@ -118,8 +118,6 @@ export class ACPClient {
|
|||||||
reject: (err: Error) => void
|
reject: (err: Error) => void
|
||||||
timer: ReturnType<typeof setTimeout>
|
timer: ReturnType<typeof setTimeout>
|
||||||
} | null = null
|
} | null = null
|
||||||
// Tracks the session ID being targeted by a load/resume operation
|
|
||||||
private pendingSessionTarget: string | null = null
|
|
||||||
|
|
||||||
private connectResolve: ((value: undefined) => void) | null = null
|
private connectResolve: ((value: undefined) => void) | null = null
|
||||||
private connectReject: ((error: Error) => void) | null = null
|
private connectReject: ((error: Error) => void) | null = null
|
||||||
|
|||||||
Reference in New Issue
Block a user