chore(tui): /clean — drop dead capture-pad path, dedupe gutter handlers

- TextInput: remove unused leftCaptureColumns prop and capture-pad math, drop
  unused mouseApi.startAt, fold mouse offset into a single offsetAt helper,
  share a MouseEventLite type across the four handlers.
- appLayout: hoist a GutterMouseEvent type and an endInputDrag callback so the
  spacer/prompt/input rows share one shape.
- _tui_need_npm_install: lift the runtime-only key set to a module constant,
  collapse nested isinstance checks, and document the mtime fallback.

Made-with: Cursor
This commit is contained in:
Brooklyn Nicholson
2026-04-27 17:46:26 -05:00
parent 892a6f887f
commit 9e3bd666e3
3 changed files with 51 additions and 70 deletions

View File

@@ -829,6 +829,9 @@ def _print_tui_exit_summary(session_id: Optional[str], active_session_file: Opti
)
_NPM_LOCK_RUNTIME_KEYS = frozenset({"ideallyInert"})
def _tui_need_npm_install(root: Path) -> bool:
"""True when @hermes/ink is missing or node_modules is behind package-lock.json."""
ink = root / "node_modules" / "@hermes" / "ink" / "package.json"
@@ -841,29 +844,32 @@ def _tui_need_npm_install(root: Path) -> bool:
if not marker.is_file():
return True
# Compare lockfile contents, not mtimes: git checkouts and npm rewrites
# can bump the root lockfile timestamp even when installed deps already
# match. Fall back to mtime when either file is unparseable.
try:
wanted = json.loads(lock.read_text(encoding="utf-8")).get("packages") or {}
installed = json.loads(marker.read_text(encoding="utf-8")).get("packages") or {}
except (OSError, json.JSONDecodeError):
return lock.stat().st_mtime > marker.stat().st_mtime
ignored = {"ideallyInert"}
def comparable(pkg: dict) -> dict:
return {k: v for k, v in pkg.items() if k not in ignored}
return {k: v for k, v in pkg.items() if k not in _NPM_LOCK_RUNTIME_KEYS}
for name, pkg in wanted.items():
if name == "":
if not name:
continue
if not isinstance(pkg, dict):
continue
if name not in installed:
if isinstance(pkg, dict) and (pkg.get("optional") or pkg.get("peer")):
if pkg.get("optional") or pkg.get("peer"):
continue
return True
if isinstance(pkg, dict) and isinstance(installed[name], dict):
if comparable(pkg) != comparable(installed[name]):
return True
if isinstance(installed[name], dict) and comparable(pkg) != comparable(installed[name]):
return True
return False

View File

