Revert "feat: 第一个可以用的 ink 组件抽象 (#158)" (#175)

This reverts commit c445f43f8d.
This commit is contained in:
claude-code-best
2026-04-07 15:05:03 +08:00
committed by GitHub
parent ca0c3265e6
commit 88d4c3ba24
645 changed files with 1214 additions and 7255 deletions

View File

@@ -17,7 +17,6 @@
"@anthropic-ai/sandbox-runtime": "^0.0.44",
"@anthropic-ai/sdk": "^0.80.0",
"@anthropic-ai/vertex-sdk": "^0.14.4",
"@anthropic/ink": "workspace:*",
"@aws-sdk/client-bedrock": "^3.1020.0",
"@aws-sdk/client-bedrock-runtime": "^3.1020.0",
"@aws-sdk/client-sts": "^3.1020.0",
@@ -139,29 +138,6 @@
"name": "@ant/computer-use-swift",
"version": "1.0.0",
},
"packages/@ant/ink": {
"name": "@anthropic/ink",
"version": "1.0.0",
"dependencies": {
"auto-bind": "^5.0.1",
"bidi-js": "^1.0.3",
"chalk": "^5.6.2",
"cli-boxes": "^4.0.1",
"emoji-regex": "^10.6.0",
"figures": "^6.1.0",
"get-east-asian-width": "^1.5.0",
"indent-string": "^5.0.0",
"lodash-es": "^4.17.23",
"react": "^19.2.4",
"react-reconciler": "^0.33.0",
"signal-exit": "^4.1.0",
"strip-ansi": "^7.2.0",
"supports-hyperlinks": "^4.4.0",
"type-fest": "^5.5.0",
"usehooks-ts": "^3.1.1",
"wrap-ansi": "^10.0.0",
},
},
"packages/audio-capture-napi": {
"name": "audio-capture-napi",
"version": "1.0.0",
@@ -214,8 +190,6 @@
"@anthropic-ai/vertex-sdk": ["@anthropic-ai/vertex-sdk@0.14.4", "https://registry.npmmirror.com/@anthropic-ai/vertex-sdk/-/vertex-sdk-0.14.4.tgz", { "dependencies": { "@anthropic-ai/sdk": ">=0.50.3 <1", "google-auth-library": "^9.4.2" } }, "sha512-BZUPRWghZxfSFtAxU563wH+jfWBPoedAwsVxG35FhmNsjeV8tyfN+lFriWhCpcZApxA4NdT6Soov+PzfnxxD5g=="],
"@anthropic/ink": ["@anthropic/ink@workspace:packages/@ant/ink"],
"@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "https://registry.npmmirror.com/@aws-crypto/crc32/-/crc32-5.2.0.tgz", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="],
"@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "https://registry.npmmirror.com/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="],
@@ -1224,7 +1198,7 @@
"open": ["open@10.2.0", "https://registry.npmmirror.com/open/-/open-10.2.0.tgz", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
"openai": ["openai@6.33.0", "https://registry.npmmirror.com/openai/-/openai-6.33.0.tgz", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-xAYN1W3YsDXJWA5F277135YfkEk6H7D3D6vWwRhJ3OEkzRgcyK8z/P5P9Gyi/wB4N8kK9kM5ZjprfvyHagKmpw=="],
"openai": ["openai@6.33.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-xAYN1W3YsDXJWA5F277135YfkEk6H7D3D6vWwRhJ3OEkzRgcyK8z/P5P9Gyi/wB4N8kK9kM5ZjprfvyHagKmpw=="],
"os-tmpdir": ["os-tmpdir@1.0.2", "https://registry.npmmirror.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="],

View File

@@ -131,7 +131,6 @@
"highlight.js": "^11.11.1",
"https-proxy-agent": "^8.0.0",
"ignore": "^7.0.5",
"@anthropic/ink": "workspace:*",
"image-processor-napi": "workspace:*",
"indent-string": "^5.0.0",
"jsonc-parser": "^3.3.1",

View File

@@ -1,30 +0,0 @@
{
"name": "@anthropic/ink",
"version": "1.0.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"dependencies": {
"auto-bind": "^5.0.1",
"bidi-js": "^1.0.3",
"chalk": "^5.6.2",
"cli-boxes": "^4.0.1",
"emoji-regex": "^10.6.0",
"figures": "^6.1.0",
"get-east-asian-width": "^1.5.0",
"indent-string": "^5.0.0",
"lodash-es": "^4.17.23",
"react": "^19.2.4",
"react-reconciler": "^0.33.0",
"signal-exit": "^4.1.0",
"strip-ansi": "^7.2.0",
"supports-hyperlinks": "^4.4.0",
"type-fest": "^5.5.0",
"usehooks-ts": "^3.1.1",
"wrap-ansi": "^10.0.0"
}
}

View File

@@ -1,53 +0,0 @@
/**
* Shared Intl object instances with lazy initialization.
*
* Intl constructors are expensive (~0.05-0.1ms each), so we cache instances
* for reuse across the codebase instead of creating new ones each time.
* Lazy initialization ensures we only pay the cost when actually needed.
*
* Vendored from src/utils/intl.ts for package independence.
*/
// Segmenters for Unicode text processing (lazily initialized)
let graphemeSegmenter: Intl.Segmenter | null = null
let wordSegmenter: Intl.Segmenter | null = null
export function getGraphemeSegmenter(): Intl.Segmenter {
if (!graphemeSegmenter) {
graphemeSegmenter = new Intl.Segmenter(undefined, {
granularity: 'grapheme',
})
}
return graphemeSegmenter
}
/**
* Extract the first grapheme cluster from a string.
* Returns '' for empty strings.
*/
export function firstGrapheme(text: string): string {
if (!text) return ''
const segments = getGraphemeSegmenter().segment(text)
const first = segments[Symbol.iterator]().next().value
return first?.segment ?? ''
}
/**
* Extract the last grapheme cluster from a string.
* Returns '' for empty strings.
*/
export function lastGrapheme(text: string): string {
if (!text) return ''
let last = ''
for (const { segment } of getGraphemeSegmenter().segment(text)) {
last = segment
}
return last
}
export function getWordSegmenter(): Intl.Segmenter {
if (!wordSegmenter) {
wordSegmenter = new Intl.Segmenter(undefined, { granularity: 'word' })
}
return wordSegmenter
}

View File

@@ -1,97 +0,0 @@
/**
* Slice a string containing ANSI escape codes.
*
* Vendored from src/utils/sliceAnsi.ts for package independence.
* The only external dependency is stringWidth from the core package.
*/
import {
type AnsiCode,
ansiCodesToString,
reduceAnsiCodes,
tokenize,
undoAnsiCodes,
} from '@alcalzone/ansi-tokenize'
import { stringWidth } from '../stringWidth.js'
// A code is an "end code" if its code equals its endCode (e.g., hyperlink close)
function isEndCode(code: AnsiCode): boolean {
return code.code === code.endCode
}
// Filter to only include "start codes" (not end codes)
function filterStartCodes(codes: AnsiCode[]): AnsiCode[] {
return codes.filter(c => !isEndCode(c))
}
/**
* Slice a string containing ANSI escape codes.
*
* Unlike the slice-ansi package, this properly handles OSC 8 hyperlink
* sequences because @alcalzone/ansi-tokenize tokenizes them correctly.
*/
export default function sliceAnsi(
str: string,
start: number,
end?: number,
): string {
// Don't pass `end` to tokenize — it counts code units, not display cells,
// so it drops tokens early for text with zero-width combining marks.
const tokens = tokenize(str)
let activeCodes: AnsiCode[] = []
let position = 0
let result = ''
let include = false
for (const token of tokens) {
// Advance by display width, not code units. Combining marks (Devanagari
// matras, virama, diacritics) are width 0 — counting them via .length
// advanced position past `end` early and truncated the slice. Callers
// pass start/end in display cells (via stringWidth), so position must
// track the same units.
const width =
token.type === 'ansi' ? 0 : token.type === 'char' ? (token.fullWidth ? 2 : stringWidth(token.value)) : 0
// Break AFTER trailing zero-width marks — a combining mark attaches to
// the preceding base char, so "भा" (भ + ा, 1 display cell) sliced at
// end=1 must include the ा. Breaking on position >= end BEFORE the
// zero-width check would drop it and render भ bare. ANSI codes are
// width 0 but must NOT be included past end (they open new style runs
// that leak into the undo sequence), so gate on char type too. The
// !include guard ensures empty slices (start===end) stay empty even
// when the string starts with a zero-width char (BOM, ZWJ).
if (end !== undefined && position >= end) {
if (token.type === 'ansi' || width > 0 || !include) break
}
if (token.type === 'ansi') {
activeCodes.push(token)
if (include) {
// Emit all ANSI codes during the slice
result += token.code
}
} else {
if (!include && position >= start) {
// Skip leading zero-width marks at the start boundary — they belong
// to the preceding base char in the left half. Without this, the
// mark appears in BOTH halves: left+right ≠ original. Only applies
// when start > 0 (otherwise there's no preceding char to own it).
if (start > 0 && width === 0) continue
include = true
// Reduce and filter to only active start codes
activeCodes = filterStartCodes(reduceAnsiCodes(activeCodes))
result = ansiCodesToString(activeCodes)
}
if (include) {
result += (token as any).value
}
position += width
}
}
// Only undo start codes that are still active
const activeStartCodes = filterStartCodes(reduceAnsiCodes(activeCodes))
result += ansiCodesToString(undoAnsiCodes(activeStartCodes))
return result
}

View File

@@ -1,134 +0,0 @@
/**
* Yoga enums — ported from yoga-layout/src/generated/YGEnums.ts
* Kept as `const` objects (not TS enums) per repo convention.
* Values match upstream exactly so callers don't change.
*/
export const Align = {
Auto: 0,
FlexStart: 1,
Center: 2,
FlexEnd: 3,
Stretch: 4,
Baseline: 5,
SpaceBetween: 6,
SpaceAround: 7,
SpaceEvenly: 8,
} as const
export type Align = (typeof Align)[keyof typeof Align]
export const BoxSizing = {
BorderBox: 0,
ContentBox: 1,
} as const
export type BoxSizing = (typeof BoxSizing)[keyof typeof BoxSizing]
export const Dimension = {
Width: 0,
Height: 1,
} as const
export type Dimension = (typeof Dimension)[keyof typeof Dimension]
export const Direction = {
Inherit: 0,
LTR: 1,
RTL: 2,
} as const
export type Direction = (typeof Direction)[keyof typeof Direction]
export const Display = {
Flex: 0,
None: 1,
Contents: 2,
} as const
export type Display = (typeof Display)[keyof typeof Display]
export const Edge = {
Left: 0,
Top: 1,
Right: 2,
Bottom: 3,
Start: 4,
End: 5,
Horizontal: 6,
Vertical: 7,
All: 8,
} as const
export type Edge = (typeof Edge)[keyof typeof Edge]
export const Errata = {
None: 0,
StretchFlexBasis: 1,
AbsolutePositionWithoutInsetsExcludesPadding: 2,
AbsolutePercentAgainstInnerSize: 4,
All: 2147483647,
Classic: 2147483646,
} as const
export type Errata = (typeof Errata)[keyof typeof Errata]
export const ExperimentalFeature = {
WebFlexBasis: 0,
} as const
export type ExperimentalFeature =
(typeof ExperimentalFeature)[keyof typeof ExperimentalFeature]
export const FlexDirection = {
Column: 0,
ColumnReverse: 1,
Row: 2,
RowReverse: 3,
} as const
export type FlexDirection = (typeof FlexDirection)[keyof typeof FlexDirection]
export const Gutter = {
Column: 0,
Row: 1,
All: 2,
} as const
export type Gutter = (typeof Gutter)[keyof typeof Gutter]
export const Justify = {
FlexStart: 0,
Center: 1,
FlexEnd: 2,
SpaceBetween: 3,
SpaceAround: 4,
SpaceEvenly: 5,
} as const
export type Justify = (typeof Justify)[keyof typeof Justify]
export const MeasureMode = {
Undefined: 0,
Exactly: 1,
AtMost: 2,
} as const
export type MeasureMode = (typeof MeasureMode)[keyof typeof MeasureMode]
export const Overflow = {
Visible: 0,
Hidden: 1,
Scroll: 2,
} as const
export type Overflow = (typeof Overflow)[keyof typeof Overflow]
export const PositionType = {
Static: 0,
Relative: 1,
Absolute: 2,
} as const
export type PositionType = (typeof PositionType)[keyof typeof PositionType]
export const Unit = {
Undefined: 0,
Point: 1,
Percent: 2,
Auto: 3,
} as const
export type Unit = (typeof Unit)[keyof typeof Unit]
export const Wrap = {
NoWrap: 0,
Wrap: 1,
WrapReverse: 2,
} as const
export type Wrap = (typeof Wrap)[keyof typeof Wrap]

File diff suppressed because it is too large Load Diff

View File

@@ -1,95 +0,0 @@
/**
* Minimal stub of useExitOnCtrlCD + useExitOnCtrlCDWithKeybindings.
*
* The original hooks depend on the keybinding system and useApp() exit.
* This stub provides the same interface with simplified Ctrl+C/D handling
* via useInput, suitable for the standalone @anthropic/ink package.
*/
import { useCallback, useState } from 'react'
import useInput from './use-input.js'
export type ExitState = {
pending: boolean
keyName: 'Ctrl-C' | 'Ctrl-D' | null
}
/**
* Minimal double-press exit handler.
* First Ctrl+C/D shows pending state, second press within timeout fires onExit.
*/
const DOUBLE_PRESS_TIMEOUT_MS = 800
function useDoublePress(
setPending: (pending: boolean) => void,
onDoublePress: () => void,
): () => void {
let lastPress = 0
let timeout: ReturnType<typeof setTimeout> | undefined
return () => {
const now = Date.now()
const timeSince = now - lastPress
const isDouble =
timeSince <= DOUBLE_PRESS_TIMEOUT_MS && timeout !== undefined
if (isDouble) {
clearTimeout(timeout)
timeout = undefined
setPending(false)
onDoublePress()
} else {
setPending(true)
clearTimeout(timeout)
timeout = setTimeout(() => {
setPending(false)
timeout = undefined
}, DOUBLE_PRESS_TIMEOUT_MS)
}
lastPress = now
}
}
/**
* Stub that provides ExitState for Ctrl+C/D double-press UI.
* In the standalone package, this uses useInput directly rather than the
* keybinding system.
*/
export function useExitOnCtrlCDWithKeybindings(
_onExit?: () => void,
_onInterrupt?: () => boolean,
isActive: boolean = true,
): ExitState {
const [exitState, setExitState] = useState<ExitState>({
pending: false,
keyName: null,
})
const handleCtrlC = useDoublePress(
(pending: boolean) =>
setExitState({ pending, keyName: pending ? 'Ctrl-C' : null }),
() => process.exit(0),
)
const handleCtrlD = useDoublePress(
(pending: boolean) =>
setExitState({ pending, keyName: pending ? 'Ctrl-D' : null }),
() => process.exit(0),
)
const handleInput = useCallback(
(_input: string, key: { ctrl?: boolean; name?: string }) => {
if (!isActive) return
if (key.ctrl && key.name === 'c') {
handleCtrlC()
} else if (key.ctrl && key.name === 'd') {
handleCtrlD()
}
},
[isActive, handleCtrlC, handleCtrlD],
)
useInput(handleInput, { isActive })
return exitState
}

View File

@@ -1,222 +0,0 @@
/**
* Minimal stub of useSearchInput for the standalone @anthropic/ink package.
*
* Provides the same interface as the full implementation but without
* kill-ring / yank support. Suitable for FuzzyPicker and other theme
* components that need text input handling.
*/
import { useCallback, useState } from 'react'
import type { KeyboardEvent } from '../core/events/keyboard-event.js'
import useInput from './use-input.js'
import { useTerminalSize } from '../hooks/useTerminalSize.js'
type UseSearchInputOptions = {
isActive: boolean
onExit: () => void
onCancel?: () => void
onExitUp?: () => void
columns?: number
passthroughCtrlKeys?: string[]
initialQuery?: string
backspaceExitsOnEmpty?: boolean
}
type UseSearchInputReturn = {
query: string
setQuery: (q: string) => void
cursorOffset: number
handleKeyDown: (e: KeyboardEvent) => void
}
const UNHANDLED_SPECIAL_KEYS = new Set([
'pageup',
'pagedown',
'insert',
'wheelup',
'wheeldown',
'mouse',
'f1',
'f2',
'f3',
'f4',
'f5',
'f6',
'f7',
'f8',
'f9',
'f10',
'f11',
'f12',
])
export function useSearchInput({
isActive,
onExit,
onCancel,
onExitUp,
columns,
initialQuery = '',
backspaceExitsOnEmpty = true,
}: UseSearchInputOptions): UseSearchInputReturn {
const { columns: terminalColumns } = useTerminalSize()
const _effectiveColumns = columns ?? terminalColumns
const [query, setQueryState] = useState(initialQuery)
const [cursorOffset, setCursorOffset] = useState(initialQuery.length)
const setQuery = useCallback((q: string) => {
setQueryState(q)
setCursorOffset(q.length)
}, [])
const handleKeyDown = (e: KeyboardEvent): void => {
if (!isActive) return
if (e.key === 'return' || e.key === 'down') {
e.preventDefault()
onExit()
return
}
if (e.key === 'up') {
e.preventDefault()
onExitUp?.()
return
}
if (e.key === 'escape') {
e.preventDefault()
if (onCancel) {
onCancel()
} else if (query.length > 0) {
setQueryState('')
setCursorOffset(0)
} else {
onExit()
}
return
}
if (e.key === 'backspace') {
e.preventDefault()
if (query.length === 0) {
if (backspaceExitsOnEmpty) (onCancel ?? onExit)()
return
}
const newOffset = Math.max(0, cursorOffset - 1)
setQueryState(query.slice(0, newOffset) + query.slice(cursorOffset))
setCursorOffset(newOffset)
return
}
if (e.key === 'delete') {
e.preventDefault()
if (cursorOffset < query.length) {
setQueryState(query.slice(0, cursorOffset) + query.slice(cursorOffset + 1))
}
return
}
if (e.key === 'left') {
e.preventDefault()
setCursorOffset(Math.max(0, cursorOffset - 1))
return
}
if (e.key === 'right') {
e.preventDefault()
setCursorOffset(Math.min(query.length, cursorOffset + 1))
return
}
if (e.key === 'home') {
e.preventDefault()
setCursorOffset(0)
return
}
if (e.key === 'end') {
e.preventDefault()
setCursorOffset(query.length)
return
}
if (e.ctrl) {
switch (e.key.toLowerCase()) {
case 'a':
e.preventDefault()
setCursorOffset(0)
return
case 'e':
e.preventDefault()
setCursorOffset(query.length)
return
case 'b':
e.preventDefault()
setCursorOffset(Math.max(0, cursorOffset - 1))
return
case 'f':
e.preventDefault()
setCursorOffset(Math.min(query.length, cursorOffset + 1))
return
case 'd': {
e.preventDefault()
if (query.length === 0) {
;(onCancel ?? onExit)()
return
}
if (cursorOffset < query.length) {
setQueryState(query.slice(0, cursorOffset) + query.slice(cursorOffset + 1))
}
return
}
case 'h': {
e.preventDefault()
if (query.length === 0) {
if (backspaceExitsOnEmpty) (onCancel ?? onExit)()
return
}
const newOffset = Math.max(0, cursorOffset - 1)
setQueryState(query.slice(0, newOffset) + query.slice(cursorOffset))
setCursorOffset(newOffset)
return
}
case 'c':
e.preventDefault()
onCancel?.()
return
case 'u':
e.preventDefault()
setQueryState(query.slice(cursorOffset))
setCursorOffset(0)
return
case 'k':
e.preventDefault()
setQueryState(query.slice(0, cursorOffset))
return
case 'w': {
e.preventDefault()
// Delete word before cursor
const before = query.slice(0, cursorOffset)
const after = query.slice(cursorOffset)
const trimmed = before.replace(/\S+\s*$/, '')
setQueryState(trimmed + after)
setCursorOffset(trimmed.length)
return
}
}
return
}
if (e.key === 'tab') {
return
}
// Regular character input
if (e.key.length >= 1 && !UNHANDLED_SPECIAL_KEYS.has(e.key)) {
e.preventDefault()
setQueryState(query.slice(0, cursorOffset) + e.key + query.slice(cursorOffset))
setCursorOffset(cursorOffset + 1)
}
}
// Bridge: subscribe via useInput and adapt to KeyboardEvent
useInput(
(_input: string, _key: unknown, event: { keypress: string }) => {
handleKeyDown(new KeyboardEvent(event.keypress))
},
{ isActive },
)
return { query, setQuery, cursorOffset, handleKeyDown }
}

View File

@@ -1,15 +0,0 @@
import { useContext } from 'react'
import {
type TerminalSize,
TerminalSizeContext,
} from '../components/TerminalSizeContext.js'
export function useTerminalSize(): TerminalSize {
const size = useContext(TerminalSizeContext)
if (!size) {
throw new Error('useTerminalSize must be used within an Ink App component')
}
return size
}

View File

@@ -1,126 +0,0 @@
/**
* @anthropic/ink — Terminal React rendering framework
*
* Three-layer architecture:
* core/ — Rendering engine (reconciler, layout, terminal I/O, screen buffer)
* components/ — UI primitives (Box, Text, ScrollBox, App, hooks)
* theme/ — Theme system (ThemeProvider, ThemedBox, ThemedText, design-system)
*/
// ============================================================
// Core API (render/createRoot)
// ============================================================
export { default as wrappedRender, renderSync, createRoot } from './core/root.js'
export type { RenderOptions, Instance, Root } from './core/root.js'
// InkCore class
export { default as Ink } from './core/ink.js'
// ============================================================
// Core types
// ============================================================
export type { DOMElement, TextNode, ElementNames, DOMNodeAttribute } from './core/dom.js'
export type { Styles, TextStyles, Color, RGBColor, HexColor, Ansi256Color, AnsiColor } from './core/styles.js'
export type { Key } from './core/events/input-event.js'
export type { FlickerReason, FrameEvent } from './core/frame.js'
export type { MatchPosition } from './core/render-to-screen.js'
export type { SelectionState, FocusMove } from './core/selection.js'
export type { Progress } from './core/terminal.js'
// ============================================================
// Core modules
// ============================================================
export { ClickEvent } from './core/events/click-event.js'
export { EventEmitter } from './core/events/emitter.js'
export { Event } from './core/events/event.js'
export { InputEvent } from './core/events/input-event.js'
export { TerminalFocusEvent, type TerminalFocusEventType } from './core/events/terminal-focus-event.js'
export { KeyboardEvent } from './core/events/keyboard-event.js'
export { FocusEvent } from './core/events/focus-event.js'
export { FocusManager } from './core/focus.js'
export { Ansi } from './core/Ansi.js'
export { stringWidth } from './core/stringWidth.js'
export { default as wrapText } from './core/wrap-text.js'
export { default as measureElement } from './core/measure-element.js'
export { supportsTabStatus } from './core/termio/osc.js'
export { setClipboard, getClipboardPath, CLEAR_ITERM2_PROGRESS, CLEAR_TAB_STATUS, CLEAR_TERMINAL_TITLE, wrapForMultiplexer } from './core/termio/osc.js'
export { DISABLE_KITTY_KEYBOARD, DISABLE_MODIFY_OTHER_KEYS } from './core/termio/csi.js'
export { SHOW_CURSOR, DBP, DFE, DISABLE_MOUSE_TRACKING, EXIT_ALT_SCREEN, HIDE_CURSOR, ENTER_ALT_SCREEN, ENABLE_MOUSE_TRACKING } from './core/termio/dec.js'
export { default as instances } from './core/instances.js'
export { default as renderBorder, type BorderTextOptions } from './core/render-border.js'
export { isSynchronizedOutputSupported, isXtermJs, hasCursorUpViewportYankBug, writeDiffToTerminal } from './core/terminal.js'
export { colorize, applyColor, applyTextStyles, type ColorType } from './core/colorize.js'
export { wrapAnsi } from './core/wrapAnsi.js'
export { default as styles } from './core/styles.js'
export { clamp } from './core/layout/geometry.js'
export { getTerminalFocusState, getTerminalFocused, subscribeTerminalFocus } from './core/terminal-focus-state.js'
export { supportsHyperlinks } from './core/supports-hyperlinks.js'
// ============================================================
// Components (Layer 2)
// ============================================================
export { default as BaseBox } from './components/Box.js'
export type { Props as BaseBoxProps } from './components/Box.js'
export { default as BaseText } from './components/Text.js'
export type { Props as BaseTextProps } from './components/Text.js'
export { default as Button, type ButtonState, type Props as ButtonProps } from './components/Button.js'
export { default as Link } from './components/Link.js'
export type { Props as LinkProps } from './components/Link.js'
export { default as Newline } from './components/Newline.js'
export type { Props as NewlineProps } from './components/Newline.js'
export { default as Spacer } from './components/Spacer.js'
export { NoSelect } from './components/NoSelect.js'
export { RawAnsi } from './components/RawAnsi.js'
export { default as ScrollBox, type ScrollBoxHandle } from './components/ScrollBox.js'
export { AlternateScreen } from './components/AlternateScreen.js'
// App types
export type { Props as AppProps } from './components/AppContext.js'
export type { Props as StdinProps } from './components/StdinContext.js'
export { TerminalSizeContext, type TerminalSize } from './components/TerminalSizeContext.js'
// ============================================================
// Hooks
// ============================================================
export { default as useApp } from './hooks/use-app.js'
export { default as useInput } from './hooks/use-input.js'
export { useAnimationFrame } from './hooks/use-animation-frame.js'
export { useAnimationTimer, useInterval } from './hooks/use-interval.js'
export { useSelection, useHasSelection } from './hooks/use-selection.js'
export { default as useStdin } from './hooks/use-stdin.js'
export { useTabStatus, type TabStatusKind } from './hooks/use-tab-status.js'
export { useTerminalFocus } from './hooks/use-terminal-focus.js'
export { useTerminalTitle } from './hooks/use-terminal-title.js'
export { useTerminalViewport } from './hooks/use-terminal-viewport.js'
export { useSearchHighlight } from './hooks/use-search-highlight.js'
export { useDeclaredCursor } from './hooks/use-declared-cursor.js'
export { TerminalWriteProvider, useTerminalNotification, type TerminalNotification } from './hooks/useTerminalNotification.js'
// ============================================================
// Theme (Layer 3)
// ============================================================
export {
ThemeProvider,
usePreviewTheme,
useTheme,
useThemeSetting,
} from './theme/ThemeProvider.js'
export { default as Box } from './theme/ThemedBox.js'
export type { Props as BoxProps } from './theme/ThemedBox.js'
export { default as Text } from './theme/ThemedText.js'
export type { Props as TextProps } from './theme/ThemedText.js'
export { color } from './theme/color.js'
// Theme sub-components
export { Dialog } from './theme/Dialog.js'
export { Divider } from './theme/Divider.js'
export { FuzzyPicker } from './theme/FuzzyPicker.js'
export { ListItem } from './theme/ListItem.js'
export { LoadingState } from './theme/LoadingState.js'
export { Pane } from './theme/Pane.js'
export { ProgressBar } from './theme/ProgressBar.js'
export { Ratchet } from './theme/Ratchet.js'
export { StatusIcon } from './theme/StatusIcon.js'
export { Tabs } from './theme/Tabs.js'
export { Byline } from './theme/Byline.js'
export { KeyboardShortcutHint } from './theme/KeyboardShortcutHint.js'

View File

@@ -1,57 +0,0 @@
import React, { Children, isValidElement } from 'react'
import { Text } from '../index.js'
type Props = {
/** The items to join with a middot separator */
children: React.ReactNode
}
/**
* Joins children with a middot separator (" · ") for inline metadata display.
*
* Named after the publishing term "byline" - the line of metadata typically
* shown below a title (e.g., "John Doe · 5 min read · Mar 12").
*
* Automatically filters out null/undefined/false children and only renders
* separators between valid elements.
*
* @example
* // Basic usage: "Enter to confirm · Esc to cancel"
* <Text dimColor>
* <Byline>
* <KeyboardShortcutHint shortcut="Enter" action="confirm" />
* <KeyboardShortcutHint shortcut="Esc" action="cancel" />
* </Byline>
* </Text>
*
* @example
* // With conditional children: "Esc to cancel" (only one item shown)
* <Text dimColor>
* <Byline>
* {showEnter && <KeyboardShortcutHint shortcut="Enter" action="confirm" />}
* <KeyboardShortcutHint shortcut="Esc" action="cancel" />
* </Byline>
* </Text>
*
*/
export function Byline({ children }: Props): React.ReactNode {
// Children.toArray already filters out null, undefined, and booleans
const validChildren = Children.toArray(children)
if (validChildren.length === 0) {
return null
}
return (
<>
{validChildren.map((child, index) => (
<React.Fragment
key={isValidElement(child) ? (child.key ?? index) : index}
>
{index > 0 && <Text dimColor> · </Text>}
{child}
</React.Fragment>
))}
</>
)
}

View File

@@ -1,35 +0,0 @@
/**
* Simplified ConfigurableShortcutHint for the standalone @anthropic/ink package.
*
* The full version reads user-configured keybindings via useShortcutDisplay.
* This stub just renders the fallback shortcut — sufficient for the package's
* internal theme components.
*/
import React from 'react'
import { KeyboardShortcutHint } from './KeyboardShortcutHint.js'
type Props = {
action: string
context: string
fallback: string
description: string
parens?: boolean
bold?: boolean
}
export function ConfigurableShortcutHint({
fallback,
description,
parens,
bold,
}: Props): React.ReactNode {
return (
<KeyboardShortcutHint
shortcut={fallback}
action={description}
parens={parens}
bold={bold}
/>
)
}

View File

@@ -1,100 +0,0 @@
import React from 'react'
import {
type ExitState,
useExitOnCtrlCDWithKeybindings,
} from '../hooks/useExitOnCtrlCD.js'
import { Box, Text } from '../index.js'
import { useKeybinding } from './keybindings.js'
import type { Theme } from './theme-types.js'
import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js'
import { Byline } from './Byline.js'
import { KeyboardShortcutHint } from './KeyboardShortcutHint.js'
import { Pane } from './Pane.js'
type DialogProps = {
title: React.ReactNode
subtitle?: React.ReactNode
children: React.ReactNode
onCancel: () => void
color?: keyof Theme
hideInputGuide?: boolean
hideBorder?: boolean
/** Custom input guide content. Receives exitState for Ctrl+C/D pending display. */
inputGuide?: (exitState: ExitState) => React.ReactNode
/**
* Controls whether Dialog's built-in confirm:no (Esc/n) and app:exit/interrupt
* (Ctrl-C/D) keybindings are active. Set to `false` while an embedded text
* field is being edited so those keys reach the field instead of being
* consumed by Dialog. TextInput has its own ctrl+c/d handlers (cancel on
* press, delete-forward on ctrl+d with text). Defaults to `true`.
*/
isCancelActive?: boolean
}
export function Dialog({
title,
subtitle,
children,
onCancel,
color = 'permission',
hideInputGuide,
hideBorder,
inputGuide,
isCancelActive = true,
}: DialogProps): React.ReactNode {
const exitState = useExitOnCtrlCDWithKeybindings(
undefined,
undefined,
isCancelActive,
)
// Use configurable keybinding for ESC to cancel.
// isCancelActive lets consumers (e.g. ElicitationDialog) disable this while
// an embedded TextInput is focused, so that keys like 'n' reach the field
// instead of being consumed here.
useKeybinding('confirm:no', onCancel, {
context: 'Confirmation',
isActive: isCancelActive,
})
const defaultInputGuide = exitState.pending ? (
<Text>Press {exitState.keyName} again to exit</Text>
) : (
<Byline>
<KeyboardShortcutHint shortcut="Enter" action="confirm" />
<ConfigurableShortcutHint
action="confirm:no"
context="Confirmation"
fallback="Esc"
description="cancel"
/>
</Byline>
)
const content = (
<>
<Box flexDirection="column" gap={1}>
<Box flexDirection="column">
<Text bold color={color}>
{title}
</Text>
{subtitle && <Text dimColor>{subtitle}</Text>}
</Box>
{children}
</Box>
{!hideInputGuide && (
<Box marginTop={1}>
<Text dimColor italic>
{inputGuide ? inputGuide(exitState) : defaultInputGuide}
</Text>
</Box>
)}
</>
)
if (hideBorder) {
return content
}
return <Pane color={color}>{content}</Pane>
}

View File

@@ -1,97 +0,0 @@
import React from 'react'
import { useTerminalSize } from '../hooks/useTerminalSize.js'
import { stringWidth } from '../core/stringWidth.js'
import { Ansi, Text } from '../index.js'
import type { Theme } from './theme-types.js'
type DividerProps = {
/**
* Width of the divider in characters.
* Defaults to terminal width.
*/
width?: number
/**
* Theme color for the divider.
* If not provided, dimColor is used.
*/
color?: keyof Theme
/**
* Character to use for the divider line.
* @default '─'
*/
char?: string
/**
* Padding to subtract from the width (e.g., for indentation).
* @default 0
*/
padding?: number
/**
* Title shown in the middle of the divider.
* May contain ANSI codes (e.g., chalk-styled text).
*
* @example
* // ─────────── Title ───────────
* <Divider title="Title" />
*/
title?: string
}
/**
* A horizontal divider line.
*
* @example
* // Full-width dimmed divider
* <Divider />
*
* @example
* // Colored divider
* <Divider color="suggestion" />
*
* @example
* // Fixed width
* <Divider width={40} />
*
* @example
* // Full width minus padding (for indented content)
* <Divider padding={4} />
*
* @example
* // With centered title
* <Divider title="3 new messages" />
*/
export function Divider({
width,
color,
char = '─',
padding = 0,
title,
}: DividerProps): React.ReactNode {
const { columns: terminalWidth } = useTerminalSize()
const effectiveWidth = Math.max(0, (width ?? terminalWidth) - padding)
if (title) {
const titleWidth = stringWidth(title) + 2 // +2 for spaces around title
const sideWidth = Math.max(0, effectiveWidth - titleWidth)
const leftWidth = Math.floor(sideWidth / 2)
const rightWidth = sideWidth - leftWidth
return (
<Text color={color} dimColor={!color}>
{char.repeat(leftWidth)}{' '}
<Text dimColor>
<Ansi>{title}</Ansi>
</Text>{' '}
{char.repeat(rightWidth)}
</Text>
)
}
return (
<Text color={color} dimColor={!color}>
{char.repeat(effectiveWidth)}
</Text>
)
}

View File

@@ -1,350 +0,0 @@
import * as React from 'react'
import { useEffect, useState } from 'react'
import { useSearchInput } from '../hooks/useSearchInput.js'
import { useTerminalSize } from '../hooks/useTerminalSize.js'
import type { KeyboardEvent } from '../core/events/keyboard-event.js'
import { clamp } from '../core/layout/geometry.js'
import { Box, Text, useTerminalFocus } from '../index.js'
import { SearchBox } from './SearchBox.js'
import { Byline } from './Byline.js'
import { KeyboardShortcutHint } from './KeyboardShortcutHint.js'
import { ListItem } from './ListItem.js'
import { Pane } from './Pane.js'
type PickerAction<T> = {
/** Hint label shown in the byline, e.g. "mention" → "Tab to mention". */
action: string
handler: (item: T) => void
}
type Props<T> = {
title: string
placeholder?: string
initialQuery?: string
items: readonly T[]
getKey: (item: T) => string
/** Keep to one line — preview handles overflow. */
renderItem: (item: T, isFocused: boolean) => React.ReactNode
renderPreview?: (item: T) => React.ReactNode
/** 'right' keeps hints stable (no bounce), but needs width. */
previewPosition?: 'bottom' | 'right'
visibleCount?: number
/**
* 'up' puts items[0] at the bottom next to the input (atuin-style). Arrows
* always match screen direction — ↑ walks visually up regardless.
*/
direction?: 'down' | 'up'
/** Caller owns filtering: re-filter on each call and pass new items. */
onQueryChange: (query: string) => void
/** Enter key. Primary action. */
onSelect: (item: T) => void
/**
* Tab key. If provided, Tab no longer aliases Enter — it gets its own
* handler and hint. Shift+Tab falls through to this if onShiftTab is unset.
*/
onTab?: PickerAction<T>
/** Shift+Tab key. Gets its own hint. */
onShiftTab?: PickerAction<T>
/**
* Fires when the focused item changes (via arrows or when items reset).
* Useful for async preview loading — keeps I/O out of renderPreview.
*/
onFocus?: (item: T | undefined) => void
onCancel: () => void
/** Shown when items is empty. Caller bakes loading/searching state into this. */
emptyMessage?: string | ((query: string) => string)
/**
* Status line below the list, e.g. "500+ matches" or "42 matches…".
* Caller decides when to show it — pass undefined to hide.
*/
matchLabel?: string
selectAction?: string
extraHints?: React.ReactNode
}
const DEFAULT_VISIBLE = 8
// Pane (paddingTop + Divider) + title + 3 gaps + SearchBox (rounded border = 3
// rows) + hints. matchLabel adds +1 when present, accounted for separately.
const CHROME_ROWS = 10
const MIN_VISIBLE = 2
export function FuzzyPicker<T>({
title,
placeholder = 'Type to search…',
initialQuery,
items,
getKey,
renderItem,
renderPreview,
previewPosition = 'bottom',
visibleCount: requestedVisible = DEFAULT_VISIBLE,
direction = 'down',
onQueryChange,
onSelect,
onTab,
onShiftTab,
onFocus,
onCancel,
emptyMessage = 'No results',
matchLabel,
selectAction = 'select',
extraHints,
}: Props<T>): React.ReactNode {
const isTerminalFocused = useTerminalFocus()
const { rows, columns } = useTerminalSize()
const [focusedIndex, setFocusedIndex] = useState(0)
// Cap visibleCount so the picker never exceeds the terminal height. When it
// overflows, each re-render (arrow key, ctrl+p) mis-positions the cursor-up
// by the overflow amount and a previously-drawn line flashes blank.
const visibleCount = Math.max(
MIN_VISIBLE,
Math.min(requestedVisible, rows - CHROME_ROWS - (matchLabel ? 1 : 0)),
)
// Full hint row with onTab+onShiftTab is ~100 chars and wraps inconsistently
// below that. Compact mode drops shift+tab and shortens labels.
const compact = columns < 120
const step = (delta: 1 | -1) => {
setFocusedIndex(i => clamp(i + delta, 0, items.length - 1))
}
// onKeyDown fires after useSearchInput's useInput, so onExit must be a
// no-op — return/downArrow are handled by handleKeyDown below. onCancel
// still covers escape/ctrl+c/ctrl+d. Backspace-on-empty is disabled so
// a held backspace doesn't eject the user from the dialog.
const { query, cursorOffset } = useSearchInput({
isActive: true,
onExit: () => {},
onCancel,
initialQuery,
backspaceExitsOnEmpty: false,
})
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'up' || (e.ctrl && e.key === 'p')) {
e.preventDefault()
e.stopImmediatePropagation()
step(direction === 'up' ? 1 : -1)
return
}
if (e.key === 'down' || (e.ctrl && e.key === 'n')) {
e.preventDefault()
e.stopImmediatePropagation()
step(direction === 'up' ? -1 : 1)
return
}
if (e.key === 'return') {
e.preventDefault()
e.stopImmediatePropagation()
const selected = items[focusedIndex]
if (selected) onSelect(selected)
return
}
if (e.key === 'tab') {
e.preventDefault()
e.stopImmediatePropagation()
const selected = items[focusedIndex]
if (!selected) return
const tabAction = e.shift ? (onShiftTab ?? onTab) : onTab
if (tabAction) {
tabAction.handler(selected)
} else {
onSelect(selected)
}
}
}
useEffect(() => {
onQueryChange(query)
setFocusedIndex(0)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query])
useEffect(() => {
setFocusedIndex(i => clamp(i, 0, items.length - 1))
}, [items.length])
const focused = items[focusedIndex]
useEffect(() => {
onFocus?.(focused)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [focused])
const windowStart = clamp(
focusedIndex - visibleCount + 1,
0,
items.length - visibleCount,
)
const visible = items.slice(windowStart, windowStart + visibleCount)
const emptyText =
typeof emptyMessage === 'function' ? emptyMessage(query) : emptyMessage
const searchBox = (
<SearchBox
query={query}
cursorOffset={cursorOffset}
placeholder={placeholder}
isFocused
isTerminalFocused={isTerminalFocused}
/>
)
const listBlock = (
<List
visible={visible}
windowStart={windowStart}
visibleCount={visibleCount}
total={items.length}
focusedIndex={focusedIndex}
direction={direction}
getKey={getKey}
renderItem={renderItem}
emptyText={emptyText}
/>
)
const preview =
renderPreview && focused ? (
<Box flexDirection="column" flexGrow={1}>
{renderPreview(focused)}
</Box>
) : null
// Structure must not depend on preview truthiness — when focused goes
// undefined (e.g. delete clears matches), switching row→fragment would
// change both layout AND gap count, bouncing the searchBox below.
const listGroup =
renderPreview && previewPosition === 'right' ? (
<Box
flexDirection="row"
gap={2}
height={visibleCount + (matchLabel ? 1 : 0)}
>
<Box flexDirection="column" flexShrink={0}>
{listBlock}
{matchLabel && <Text dimColor>{matchLabel}</Text>}
</Box>
{preview ?? <Box flexGrow={1} />}
</Box>
) : (
// Box (not fragment) so the outer gap={1} doesn't insert a blank line
// between list/matchLabel/preview — that read as extra space above the
// prompt in direction='up'.
<Box flexDirection="column">
{listBlock}
{matchLabel && <Text dimColor>{matchLabel}</Text>}
{preview}
</Box>
)
const inputAbove = direction !== 'up'
return (
<Pane color="permission">
<Box
flexDirection="column"
gap={1}
tabIndex={0}
autoFocus
onKeyDown={handleKeyDown}
>
<Text bold color="permission">
{title}
</Text>
{inputAbove && searchBox}
{listGroup}
{!inputAbove && searchBox}
<Text dimColor>
<Byline>
<KeyboardShortcutHint
shortcut="↑/↓"
action={compact ? 'nav' : 'navigate'}
/>
<KeyboardShortcutHint
shortcut="Enter"
action={compact ? firstWord(selectAction) : selectAction}
/>
{onTab && (
<KeyboardShortcutHint shortcut="Tab" action={onTab.action} />
)}
{onShiftTab && !compact && (
<KeyboardShortcutHint
shortcut="shift+tab"
action={onShiftTab.action}
/>
)}
<KeyboardShortcutHint shortcut="Esc" action="cancel" />
{extraHints}
</Byline>
</Text>
</Box>
</Pane>
)
}
type ListProps<T> = Pick<
Props<T>,
'visibleCount' | 'direction' | 'getKey' | 'renderItem'
> & {
visible: readonly T[]
windowStart: number
total: number
focusedIndex: number
emptyText: string
}
function List<T>({
visible,
windowStart,
visibleCount,
total,
focusedIndex,
direction,
getKey,
renderItem,
emptyText,
}: ListProps<T>): React.ReactNode {
if (visible.length === 0) {
return (
<Box height={visibleCount} flexShrink={0}>
<Text dimColor>{emptyText}</Text>
</Box>
)
}
const rows = visible.map((item, i) => {
const actualIndex = windowStart + i
const isFocused = actualIndex === focusedIndex
const atLowEdge = i === 0 && windowStart > 0
const atHighEdge =
i === visible.length - 1 && windowStart + visibleCount! < total
return (
<ListItem
key={getKey(item)}
isFocused={isFocused}
showScrollUp={direction === 'up' ? atHighEdge : atLowEdge}
showScrollDown={direction === 'up' ? atLowEdge : atHighEdge}
styled={false}
>
{renderItem(item, isFocused)}
</ListItem>
)
})
return (
<Box
height={visibleCount}
flexShrink={0}
flexDirection={direction === 'up' ? 'column-reverse' : 'column'}
>
{rows}
</Box>
)
}
function firstWord(s: string): string {
const i = s.indexOf(' ')
return i === -1 ? s : s.slice(0, i)
}

View File

@@ -1,58 +0,0 @@
import React from 'react'
import Text from '../components/Text.js'
type Props = {
/** The key or chord to display (e.g., "ctrl+o", "Enter", "↑/↓") */
shortcut: string
/** The action the key performs (e.g., "expand", "select", "navigate") */
action: string
/** Whether to wrap the hint in parentheses. Default: false */
parens?: boolean
/** Whether to render the shortcut in bold. Default: false */
bold?: boolean
}
/**
* Renders a keyboard shortcut hint like "ctrl+o to expand" or "(tab to toggle)"
*
* Wrap in <Text dimColor> for the common dim styling.
*
* @example
* // Simple hint wrapped in dim Text
* <Text dimColor><KeyboardShortcutHint shortcut="esc" action="cancel" /></Text>
*
* // With parentheses: "(ctrl+o to expand)"
* <Text dimColor><KeyboardShortcutHint shortcut="ctrl+o" action="expand" parens /></Text>
*
* // With bold shortcut: "Enter to confirm" (Enter is bold)
* <Text dimColor><KeyboardShortcutHint shortcut="Enter" action="confirm" bold /></Text>
*
* // Multiple hints with middot separator - use Byline
* <Text dimColor>
* <Byline>
* <KeyboardShortcutHint shortcut="Enter" action="confirm" />
* <KeyboardShortcutHint shortcut="Esc" action="cancel" />
* </Byline>
* </Text>
*/
export function KeyboardShortcutHint({
shortcut,
action,
parens = false,
bold = false,
}: Props): React.ReactNode {
const shortcutText = bold ? <Text bold>{shortcut}</Text> : shortcut
if (parens) {
return (
<Text>
({shortcutText} to {action})
</Text>
)
}
return (
<Text>
{shortcutText} to {action}
</Text>
)
}

View File

@@ -1,188 +0,0 @@
import figures from 'figures'
import type { ReactNode } from 'react'
import React from 'react'
import { useDeclaredCursor } from '../hooks/use-declared-cursor.js'
import { Box, Text } from '../index.js'
type ListItemProps = {
/**
* Whether this item is currently focused (keyboard selection).
* Shows the pointer indicator () when true.
*/
isFocused: boolean
/**
* Whether this item is selected (chosen/checked).
* Shows the checkmark indicator (✓) when true.
* @default false
*/
isSelected?: boolean
/**
* The content to display for this item.
*/
children: ReactNode
/**
* Optional description text displayed below the main content.
*/
description?: string
/**
* Show a down arrow indicator instead of pointer (for scroll hints).
* Only applies when not focused.
*/
showScrollDown?: boolean
/**
* Show an up arrow indicator instead of pointer (for scroll hints).
* Only applies when not focused.
*/
showScrollUp?: boolean
/**
* Whether to apply automatic styling to the children based on focus/selection state.
* - When true (default): children are wrapped in Text with state-based colors
* - When false: children are rendered as-is, allowing custom styling
* @default true
*/
styled?: boolean
/**
* Whether this item is disabled. Disabled items show dimmed text and no indicators.
* @default false
*/
disabled?: boolean
/**
* Whether this ListItem should declare the terminal cursor position.
* Set false when a child (e.g. BaseTextInput) declares its own cursor.
* @default true
*/
declareCursor?: boolean
}
/**
* A list item component for selection UIs (dropdowns, multi-selects, menus).
*
* Handles the common pattern of:
* - Pointer indicator () for focused items
* - Checkmark indicator (✓) for selected items
* - Scroll indicators (↓↑) for truncated lists
* - Color states for focus/selection
*
* @example
* // Basic usage in a selection list
* {options.map((option, i) => (
* <ListItem
* key={option.id}
* isFocused={focusIndex === i}
* isSelected={selectedId === option.id}
* >
* {option.label}
* </ListItem>
* ))}
*
* @example
* // With scroll indicators
* <ListItem isFocused={false} showScrollUp>First visible item</ListItem>
* ...
* <ListItem isFocused={false} showScrollDown>Last visible item</ListItem>
*
* @example
* // With description
* <ListItem isFocused isSelected={false} description="Secondary text here">
* Primary text
* </ListItem>
*
* @example
* // Custom children styling (styled=false)
* <ListItem isFocused styled={false}>
* <Text color="claude">Custom styled content</Text>
* </ListItem>
*/
export function ListItem({
isFocused,
isSelected = false,
children,
description,
showScrollDown,
showScrollUp,
styled = true,
disabled = false,
declareCursor,
}: ListItemProps): React.ReactNode {
// Determine which indicator to show
function renderIndicator(): ReactNode {
if (disabled) {
return <Text> </Text>
}
if (isFocused) {
return <Text color="suggestion">{figures.pointer}</Text>
}
if (showScrollDown) {
return <Text dimColor>{figures.arrowDown}</Text>
}
if (showScrollUp) {
return <Text dimColor>{figures.arrowUp}</Text>
}
return <Text> </Text>
}
// Determine text color based on state
function getTextColor(): 'success' | 'suggestion' | 'inactive' | undefined {
if (disabled) {
return 'inactive'
}
if (!styled) {
return undefined
}
if (isSelected) {
return 'success'
}
if (isFocused) {
return 'suggestion'
}
return undefined
}
const textColor = getTextColor()
// Park the native terminal cursor on the pointer indicator so screen
// readers / magnifiers track the focused item. (0,0) is the top-left of
// this Box, where the pointer renders.
const cursorRef = useDeclaredCursor({
line: 0,
column: 0,
active: isFocused && !disabled && declareCursor !== false,
})
return (
<Box ref={cursorRef} flexDirection="column">
<Box flexDirection="row" gap={1}>
{renderIndicator()}
{styled ? (
<Text color={textColor} dimColor={disabled}>
{children}
</Text>
) : (
children
)}
{isSelected && !disabled && <Text color="success">{figures.tick}</Text>}
</Box>
{description && (
<Box paddingLeft={2}>
<Text color="inactive">{description}</Text>
</Box>
)}
</Box>
)
}

View File

@@ -1,66 +0,0 @@
import React from 'react'
import { Box, Text } from '../index.js'
import { Spinner } from './Spinner.js'
type LoadingStateProps = {
/**
* The loading message to display next to the spinner.
*/
message: string
/**
* Display the message in bold.
* @default false
*/
bold?: boolean
/**
* Display the message in dimmed color.
* @default false
*/
dimColor?: boolean
/**
* Optional subtitle displayed below the main message.
*/
subtitle?: string
}
/**
* A spinner with loading message for async operations.
*
* @example
* // Basic loading
* <LoadingState message="Loading..." />
*
* @example
* // Bold loading message
* <LoadingState message="Loading sessions" bold />
*
* @example
* // With subtitle
* <LoadingState
* message="Loading sessions"
* bold
* subtitle="Fetching your Claude Code sessions..."
* />
*/
export function LoadingState({
message,
bold = false,
dimColor = false,
subtitle,
}: LoadingStateProps): React.ReactNode {
return (
<Box flexDirection="column">
<Box flexDirection="row">
<Spinner />
<Text bold={bold} dimColor={dimColor}>
{' '}
{message}
</Text>
</Box>
{subtitle && <Text dimColor>{subtitle}</Text>}
</Box>
)
}

View File

@@ -1,57 +0,0 @@
import React from 'react'
import { useIsInsideModal } from './modalContext.js'
import { Box } from '../index.js'
import type { Theme } from './theme-types.js'
import { Divider } from './Divider.js'
type PaneProps = {
children: React.ReactNode
/**
* Theme color for the top border line.
*/
color?: keyof Theme
}
/**
* A pane — a region of the terminal that appears below the REPL prompt,
* bounded by a colored top line with a one-row gap above and horizontal
* padding. Used by all slash-command screens: /config, /help, /plugins,
* /sandbox, /stats, /permissions.
*
* For confirm/cancel dialogs (Esc to dismiss, Enter to confirm), use
* `<Dialog>` instead — it registers its own keybindings. For a full
* rounded-border card, use `<Panel>`.
*
* Submenus rendered inside a Pane should use `hideBorder` on their Dialog
* so the Pane's border remains the single frame.
*
* @example
* <Pane color="permission">
* <Tabs title="Sandbox:">...</Tabs>
* </Pane>
*/
export function Pane({ children, color }: PaneProps): React.ReactNode {
// When rendered inside FullscreenLayout's modal slot, its ▔ divider IS
// the frame. Skip our own Divider (would double-frame) and the extra top
// padding. This lets slash-command screens that wrap in Pane (e.g.
// /model → ModelPicker) route through the modal slot unchanged.
if (useIsInsideModal()) {
// flexShrink=0: the modal slot's absolute Box has no explicit height
// (grows to fit, maxHeight cap). With flexGrow=1, re-renders cause
// yoga to resolve this Box's height to 0 against the undetermined
// parent — /permissions body blanks on Down arrow. See #23592.
return (
<Box flexDirection="column" paddingX={1} flexShrink={0}>
{children}
</Box>
)
}
return (
<Box flexDirection="column" paddingTop={1}>
<Divider color={color} />
<Box flexDirection="column" paddingX={2}>
{children}
</Box>
</Box>
)
}

View File

@@ -1,54 +0,0 @@
import React from 'react'
import { Text } from '../index.js'
import type { Theme } from './theme-types.js'
type Props = {
/**
* How much progress to display, between 0 and 1 inclusive
*/
ratio: number // [0, 1]
/**
* How many characters wide to draw the progress bar
*/
width: number // how many characters wide
/**
* Optional color for the filled portion of the bar
*/
fillColor?: keyof Theme
/**
* Optional color for the empty portion of the bar
*/
emptyColor?: keyof Theme
}
const BLOCKS = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█']
export function ProgressBar({
ratio: inputRatio,
width,
fillColor,
emptyColor,
}: Props): React.ReactNode {
const ratio = Math.min(1, Math.max(0, inputRatio))
const whole = Math.floor(ratio * width)
const segments = [BLOCKS[BLOCKS.length - 1]!.repeat(whole)]
if (whole < width) {
const remainder = ratio * width - whole
const middle = Math.floor(remainder * BLOCKS.length)
segments.push(BLOCKS[middle]!)
const empty = width - whole - 1
if (empty > 0) {
segments.push(BLOCKS[0]!.repeat(empty))
}
}
return (
<Text color={fillColor} backgroundColor={emptyColor}>
{segments.join('')}
</Text>
)
}

View File

@@ -1,45 +0,0 @@
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react'
import { useTerminalSize } from '../hooks/useTerminalSize.js'
import { useTerminalViewport } from '../hooks/use-terminal-viewport.js'
import { Box, type DOMElement, measureElement } from '../index.js'
type Props = {
children: React.ReactNode
lock?: 'always' | 'offscreen'
}
export function Ratchet({ children, lock = 'always' }: Props): React.ReactNode {
const [viewportRef, { isVisible }] = useTerminalViewport()
const { rows } = useTerminalSize()
const innerRef = useRef<DOMElement | null>(null)
const maxHeight = useRef(0)
const [minHeight, setMinHeight] = useState(0)
const outerRef = useCallback(
(el: DOMElement | null) => {
viewportRef(el)
},
[viewportRef],
)
const engaged = lock === 'always' || !isVisible
useLayoutEffect(() => {
if (!innerRef.current) {
return
}
const { height } = measureElement(innerRef.current)
if (height > maxHeight.current) {
maxHeight.current = Math.min(height, rows)
setMinHeight(maxHeight.current)
}
})
return (
<Box minHeight={engaged ? minHeight : undefined} ref={outerRef}>
<Box ref={innerRef} flexDirection="column">
{children}
</Box>
</Box>
)
}

View File

@@ -1,71 +0,0 @@
import React from 'react'
import { Box, Text } from '../index.js'
type Props = {
query: string
placeholder?: string
isFocused: boolean
isTerminalFocused: boolean
prefix?: string
width?: number | string
cursorOffset?: number
borderless?: boolean
}
export function SearchBox({
query,
placeholder = 'Search…',
isFocused,
isTerminalFocused,
prefix = '\u2315',
width,
cursorOffset,
borderless = false,
}: Props): React.ReactNode {
const offset = cursorOffset ?? query.length
return (
<Box
flexShrink={0}
borderStyle={borderless ? undefined : 'round'}
borderColor={isFocused ? 'suggestion' : undefined}
borderDimColor={!isFocused}
paddingX={borderless ? 0 : 1}
width={width}
>
<Text dimColor={!isFocused}>
{prefix}{' '}
{isFocused ? (
<>
{query ? (
isTerminalFocused ? (
<>
<Text>{query.slice(0, offset)}</Text>
<Text inverse>
{offset < query.length ? query[offset] : ' '}
</Text>
{offset < query.length && (
<Text>{query.slice(offset + 1)}</Text>
)}
</>
) : (
<Text>{query}</Text>
)
) : isTerminalFocused ? (
<>
<Text inverse>{placeholder.charAt(0)}</Text>
<Text dimColor>{placeholder.slice(1)}</Text>
</>
) : (
<Text dimColor>{placeholder}</Text>
)}
</>
) : query ? (
<Text>{query}</Text>
) : (
<Text>{placeholder}</Text>
)}
</Text>
</Box>
)
}

View File

@@ -1,20 +0,0 @@
import React, { useState, useEffect } from 'react'
import { Text } from '../index.js'
const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
/**
* A simple animated spinner for loading states.
*/
export function Spinner(): React.ReactNode {
const [frame, setFrame] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setFrame(f => (f + 1) % FRAMES.length)
}, 80)
return () => clearInterval(timer)
}, [])
return <Text>{FRAMES[frame]}</Text>
}

