style: 修复所有 lint 错误,覆盖 @ant forked 代码

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
claude-code-best
2026-05-01 21:49:21 +08:00
parent 6182015005
commit c32f26cf21
12 changed files with 1207 additions and 2685 deletions

1330
V6.md

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 {

View File

@@ -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`,
)
} }
} }
}, },

View File

@@ -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`,
) )

View File

@@ -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.

View File

@@ -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

View File

@@ -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