mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
This reverts commit c445f43f8d.
This commit is contained in:
28
bun.lock
28
bun.lock
@@ -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=="],
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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'
|
||||
@@ -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>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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'
|
||||
49
packages/@ant/ink/src/types/ink-elements.d.ts
vendored
49
packages/@ant/ink/src/types/ink-elements.d.ts
vendored
@@ -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 {};
|
||||
54
packages/@ant/ink/src/types/ink-jsx.d.ts
vendored
54
packages/@ant/ink/src/types/ink-jsx.d.ts
vendored
@@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
// Stub debug logger for package independence
|
||||
export function logForDebugging(..._args: unknown[]): void {}
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { Box, Text } from '@anthropic/ink'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
|
||||
type SuccessStepProps = {
|
||||
secretExists: boolean
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
/**
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user