View File

@@ -1,71 +0,0 @@
import figures from 'figures'
import React from 'react'
import { Text } from '../index.js'
type Status = 'success' | 'error' | 'warning' | 'info' | 'pending' | 'loading'
type Props = {
/**
* The status to display. Determines both the icon and color.
*
* - `success`: Green checkmark (✓)
* - `error`: Red cross (✗)
* - `warning`: Yellow warning symbol (⚠)
* - `info`: Blue info symbol ()
* - `pending`: Dimmed circle (○)
* - `loading`: Dimmed ellipsis (…)
*/
status: Status
/**
* Include a trailing space after the icon. Useful when followed by text.
* @default false
*/
withSpace?: boolean
}
const STATUS_CONFIG: Record<
Status,
{
icon: string
color: 'success' | 'error' | 'warning' | 'suggestion' | undefined
}
> = {
success: { icon: figures.tick, color: 'success' },
error: { icon: figures.cross, color: 'error' },
warning: { icon: figures.warning, color: 'warning' },
info: { icon: figures.info, color: 'suggestion' },
pending: { icon: figures.circle, color: undefined },
loading: { icon: '…', color: undefined },
}
/**
* Renders a status indicator icon with appropriate color.
*
* @example
* // Success indicator
* <StatusIcon status="success" />
*
* @example
* // Error with trailing space for text
* <Text><StatusIcon status="error" withSpace />Failed to connect</Text>
*
* @example
* // Status line pattern
* <Text>
* <StatusIcon status="pending" withSpace />
* Waiting for response
* </Text>
*/
export function StatusIcon({
status,
withSpace = false,
}: Props): React.ReactNode {
const config = STATUS_CONFIG[status]
return (
<Text color={config.color} dimColor={!config.color}>
{config.icon}
{withSpace && ' '}
</Text>
)
}