@@ -129,7 +129,7 @@ const ComposerPane = memo(function ComposerPane({
const inputHeight = inputVisualHeight(composer.input, inputColumns)
const inputMouseRef = useRef<null | TextInputMouseApi>(null)
const captureInputDrag = (e: { button: number; localCol?: number; localRow?: number; stopImmediatePropagation?: () => void }) => {
const captureInputDrag = (e: GutterMouseEvent) => {
if (e.button !== 0) {
return
}
@@ -138,7 +138,7 @@ const ComposerPane = memo(function ComposerPane({
inputMouseRef.current?.startAtBeginning()
}
const dragIntoInput = (e: { button: number; localCol?: number; localRow?: number; stopImmediatePropagation?: () => void }) => {
const dragIntoInput = (e: GutterMouseEvent) => {
if (e.button !== 0) {
return
}
@@ -147,6 +147,8 @@ const ComposerPane = memo(function ComposerPane({
inputMouseRef.current?.dragAt(e.localRow ?? 0, (e.localCol ?? 0) - pw)
}
const endInputDrag = () => inputMouseRef.current?.end()
return (
<NoSelect
flexDirection="column"
@@ -179,12 +181,7 @@ const ComposerPane = memo(function ComposerPane({
{status.stickyPrompt}
</Text>
) : (
<Box
height={1}
onMouseDown={captureInputDrag}
onMouseDrag={dragIntoInput}
onMouseUp={() => inputMouseRef.current?.end()}
/>
<Box height={1} onMouseDown={captureInputDrag} onMouseDrag={dragIntoInput} onMouseUp={endInputDrag} />
)}
<StatusRulePane at="top" composer={composer} status={status} />
@@ -211,12 +208,7 @@ const ComposerPane = memo(function ComposerPane({
</Box>
))}
<Box
onMouseDown={captureInputDrag}
onMouseDrag={dragIntoInput}
onMouseUp={() => inputMouseRef.current?.end()}
position="relative"
>
<Box onMouseDown={captureInputDrag} onMouseDrag={dragIntoInput} onMouseUp={endInputDrag} position="relative">
<Box width={pw}>
{sh ? (
<Text color={ui.theme.color.shellDollar}>$ </Text>
@@ -362,3 +354,10 @@ export const AppLayout = memo(function AppLayout({
</Shell>
)
})
type GutterMouseEvent = {
button: number
localCol?: number
localRow?: number
stopImmediatePropagation?: () => void
}

View File

@@ -282,7 +282,6 @@ const isPasteResultPromise = (
export function TextInput({
columns = 80,
leftCaptureColumns = 0,
value,
onChange,
onPaste,
@@ -332,25 +331,18 @@ export function TextInput({
)
const layout = useMemo(() => cursorLayout(display, cur, columns), [columns, cur, display])
const capturePad = Math.max(0, leftCaptureColumns)
const boxRef = useDeclaredCursor({
line: layout.line,
column: layout.column + capturePad,
column: layout.column,
active: focus && termFocus && !selected
})
// Hide the hardware cursor while an input selection is active so it can't
// auto-wrap below the prompt when the rendered (inverted) text exactly
// fills the row, and so it doesn't paint a ghost block on the first
// selected cell when we re-park it. Restore on unmount or when selection
// clears so the cursor reappears as soon as the user resumes typing.
// Hide the hardware cursor during a selection: prevents auto-wrap into
// the row below when inverted text exactly fills the column width, and
// avoids parking a ghost block on the first selected cell.
useEffect(() => {
if (!focus || !stdout?.isTTY) {
return
}
if (!selected) {
if (!focus || !selected || !stdout?.isTTY) {
return
}
@@ -677,11 +669,6 @@ export function TextInput({
commit(nextValue, nextCursor)
}
const mouseOffset = (e: { localCol?: number; localRow?: number }) => ({
col: (e.localCol ?? 0) - capturePad,
row: e.localRow ?? 0
})
const startMouseSelection = (next: number) => {
const c = snapPos(vRef.current, next)
@@ -716,11 +703,13 @@ export function TextInput({
}
}
const offsetAt = (e: { localCol?: number; localRow?: number }) =>
offsetFromPosition(display, e.localRow ?? 0, e.localCol ?? 0, columns)
if (mouseApiRef) {
mouseApiRef.current = {
dragAt: (row, col) => dragMouseSelection(offsetFromPosition(display, row, col, columns)),
end: endMouseSelection,
startAt: (row, col) => startMouseSelection(offsetFromPosition(display, row, col, columns)),
startAtBeginning: () => startMouseSelection(0)
}
}
@@ -983,30 +972,24 @@ export function TextInput({
return (
<Box
onClick={(e: { localCol?: number; localRow?: number; stopImmediatePropagation?: () => void }) => {
onClick={(e: MouseEventLite) => {
if (!focus) {
return
}
e.stopImmediatePropagation?.()
clearSel()
const pos = mouseOffset(e)
const next = offsetFromPosition(display, pos.row, pos.col, columns)
const next = offsetAt(e)
setCur(next)
curRef.current = next
}}
onMouseDown={(e: {
button: number
localCol?: number
localRow?: number
stopImmediatePropagation?: () => void
}) => {
onMouseDown={(e: MouseEventLite) => {
if (!focus) {
return
}
// Right-click to paste: route through the same hotkey path as
// Alt+V so the composer's clipboard RPC (text or image) handles it.
// Right-click route through the same path as Alt+V so the composer
// clipboard RPC (text or image) handles it.
if (e.button === 2) {
e.stopImmediatePropagation?.()
emitPaste({ cursor: curRef.current, hotkey: true, text: '', value: vRef.current })
@@ -1019,39 +1002,34 @@ export function TextInput({
}
e.stopImmediatePropagation?.()
const pos = mouseOffset(e)
const next = offsetFromPosition(display, pos.row, pos.col, columns)
startMouseSelection(next)
startMouseSelection(offsetAt(e))
}}
onMouseDrag={(e: {
button: number
localCol?: number
localRow?: number
stopImmediatePropagation?: () => void
}) => {
onMouseDrag={(e: MouseEventLite) => {
if (!focus || e.button !== 0 || mouseAnchorRef.current === null) {
return
}
e.stopImmediatePropagation?.()
const pos = mouseOffset(e)
const next = offsetFromPosition(display, pos.row, pos.col, columns)
dragMouseSelection(next)
dragMouseSelection(offsetAt(e))
}}
onMouseUp={(e: { stopImmediatePropagation?: () => void }) => {
onMouseUp={(e: MouseEventLite) => {
e.stopImmediatePropagation?.()
endMouseSelection()
}}
marginLeft={capturePad ? -capturePad : undefined}
paddingLeft={capturePad || undefined}
ref={boxRef}
width={columns + capturePad}
>
<Text wrap="wrap-char">{rendered}</Text>
</Box>
)
}
type MouseEventLite = {
button?: number
localCol?: number
localRow?: number
stopImmediatePropagation?: () => void
}
export interface PasteEvent {
bracketed?: boolean
cursor: number
@@ -1063,7 +1041,6 @@ export interface PasteEvent {
interface TextInputProps {
columns?: number
focus?: boolean
leftCaptureColumns?: number
mask?: string
mouseApiRef?: MutableRefObject<null | TextInputMouseApi>
onChange: (v: string) => void
@@ -1078,6 +1055,5 @@ interface TextInputProps {
export interface TextInputMouseApi {
dragAt: (row: number, col: number) => void
end: () => void
startAt: (row: number, col: number) => void
startAtBeginning: () => void
}