🎯 Use case
This file lives under “hooks/”, which covers reusable UI or integration hooks. On the API surface it exposes SelectionPoint, SelectionData, IDESelection, and useIdeSelection — mainly types, interfaces, or factory objects. Dependencies touch React UI, src, and schema validation. It composes internal code from services and utils (relative imports).
Generated from folder role, exports, dependency roots, and inline comments — not hand-reviewed for every path.
🧠 Inline summary
import { useEffect, useRef } from 'react' import { logError } from 'src/utils/log.js' import { z } from 'zod/v4' import type { ConnectedMCPServer,
📤 Exports (heuristic)
SelectionPointSelectionDataIDESelectionuseIdeSelection
📚 External import roots
Package roots from from "…" (relative paths omitted).
reactsrczod
🖥️ Source preview
import { useEffect, useRef } from 'react'
import { logError } from 'src/utils/log.js'
import { z } from 'zod/v4'
import type {
ConnectedMCPServer,
MCPServerConnection,
} from '../services/mcp/types.js'
import { getConnectedIdeClient } from '../utils/ide.js'
import { lazySchema } from '../utils/lazySchema.js'
export type SelectionPoint = {
line: number
character: number
}
export type SelectionData = {
selection: {
start: SelectionPoint
end: SelectionPoint
} | null
text?: string
filePath?: string
}
export type IDESelection = {
lineCount: number
lineStart?: number
text?: string
filePath?: string
}
// Define the selection changed notification schema
const SelectionChangedSchema = lazySchema(() =>
z.object({
method: z.literal('selection_changed'),
params: z.object({
selection: z
.object({
start: z.object({
line: z.number(),
character: z.number(),
}),
end: z.object({
line: z.number(),
character: z.number(),
}),
})
.nullable()
.optional(),
text: z.string().optional(),
filePath: z.string().optional(),
}),
}),
)
/**
* A hook that tracks IDE text selection information by directly registering
* with MCP client notification handlers
*/
export function useIdeSelection(
mcpClients: MCPServerConnection[],
onSelect: (selection: IDESelection) => void,
): void {
const handlersRegistered = useRef(false)
const currentIDERef = useRef<ConnectedMCPServer | null>(null)
useEffect(() => {
// Find the IDE client from the MCP clients list
const ideClient = getConnectedIdeClient(mcpClients)
// If the IDE client changed, we need to re-register handlers.
// Normalize undefined to null so the initial ref value (null) matches
// "no IDE found" (undefined), avoiding spurious resets on every MCP update.
if (currentIDERef.current !== (ideClient ?? null)) {
handlersRegistered.current = false
currentIDERef.current = ideClient || null
// Reset the selection when the IDE client changes.
onSelect({
lineCount: 0,
lineStart: undefined,
text: undefined,
filePath: undefined,
})
}
// Skip if we've already registered handlers for the current IDE or if there's no IDE client
if (handlersRegistered.current || !ideClient) {
return
}
// Handler function for selection changes
const selectionChangeHandler = (data: SelectionData) => {
if (data.selection?.start && data.selection?.end) {
const { start, end } = data.selection
let lineCount = end.line - start.line + 1
// If on the first character of the line, do not count the line
// as being selected.
if (end.character === 0) {
lineCount--
}
const selection = {
lineCount,
lineStart: start.line,
text: data.text,
filePath: data.filePath,
}
onSelect(selection)
}
}
// Register notification handler for selection_changed events
ideClient.client.setNotificationHandler(
SelectionChangedSchema(),
notification => {
if (currentIDERef.current !== ideClient) {
return
}
try {
// Get the selection data from the notification params
const selectionData = notification.params
// Process selection data - validate it has required properties
if (
selectionData.selection &&
selectionData.selection.start &&
selectionData.selection.end
) {
// Handle selection changes
selectionChangeHandler(selectionData as SelectionData)
} else if (selectionData.text !== undefined) {
// Handle empty selection (when text is empty string)
selectionChangeHandler({
selection: null,
text: selectionData.text,
filePath: selectionData.filePath,
})
}
} catch (error) {
logError(error as Error)
}
},
)
// Mark that we've registered handlers
handlersRegistered.current = true
// No cleanup needed as MCP clients manage their own lifecycle
}, [mcpClients, onSelect])
}