View File

@@ -1,339 +0,0 @@
import React, {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from 'react'
import {
useIsInsideModal,
useModalScrollRef,
} from './modalContext.js'
import { useTerminalSize } from '../hooks/useTerminalSize.js'
import ScrollBox from '../components/ScrollBox.js'
import type { KeyboardEvent } from '../core/events/keyboard-event.js'
import { stringWidth } from '../core/stringWidth.js'
import { Box, Text } from '../index.js'
import { useKeybindings } from './keybindings.js'
import type { Theme } from './theme-types.js'
type TabsProps = {
children: Array<React.ReactElement<TabProps>>
title?: string
color?: keyof Theme
defaultTab?: string
hidden?: boolean
useFullWidth?: boolean
/** Controlled mode: current selected tab id/title */
selectedTab?: string
/** Controlled mode: callback when tab changes */
onTabChange?: (tabId: string) => void
/** Optional banner to display below tabs header */
banner?: React.ReactNode
/** Disable keyboard navigation (e.g. when a child component handles arrow keys) */
disableNavigation?: boolean
/**
* Initial focus state for the tab header row. Defaults to true (header
* focused, nav always works). Keep the default for Select/list content —
* those only use up/down so there's no conflict; pass
* isDisabled={headerFocused} to the Select instead. Only set false when
* content actually binds left/right/tab (e.g. enum cycling), and show a
* "↑ tabs" footer hint — without it tabs look broken.
*/
initialHeaderFocused?: boolean
/**
* Fixed height for the content area. When set, all tabs render within the
* same height (overflow hidden) so switching tabs doesn't cause layout
* shifts. Shorter tabs get whitespace; taller tabs are clipped.
*/
contentHeight?: number
/**
* Let Tab/←/→ switch tabs from focused content. Opt-in since some
* content uses those keys; pass a reactive boolean to cede them when
* needed. Switching from content focuses the header.
*/
navFromContent?: boolean
}
type TabsContextValue = {
selectedTab: string | undefined
width: number | undefined
headerFocused: boolean
focusHeader: () => void
blurHeader: () => void
registerOptIn: () => () => void
}
const TabsContext = createContext<TabsContextValue>({
selectedTab: undefined,
width: undefined,
// Default for components rendered outside a Tabs (tests, standalone):
// content has focus, focusHeader is a no-op.
headerFocused: false,
focusHeader: () => {},
blurHeader: () => {},
registerOptIn: () => () => {},
})
export function Tabs({
title,
color,
defaultTab,
children,
hidden,
useFullWidth,
selectedTab: controlledSelectedTab,
onTabChange,
banner,
disableNavigation,
initialHeaderFocused = true,
contentHeight,
navFromContent = false,
}: TabsProps): React.ReactNode {
const { columns: terminalWidth } = useTerminalSize()
const tabs = children.map(child => [
child.props.id ?? child.props.title,
child.props.title,
])
const defaultTabIndex = defaultTab
? tabs.findIndex(tab => defaultTab === tab[0])
: 0
// Support both controlled and uncontrolled modes
const isControlled = controlledSelectedTab !== undefined
const [internalSelectedTab, setInternalSelectedTab] = useState(
defaultTabIndex !== -1 ? defaultTabIndex : 0,
)
// In controlled mode, find the index of the controlled tab
const controlledTabIndex = isControlled
? tabs.findIndex(tab => tab[0] === controlledSelectedTab)
: -1
const selectedTabIndex = isControlled
? controlledTabIndex !== -1
? controlledTabIndex
: 0
: internalSelectedTab
const modalScrollRef = useModalScrollRef()
// Header focus: left/right/tab only switch tabs when the header row is
// focused. Children with interactive content call focusHeader() (via
// useTabHeaderFocus) on up-arrow to hand focus back here; down-arrow
// returns it. Tabs that never call the hook see no behavior change —
// initialHeaderFocused defaults to true so nav always works.
const [headerFocused, setHeaderFocused] = useState(initialHeaderFocused)
const focusHeader = useCallback(() => setHeaderFocused(true), [])
const blurHeader = useCallback(() => setHeaderFocused(false), [])
// Count of mounted children using useTabHeaderFocus(). Down-arrow blur and
// the ↓ hint only engage when at least one child has opted in — otherwise
// pressing down on a legacy tab would strand the user with nav disabled.
const [optInCount, setOptInCount] = useState(0)
const registerOptIn = useCallback(() => {
setOptInCount(n => n + 1)
return () => setOptInCount(n => n - 1)
}, [])
const optedIn = optInCount > 0
const handleTabChange = (offset: number) => {
const newIndex = (selectedTabIndex + tabs.length + offset) % tabs.length
const newTabId = tabs[newIndex]?.[0]
if (isControlled && onTabChange && newTabId) {
onTabChange(newTabId)
} else {
setInternalSelectedTab(newIndex)
}
// Tab switching is a header action — stay focused so the user can keep
// cycling. The newly mounted tab can blur via its own interaction.
setHeaderFocused(true)
}
useKeybindings(
{
'tabs:next': () => handleTabChange(1),
'tabs:previous': () => handleTabChange(-1),
},
{
context: 'Tabs',
isActive: !hidden && !disableNavigation && headerFocused,
},
)
// When the header is focused, down-arrow returns focus to content. Only
// active when the selected tab has opted in via useTabHeaderFocus() —
// legacy tabs have nowhere to return focus to.
const handleKeyDown = (e: KeyboardEvent) => {
if (!headerFocused || !optedIn || hidden) return
if (e.key === 'down') {
e.preventDefault()
setHeaderFocused(false)
}
}
// Opt-in: same tabs:next/previous actions, active from content. Focuses
// the header so subsequent presses cycle via the handler above.
useKeybindings(
{
'tabs:next': () => {
handleTabChange(1)
setHeaderFocused(true)
},
'tabs:previous': () => {
handleTabChange(-1)
setHeaderFocused(true)
},
},
{
context: 'Tabs',
isActive:
navFromContent &&
!headerFocused &&
optedIn &&
!hidden &&
!disableNavigation,
},
)
// Calculate spacing to fill the available width. No keyboard hint in the
// header row — content footers own hints (see useTabHeaderFocus docs).
const titleWidth = title ? stringWidth(title) + 1 : 0 // +1 for gap
const tabsWidth = tabs.reduce(
(sum, [, tabTitle]) => sum + (tabTitle ? stringWidth(tabTitle) : 0) + 2 + 1, // +2 for padding, +1 for gap
0,
)
const usedWidth = titleWidth + tabsWidth
const spacerWidth = useFullWidth ? Math.max(0, terminalWidth - usedWidth) : 0
const contentWidth = useFullWidth ? terminalWidth : undefined
return (
<TabsContext.Provider
value={{
selectedTab: tabs[selectedTabIndex]![0],
width: contentWidth,
headerFocused,
focusHeader,
blurHeader,
registerOptIn,
}}
>
<Box
flexDirection="column"
tabIndex={0}
autoFocus
onKeyDown={handleKeyDown}
// flexShrink=0 inside modal slot — the modal's absolute Box has no
// explicit height (grows to fit, maxHeight cap), so flexGrow=1 here
// resolves to 0 on re-render and the body blanks on Down arrow.
// See #23592. Outside modal, leave layout alone.
flexShrink={modalScrollRef ? 0 : undefined}
>
{!hidden && (
<Box
flexDirection="row"
gap={1}
flexShrink={modalScrollRef ? 0 : undefined}
>
{title !== undefined && (
<Text bold color={color}>
{title}
</Text>
)}
{tabs.map(([id, title], i) => {
const isCurrent = selectedTabIndex === i
const hasColorCursor = color && isCurrent && headerFocused
return (
<Text
key={id}
backgroundColor={hasColorCursor ? color : undefined}
color={hasColorCursor ? 'inverseText' : undefined}
inverse={isCurrent && !hasColorCursor}
bold={isCurrent}
>
{' '}
{title}{' '}
</Text>
)
})}
{spacerWidth > 0 && <Text>{' '.repeat(spacerWidth)}</Text>}
</Box>
)}
{banner}
{modalScrollRef ? (
// Inside the modal slot: own the ScrollBox here so the tabs
// header row above sits OUTSIDE the scroll area — it can never
// scroll off. The ref reaches REPL's ScrollKeybindingHandler via
// ModalContext. Keyed by selectedTabIndex → remounts on tab
// switch, resetting scrollTop to 0 without scrollTo() timing games.
<Box width={contentWidth} marginTop={hidden ? 0 : 1} flexShrink={0}>
<ScrollBox
key={selectedTabIndex}
ref={modalScrollRef}
flexDirection="column"
flexShrink={0}
>
{children}
</ScrollBox>
</Box>
) : (
<Box
width={contentWidth}
marginTop={hidden ? 0 : 1}
height={contentHeight}
overflowY={contentHeight !== undefined ? 'hidden' : undefined}
>
{children}
</Box>
)}
</Box>
</TabsContext.Provider>
)
}
type TabProps = {
title: string
id?: string
children: React.ReactNode
}
export function Tab({ title, id, children }: TabProps): React.ReactNode {
const { selectedTab, width } = useContext(TabsContext)
const insideModal = useIsInsideModal()
if (selectedTab !== (id ?? title)) {
return null
}
return (
<Box width={width} flexShrink={insideModal ? 0 : undefined}>
{children}
</Box>
)
}
export function useTabsWidth(): number | undefined {
const { width } = useContext(TabsContext)
return width
}
/**
* Opt into header-focus gating. Returns the current header focus state and a
* callback to hand focus back to the tab row. For a Select, pass
* `isDisabled={headerFocused}` and `onUpFromFirstItem={focusHeader}`; keep the
* parent Tabs' initialHeaderFocused at its default so tab/←/→ work on mount.
*
* Calling this hook registers a ↓-blurs-header opt-in on mount. Don't call it
* above an early return that renders static text — ↓ will blur the header with
* no onUpFromFirstItem to recover. Split the component so the hook only runs
* when the Select renders.
*/
export function useTabHeaderFocus(): {
headerFocused: boolean
focusHeader: () => void
blurHeader: () => void
} {
const { headerFocused, focusHeader, blurHeader, registerOptIn } =
useContext(TabsContext)
useEffect(registerOptIn, [registerOptIn])
return { headerFocused, focusHeader, blurHeader }
}

View File

@@ -1,172 +0,0 @@
import { feature } from 'bun:bundle'
import React, {
createContext,
useContext,
useEffect,
useMemo,
useState,
} from 'react'
import useStdin from '../hooks/use-stdin.js'
import { getSystemThemeName, type SystemTheme } from './systemTheme.js'
import type { ThemeName, ThemeSetting } from './theme-types.js'
// -- Config persistence injection --
// Business layer provides these via setThemeConfigCallbacks().
// Defaults read/write from a simple module-level store.
let _loadTheme: () => ThemeSetting = () => 'dark'
let _saveTheme: (setting: ThemeSetting) => void = () => {}
/** Inject config persistence from the business layer. Call once at startup. */
export function setThemeConfigCallbacks(opts: {
loadTheme: () => ThemeSetting
saveTheme: (setting: ThemeSetting) => void
}): void {
_loadTheme = opts.loadTheme
_saveTheme = opts.saveTheme
}
type ThemeContextValue = {
/** The saved user preference. May be 'auto'. */
themeSetting: ThemeSetting
setThemeSetting: (setting: ThemeSetting) => void
setPreviewTheme: (setting: ThemeSetting) => void
savePreview: () => void
cancelPreview: () => void
/** The resolved theme to render with. Never 'auto'. */
currentTheme: ThemeName
}
// Non-'auto' default so useTheme() works without a provider (tests, tooling).
const DEFAULT_THEME: ThemeName = 'dark'
const ThemeContext = createContext<ThemeContextValue>({
themeSetting: DEFAULT_THEME,
setThemeSetting: () => {},
setPreviewTheme: () => {},
savePreview: () => {},
cancelPreview: () => {},
currentTheme: DEFAULT_THEME,
})
type Props = {
children: React.ReactNode
initialState?: ThemeSetting
onThemeSave?: (setting: ThemeSetting) => void
}
function defaultInitialTheme(): ThemeSetting {
return _loadTheme()
}
function defaultSaveTheme(setting: ThemeSetting): void {
_saveTheme(setting)
}
export function ThemeProvider({
children,
initialState,
onThemeSave = defaultSaveTheme,
}: Props) {
const [themeSetting, setThemeSetting] = useState(
initialState ?? defaultInitialTheme,
)
const [previewTheme, setPreviewTheme] = useState<ThemeSetting | null>(null)
// Track terminal theme for 'auto' resolution. Seeds from $COLORFGBG (or
// 'dark' if unset); the OSC 11 watcher corrects it on first poll.
const [systemTheme, setSystemTheme] = useState<SystemTheme>(() =>
(initialState ?? themeSetting) === 'auto' ? getSystemThemeName() : 'dark',
)
// The setting currently in effect (preview wins while picker is open)
const activeSetting = previewTheme ?? themeSetting
const { internal_querier } = useStdin()
// Watch for live terminal theme changes while 'auto' is active.
// Positive feature() pattern so the watcher import is dead-code-eliminated
// in external builds.
useEffect(() => {
if (feature('AUTO_THEME')) {
if (activeSetting !== 'auto' || !internal_querier) return
let cleanup: (() => void) | undefined
let cancelled = false
void import('../../utils/systemThemeWatcher.js').then(
({ watchSystemTheme }) => {
if (cancelled) return
cleanup = watchSystemTheme(internal_querier, setSystemTheme)
},
)
return () => {
cancelled = true
cleanup?.()
}
}
}, [activeSetting, internal_querier])
const currentTheme: ThemeName =
activeSetting === 'auto' ? systemTheme : activeSetting
const value = useMemo<ThemeContextValue>(
() => ({
themeSetting,
setThemeSetting: (newSetting: ThemeSetting) => {
setThemeSetting(newSetting)
setPreviewTheme(null)
// Switching to 'auto' restarts the watcher (activeSetting dep), whose
// first poll fires immediately. Seed from the cache so the OSC
// round-trip doesn't flash the wrong palette.
if (newSetting === 'auto') {
setSystemTheme(getSystemThemeName())
}
onThemeSave?.(newSetting)
},
setPreviewTheme: (newSetting: ThemeSetting) => {
setPreviewTheme(newSetting)
if (newSetting === 'auto') {
setSystemTheme(getSystemThemeName())
}
},
savePreview: () => {
if (previewTheme !== null) {
setThemeSetting(previewTheme)
setPreviewTheme(null)
onThemeSave?.(previewTheme)
}
},
cancelPreview: () => {
if (previewTheme !== null) {
setPreviewTheme(null)
}
},
currentTheme,
}),
[themeSetting, previewTheme, currentTheme, onThemeSave],
)
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
}
/**
* Returns the resolved theme for rendering (never 'auto') and a setter that
* accepts any ThemeSetting (including 'auto').
*/
export function useTheme(): [ThemeName, (setting: ThemeSetting) => void] {
const { currentTheme, setThemeSetting } = useContext(ThemeContext)
return [currentTheme, setThemeSetting]
}
/**
* Returns the raw theme setting as stored in config. Use this in UI that
* needs to show 'auto' as a distinct choice (e.g., ThemePicker).
*/
export function useThemeSetting(): ThemeSetting {
return useContext(ThemeContext).themeSetting
}
export function usePreviewTheme() {
const { setPreviewTheme, savePreview, cancelPreview } =
useContext(ThemeContext)
return { setPreviewTheme, savePreview, cancelPreview }
}

View File

@@ -1,112 +0,0 @@
import React, { type PropsWithChildren, type Ref } from 'react'
import Box from '../components/Box.js'
import type { DOMElement } from '../core/dom.js'
import type { ClickEvent } from '../core/events/click-event.js'
import type { FocusEvent } from '../core/events/focus-event.js'
import type { KeyboardEvent } from '../core/events/keyboard-event.js'
import type { Color, Styles } from '../core/styles.js'
import { getTheme, type Theme } from './theme-types.js'
import { useTheme } from './ThemeProvider.js'
// Color props that accept theme keys
type ThemedColorProps = {
readonly borderColor?: keyof Theme | Color
readonly borderTopColor?: keyof Theme | Color
readonly borderBottomColor?: keyof Theme | Color
readonly borderLeftColor?: keyof Theme | Color
readonly borderRightColor?: keyof Theme | Color
readonly backgroundColor?: keyof Theme | Color
}
// Base Styles without color props (they'll be overridden)
type BaseStylesWithoutColors = Omit<
Styles,
| 'textWrap'
| 'borderColor'
| 'borderTopColor'
| 'borderBottomColor'
| 'borderLeftColor'
| 'borderRightColor'
| 'backgroundColor'
>
export type Props = BaseStylesWithoutColors &
ThemedColorProps & {
ref?: Ref<DOMElement>
tabIndex?: number
autoFocus?: boolean
onClick?: (event: ClickEvent) => void
onFocus?: (event: FocusEvent) => void
onFocusCapture?: (event: FocusEvent) => void
onBlur?: (event: FocusEvent) => void
onBlurCapture?: (event: FocusEvent) => void
onKeyDown?: (event: KeyboardEvent) => void
onKeyDownCapture?: (event: KeyboardEvent) => void
onMouseEnter?: () => void
onMouseLeave?: () => void
}
/**
* Resolves a color value that may be a theme key to a raw Color.
*/
function resolveColor(
color: keyof Theme | Color | undefined,
theme: Theme,
): Color | undefined {
if (!color) return undefined
// Check if it's a raw color (starts with rgb(, #, ansi256(, or ansi:)
if (
color.startsWith('rgb(') ||
color.startsWith('#') ||
color.startsWith('ansi256(') ||
color.startsWith('ansi:')
) {
return color as Color
}
// It's a theme key - resolve it
return theme[color as keyof Theme] as Color
}
/**
* Theme-aware Box component that resolves theme color keys to raw colors.
* This wraps the base Box component with theme resolution for border colors.
*/
function ThemedBox({
borderColor,
borderTopColor,
borderBottomColor,
borderLeftColor,
borderRightColor,
backgroundColor,
children,
ref,
...rest
}: PropsWithChildren<Props>): React.ReactNode {
const [themeName] = useTheme()
const theme = getTheme(themeName)
// Resolve theme keys to raw colors
const resolvedBorderColor = resolveColor(borderColor, theme)
const resolvedBorderTopColor = resolveColor(borderTopColor, theme)
const resolvedBorderBottomColor = resolveColor(borderBottomColor, theme)
const resolvedBorderLeftColor = resolveColor(borderLeftColor, theme)
const resolvedBorderRightColor = resolveColor(borderRightColor, theme)
const resolvedBackgroundColor = resolveColor(backgroundColor, theme)
return (
<Box
ref={ref}
borderColor={resolvedBorderColor}
borderTopColor={resolvedBorderTopColor}
borderBottomColor={resolvedBorderBottomColor}
borderLeftColor={resolvedBorderLeftColor}
borderRightColor={resolvedBorderRightColor}
backgroundColor={resolvedBackgroundColor}
{...rest}
>
{children}
</Box>
)
}
export default ThemedBox

View File

@@ -1,132 +0,0 @@
import type { ReactNode } from 'react'
import React, { useContext } from 'react'
import Text from '../components/Text.js'
import type { Color, Styles } from '../core/styles.js'
import { getTheme, type Theme } from './theme-types.js'
import { useTheme } from './ThemeProvider.js'
/** Colors uncolored ThemedText in the subtree. Precedence: explicit `color` >
* this > dimColor. Crosses Box boundaries (Ink's style cascade doesn't). */
export const TextHoverColorContext = React.createContext<
keyof Theme | undefined
>(undefined)
export type Props = {
/**
* Change text color. Accepts a theme key or raw color value.
*/
readonly color?: keyof Theme | Color
/**
* Same as `color`, but for background. Must be a theme key.
*/
readonly backgroundColor?: keyof Theme
/**
* Dim the color using the theme's inactive color.
* This is compatible with bold (unlike ANSI dim).
*/
readonly dimColor?: boolean
/**
* Make the text bold.
*/
readonly bold?: boolean
/**
* Make the text italic.
*/
readonly italic?: boolean
/**
* Make the text underlined.
*/
readonly underline?: boolean
/**
* Make the text crossed with a line.
*/
readonly strikethrough?: boolean
/**
* Inverse background and foreground colors.
*/
readonly inverse?: boolean
/**
* This property tells Ink to wrap or truncate text if its width is larger than container.
* If `wrap` is passed (by default), Ink will wrap text and split it into multiple lines.
* If `truncate-*` is passed, Ink will truncate text instead, which will result in one line of text with the rest cut off.
*/
readonly wrap?: Styles['textWrap']
readonly children?: ReactNode
}
/**
* Resolves a color value that may be a theme key to a raw Color.
*/
function resolveColor(
color: keyof Theme | Color | undefined,
theme: Theme,
): Color | undefined {
if (!color) return undefined
// Check if it's a raw color (starts with rgb(, #, ansi256(, or ansi:)
if (
color.startsWith('rgb(') ||
color.startsWith('#') ||
color.startsWith('ansi256(') ||
color.startsWith('ansi:')
) {
return color as Color
}
// It's a theme key - resolve it
return theme[color as keyof Theme] as Color
}
/**
* Theme-aware Text component that resolves theme color keys to raw colors.
* This wraps the base Text component with theme resolution.
*/
export default function ThemedText({
color,
backgroundColor,
dimColor = false,
bold = false,
italic = false,
underline = false,
strikethrough = false,
inverse = false,
wrap = 'wrap',
children,
}: Props): React.ReactNode {
const [themeName] = useTheme()
const theme = getTheme(themeName)
const hoverColor = useContext(TextHoverColorContext)
// Resolve theme keys to raw colors
const resolvedColor =
!color && hoverColor
? resolveColor(hoverColor, theme)
: dimColor
? (theme.inactive as Color)
: resolveColor(color, theme)
const resolvedBackgroundColor = backgroundColor
? (theme[backgroundColor] as Color)
: undefined
return (
<Text
color={resolvedColor}
backgroundColor={resolvedBackgroundColor}
bold={bold}
italic={italic}
underline={underline}
strikethrough={strikethrough}
inverse={inverse}
wrap={wrap}
>
{children}
</Text>
)
}

View File

@@ -1,30 +0,0 @@
import { type ColorType, colorize } from '../core/colorize.js'
import type { Color } from '../core/styles.js'
import { getTheme, type Theme, type ThemeName } from './theme-types.js'
/**
* Curried theme-aware color function. Resolves theme keys to raw color
* values before delegating to the ink renderer's colorize.
*/
export function color(
c: keyof Theme | Color | undefined,
theme: ThemeName,
type: ColorType = 'foreground',
): (text: string) => string {
return text => {
if (!c) {
return text
}
// Raw color values bypass theme lookup
if (
c.startsWith('rgb(') ||
c.startsWith('#') ||
c.startsWith('ansi256(') ||
c.startsWith('ansi:')
) {
return colorize(text, c, type)
}
// Theme key lookup
return colorize(text, getTheme(theme)[c as keyof Theme], type)
}
}

View File

@@ -1,87 +0,0 @@
/**
* Minimal stub of the keybinding system for the standalone @anthropic/ink package.
*
* The full keybinding system (src/keybindings/) depends on KeybindingContext,
* KeybindingRegistry, and chord handling. This stub provides the same hook
* interfaces (useKeybinding / useKeybindings) but routes directly through
* useInput, matching common key sequences to action names.
*
* Only the keybindings used by theme components are mapped:
* - confirm:no → Escape
* - tabs:next → Tab / Right arrow
* - tabs:previous → Shift+Tab / Left arrow
*/
import { useCallback } from 'react'
import useInput from '../hooks/use-input.js'
import type { Key } from '../core/events/input-event.js'
type Options = {
context?: string
isActive?: boolean
}
/** Maps action names to key matching logic. */
const ACTION_MATCHERS: Record<
string,
(input: string, key: Key) => boolean
> = {
'confirm:no': (_input, key) => key.escape === true,
'tabs:next': (input, key) =>
(key.tab && !key.shift) || (key.rightArrow && !key.shift),
'tabs:previous': (_input, key) =>
(key.tab && key.shift) || (key.leftArrow && !key.shift),
}
/**
* Register a single keybinding action handler.
*/
export function useKeybinding(
action: string,
handler: () => void | false | Promise<void>,
options: Options = {},
): void {
const { isActive = true } = options
const handleInput = useCallback(
(input: string, key: Key) => {
if (!isActive) return
const matcher = ACTION_MATCHERS[action]
if (matcher && matcher(input, key)) {
if (handler() !== false) {
// consumed
}
}
},
[action, handler, isActive],
)
useInput(handleInput, { isActive })
}
/**
* Register multiple keybinding action handlers in one hook.
*/
export function useKeybindings(
handlers: Record<string, () => void | false | Promise<void>>,
options: Options = {},
): void {
const { isActive = true } = options
const handleInput = useCallback(
(input: string, key: Key) => {
if (!isActive) return
for (const [action, handler] of Object.entries(handlers)) {
const matcher = ACTION_MATCHERS[action]
if (matcher && matcher(input, key)) {
if (handler() !== false) {
break // consumed, stop checking other handlers
}
}
}
},
[handlers, isActive],
)
useInput(handleInput, { isActive })
}

View File

@@ -1,25 +0,0 @@
/**
* Minimal modal context for the standalone @anthropic/ink package.
*
* Provides useIsInsideModal() and useModalScrollRef() used by Pane and Tabs
* to adjust rendering when inside a FullscreenLayout modal slot.
*/
import { createContext, type RefObject, useContext } from 'react'
import type { ScrollBoxHandle } from '../components/ScrollBox.js'
type ModalCtx = {
rows: number
columns: number
scrollRef: RefObject<ScrollBoxHandle | null> | null
}
export const ModalContext = createContext<ModalCtx | null>(null)
export function useIsInsideModal(): boolean {
return useContext(ModalContext) !== null
}
export function useModalScrollRef(): RefObject<ScrollBoxHandle | null> | null {
return useContext(ModalContext)?.scrollRef ?? null
}

View File

@@ -1,40 +0,0 @@
/**
* Terminal dark/light mode detection.
*
* Detection is based on the terminal's actual background color (queried via
* OSC 11) rather than the OS appearance setting.
*
* Vendored from src/utils/systemTheme.ts for package independence.
*/
export type SystemTheme = 'dark' | 'light'
let cachedSystemTheme: SystemTheme | undefined
/**
* Detect theme from $COLORFGBG environment variable (set by some terminals).
*/
function detectFromColorFgBg(): SystemTheme | undefined {
const colorFgBg = process.env.COLORFGBG
if (!colorFgBg) return undefined
const parts = colorFgBg.split(';')
if (parts.length < 2) return undefined
const bg = parseInt(parts[parts.length - 1]!, 10)
// Standard ANSI color indices: 0-7 are dark, 8-15 are bright/light
if (isNaN(bg)) return undefined
return bg >= 8 ? 'light' : 'dark'
}
/**
* Get the current terminal theme. Cached after first detection.
*/
export function getSystemThemeName(): SystemTheme {
if (cachedSystemTheme === undefined) {
cachedSystemTheme = detectFromColorFgBg() ?? 'dark'
}
return cachedSystemTheme
}
export function setCachedSystemTheme(theme: SystemTheme): void {
cachedSystemTheme = theme
}

View File

@@ -1,639 +0,0 @@
import chalk, { Chalk } from 'chalk'
// env import replaced with process.env
export type Theme = {
autoAccept: string
bashBorder: string
claude: string
claudeShimmer: string // Lighter version of claude color for shimmer effect
claudeBlue_FOR_SYSTEM_SPINNER: string
claudeBlueShimmer_FOR_SYSTEM_SPINNER: string
permission: string
permissionShimmer: string // Lighter version of permission color for shimmer effect
planMode: string
ide: string
promptBorder: string
promptBorderShimmer: string // Lighter version of promptBorder color for shimmer effect
text: string
inverseText: string
inactive: string
inactiveShimmer: string // Lighter version of inactive color for shimmer effect
subtle: string
suggestion: string
remember: string
background: string
// Semantic colors
success: string
error: string
warning: string
merged: string
warningShimmer: string // Lighter version of warning color for shimmer effect
// Diff colors
diffAdded: string
diffRemoved: string
diffAddedDimmed: string
diffRemovedDimmed: string
// Word-level diff highlighting
diffAddedWord: string
diffRemovedWord: string
// Agent colors
red_FOR_SUBAGENTS_ONLY: string
blue_FOR_SUBAGENTS_ONLY: string
green_FOR_SUBAGENTS_ONLY: string
yellow_FOR_SUBAGENTS_ONLY: string
purple_FOR_SUBAGENTS_ONLY: string
orange_FOR_SUBAGENTS_ONLY: string
pink_FOR_SUBAGENTS_ONLY: string
cyan_FOR_SUBAGENTS_ONLY: string
// Grove colors
professionalBlue: string
// Chrome colors
chromeYellow: string
// TUI V2 colors
clawd_body: string
clawd_background: string
userMessageBackground: string
userMessageBackgroundHover: string
/** Message-actions selection. Cool shift toward `suggestion` blue; distinct from default AND userMessageBackground. */
messageActionsBackground: string
/** Text-selection highlight background (alt-screen mouse selection). Solid
* bg that REPLACES the cell's bg while preserving its fg — matches native
* terminal selection. Previously SGR-7 inverse (swapped fg/bg per cell),
* which fragmented badly over syntax highlighting. */
selectionBg: string
bashMessageBackgroundColor: string
memoryBackgroundColor: string
rate_limit_fill: string
rate_limit_empty: string
fastMode: string
fastModeShimmer: string
// Brief/assistant mode label colors
briefLabelYou: string
briefLabelClaude: string
// Rainbow colors for ultrathink keyword highlighting
rainbow_red: string
rainbow_orange: string
rainbow_yellow: string
rainbow_green: string
rainbow_blue: string
rainbow_indigo: string
rainbow_violet: string
rainbow_red_shimmer: string
rainbow_orange_shimmer: string
rainbow_yellow_shimmer: string
rainbow_green_shimmer: string
rainbow_blue_shimmer: string
rainbow_indigo_shimmer: string
rainbow_violet_shimmer: string
}
export const THEME_NAMES = [
'dark',
'light',
'light-daltonized',
'dark-daltonized',
'light-ansi',
'dark-ansi',
] as const
/** A renderable theme. Always resolvable to a concrete color palette. */
export type ThemeName = (typeof THEME_NAMES)[number]
export const THEME_SETTINGS = ['auto', ...THEME_NAMES] as const
/**
* A theme preference as stored in user config. `'auto'` follows the system
* dark/light mode and is resolved to a ThemeName at runtime.
*/
export type ThemeSetting = (typeof THEME_SETTINGS)[number]
/**
* Light theme using explicit RGB values to avoid inconsistencies
* from users' custom terminal ANSI color definitions
*/
const lightTheme: Theme = {
autoAccept: 'rgb(135,0,255)', // Electric violet
bashBorder: 'rgb(255,0,135)', // Vibrant pink
claude: 'rgb(215,119,87)', // Claude orange
claudeShimmer: 'rgb(245,149,117)', // Lighter claude orange for shimmer effect
claudeBlue_FOR_SYSTEM_SPINNER: 'rgb(87,105,247)', // Medium blue for system spinner
claudeBlueShimmer_FOR_SYSTEM_SPINNER: 'rgb(117,135,255)', // Lighter blue for system spinner shimmer
permission: 'rgb(87,105,247)', // Medium blue
permissionShimmer: 'rgb(137,155,255)', // Lighter blue for shimmer effect
planMode: 'rgb(0,102,102)', // Muted teal
ide: 'rgb(71,130,200)', // Muted blue
promptBorder: 'rgb(153,153,153)', // Medium gray
promptBorderShimmer: 'rgb(183,183,183)', // Lighter gray for shimmer effect
text: 'rgb(0,0,0)', // Black
inverseText: 'rgb(255,255,255)', // White
inactive: 'rgb(102,102,102)', // Dark gray
inactiveShimmer: 'rgb(142,142,142)', // Lighter gray for shimmer effect
subtle: 'rgb(175,175,175)', // Light gray
suggestion: 'rgb(87,105,247)', // Medium blue
remember: 'rgb(0,0,255)', // Blue
background: 'rgb(0,153,153)', // Cyan
success: 'rgb(44,122,57)', // Green
error: 'rgb(171,43,63)', // Red
warning: 'rgb(150,108,30)', // Amber
merged: 'rgb(135,0,255)', // Electric violet (matches autoAccept)
warningShimmer: 'rgb(200,158,80)', // Lighter amber for shimmer effect
diffAdded: 'rgb(105,219,124)', // Light green
diffRemoved: 'rgb(255,168,180)', // Light red
diffAddedDimmed: 'rgb(199,225,203)', // Very light green
diffRemovedDimmed: 'rgb(253,210,216)', // Very light red
diffAddedWord: 'rgb(47,157,68)', // Medium green
diffRemovedWord: 'rgb(209,69,75)', // Medium red
// Agent colors
red_FOR_SUBAGENTS_ONLY: 'rgb(220,38,38)', // Red 600
blue_FOR_SUBAGENTS_ONLY: 'rgb(37,99,235)', // Blue 600
green_FOR_SUBAGENTS_ONLY: 'rgb(22,163,74)', // Green 600
yellow_FOR_SUBAGENTS_ONLY: 'rgb(202,138,4)', // Yellow 600
purple_FOR_SUBAGENTS_ONLY: 'rgb(147,51,234)', // Purple 600
orange_FOR_SUBAGENTS_ONLY: 'rgb(234,88,12)', // Orange 600
pink_FOR_SUBAGENTS_ONLY: 'rgb(219,39,119)', // Pink 600
cyan_FOR_SUBAGENTS_ONLY: 'rgb(8,145,178)', // Cyan 600
// Grove colors
professionalBlue: 'rgb(106,155,204)',
// Chrome colors
chromeYellow: 'rgb(251,188,4)', // Chrome yellow
// TUI V2 colors
clawd_body: 'rgb(215,119,87)',
clawd_background: 'rgb(0,0,0)',
userMessageBackground: 'rgb(240, 240, 240)', // Slightly darker grey for optimal contrast
userMessageBackgroundHover: 'rgb(252, 252, 252)', // ≥250 to quantize distinct from base at 256-color level
messageActionsBackground: 'rgb(232, 236, 244)', // cool gray — darker than userMsg 240 (visible on white), slight blue toward `suggestion`
selectionBg: 'rgb(180, 213, 255)', // classic light-mode selection blue (macOS/VS Code-ish); dark fgs stay readable
bashMessageBackgroundColor: 'rgb(250, 245, 250)',
memoryBackgroundColor: 'rgb(230, 245, 250)',
rate_limit_fill: 'rgb(87,105,247)', // Medium blue
rate_limit_empty: 'rgb(39,47,111)', // Dark blue
fastMode: 'rgb(255,106,0)', // Electric orange
fastModeShimmer: 'rgb(255,150,50)', // Lighter orange for shimmer
// Brief/assistant mode
briefLabelYou: 'rgb(37,99,235)', // Blue
briefLabelClaude: 'rgb(215,119,87)', // Brand orange
rainbow_red: 'rgb(235,95,87)',
rainbow_orange: 'rgb(245,139,87)',
rainbow_yellow: 'rgb(250,195,95)',
rainbow_green: 'rgb(145,200,130)',
rainbow_blue: 'rgb(130,170,220)',
rainbow_indigo: 'rgb(155,130,200)',
rainbow_violet: 'rgb(200,130,180)',
rainbow_red_shimmer: 'rgb(250,155,147)',
rainbow_orange_shimmer: 'rgb(255,185,137)',
rainbow_yellow_shimmer: 'rgb(255,225,155)',
rainbow_green_shimmer: 'rgb(185,230,180)',
rainbow_blue_shimmer: 'rgb(180,205,240)',
rainbow_indigo_shimmer: 'rgb(195,180,230)',
rainbow_violet_shimmer: 'rgb(230,180,210)',
}
/**
* Light ANSI theme using only the 16 standard ANSI colors
* for terminals without true color support
*/
const lightAnsiTheme: Theme = {
autoAccept: 'ansi:magenta',
bashBorder: 'ansi:magenta',
claude: 'ansi:redBright',
claudeShimmer: 'ansi:yellowBright',
claudeBlue_FOR_SYSTEM_SPINNER: 'ansi:blue',
claudeBlueShimmer_FOR_SYSTEM_SPINNER: 'ansi:blueBright',
permission: 'ansi:blue',
permissionShimmer: 'ansi:blueBright',
planMode: 'ansi:cyan',
ide: 'ansi:blueBright',
promptBorder: 'ansi:white',
promptBorderShimmer: 'ansi:whiteBright',
text: 'ansi:black',
inverseText: 'ansi:white',
inactive: 'ansi:blackBright',
inactiveShimmer: 'ansi:white',
subtle: 'ansi:blackBright',
suggestion: 'ansi:blue',
remember: 'ansi:blue',
background: 'ansi:cyan',
success: 'ansi:green',
error: 'ansi:red',
warning: 'ansi:yellow',
merged: 'ansi:magenta',
warningShimmer: 'ansi:yellowBright',
diffAdded: 'ansi:green',
diffRemoved: 'ansi:red',
diffAddedDimmed: 'ansi:green',
diffRemovedDimmed: 'ansi:red',
diffAddedWord: 'ansi:greenBright',
diffRemovedWord: 'ansi:redBright',
// Agent colors
red_FOR_SUBAGENTS_ONLY: 'ansi:red',
blue_FOR_SUBAGENTS_ONLY: 'ansi:blue',
green_FOR_SUBAGENTS_ONLY: 'ansi:green',
yellow_FOR_SUBAGENTS_ONLY: 'ansi:yellow',
purple_FOR_SUBAGENTS_ONLY: 'ansi:magenta',
orange_FOR_SUBAGENTS_ONLY: 'ansi:redBright',
pink_FOR_SUBAGENTS_ONLY: 'ansi:magentaBright',
cyan_FOR_SUBAGENTS_ONLY: 'ansi:cyan',
// Grove colors
professionalBlue: 'ansi:blueBright',
// Chrome colors
chromeYellow: 'ansi:yellow', // Chrome yellow
// TUI V2 colors
clawd_body: 'ansi:redBright',
clawd_background: 'ansi:black',
userMessageBackground: 'ansi:white',
userMessageBackgroundHover: 'ansi:whiteBright',
messageActionsBackground: 'ansi:white',
selectionBg: 'ansi:cyan', // lighter named bg for light-ansi; dark fgs stay readable
bashMessageBackgroundColor: 'ansi:whiteBright',
memoryBackgroundColor: 'ansi:white',
rate_limit_fill: 'ansi:yellow',
rate_limit_empty: 'ansi:black',
fastMode: 'ansi:red',
fastModeShimmer: 'ansi:redBright',
briefLabelYou: 'ansi:blue',
briefLabelClaude: 'ansi:redBright',
rainbow_red: 'ansi:red',
rainbow_orange: 'ansi:redBright',
rainbow_yellow: 'ansi:yellow',
rainbow_green: 'ansi:green',
rainbow_blue: 'ansi:cyan',
rainbow_indigo: 'ansi:blue',
rainbow_violet: 'ansi:magenta',
rainbow_red_shimmer: 'ansi:redBright',
rainbow_orange_shimmer: 'ansi:yellow',
rainbow_yellow_shimmer: 'ansi:yellowBright',
rainbow_green_shimmer: 'ansi:greenBright',
rainbow_blue_shimmer: 'ansi:cyanBright',
rainbow_indigo_shimmer: 'ansi:blueBright',
rainbow_violet_shimmer: 'ansi:magentaBright',
}
/**
* Dark ANSI theme using only the 16 standard ANSI colors
* for terminals without true color support
*/
const darkAnsiTheme: Theme = {
autoAccept: 'ansi:magentaBright',
bashBorder: 'ansi:magentaBright',
claude: 'ansi:redBright',
claudeShimmer: 'ansi:yellowBright',
claudeBlue_FOR_SYSTEM_SPINNER: 'ansi:blueBright',
claudeBlueShimmer_FOR_SYSTEM_SPINNER: 'ansi:blueBright',
permission: 'ansi:blueBright',
permissionShimmer: 'ansi:blueBright',
planMode: 'ansi:cyanBright',
ide: 'ansi:blue',
promptBorder: 'ansi:white',
promptBorderShimmer: 'ansi:whiteBright',
text: 'ansi:whiteBright',
inverseText: 'ansi:black',
inactive: 'ansi:white',
inactiveShimmer: 'ansi:whiteBright',
subtle: 'ansi:white',
suggestion: 'ansi:blueBright',
remember: 'ansi:blueBright',
background: 'ansi:cyanBright',
success: 'ansi:greenBright',
error: 'ansi:redBright',
warning: 'ansi:yellowBright',
merged: 'ansi:magentaBright',
warningShimmer: 'ansi:yellowBright',
diffAdded: 'ansi:green',
diffRemoved: 'ansi:red',
diffAddedDimmed: 'ansi:green',
diffRemovedDimmed: 'ansi:red',
diffAddedWord: 'ansi:greenBright',
diffRemovedWord: 'ansi:redBright',
// Agent colors
red_FOR_SUBAGENTS_ONLY: 'ansi:redBright',
blue_FOR_SUBAGENTS_ONLY: 'ansi:blueBright',
green_FOR_SUBAGENTS_ONLY: 'ansi:greenBright',
yellow_FOR_SUBAGENTS_ONLY: 'ansi:yellowBright',
purple_FOR_SUBAGENTS_ONLY: 'ansi:magentaBright',
orange_FOR_SUBAGENTS_ONLY: 'ansi:redBright',
pink_FOR_SUBAGENTS_ONLY: 'ansi:magentaBright',
cyan_FOR_SUBAGENTS_ONLY: 'ansi:cyanBright',
// Grove colors
professionalBlue: 'rgb(106,155,204)',
// Chrome colors
chromeYellow: 'ansi:yellowBright', // Chrome yellow
// TUI V2 colors
clawd_body: 'ansi:redBright',
clawd_background: 'ansi:black',
userMessageBackground: 'ansi:blackBright',
userMessageBackgroundHover: 'ansi:white',
messageActionsBackground: 'ansi:blackBright',
selectionBg: 'ansi:blue', // darker named bg for dark-ansi; bright fgs stay readable
bashMessageBackgroundColor: 'ansi:black',
memoryBackgroundColor: 'ansi:blackBright',
rate_limit_fill: 'ansi:yellow',
rate_limit_empty: 'ansi:white',
fastMode: 'ansi:redBright',
fastModeShimmer: 'ansi:redBright',
briefLabelYou: 'ansi:blueBright',
briefLabelClaude: 'ansi:redBright',
rainbow_red: 'ansi:red',
rainbow_orange: 'ansi:redBright',
rainbow_yellow: 'ansi:yellow',
rainbow_green: 'ansi:green',
rainbow_blue: 'ansi:cyan',
rainbow_indigo: 'ansi:blue',
rainbow_violet: 'ansi:magenta',
rainbow_red_shimmer: 'ansi:redBright',
rainbow_orange_shimmer: 'ansi:yellow',
rainbow_yellow_shimmer: 'ansi:yellowBright',
rainbow_green_shimmer: 'ansi:greenBright',
rainbow_blue_shimmer: 'ansi:cyanBright',
rainbow_indigo_shimmer: 'ansi:blueBright',
rainbow_violet_shimmer: 'ansi:magentaBright',
}
/**
* Light daltonized theme (color-blind friendly) using explicit RGB values
* to avoid inconsistencies from users' custom terminal ANSI color definitions
*/
const lightDaltonizedTheme: Theme = {
autoAccept: 'rgb(135,0,255)', // Electric violet
bashBorder: 'rgb(0,102,204)', // Blue instead of pink
claude: 'rgb(255,153,51)', // Orange adjusted for deuteranopia
claudeShimmer: 'rgb(255,183,101)', // Lighter orange for shimmer effect
claudeBlue_FOR_SYSTEM_SPINNER: 'rgb(51,102,255)', // Bright blue for system spinner
claudeBlueShimmer_FOR_SYSTEM_SPINNER: 'rgb(101,152,255)', // Lighter bright blue for system spinner shimmer
permission: 'rgb(51,102,255)', // Bright blue
permissionShimmer: 'rgb(101,152,255)', // Lighter bright blue for shimmer
planMode: 'rgb(51,102,102)', // Muted blue-gray (works for color-blind)
ide: 'rgb(71,130,200)', // Muted blue
promptBorder: 'rgb(153,153,153)', // Medium gray
promptBorderShimmer: 'rgb(183,183,183)', // Lighter gray for shimmer
text: 'rgb(0,0,0)', // Black
inverseText: 'rgb(255,255,255)', // White
inactive: 'rgb(102,102,102)', // Dark gray
inactiveShimmer: 'rgb(142,142,142)', // Lighter gray for shimmer effect
subtle: 'rgb(175,175,175)', // Light gray
suggestion: 'rgb(51,102,255)', // Bright blue
remember: 'rgb(51,102,255)', // Bright blue
background: 'rgb(0,153,153)', // Cyan (color-blind friendly)
success: 'rgb(0,102,153)', // Blue instead of green for deuteranopia
error: 'rgb(204,0,0)', // Pure red for better distinction
warning: 'rgb(255,153,0)', // Orange adjusted for deuteranopia
merged: 'rgb(135,0,255)', // Electric violet (matches autoAccept)
warningShimmer: 'rgb(255,183,50)', // Lighter orange for shimmer
diffAdded: 'rgb(153,204,255)', // Light blue instead of green
diffRemoved: 'rgb(255,204,204)', // Light red
diffAddedDimmed: 'rgb(209,231,253)', // Very light blue
diffRemovedDimmed: 'rgb(255,233,233)', // Very light red
diffAddedWord: 'rgb(51,102,204)', // Medium blue (less intense than deep blue)
diffRemovedWord: 'rgb(153,51,51)', // Softer red (less intense than deep red)
// Agent colors (daltonism-friendly)
red_FOR_SUBAGENTS_ONLY: 'rgb(204,0,0)', // Pure red
blue_FOR_SUBAGENTS_ONLY: 'rgb(0,102,204)', // Pure blue
green_FOR_SUBAGENTS_ONLY: 'rgb(0,204,0)', // Pure green
yellow_FOR_SUBAGENTS_ONLY: 'rgb(255,204,0)', // Golden yellow
purple_FOR_SUBAGENTS_ONLY: 'rgb(128,0,128)', // True purple
orange_FOR_SUBAGENTS_ONLY: 'rgb(255,128,0)', // True orange
pink_FOR_SUBAGENTS_ONLY: 'rgb(255,102,178)', // Adjusted pink
cyan_FOR_SUBAGENTS_ONLY: 'rgb(0,178,178)', // Adjusted cyan
// Grove colors
professionalBlue: 'rgb(106,155,204)',
// Chrome colors
chromeYellow: 'rgb(251,188,4)', // Chrome yellow
// TUI V2 colors
clawd_body: 'rgb(215,119,87)',
clawd_background: 'rgb(0,0,0)',
userMessageBackground: 'rgb(220, 220, 220)', // Slightly darker grey for optimal contrast
userMessageBackgroundHover: 'rgb(232, 232, 232)', // ≥230 to quantize distinct from base at 256-color level
messageActionsBackground: 'rgb(210, 216, 226)', // cool gray — darker than userMsg 220, slight blue
selectionBg: 'rgb(180, 213, 255)', // light selection blue; daltonized fgs are yellows/blues, both readable on light blue
bashMessageBackgroundColor: 'rgb(250, 245, 250)',
memoryBackgroundColor: 'rgb(230, 245, 250)',
rate_limit_fill: 'rgb(51,102,255)', // Bright blue
rate_limit_empty: 'rgb(23,46,114)', // Dark blue
fastMode: 'rgb(255,106,0)', // Electric orange (color-blind safe)
fastModeShimmer: 'rgb(255,150,50)', // Lighter orange for shimmer
briefLabelYou: 'rgb(37,99,235)', // Blue
briefLabelClaude: 'rgb(255,153,51)', // Orange adjusted for deuteranopia (matches claude)
rainbow_red: 'rgb(235,95,87)',
rainbow_orange: 'rgb(245,139,87)',
rainbow_yellow: 'rgb(250,195,95)',
rainbow_green: 'rgb(145,200,130)',
rainbow_blue: 'rgb(130,170,220)',
rainbow_indigo: 'rgb(155,130,200)',
rainbow_violet: 'rgb(200,130,180)',
rainbow_red_shimmer: 'rgb(250,155,147)',
rainbow_orange_shimmer: 'rgb(255,185,137)',
rainbow_yellow_shimmer: 'rgb(255,225,155)',
rainbow_green_shimmer: 'rgb(185,230,180)',
rainbow_blue_shimmer: 'rgb(180,205,240)',
rainbow_indigo_shimmer: 'rgb(195,180,230)',
rainbow_violet_shimmer: 'rgb(230,180,210)',
}
/**
* Dark theme using explicit RGB values to avoid inconsistencies
* from users' custom terminal ANSI color definitions
*/
const darkTheme: Theme = {
autoAccept: 'rgb(175,135,255)', // Electric violet
bashBorder: 'rgb(253,93,177)', // Bright pink
claude: 'rgb(215,119,87)', // Claude orange
claudeShimmer: 'rgb(235,159,127)', // Lighter claude orange for shimmer effect
claudeBlue_FOR_SYSTEM_SPINNER: 'rgb(147,165,255)', // Blue for system spinner
claudeBlueShimmer_FOR_SYSTEM_SPINNER: 'rgb(177,195,255)', // Lighter blue for system spinner shimmer
permission: 'rgb(177,185,249)', // Light blue-purple
permissionShimmer: 'rgb(207,215,255)', // Lighter blue-purple for shimmer
planMode: 'rgb(72,150,140)', // Muted sage green
ide: 'rgb(71,130,200)', // Muted blue
promptBorder: 'rgb(136,136,136)', // Medium gray
promptBorderShimmer: 'rgb(166,166,166)', // Lighter gray for shimmer
text: 'rgb(255,255,255)', // White
inverseText: 'rgb(0,0,0)', // Black
inactive: 'rgb(153,153,153)', // Light gray
inactiveShimmer: 'rgb(193,193,193)', // Lighter gray for shimmer effect
subtle: 'rgb(80,80,80)', // Dark gray
suggestion: 'rgb(177,185,249)', // Light blue-purple
remember: 'rgb(177,185,249)', // Light blue-purple
background: 'rgb(0,204,204)', // Bright cyan
success: 'rgb(78,186,101)', // Bright green
error: 'rgb(255,107,128)', // Bright red
warning: 'rgb(255,193,7)', // Bright amber
merged: 'rgb(175,135,255)', // Electric violet (matches autoAccept)
warningShimmer: 'rgb(255,223,57)', // Lighter amber for shimmer
diffAdded: 'rgb(34,92,43)', // Dark green
diffRemoved: 'rgb(122,41,54)', // Dark red
diffAddedDimmed: 'rgb(71,88,74)', // Very dark green
diffRemovedDimmed: 'rgb(105,72,77)', // Very dark red
diffAddedWord: 'rgb(56,166,96)', // Medium green
diffRemovedWord: 'rgb(179,89,107)', // Softer red (less intense than bright red)
// Agent colors
red_FOR_SUBAGENTS_ONLY: 'rgb(220,38,38)', // Red 600
blue_FOR_SUBAGENTS_ONLY: 'rgb(37,99,235)', // Blue 600
green_FOR_SUBAGENTS_ONLY: 'rgb(22,163,74)', // Green 600
yellow_FOR_SUBAGENTS_ONLY: 'rgb(202,138,4)', // Yellow 600
purple_FOR_SUBAGENTS_ONLY: 'rgb(147,51,234)', // Purple 600
orange_FOR_SUBAGENTS_ONLY: 'rgb(234,88,12)', // Orange 600
pink_FOR_SUBAGENTS_ONLY: 'rgb(219,39,119)', // Pink 600
cyan_FOR_SUBAGENTS_ONLY: 'rgb(8,145,178)', // Cyan 600
// Grove colors
professionalBlue: 'rgb(106,155,204)',
// Chrome colors
chromeYellow: 'rgb(251,188,4)', // Chrome yellow
// TUI V2 colors
clawd_body: 'rgb(215,119,87)',
clawd_background: 'rgb(0,0,0)',
userMessageBackground: 'rgb(55, 55, 55)', // Lighter grey for better visual contrast
userMessageBackgroundHover: 'rgb(70, 70, 70)',
messageActionsBackground: 'rgb(44, 50, 62)', // cool gray, slight blue
selectionBg: 'rgb(38, 79, 120)', // classic dark-mode selection blue (VS Code dark default); light fgs stay readable
bashMessageBackgroundColor: 'rgb(65, 60, 65)',
memoryBackgroundColor: 'rgb(55, 65, 70)',
rate_limit_fill: 'rgb(177,185,249)', // Light blue-purple
rate_limit_empty: 'rgb(80,83,112)', // Medium blue-purple
fastMode: 'rgb(255,120,20)', // Electric orange for dark bg
fastModeShimmer: 'rgb(255,165,70)', // Lighter orange for shimmer
briefLabelYou: 'rgb(122,180,232)', // Light blue
briefLabelClaude: 'rgb(215,119,87)', // Brand orange
rainbow_red: 'rgb(235,95,87)',
rainbow_orange: 'rgb(245,139,87)',
rainbow_yellow: 'rgb(250,195,95)',
rainbow_green: 'rgb(145,200,130)',
rainbow_blue: 'rgb(130,170,220)',
rainbow_indigo: 'rgb(155,130,200)',
rainbow_violet: 'rgb(200,130,180)',
rainbow_red_shimmer: 'rgb(250,155,147)',
rainbow_orange_shimmer: 'rgb(255,185,137)',
rainbow_yellow_shimmer: 'rgb(255,225,155)',
rainbow_green_shimmer: 'rgb(185,230,180)',
rainbow_blue_shimmer: 'rgb(180,205,240)',
rainbow_indigo_shimmer: 'rgb(195,180,230)',
rainbow_violet_shimmer: 'rgb(230,180,210)',
}
/**
* Dark daltonized theme (color-blind friendly) using explicit RGB values
* to avoid inconsistencies from users' custom terminal ANSI color definitions
*/
const darkDaltonizedTheme: Theme = {
autoAccept: 'rgb(175,135,255)', // Electric violet
bashBorder: 'rgb(51,153,255)', // Bright blue
claude: 'rgb(255,153,51)', // Orange adjusted for deuteranopia
claudeShimmer: 'rgb(255,183,101)', // Lighter orange for shimmer effect
claudeBlue_FOR_SYSTEM_SPINNER: 'rgb(153,204,255)', // Light blue for system spinner
claudeBlueShimmer_FOR_SYSTEM_SPINNER: 'rgb(183,224,255)', // Lighter blue for system spinner shimmer
permission: 'rgb(153,204,255)', // Light blue
permissionShimmer: 'rgb(183,224,255)', // Lighter blue for shimmer
planMode: 'rgb(102,153,153)', // Muted gray-teal (works for color-blind)
ide: 'rgb(71,130,200)', // Muted blue
promptBorder: 'rgb(136,136,136)', // Medium gray
promptBorderShimmer: 'rgb(166,166,166)', // Lighter gray for shimmer
text: 'rgb(255,255,255)', // White
inverseText: 'rgb(0,0,0)', // Black
inactive: 'rgb(153,153,153)', // Light gray
inactiveShimmer: 'rgb(193,193,193)', // Lighter gray for shimmer effect
subtle: 'rgb(80,80,80)', // Dark gray
suggestion: 'rgb(153,204,255)', // Light blue
remember: 'rgb(153,204,255)', // Light blue
background: 'rgb(0,204,204)', // Bright cyan (color-blind friendly)
success: 'rgb(51,153,255)', // Blue instead of green
error: 'rgb(255,102,102)', // Bright red
warning: 'rgb(255,204,0)', // Yellow-orange for deuteranopia
merged: 'rgb(175,135,255)', // Electric violet (matches autoAccept)
warningShimmer: 'rgb(255,234,50)', // Lighter yellow-orange for shimmer
diffAdded: 'rgb(0,68,102)', // Dark blue
diffRemoved: 'rgb(102,0,0)', // Dark red
diffAddedDimmed: 'rgb(62,81,91)', // Dimmed blue
diffRemovedDimmed: 'rgb(62,44,44)', // Dimmed red
diffAddedWord: 'rgb(0,119,179)', // Medium blue
diffRemovedWord: 'rgb(179,0,0)', // Medium red
// Agent colors (daltonism-friendly, dark mode)
red_FOR_SUBAGENTS_ONLY: 'rgb(255,102,102)', // Bright red
blue_FOR_SUBAGENTS_ONLY: 'rgb(102,178,255)', // Bright blue
green_FOR_SUBAGENTS_ONLY: 'rgb(102,255,102)', // Bright green
yellow_FOR_SUBAGENTS_ONLY: 'rgb(255,255,102)', // Bright yellow
purple_FOR_SUBAGENTS_ONLY: 'rgb(178,102,255)', // Bright purple
orange_FOR_SUBAGENTS_ONLY: 'rgb(255,178,102)', // Bright orange
pink_FOR_SUBAGENTS_ONLY: 'rgb(255,153,204)', // Bright pink
cyan_FOR_SUBAGENTS_ONLY: 'rgb(102,204,204)', // Bright cyan
// Grove colors
professionalBlue: 'rgb(106,155,204)',
// Chrome colors
chromeYellow: 'rgb(251,188,4)', // Chrome yellow
// TUI V2 colors
clawd_body: 'rgb(215,119,87)',
clawd_background: 'rgb(0,0,0)',
userMessageBackground: 'rgb(55, 55, 55)', // Lighter grey for better visual contrast
userMessageBackgroundHover: 'rgb(70, 70, 70)',
messageActionsBackground: 'rgb(44, 50, 62)', // cool gray, slight blue
selectionBg: 'rgb(38, 79, 120)', // classic dark-mode selection blue (VS Code dark default); light fgs stay readable
bashMessageBackgroundColor: 'rgb(65, 60, 65)',
memoryBackgroundColor: 'rgb(55, 65, 70)',
rate_limit_fill: 'rgb(153,204,255)', // Light blue
rate_limit_empty: 'rgb(69,92,115)', // Dark blue
fastMode: 'rgb(255,120,20)', // Electric orange for dark bg (color-blind safe)
fastModeShimmer: 'rgb(255,165,70)', // Lighter orange for shimmer
briefLabelYou: 'rgb(122,180,232)', // Light blue
briefLabelClaude: 'rgb(255,153,51)', // Orange adjusted for deuteranopia (matches claude)
rainbow_red: 'rgb(235,95,87)',
rainbow_orange: 'rgb(245,139,87)',
rainbow_yellow: 'rgb(250,195,95)',
rainbow_green: 'rgb(145,200,130)',
rainbow_blue: 'rgb(130,170,220)',
rainbow_indigo: 'rgb(155,130,200)',
rainbow_violet: 'rgb(200,130,180)',
rainbow_red_shimmer: 'rgb(250,155,147)',
rainbow_orange_shimmer: 'rgb(255,185,137)',
rainbow_yellow_shimmer: 'rgb(255,225,155)',
rainbow_green_shimmer: 'rgb(185,230,180)',
rainbow_blue_shimmer: 'rgb(180,205,240)',
rainbow_indigo_shimmer: 'rgb(195,180,230)',
rainbow_violet_shimmer: 'rgb(230,180,210)',
}
export function getTheme(themeName: ThemeName): Theme {
switch (themeName) {
case 'light':
return lightTheme
case 'light-ansi':
return lightAnsiTheme
case 'dark-ansi':
return darkAnsiTheme
case 'light-daltonized':
return lightDaltonizedTheme
case 'dark-daltonized':
return darkDaltonizedTheme
default:
return darkTheme
}
}
// Create a chalk instance with 256-color level for Apple Terminal
// Apple Terminal doesn't handle 24-bit color escape sequences well
const chalkForChart =
process.env.TERM_PROGRAM === 'Apple_Terminal'
? new Chalk({ level: 2 }) // 256 colors
: chalk
/**
* Converts a theme color to an ANSI escape sequence for use with asciichart.
* Uses chalk to generate the escape codes, with 256-color mode for Apple Terminal.
*/
export function themeColorToAnsi(themeColor: string): string {
const rgbMatch = themeColor.match(/rgb\(\s?(\d+),\s?(\d+),\s?(\d+)\s?\)/)
if (rgbMatch) {
const r = parseInt(rgbMatch[1]!, 10)
const g = parseInt(rgbMatch[2]!, 10)
const b = parseInt(rgbMatch[3]!, 10)
// Use chalk.rgb which auto-converts to 256 colors when level is 2
// Extract just the opening escape sequence by using a marker
const colored = chalkForChart.rgb(r, g, b)('X')
return colored.slice(0, colored.indexOf('X'))
}
// Fallback to magenta if parsing fails
return '\x1b[35m'
}

View File

@@ -1,11 +0,0 @@
/**
* Theme type re-exports.
*
* ThemeName and ThemeSetting are business-level concepts stored in config;
* they live in theme-types.ts and are re-exported here for convenient
* consumption by theme-layer components.
*/
export type { Theme, ThemeName, ThemeSetting } from './theme-types.js'
export { getTheme } from './theme-types.js'
export type { ColorType } from '../core/colorize.js'
export { colorize } from '../core/colorize.js'

View File

@@ -1,49 +0,0 @@
// Type declarations for custom Ink JSX elements
// Note: The detailed prop types are defined in ink-jsx.d.ts via React module augmentation.
// This file provides the global JSX namespace fallback declarations.
import type { ReactNode, Ref } from 'react';
import type { ClickEvent } from '../core/events/click-event.js';
import type { FocusEvent } from '../core/events/focus-event.js';
import type { KeyboardEvent } from '../core/events/keyboard-event.js';
import type { Styles, TextStyles } from '../core/styles.js';
import type { DOMElement } from '../core/dom.js';
declare global {
namespace JSX {
interface IntrinsicElements {
'ink-box': {
ref?: Ref<DOMElement>;
tabIndex?: number;
autoFocus?: boolean;
onClick?: (event: ClickEvent) => void;
onFocus?: (event: FocusEvent) => void;
onFocusCapture?: (event: FocusEvent) => void;
onBlur?: (event: FocusEvent) => void;
onBlurCapture?: (event: FocusEvent) => void;
onMouseEnter?: () => void;
onMouseLeave?: () => void;
onKeyDown?: (event: KeyboardEvent) => void;
onKeyDownCapture?: (event: KeyboardEvent) => void;
style?: Styles;
stickyScroll?: boolean;
children?: ReactNode;
};
'ink-text': {
style?: Styles;
textStyles?: TextStyles;
children?: ReactNode;
};
'ink-link': {
href?: string;
children?: ReactNode;
};
'ink-raw-ansi': {
rawText?: string;
rawWidth?: number;
rawHeight?: number;
};
}
}
}
export {};

View File

@@ -1,54 +0,0 @@
/**
* Ink custom JSX intrinsic elements.
*
* With "jsx": "react-jsx", TypeScript resolves JSX types from react/jsx-runtime
* whose IntrinsicElements extends React.JSX.IntrinsicElements. We augment the
* 'react' module to inject our custom elements into React.JSX.IntrinsicElements.
*
* This file must be a module (have an import/export) for `declare module`
* augmentation to work correctly.
*/
import type { ReactNode, Ref } from 'react';
import type { ClickEvent } from '../core/events/click-event.js';
import type { FocusEvent } from '../core/events/focus-event.js';
import type { KeyboardEvent } from '../core/events/keyboard-event.js';
import type { Styles, TextStyles } from '../core/styles.js';
import type { DOMElement } from '../core/dom.js';
declare module 'react' {
namespace JSX {
interface IntrinsicElements {
'ink-box': {
ref?: Ref<DOMElement>;
tabIndex?: number;
autoFocus?: boolean;
onClick?: (event: ClickEvent) => void;
onFocus?: (event: FocusEvent) => void;
onFocusCapture?: (event: FocusEvent) => void;
onBlur?: (event: FocusEvent) => void;
onBlurCapture?: (event: FocusEvent) => void;
onMouseEnter?: () => void;
onMouseLeave?: () => void;
onKeyDown?: (event: KeyboardEvent) => void;
onKeyDownCapture?: (event: KeyboardEvent) => void;
style?: Styles;
stickyScroll?: boolean;
children?: ReactNode;
};
'ink-text': {
style?: Styles;
textStyles?: TextStyles;
children?: ReactNode;
};
'ink-link': {
href?: string;
children?: ReactNode;
};
'ink-raw-ansi': {
rawText?: string;
rawWidth?: number;
rawHeight?: number;
};
}
}
}

View File

@@ -1,2 +0,0 @@
// Stub debug logger for package independence
export function logForDebugging(..._args: unknown[]): void {}

View File

@@ -2,7 +2,7 @@ import {
getClaudeAiBaseUrl,
getRemoteSessionUrl,
} from '../constants/product.js'
import { stringWidth } from '@anthropic/ink'
import { stringWidth } from '../ink/stringWidth.js'
import { formatDuration, truncateToWidth } from '../utils/format.js'
import { getGraphemeSegmenter } from '../utils/intl.js'

View File

@@ -5,7 +5,7 @@ import {
BRIDGE_READY_INDICATOR,
BRIDGE_SPINNER_FRAMES,
} from '../constants/figures.js'
import { stringWidth } from '@anthropic/ink'
import { stringWidth } from '../ink/stringWidth.js'
import { logForDebugging } from '../utils/debug.js'
import {
buildActiveFooterText,

View File

@@ -3,8 +3,8 @@
* Mirrors official vc8 component: bordered box with sprite, stats, last reaction.
*/
import React from 'react';
import { Box, Text } from '@anthropic/ink';
import { useInput } from '@anthropic/ink';
import { Box, Text } from '../ink.js';
import { useInput } from '../ink.js';
import { renderSprite } from './sprites.js';
import { RARITY_COLORS, RARITY_STARS, STAT_NAMES, type Companion } from './types.js';

View File

@@ -2,7 +2,8 @@ import { feature } from 'bun:bundle'
import figures from 'figures'
import React, { useEffect, useRef, useState } from 'react'
import { useTerminalSize } from '../hooks/useTerminalSize.js'
import { Box, Text, stringWidth } from '@anthropic/ink'
import { stringWidth } from '../ink/stringWidth.js'
import { Box, Text } from '../ink.js'
import { useAppState, useSetAppState } from '../state/AppState.js'
import type { AppState } from '../state/AppStateStore.js'
import { getGlobalConfig } from '../utils/config.js'

View File

@@ -1,7 +1,7 @@
import { feature } from 'bun:bundle'
import React, { useEffect } from 'react'
import { useNotifications } from '../context/notifications.js'
import { Text } from '@anthropic/ink'
import { Text } from '../ink.js'
import { getGlobalConfig } from '../utils/config.js'
import { getRainbowColor } from '../utils/thinking.js'

View File

@@ -8,7 +8,7 @@ import pMap from 'p-map'
import { cwd } from 'process'
import React from 'react'
import { MCPServerDesktopImportDialog } from '../../components/MCPServerDesktopImportDialog.js'
import { wrappedRender as render } from '@anthropic/ink'
import { render } from '../../ink.js'
import { KeybindingSetup } from '../../keybindings/KeybindingProviderSetup.js'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,

View File

@@ -8,8 +8,8 @@ import { cwd } from 'process'
import React from 'react'
import { WelcomeV2 } from '../../components/LogoV2/WelcomeV2.js'
import { useManagePlugins } from '../../hooks/useManagePlugins.js'
import type { Root } from '@anthropic/ink'
import { Box, Text } from '@anthropic/ink'
import type { Root } from '../../ink.js'
import { Box, Text } from '../../ink.js'
import { KeybindingSetup } from '../../keybindings/KeybindingProviderSetup.js'
import { logEvent } from '../../services/analytics/index.js'
import { MCPConnectionManager } from '../../services/mcp/MCPConnectionManager.js'

View File

@@ -8,7 +8,7 @@ import {
import type { LocalJSXCommandContext } from '../../commands.js'
import { MessageResponse } from '../../components/MessageResponse.js'
import { AddWorkspaceDirectory } from '../../components/permissions/rules/AddWorkspaceDirectory.js'
import { Box, Text } from '@anthropic/ink'
import { Box, Text } from '../../ink.js'
import type { LocalJSXCommandOnDone } from '../../types/command.js'
import {
applyPermissionUpdate,

View File

@@ -13,10 +13,11 @@ import {
BRIDGE_LOGIN_INSTRUCTION,
REMOTE_CONTROL_DISCONNECTED_MSG,
} from '../../bridge/types.js'
import { Dialog, ListItem } from '@anthropic/ink'
import { Dialog } from '../../components/design-system/Dialog.js'
import { ListItem } from '../../components/design-system/ListItem.js'
import { shouldShowRemoteCallout } from '../../components/RemoteCallout.js'
import { useRegisterOverlay } from '../../context/overlayContext.js'
import { Box, Text } from '@anthropic/ink'
import { Box, Text } from '../../ink.js'
import { useKeybindings } from '../../keybindings/useKeybinding.js'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,

View File

@@ -9,8 +9,11 @@ import { getSystemPrompt } from '../../constants/prompts.js'
import { useModalOrTerminalSize } from '../../context/modalContext.js'
import { getSystemContext, getUserContext } from '../../context.js'
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
import { type KeyboardEvent, type ScrollBoxHandle, ScrollBox } from '@anthropic/ink'
import { Box, Text } from '@anthropic/ink'
import ScrollBox, {
type ScrollBoxHandle,
} from '../../ink/components/ScrollBox.js'
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
import { Box, Text } from '../../ink.js'
import type { LocalJSXCommandOnDone } from '../../types/command.js'
import type { Message } from '../../types/message.js'
import { createAbortController } from '../../utils/abortController.js'

View File

@@ -3,8 +3,8 @@ import {
type OptionWithDescription,
Select,
} from '../../components/CustomSelect/select.js'
import { Dialog } from '@anthropic/ink'
import { Box, Text } from '@anthropic/ink'
import { Dialog } from '../../components/design-system/Dialog.js'
import { Box, Text } from '../../ink.js'
import { useAppState } from '../../state/AppState.js'
import { isClaudeAISubscriber } from '../../utils/auth.js'
import { openBrowser } from '../../utils/browser.js'

View File

@@ -6,8 +6,13 @@ import React, { useRef } from 'react'
import type { CommandResultDisplay } from '../../commands.js'
import type { OptionWithDescription } from '../../components/CustomSelect/select.js'
import { Select } from '../../components/CustomSelect/select.js'
import { Byline, KeyboardShortcutHint, Pane } from '@anthropic/ink'
import { Box, setClipboard, Text, stringWidth, type KeyboardEvent } from '@anthropic/ink'
import { Byline } from '../../components/design-system/Byline.js'
import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js'
import { Pane } from '../../components/design-system/Pane.js'
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
import { stringWidth } from '../../ink/stringWidth.js'
import { setClipboard } from '../../ink/termio/osc.js'
import { Box, Text } from '../../ink.js'
import { logEvent } from '../../services/analytics/index.js'
import type { LocalJSXCommandCall } from '../../types/command.js'
import type { AssistantMessage, Message } from '../../types/message.js'

View File

@@ -4,9 +4,9 @@ import type {
CommandResultDisplay,
LocalJSXCommandContext,
} from '../../commands.js'
import { Dialog } from '@anthropic/ink'
import { Dialog } from '../../components/design-system/Dialog.js'
import { FastIcon, getFastIconString } from '../../components/FastIcon.js'
import { Box, Link, Text } from '@anthropic/ink'
import { Box, Link, Text } from '../../ink.js'
import { useKeybindings } from '../../keybindings/useKeybinding.js'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,

View File

@@ -7,14 +7,14 @@ import type {
LocalJSXCommandContext,
} from '../../commands.js'
import { Select } from '../../components/CustomSelect/index.js'
import { Dialog } from '@anthropic/ink'
import { Dialog } from '../../components/design-system/Dialog.js'
import {
IdeAutoConnectDialog,
IdeDisableAutoConnectDialog,
shouldShowAutoConnectDialog,
shouldShowDisableAutoConnectDialog,
} from '../../components/IdeAutoConnectDialog.js'
import { Box, Text } from '@anthropic/ink'
import { Box, Text } from '../../ink.js'
import { clearServerCache } from '../../services/mcp/client.js'
import type { ScopedMcpServerConfig } from '../../services/mcp/types.js'
import { useAppState, useSetAppState } from '../../state/AppState.js'

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useState } from 'react'
import TextInput from '../../components/TextInput.js'
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
import { Box, color, Text, useTheme } from '@anthropic/ink'
import { Box, color, Text, useTheme } from '../../ink.js'
import { useKeybindings } from '../../keybindings/useKeybinding.js'
interface ApiKeyStepProps {

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useState } from 'react'
import TextInput from '../../components/TextInput.js'
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
import { Box, color, Text, useTheme } from '@anthropic/ink'
import { Box, color, Text, useTheme } from '../../ink.js'
import { useKeybindings } from '../../keybindings/useKeybinding.js'
interface CheckExistingSecretStepProps {

View File

@@ -1,5 +1,5 @@
import React from 'react'
import { Text } from '@anthropic/ink'
import { Text } from '../../ink.js'
export function CheckGitHubStep() {
return <Text>Checking GitHub CLI installation</Text>

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useState } from 'react'
import TextInput from '../../components/TextInput.js'
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
import { Box, Text } from '@anthropic/ink'
import { Box, Text } from '../../ink.js'
import { useKeybindings } from '../../keybindings/useKeybinding.js'
interface ChooseRepoStepProps {

View File

@@ -1,5 +1,5 @@
import React from 'react'
import { Box, Text } from '@anthropic/ink'
import { Box, Text } from '../../ink.js'
import type { Workflow } from './types.js'
interface CreatingStepProps {

View File

@@ -1,6 +1,6 @@
import React from 'react'
import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js'
import { Box, Text } from '@anthropic/ink'
import { Box, Text } from '../../ink.js'
interface ErrorStepProps {
error: string | undefined

View File

@@ -1,6 +1,6 @@
import React from 'react'
import { Select } from 'src/components/CustomSelect/index.js'
import { Box, Text } from '@anthropic/ink'
import { Box, Text } from '../../ink.js'
interface ExistingWorkflowStepProps {
repoName: string

View File

@@ -1,7 +1,7 @@
import figures from 'figures'
import React from 'react'
import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js'
import { Box, Text } from '@anthropic/ink'
import { Box, Text } from '../../ink.js'
import { useKeybinding } from '../../keybindings/useKeybinding.js'
interface InstallAppStepProps {

View File

@@ -3,11 +3,13 @@ import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from 'src/services/analytics/index.js'
import { KeyboardShortcutHint } from '@anthropic/ink'
import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js'
import { Spinner } from '../../components/Spinner.js'
import TextInput from '../../components/TextInput.js'
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
import { type KeyboardEvent, setClipboard, Box, Link, Text } from '@anthropic/ink'
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
import { setClipboard } from '../../ink/termio/osc.js'
import { Box, Link, Text } from '../../ink.js'
import { OAuthService } from '../../services/oauth/index.js'
import { saveOAuthTokensIfNeeded } from '../../utils/auth.js'
import { logError } from '../../utils/log.js'

View File

@@ -1,5 +1,5 @@
import React from 'react'
import { Box, Text } from '@anthropic/ink'
import { Box, Text } from '../../ink.js'
type SuccessStepProps = {
secretExists: boolean

View File

@@ -1,7 +1,7 @@
import figures from 'figures'
import React from 'react'
import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js'
import { Box, Text } from '@anthropic/ink'
import { Box, Text } from '../../ink.js'
import { useKeybinding } from '../../keybindings/useKeybinding.js'
import type { Warning } from './types.js'

View File

@@ -7,7 +7,8 @@ import {
import { WorkflowMultiselectDialog } from '../../components/WorkflowMultiselectDialog.js'
import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js'
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
import { type KeyboardEvent, Box } from '@anthropic/ink'
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
import { Box } from '../../ink.js'
import type { LocalJSXCommandOnDone } from '../../types/command.js'
import { getAnthropicApiKey, isAnthropicAuthEnabled } from '../../utils/auth.js'
import { openBrowser } from '../../utils/browser.js'

View File

@@ -3,8 +3,8 @@ import { join } from 'node:path'
import React, { useEffect, useState } from 'react'
import type { CommandResultDisplay } from 'src/commands.js'
import { logEvent } from 'src/services/analytics/index.js'
import { StatusIcon } from '@anthropic/ink'
import { Box, wrappedRender as render, Text } from '@anthropic/ink'
import { StatusIcon } from '../components/design-system/StatusIcon.js'
import { Box, render, Text } from '../ink.js'
import { logForDebugging } from '../utils/debug.js'
import { env } from '../utils/env.js'
import { errorMessage } from '../utils/errors.js'

View File

@@ -8,9 +8,9 @@ import {
import type { LocalJSXCommandContext } from '../../commands.js'
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js'
import { ConsoleOAuthFlow } from '../../components/ConsoleOAuthFlow.js'
import { Dialog } from '@anthropic/ink'
import { Dialog } from '../../components/design-system/Dialog.js'
import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'
import { Text } from '@anthropic/ink'
import { Text } from '../../ink.js'
import { refreshGrowthBookAfterAuthChange } from '../../services/analytics/growthbook.js'
import { refreshPolicyLimits } from '../../services/policyLimits/index.js'
import { refreshRemoteManagedSettings } from '../../services/remoteManagedSettings/index.js'

View File

@@ -1,6 +1,6 @@
import * as React from 'react'
import { clearTrustedDeviceTokenCache } from '../../bridge/trustedDevice.js'
import { Text } from '@anthropic/ink'
import { Text } from '../../ink.js'
import { refreshGrowthBookAfterAuthChange } from '../../services/analytics/growthbook.js'
import {
getGroveNoticeConfig,

View File

@@ -1,10 +1,10 @@
import { mkdir, writeFile } from 'fs/promises'
import * as React from 'react'
import type { CommandResultDisplay } from '../../commands.js'
import { Dialog } from '@anthropic/ink'
import { Dialog } from '../../components/design-system/Dialog.js'
import { MemoryFileSelector } from '../../components/memory/MemoryFileSelector.js'
import { getRelativeMemoryPath } from '../../components/memory/MemoryUpdateNotification.js'
import { Box, Link, Text } from '@anthropic/ink'
import { Box, Link, Text } from '../../ink.js'
import type { LocalJSXCommandCall } from '../../types/command.js'
import { clearMemoryFileCaches, getMemoryFiles } from '../../utils/claudemd.js'
import { getClaudeConfigHomeDir } from '../../utils/envUtils.js'

View File

@@ -1,8 +1,9 @@
import { toString as qrToString } from 'qrcode'
import * as React from 'react'
import { useCallback, useEffect, useState } from 'react'
import { Pane } from '@anthropic/ink'
import { type KeyboardEvent, Box, Text } from '@anthropic/ink'
import { Pane } from '../../components/design-system/Pane.js'
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
import { Box, Text } from '../../ink.js'
import { useKeybinding } from '../../keybindings/useKeybinding.js'
import type { LocalJSXCommandOnDone } from '../../types/command.js'

View File

@@ -1,7 +1,7 @@
import * as React from 'react'
import { handlePlanModeTransition } from '../../bootstrap/state.js'
import type { LocalJSXCommandContext } from '../../commands.js'
import { Box, Text } from '@anthropic/ink'
import { Box, Text } from '../../ink.js'
import type { LocalJSXCommandOnDone } from '../../types/command.js'
import { getExternalEditor } from '../../utils/editor.js'
import { toIDEDisplayName } from '../../utils/ide.js'

View File

@@ -5,10 +5,11 @@ import {
logEvent,
} from 'src/services/analytics/index.js'
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js'
import { Byline, KeyboardShortcutHint } from '@anthropic/ink'
import { Byline } from '../../components/design-system/Byline.js'
import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js'
import { Spinner } from '../../components/Spinner.js'
import TextInput from '../../components/TextInput.js'
import { Box, Text } from '@anthropic/ink'
import { Box, Text } from '../../ink.js'
import { toError } from '../../utils/errors.js'
import { logError } from '../../utils/log.js'
import { clearAllCaches } from '../../utils/plugins/cacheUtils.js'

View File

@@ -2,7 +2,8 @@ import figures from 'figures'
import * as React from 'react'
import { useEffect, useState } from 'react'
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js'
import { Box, Byline, Text } from '@anthropic/ink'
import { Byline } from '../../components/design-system/Byline.js'
import { Box, Text } from '../../ink.js'
import {
useKeybinding,
useKeybindings,

View File

@@ -2,12 +2,12 @@ import figures from 'figures'
import * as React from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js'
import { Byline } from '../../components/design-system/Byline.js'
import { SearchBox } from '../../components/SearchBox.js'
import { Byline } from '@anthropic/ink'
import { useSearchInput } from '../../hooks/useSearchInput.js'
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
// eslint-disable-next-line custom-rules/prefer-use-keybindings -- useInput needed for raw search mode text input
import { Box, Text, useInput, useTerminalFocus } from '@anthropic/ink'
import { Box, Text, useInput, useTerminalFocus } from '../../ink.js'
import {
useKeybinding,
useKeybindings,

View File

@@ -6,9 +6,10 @@ import {
logEvent,
} from 'src/services/analytics/index.js'
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js'
import { Byline, KeyboardShortcutHint } from '@anthropic/ink'
import { Byline } from '../../components/design-system/Byline.js'
import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js'
// eslint-disable-next-line custom-rules/prefer-use-keybindings -- useInput needed for marketplace-specific u/r shortcuts and y/n confirmation not in keybinding schema
import { Box, Text, useInput } from '@anthropic/ink'
import { Box, Text, useInput } from '../../ink.js'
import {
useKeybinding,
useKeybindings,

View File

@@ -5,7 +5,7 @@ import * as path from 'path'
import * as React from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js'
import { Byline } from '@anthropic/ink'
import { Byline } from '../../components/design-system/Byline.js'
import { MCPRemoteServerMenu } from '../../components/mcp/MCPRemoteServerMenu.js'
import { MCPStdioServerMenu } from '../../components/mcp/MCPStdioServerMenu.js'
import { MCPToolDetailView } from '../../components/mcp/MCPToolDetailView.js'
@@ -20,7 +20,7 @@ import { SearchBox } from '../../components/SearchBox.js'
import { useSearchInput } from '../../hooks/useSearchInput.js'
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
// eslint-disable-next-line custom-rules/prefer-use-keybindings -- useInput needed for raw search mode text input
import { Box, Text, useInput, useTerminalFocus } from '@anthropic/ink'
import { Box, Text, useInput, useTerminalFocus } from '../../ink.js'
import {
useKeybinding,
useKeybindings,

View File

@@ -1,8 +1,9 @@
import figures from 'figures'
import React, { useCallback, useState } from 'react'
import { Dialog } from '@anthropic/ink'
import { Dialog } from '../../components/design-system/Dialog.js'
import { stringWidth } from '../../ink/stringWidth.js'
// eslint-disable-next-line custom-rules/prefer-use-keybindings -- raw text input for config dialog
import { Box, Text, useInput, stringWidth } from '@anthropic/ink'
import { Box, Text, useInput } from '../../ink.js'
import {
useKeybinding,
useKeybindings,

View File

@@ -2,10 +2,11 @@ import figures from 'figures'
import * as React from 'react'
import { useCallback, useEffect, useState } from 'react'
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js'
import { Byline, Pane, Tabs } from '@anthropic/ink'
import { Tab } from '../../components/design-system/Tabs.js'
import { Byline } from '../../components/design-system/Byline.js'
import { Pane } from '../../components/design-system/Pane.js'
import { Tab, Tabs } from '../../components/design-system/Tabs.js'
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
import { Box, Text } from '@anthropic/ink'
import { Box, Text } from '../../ink.js'
import {
useKeybinding,
useKeybindings,

View File

@@ -1,6 +1,6 @@
import figures from 'figures'
import * as React from 'react'
import { Box, Text } from '@anthropic/ink'
import { Box, Text } from '../../ink.js'
import { getPluginTrustMessage } from '../../utils/plugins/marketplaceHelpers.js'
export function PluginTrustWarning(): React.ReactNode {

View File

@@ -1,6 +1,6 @@
import figures from 'figures'
import * as React from 'react'
import { Box, color, Text, useTheme } from '@anthropic/ink'
import { Box, color, Text, useTheme } from '../../ink.js'
import { plural } from '../../utils/stringUtils.js'
import type { UnifiedInstalledItem } from './unifiedTypes.js'

View File

@@ -1,7 +1,7 @@
import figures from 'figures'
import * as React from 'react'
import { useEffect } from 'react'
import { Box, Text } from '@anthropic/ink'
import { Box, Text } from '../../ink.js'
import { errorMessage } from '../../utils/errors.js'
import { logError } from '../../utils/log.js'
import { validateManifest } from '../../utils/plugins/validatePlugin.js'

View File

@@ -6,7 +6,8 @@
import * as React from 'react'
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js'
import { Box, Byline, Text } from '@anthropic/ink'
import { Byline } from '../../components/design-system/Byline.js'
import { Box, Text } from '../../ink.js'
import type { PluginMarketplaceEntry } from '../../utils/plugins/schemas.js'
/**

View File

@@ -7,7 +7,7 @@ import {
type OptionWithDescription,
Select,
} from '../../components/CustomSelect/select.js'
import { Dialog } from '@anthropic/ink'
import { Dialog } from '../../components/design-system/Dialog.js'
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
import { logEvent } from '../../services/analytics/index.js'
import { useClaudeAiLimits } from '../../services/claudeAiLimitsHook.js'

View File

@@ -2,7 +2,9 @@ import { execa } from 'execa'
import * as React from 'react'
import { useEffect, useState } from 'react'
import { Select } from '../../components/CustomSelect/index.js'
import { Box, Dialog, LoadingState, Text } from '@anthropic/ink'
import { Dialog } from '../../components/design-system/Dialog.js'
import { LoadingState } from '../../components/design-system/LoadingState.js'
import { Box, Text } from '../../ink.js'
import {
logEvent,
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS as SafeString,

View File

@@ -9,8 +9,8 @@ import { MessageResponse } from '../../components/MessageResponse.js'
import { Spinner } from '../../components/Spinner.js'
import { useIsInsideModal } from '../../context/modalContext.js'
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
import { setClipboard } from '@anthropic/ink'
import { Box, Text } from '@anthropic/ink'
import { setClipboard } from '../../ink/termio/osc.js'
import { Box, Text } from '../../ink.js'
import type { LocalJSXCommandCall } from '../../types/command.js'
import type { LogOption } from '../../types/logs.js'
import { agenticSessionSearch } from '../../utils/agenticSessionSearch.js'

View File

@@ -1,6 +1,7 @@
import React, { useCallback, useRef, useState } from 'react'
import { Select } from '../../components/CustomSelect/select.js'
import { Box, Dialog, Text } from '@anthropic/ink'
import { Dialog } from '../../components/design-system/Dialog.js'
import { Box, Text } from '../../ink.js'
type Props = {
onProceed: (signal: AbortSignal) => Promise<void>

View File

@@ -2,7 +2,7 @@ import { relative } from 'path'
import React from 'react'
import { getCwdState } from '../../bootstrap/state.js'
import { SandboxSettings } from '../../components/sandbox/SandboxSettings.js'
import { color } from '@anthropic/ink'
import { color } from '../../ink.js'
import { getPlatform } from '../../utils/platform.js'
import {
addToExcludedCommands,

View File

@@ -1,7 +1,8 @@
import { toString as qrToString } from 'qrcode'
import * as React from 'react'
import { useEffect, useState } from 'react'
import { Box, Pane, Text } from '@anthropic/ink'
import { Pane } from '../../components/design-system/Pane.js'
import { Box, Text } from '../../ink.js'
import { useKeybinding } from '../../keybindings/useKeybinding.js'
import { useAppState } from '../../state/AppState.js'
import type { LocalJSXCommandCall } from '../../types/command.js'

View File

@@ -4,9 +4,9 @@ import * as React from 'react'
import { getSessionId } from '../../bootstrap/state.js'
import type { CommandResultDisplay } from '../../commands.js'
import { Select } from '../../components/CustomSelect/select.js'
import { Dialog } from '@anthropic/ink'
import { Dialog } from '../../components/design-system/Dialog.js'
import { COMMON_HELP_ARGS, COMMON_INFO_ARGS } from '../../constants/xml.js'
import { Box, Text } from '@anthropic/ink'
import { Box, Text } from '../../ink.js'
import { logEvent } from '../../services/analytics/index.js'
import type { LocalJSXCommandOnDone } from '../../types/command.js'
import { recursivelySanitizeUnicode } from '../../utils/sanitization.js'

View File

@@ -5,8 +5,8 @@ import { homedir, platform } from 'os'
import { dirname, join } from 'path'
import type { ThemeName } from 'src/utils/theme.js'
import { pathToFileURL } from 'url'
import { supportsHyperlinks } from '@anthropic/ink'
import { color } from '@anthropic/ink'
import { supportsHyperlinks } from '../../ink/supports-hyperlinks.js'
import { color } from '../../ink.js'
import { maybeMarkProjectOnboardingComplete } from '../../projectOnboardingState.js'
import type { ToolUseContext } from '../../Tool.js'
import type {

View File

@@ -1,8 +1,8 @@
import * as React from 'react'
import type { CommandResultDisplay } from '../../commands.js'
import { Pane } from '@anthropic/ink'
import { Pane } from '../../components/design-system/Pane.js'
import { ThemePicker } from '../../components/ThemePicker.js'
import { useTheme } from '@anthropic/ink'
import { useTheme } from '../../ink.js'
import type { LocalJSXCommandCall } from '../../types/command.js'
type Props = {

View File

@@ -5,9 +5,10 @@ import * as React from 'react'
import { useCallback, useEffect, useState } from 'react'
import type { CommandResultDisplay } from '../../commands.js'
import { Select } from '../../components/CustomSelect/select.js'
import { Dialog } from '@anthropic/ink'
import { Dialog } from '../../components/design-system/Dialog.js'
import { Spinner } from '../../components/Spinner.js'
import { Box, Text, instances } from '@anthropic/ink'
import instances from '../../ink/instances.js'
import { Box, Text } from '../../ink.js'
import { enablePluginOp } from '../../services/plugins/pluginOperations.js'
import { logForDebugging } from '../../utils/debug.js'
import { isENOENT, toError } from '../../utils/errors.js'

View File

@@ -1,5 +1,5 @@
import * as React from 'react'
import { Box, Text } from '@anthropic/ink'
import { Box, Text } from '../ink.js'
import { formatNumber } from '../utils/format.js'
import type { Theme } from '../utils/theme.js'

View File

@@ -1,7 +1,8 @@
import React from 'react'
import { Text, Dialog } from '@anthropic/ink'
import { Text } from '../ink.js'
import { saveGlobalConfig } from '../utils/config.js'
import { Select } from './CustomSelect/index.js'
import { Dialog } from './design-system/Dialog.js'
type Props = {
customApiKeyTruncated: string

View File

@@ -1,8 +1,9 @@
import React from 'react'
import { logEvent } from 'src/services/analytics/index.js'
import { Box, Dialog, Link, Text } from '@anthropic/ink'
import { Box, Link, Text } from '../ink.js'
import { updateSettingsForSource } from '../utils/settings/settings.js'
import { Select } from './CustomSelect/index.js'
import { Dialog } from './design-system/Dialog.js'
// NOTE: This copy is legally reviewed — do not modify without Legal team approval.
export const AUTO_MODE_DESCRIPTION =

View File

@@ -6,7 +6,7 @@ import {
} from 'src/services/analytics/index.js'
import { useInterval } from 'usehooks-ts'
import { useUpdateNotification } from '../hooks/useUpdateNotification.js'
import { Box, Text } from '@anthropic/ink'
import { Box, Text } from '../ink.js'
import {
type AutoUpdaterResult,
getLatestVersion,

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react'
import { Box, Link, Text } from '@anthropic/ink'
import { Box, Link, Text } from '../ink.js'
import {
type AwsAuthStatus,
AwsAuthStatusManager,

View File

@@ -1,8 +1,8 @@
import React from 'react'
import { renderPlaceholder } from '../hooks/renderPlaceholder.js'
import { usePasteHandler } from '../hooks/usePasteHandler.js'
import { useDeclaredCursor } from '@anthropic/ink'
import { Ansi, Box, Text, useInput } from '@anthropic/ink'
import { useDeclaredCursor } from '../ink/hooks/use-declared-cursor.js'
import { Ansi, Box, Text, useInput } from '../ink.js'
import type {
BaseInputState,
BaseTextInputProps,

View File

@@ -1,5 +1,5 @@
import React from 'react'
import { Box } from '@anthropic/ink'
import { Box } from '../ink.js'
import { BashTool } from '../tools/BashTool/BashTool.js'
import type { ShellProgress } from '../types/tools.js'
import { UserBashInputMessage } from './messages/UserBashInputMessage.js'

View File

@@ -15,12 +15,12 @@ import {
} from '../constants/figures.js'
import { useRegisterOverlay } from '../context/overlayContext.js'
// eslint-disable-next-line custom-rules/prefer-use-keybindings -- raw 'd' key for disconnect, not a configurable keybinding action
import { Box, Text, useInput } from '@anthropic/ink'
import { Box, Text, useInput } from '../ink.js'
import { useKeybindings } from '../keybindings/useKeybinding.js'
import { useAppState, useSetAppState } from '../state/AppState.js'
import { saveGlobalConfig } from '../utils/config.js'
import { getBranch } from '../utils/git.js'
import { Dialog } from '@anthropic/ink'
import { Dialog } from './design-system/Dialog.js'
type Props = {
onDone: () => void

View File

@@ -1,7 +1,8 @@
import React, { useEffect, useState } from 'react';
import { formatCost } from '../cost-tracker.js';
import { Box, Text, ProgressBar } from '@anthropic/ink';
import { Box, Text } from '../ink.js';
import { formatTokens } from '../utils/format.js';
import { ProgressBar } from './design-system/ProgressBar.js';
import { useTerminalSize } from '../hooks/useTerminalSize.js';
type RateLimitBucket = {

Some files were not shown because too many files have changed in this diff Show More