mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 08:15:53 +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/sandbox-runtime": "^0.0.44",
|
||||||
"@anthropic-ai/sdk": "^0.80.0",
|
"@anthropic-ai/sdk": "^0.80.0",
|
||||||
"@anthropic-ai/vertex-sdk": "^0.14.4",
|
"@anthropic-ai/vertex-sdk": "^0.14.4",
|
||||||
"@anthropic/ink": "workspace:*",
|
|
||||||
"@aws-sdk/client-bedrock": "^3.1020.0",
|
"@aws-sdk/client-bedrock": "^3.1020.0",
|
||||||
"@aws-sdk/client-bedrock-runtime": "^3.1020.0",
|
"@aws-sdk/client-bedrock-runtime": "^3.1020.0",
|
||||||
"@aws-sdk/client-sts": "^3.1020.0",
|
"@aws-sdk/client-sts": "^3.1020.0",
|
||||||
@@ -139,29 +138,6 @@
|
|||||||
"name": "@ant/computer-use-swift",
|
"name": "@ant/computer-use-swift",
|
||||||
"version": "1.0.0",
|
"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": {
|
"packages/audio-capture-napi": {
|
||||||
"name": "audio-capture-napi",
|
"name": "audio-capture-napi",
|
||||||
"version": "1.0.0",
|
"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-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/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=="],
|
"@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=="],
|
"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=="],
|
"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",
|
"highlight.js": "^11.11.1",
|
||||||
"https-proxy-agent": "^8.0.0",
|
"https-proxy-agent": "^8.0.0",
|
||||||
"ignore": "^7.0.5",
|
"ignore": "^7.0.5",
|
||||||
"@anthropic/ink": "workspace:*",
|
|
||||||
"image-processor-napi": "workspace:*",
|
"image-processor-napi": "workspace:*",
|
||||||
"indent-string": "^5.0.0",
|
"indent-string": "^5.0.0",
|
||||||
"jsonc-parser": "^3.3.1",
|
"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,
|
getClaudeAiBaseUrl,
|
||||||
getRemoteSessionUrl,
|
getRemoteSessionUrl,
|
||||||
} from '../constants/product.js'
|
} from '../constants/product.js'
|
||||||
import { stringWidth } from '@anthropic/ink'
|
import { stringWidth } from '../ink/stringWidth.js'
|
||||||
import { formatDuration, truncateToWidth } from '../utils/format.js'
|
import { formatDuration, truncateToWidth } from '../utils/format.js'
|
||||||
import { getGraphemeSegmenter } from '../utils/intl.js'
|
import { getGraphemeSegmenter } from '../utils/intl.js'
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
BRIDGE_READY_INDICATOR,
|
BRIDGE_READY_INDICATOR,
|
||||||
BRIDGE_SPINNER_FRAMES,
|
BRIDGE_SPINNER_FRAMES,
|
||||||
} from '../constants/figures.js'
|
} from '../constants/figures.js'
|
||||||
import { stringWidth } from '@anthropic/ink'
|
import { stringWidth } from '../ink/stringWidth.js'
|
||||||
import { logForDebugging } from '../utils/debug.js'
|
import { logForDebugging } from '../utils/debug.js'
|
||||||
import {
|
import {
|
||||||
buildActiveFooterText,
|
buildActiveFooterText,
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
* Mirrors official vc8 component: bordered box with sprite, stats, last reaction.
|
* Mirrors official vc8 component: bordered box with sprite, stats, last reaction.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, Text } from '@anthropic/ink';
|
import { Box, Text } from '../ink.js';
|
||||||
import { useInput } from '@anthropic/ink';
|
import { useInput } from '../ink.js';
|
||||||
import { renderSprite } from './sprites.js';
|
import { renderSprite } from './sprites.js';
|
||||||
import { RARITY_COLORS, RARITY_STARS, STAT_NAMES, type Companion } from './types.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 figures from 'figures'
|
||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import { useTerminalSize } from '../hooks/useTerminalSize.js'
|
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 { useAppState, useSetAppState } from '../state/AppState.js'
|
||||||
import type { AppState } from '../state/AppStateStore.js'
|
import type { AppState } from '../state/AppStateStore.js'
|
||||||
import { getGlobalConfig } from '../utils/config.js'
|
import { getGlobalConfig } from '../utils/config.js'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { feature } from 'bun:bundle'
|
import { feature } from 'bun:bundle'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { useNotifications } from '../context/notifications.js'
|
import { useNotifications } from '../context/notifications.js'
|
||||||
import { Text } from '@anthropic/ink'
|
import { Text } from '../ink.js'
|
||||||
import { getGlobalConfig } from '../utils/config.js'
|
import { getGlobalConfig } from '../utils/config.js'
|
||||||
import { getRainbowColor } from '../utils/thinking.js'
|
import { getRainbowColor } from '../utils/thinking.js'
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import pMap from 'p-map'
|
|||||||
import { cwd } from 'process'
|
import { cwd } from 'process'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { MCPServerDesktopImportDialog } from '../../components/MCPServerDesktopImportDialog.js'
|
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 { KeybindingSetup } from '../../keybindings/KeybindingProviderSetup.js'
|
||||||
import {
|
import {
|
||||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import { cwd } from 'process'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { WelcomeV2 } from '../../components/LogoV2/WelcomeV2.js'
|
import { WelcomeV2 } from '../../components/LogoV2/WelcomeV2.js'
|
||||||
import { useManagePlugins } from '../../hooks/useManagePlugins.js'
|
import { useManagePlugins } from '../../hooks/useManagePlugins.js'
|
||||||
import type { Root } from '@anthropic/ink'
|
import type { Root } from '../../ink.js'
|
||||||
import { Box, Text } from '@anthropic/ink'
|
import { Box, Text } from '../../ink.js'
|
||||||
import { KeybindingSetup } from '../../keybindings/KeybindingProviderSetup.js'
|
import { KeybindingSetup } from '../../keybindings/KeybindingProviderSetup.js'
|
||||||
import { logEvent } from '../../services/analytics/index.js'
|
import { logEvent } from '../../services/analytics/index.js'
|
||||||
import { MCPConnectionManager } from '../../services/mcp/MCPConnectionManager.js'
|
import { MCPConnectionManager } from '../../services/mcp/MCPConnectionManager.js'
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
import type { LocalJSXCommandContext } from '../../commands.js'
|
import type { LocalJSXCommandContext } from '../../commands.js'
|
||||||
import { MessageResponse } from '../../components/MessageResponse.js'
|
import { MessageResponse } from '../../components/MessageResponse.js'
|
||||||
import { AddWorkspaceDirectory } from '../../components/permissions/rules/AddWorkspaceDirectory.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 type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||||
import {
|
import {
|
||||||
applyPermissionUpdate,
|
applyPermissionUpdate,
|
||||||
|
|||||||
@@ -13,10 +13,11 @@ import {
|
|||||||
BRIDGE_LOGIN_INSTRUCTION,
|
BRIDGE_LOGIN_INSTRUCTION,
|
||||||
REMOTE_CONTROL_DISCONNECTED_MSG,
|
REMOTE_CONTROL_DISCONNECTED_MSG,
|
||||||
} from '../../bridge/types.js'
|
} 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 { shouldShowRemoteCallout } from '../../components/RemoteCallout.js'
|
||||||
import { useRegisterOverlay } from '../../context/overlayContext.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 { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||||
import {
|
import {
|
||||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
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 { useModalOrTerminalSize } from '../../context/modalContext.js'
|
||||||
import { getSystemContext, getUserContext } from '../../context.js'
|
import { getSystemContext, getUserContext } from '../../context.js'
|
||||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||||
import { type KeyboardEvent, type ScrollBoxHandle, ScrollBox } from '@anthropic/ink'
|
import ScrollBox, {
|
||||||
import { Box, Text } from '@anthropic/ink'
|
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 { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||||
import type { Message } from '../../types/message.js'
|
import type { Message } from '../../types/message.js'
|
||||||
import { createAbortController } from '../../utils/abortController.js'
|
import { createAbortController } from '../../utils/abortController.js'
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import {
|
|||||||
type OptionWithDescription,
|
type OptionWithDescription,
|
||||||
Select,
|
Select,
|
||||||
} from '../../components/CustomSelect/select.js'
|
} from '../../components/CustomSelect/select.js'
|
||||||
import { Dialog } from '@anthropic/ink'
|
import { Dialog } from '../../components/design-system/Dialog.js'
|
||||||
import { Box, Text } from '@anthropic/ink'
|
import { Box, Text } from '../../ink.js'
|
||||||
import { useAppState } from '../../state/AppState.js'
|
import { useAppState } from '../../state/AppState.js'
|
||||||
import { isClaudeAISubscriber } from '../../utils/auth.js'
|
import { isClaudeAISubscriber } from '../../utils/auth.js'
|
||||||
import { openBrowser } from '../../utils/browser.js'
|
import { openBrowser } from '../../utils/browser.js'
|
||||||
|
|||||||
@@ -6,8 +6,13 @@ import React, { useRef } from 'react'
|
|||||||
import type { CommandResultDisplay } from '../../commands.js'
|
import type { CommandResultDisplay } from '../../commands.js'
|
||||||
import type { OptionWithDescription } from '../../components/CustomSelect/select.js'
|
import type { OptionWithDescription } from '../../components/CustomSelect/select.js'
|
||||||
import { Select } from '../../components/CustomSelect/select.js'
|
import { Select } from '../../components/CustomSelect/select.js'
|
||||||
import { Byline, KeyboardShortcutHint, Pane } from '@anthropic/ink'
|
import { Byline } from '../../components/design-system/Byline.js'
|
||||||
import { Box, setClipboard, Text, stringWidth, type KeyboardEvent } from '@anthropic/ink'
|
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 { logEvent } from '../../services/analytics/index.js'
|
||||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||||
import type { AssistantMessage, Message } from '../../types/message.js'
|
import type { AssistantMessage, Message } from '../../types/message.js'
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import type {
|
|||||||
CommandResultDisplay,
|
CommandResultDisplay,
|
||||||
LocalJSXCommandContext,
|
LocalJSXCommandContext,
|
||||||
} from '../../commands.js'
|
} from '../../commands.js'
|
||||||
import { Dialog } from '@anthropic/ink'
|
import { Dialog } from '../../components/design-system/Dialog.js'
|
||||||
import { FastIcon, getFastIconString } from '../../components/FastIcon.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 { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||||
import {
|
import {
|
||||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ import type {
|
|||||||
LocalJSXCommandContext,
|
LocalJSXCommandContext,
|
||||||
} from '../../commands.js'
|
} from '../../commands.js'
|
||||||
import { Select } from '../../components/CustomSelect/index.js'
|
import { Select } from '../../components/CustomSelect/index.js'
|
||||||
import { Dialog } from '@anthropic/ink'
|
import { Dialog } from '../../components/design-system/Dialog.js'
|
||||||
import {
|
import {
|
||||||
IdeAutoConnectDialog,
|
IdeAutoConnectDialog,
|
||||||
IdeDisableAutoConnectDialog,
|
IdeDisableAutoConnectDialog,
|
||||||
shouldShowAutoConnectDialog,
|
shouldShowAutoConnectDialog,
|
||||||
shouldShowDisableAutoConnectDialog,
|
shouldShowDisableAutoConnectDialog,
|
||||||
} from '../../components/IdeAutoConnectDialog.js'
|
} from '../../components/IdeAutoConnectDialog.js'
|
||||||
import { Box, Text } from '@anthropic/ink'
|
import { Box, Text } from '../../ink.js'
|
||||||
import { clearServerCache } from '../../services/mcp/client.js'
|
import { clearServerCache } from '../../services/mcp/client.js'
|
||||||
import type { ScopedMcpServerConfig } from '../../services/mcp/types.js'
|
import type { ScopedMcpServerConfig } from '../../services/mcp/types.js'
|
||||||
import { useAppState, useSetAppState } from '../../state/AppState.js'
|
import { useAppState, useSetAppState } from '../../state/AppState.js'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useCallback, useState } from 'react'
|
import React, { useCallback, useState } from 'react'
|
||||||
import TextInput from '../../components/TextInput.js'
|
import TextInput from '../../components/TextInput.js'
|
||||||
import { useTerminalSize } from '../../hooks/useTerminalSize.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'
|
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||||
|
|
||||||
interface ApiKeyStepProps {
|
interface ApiKeyStepProps {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useCallback, useState } from 'react'
|
import React, { useCallback, useState } from 'react'
|
||||||
import TextInput from '../../components/TextInput.js'
|
import TextInput from '../../components/TextInput.js'
|
||||||
import { useTerminalSize } from '../../hooks/useTerminalSize.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'
|
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||||
|
|
||||||
interface CheckExistingSecretStepProps {
|
interface CheckExistingSecretStepProps {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Text } from '@anthropic/ink'
|
import { Text } from '../../ink.js'
|
||||||
|
|
||||||
export function CheckGitHubStep() {
|
export function CheckGitHubStep() {
|
||||||
return <Text>Checking GitHub CLI installation…</Text>
|
return <Text>Checking GitHub CLI installation…</Text>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useCallback, useState } from 'react'
|
import React, { useCallback, useState } from 'react'
|
||||||
import TextInput from '../../components/TextInput.js'
|
import TextInput from '../../components/TextInput.js'
|
||||||
import { useTerminalSize } from '../../hooks/useTerminalSize.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'
|
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||||
|
|
||||||
interface ChooseRepoStepProps {
|
interface ChooseRepoStepProps {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Box, Text } from '@anthropic/ink'
|
import { Box, Text } from '../../ink.js'
|
||||||
import type { Workflow } from './types.js'
|
import type { Workflow } from './types.js'
|
||||||
|
|
||||||
interface CreatingStepProps {
|
interface CreatingStepProps {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js'
|
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 {
|
interface ErrorStepProps {
|
||||||
error: string | undefined
|
error: string | undefined
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Select } from 'src/components/CustomSelect/index.js'
|
import { Select } from 'src/components/CustomSelect/index.js'
|
||||||
import { Box, Text } from '@anthropic/ink'
|
import { Box, Text } from '../../ink.js'
|
||||||
|
|
||||||
interface ExistingWorkflowStepProps {
|
interface ExistingWorkflowStepProps {
|
||||||
repoName: string
|
repoName: string
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import figures from 'figures'
|
import figures from 'figures'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js'
|
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 { useKeybinding } from '../../keybindings/useKeybinding.js'
|
||||||
|
|
||||||
interface InstallAppStepProps {
|
interface InstallAppStepProps {
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ import {
|
|||||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
logEvent,
|
logEvent,
|
||||||
} from 'src/services/analytics/index.js'
|
} 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 { Spinner } from '../../components/Spinner.js'
|
||||||
import TextInput from '../../components/TextInput.js'
|
import TextInput from '../../components/TextInput.js'
|
||||||
import { useTerminalSize } from '../../hooks/useTerminalSize.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 { OAuthService } from '../../services/oauth/index.js'
|
||||||
import { saveOAuthTokensIfNeeded } from '../../utils/auth.js'
|
import { saveOAuthTokensIfNeeded } from '../../utils/auth.js'
|
||||||
import { logError } from '../../utils/log.js'
|
import { logError } from '../../utils/log.js'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Box, Text } from '@anthropic/ink'
|
import { Box, Text } from '../../ink.js'
|
||||||
|
|
||||||
type SuccessStepProps = {
|
type SuccessStepProps = {
|
||||||
secretExists: boolean
|
secretExists: boolean
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import figures from 'figures'
|
import figures from 'figures'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js'
|
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 { useKeybinding } from '../../keybindings/useKeybinding.js'
|
||||||
import type { Warning } from './types.js'
|
import type { Warning } from './types.js'
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import {
|
|||||||
import { WorkflowMultiselectDialog } from '../../components/WorkflowMultiselectDialog.js'
|
import { WorkflowMultiselectDialog } from '../../components/WorkflowMultiselectDialog.js'
|
||||||
import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js'
|
import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js'
|
||||||
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.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 type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||||
import { getAnthropicApiKey, isAnthropicAuthEnabled } from '../../utils/auth.js'
|
import { getAnthropicApiKey, isAnthropicAuthEnabled } from '../../utils/auth.js'
|
||||||
import { openBrowser } from '../../utils/browser.js'
|
import { openBrowser } from '../../utils/browser.js'
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { join } from 'node:path'
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import type { CommandResultDisplay } from 'src/commands.js'
|
import type { CommandResultDisplay } from 'src/commands.js'
|
||||||
import { logEvent } from 'src/services/analytics/index.js'
|
import { logEvent } from 'src/services/analytics/index.js'
|
||||||
import { StatusIcon } from '@anthropic/ink'
|
import { StatusIcon } from '../components/design-system/StatusIcon.js'
|
||||||
import { Box, wrappedRender as render, Text } from '@anthropic/ink'
|
import { Box, render, Text } from '../ink.js'
|
||||||
import { logForDebugging } from '../utils/debug.js'
|
import { logForDebugging } from '../utils/debug.js'
|
||||||
import { env } from '../utils/env.js'
|
import { env } from '../utils/env.js'
|
||||||
import { errorMessage } from '../utils/errors.js'
|
import { errorMessage } from '../utils/errors.js'
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import {
|
|||||||
import type { LocalJSXCommandContext } from '../../commands.js'
|
import type { LocalJSXCommandContext } from '../../commands.js'
|
||||||
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js'
|
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js'
|
||||||
import { ConsoleOAuthFlow } from '../../components/ConsoleOAuthFlow.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 { useMainLoopModel } from '../../hooks/useMainLoopModel.js'
|
||||||
import { Text } from '@anthropic/ink'
|
import { Text } from '../../ink.js'
|
||||||
import { refreshGrowthBookAfterAuthChange } from '../../services/analytics/growthbook.js'
|
import { refreshGrowthBookAfterAuthChange } from '../../services/analytics/growthbook.js'
|
||||||
import { refreshPolicyLimits } from '../../services/policyLimits/index.js'
|
import { refreshPolicyLimits } from '../../services/policyLimits/index.js'
|
||||||
import { refreshRemoteManagedSettings } from '../../services/remoteManagedSettings/index.js'
|
import { refreshRemoteManagedSettings } from '../../services/remoteManagedSettings/index.js'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { clearTrustedDeviceTokenCache } from '../../bridge/trustedDevice.js'
|
import { clearTrustedDeviceTokenCache } from '../../bridge/trustedDevice.js'
|
||||||
import { Text } from '@anthropic/ink'
|
import { Text } from '../../ink.js'
|
||||||
import { refreshGrowthBookAfterAuthChange } from '../../services/analytics/growthbook.js'
|
import { refreshGrowthBookAfterAuthChange } from '../../services/analytics/growthbook.js'
|
||||||
import {
|
import {
|
||||||
getGroveNoticeConfig,
|
getGroveNoticeConfig,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { mkdir, writeFile } from 'fs/promises'
|
import { mkdir, writeFile } from 'fs/promises'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import type { CommandResultDisplay } from '../../commands.js'
|
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 { MemoryFileSelector } from '../../components/memory/MemoryFileSelector.js'
|
||||||
import { getRelativeMemoryPath } from '../../components/memory/MemoryUpdateNotification.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 type { LocalJSXCommandCall } from '../../types/command.js'
|
||||||
import { clearMemoryFileCaches, getMemoryFiles } from '../../utils/claudemd.js'
|
import { clearMemoryFileCaches, getMemoryFiles } from '../../utils/claudemd.js'
|
||||||
import { getClaudeConfigHomeDir } from '../../utils/envUtils.js'
|
import { getClaudeConfigHomeDir } from '../../utils/envUtils.js'
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { toString as qrToString } from 'qrcode'
|
import { toString as qrToString } from 'qrcode'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { Pane } from '@anthropic/ink'
|
import { Pane } from '../../components/design-system/Pane.js'
|
||||||
import { type KeyboardEvent, Box, Text } from '@anthropic/ink'
|
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||||
|
import { Box, Text } from '../../ink.js'
|
||||||
import { useKeybinding } from '../../keybindings/useKeybinding.js'
|
import { useKeybinding } from '../../keybindings/useKeybinding.js'
|
||||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { handlePlanModeTransition } from '../../bootstrap/state.js'
|
import { handlePlanModeTransition } from '../../bootstrap/state.js'
|
||||||
import type { LocalJSXCommandContext } from '../../commands.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 type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||||
import { getExternalEditor } from '../../utils/editor.js'
|
import { getExternalEditor } from '../../utils/editor.js'
|
||||||
import { toIDEDisplayName } from '../../utils/ide.js'
|
import { toIDEDisplayName } from '../../utils/ide.js'
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import {
|
|||||||
logEvent,
|
logEvent,
|
||||||
} from 'src/services/analytics/index.js'
|
} from 'src/services/analytics/index.js'
|
||||||
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.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 { Spinner } from '../../components/Spinner.js'
|
||||||
import TextInput from '../../components/TextInput.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 { toError } from '../../utils/errors.js'
|
||||||
import { logError } from '../../utils/log.js'
|
import { logError } from '../../utils/log.js'
|
||||||
import { clearAllCaches } from '../../utils/plugins/cacheUtils.js'
|
import { clearAllCaches } from '../../utils/plugins/cacheUtils.js'
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import figures from 'figures'
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js'
|
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 {
|
import {
|
||||||
useKeybinding,
|
useKeybinding,
|
||||||
useKeybindings,
|
useKeybindings,
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import figures from 'figures'
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js'
|
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js'
|
||||||
|
import { Byline } from '../../components/design-system/Byline.js'
|
||||||
import { SearchBox } from '../../components/SearchBox.js'
|
import { SearchBox } from '../../components/SearchBox.js'
|
||||||
import { Byline } from '@anthropic/ink'
|
|
||||||
import { useSearchInput } from '../../hooks/useSearchInput.js'
|
import { useSearchInput } from '../../hooks/useSearchInput.js'
|
||||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||||
// eslint-disable-next-line custom-rules/prefer-use-keybindings -- useInput needed for raw search mode text input
|
// 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 {
|
import {
|
||||||
useKeybinding,
|
useKeybinding,
|
||||||
useKeybindings,
|
useKeybindings,
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import {
|
|||||||
logEvent,
|
logEvent,
|
||||||
} from 'src/services/analytics/index.js'
|
} from 'src/services/analytics/index.js'
|
||||||
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.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
|
// 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 {
|
import {
|
||||||
useKeybinding,
|
useKeybinding,
|
||||||
useKeybindings,
|
useKeybindings,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import * as path from 'path'
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js'
|
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 { MCPRemoteServerMenu } from '../../components/mcp/MCPRemoteServerMenu.js'
|
||||||
import { MCPStdioServerMenu } from '../../components/mcp/MCPStdioServerMenu.js'
|
import { MCPStdioServerMenu } from '../../components/mcp/MCPStdioServerMenu.js'
|
||||||
import { MCPToolDetailView } from '../../components/mcp/MCPToolDetailView.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 { useSearchInput } from '../../hooks/useSearchInput.js'
|
||||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||||
// eslint-disable-next-line custom-rules/prefer-use-keybindings -- useInput needed for raw search mode text input
|
// 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 {
|
import {
|
||||||
useKeybinding,
|
useKeybinding,
|
||||||
useKeybindings,
|
useKeybindings,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import figures from 'figures'
|
import figures from 'figures'
|
||||||
import React, { useCallback, useState } from 'react'
|
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
|
// 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 {
|
import {
|
||||||
useKeybinding,
|
useKeybinding,
|
||||||
useKeybindings,
|
useKeybindings,
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import figures from 'figures'
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js'
|
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js'
|
||||||
import { Byline, Pane, Tabs } from '@anthropic/ink'
|
import { Byline } from '../../components/design-system/Byline.js'
|
||||||
import { Tab } from '../../components/design-system/Tabs.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 { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
|
||||||
import { Box, Text } from '@anthropic/ink'
|
import { Box, Text } from '../../ink.js'
|
||||||
import {
|
import {
|
||||||
useKeybinding,
|
useKeybinding,
|
||||||
useKeybindings,
|
useKeybindings,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import figures from 'figures'
|
import figures from 'figures'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { Box, Text } from '@anthropic/ink'
|
import { Box, Text } from '../../ink.js'
|
||||||
import { getPluginTrustMessage } from '../../utils/plugins/marketplaceHelpers.js'
|
import { getPluginTrustMessage } from '../../utils/plugins/marketplaceHelpers.js'
|
||||||
|
|
||||||
export function PluginTrustWarning(): React.ReactNode {
|
export function PluginTrustWarning(): React.ReactNode {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import figures from 'figures'
|
import figures from 'figures'
|
||||||
import * as React from 'react'
|
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 { plural } from '../../utils/stringUtils.js'
|
||||||
import type { UnifiedInstalledItem } from './unifiedTypes.js'
|
import type { UnifiedInstalledItem } from './unifiedTypes.js'
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import figures from 'figures'
|
import figures from 'figures'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useEffect } 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 { errorMessage } from '../../utils/errors.js'
|
||||||
import { logError } from '../../utils/log.js'
|
import { logError } from '../../utils/log.js'
|
||||||
import { validateManifest } from '../../utils/plugins/validatePlugin.js'
|
import { validateManifest } from '../../utils/plugins/validatePlugin.js'
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
|
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js'
|
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'
|
import type { PluginMarketplaceEntry } from '../../utils/plugins/schemas.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
type OptionWithDescription,
|
type OptionWithDescription,
|
||||||
Select,
|
Select,
|
||||||
} from '../../components/CustomSelect/select.js'
|
} 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 { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
|
||||||
import { logEvent } from '../../services/analytics/index.js'
|
import { logEvent } from '../../services/analytics/index.js'
|
||||||
import { useClaudeAiLimits } from '../../services/claudeAiLimitsHook.js'
|
import { useClaudeAiLimits } from '../../services/claudeAiLimitsHook.js'
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import { execa } from 'execa'
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Select } from '../../components/CustomSelect/index.js'
|
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 {
|
import {
|
||||||
logEvent,
|
logEvent,
|
||||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS as SafeString,
|
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 { Spinner } from '../../components/Spinner.js'
|
||||||
import { useIsInsideModal } from '../../context/modalContext.js'
|
import { useIsInsideModal } from '../../context/modalContext.js'
|
||||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||||
import { setClipboard } from '@anthropic/ink'
|
import { setClipboard } from '../../ink/termio/osc.js'
|
||||||
import { Box, Text } from '@anthropic/ink'
|
import { Box, Text } from '../../ink.js'
|
||||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||||
import type { LogOption } from '../../types/logs.js'
|
import type { LogOption } from '../../types/logs.js'
|
||||||
import { agenticSessionSearch } from '../../utils/agenticSessionSearch.js'
|
import { agenticSessionSearch } from '../../utils/agenticSessionSearch.js'
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useCallback, useRef, useState } from 'react'
|
import React, { useCallback, useRef, useState } from 'react'
|
||||||
import { Select } from '../../components/CustomSelect/select.js'
|
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 = {
|
type Props = {
|
||||||
onProceed: (signal: AbortSignal) => Promise<void>
|
onProceed: (signal: AbortSignal) => Promise<void>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { relative } from 'path'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { getCwdState } from '../../bootstrap/state.js'
|
import { getCwdState } from '../../bootstrap/state.js'
|
||||||
import { SandboxSettings } from '../../components/sandbox/SandboxSettings.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 { getPlatform } from '../../utils/platform.js'
|
||||||
import {
|
import {
|
||||||
addToExcludedCommands,
|
addToExcludedCommands,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { toString as qrToString } from 'qrcode'
|
import { toString as qrToString } from 'qrcode'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useEffect, useState } 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 { useKeybinding } from '../../keybindings/useKeybinding.js'
|
||||||
import { useAppState } from '../../state/AppState.js'
|
import { useAppState } from '../../state/AppState.js'
|
||||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import * as React from 'react'
|
|||||||
import { getSessionId } from '../../bootstrap/state.js'
|
import { getSessionId } from '../../bootstrap/state.js'
|
||||||
import type { CommandResultDisplay } from '../../commands.js'
|
import type { CommandResultDisplay } from '../../commands.js'
|
||||||
import { Select } from '../../components/CustomSelect/select.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 { 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 { logEvent } from '../../services/analytics/index.js'
|
||||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||||
import { recursivelySanitizeUnicode } from '../../utils/sanitization.js'
|
import { recursivelySanitizeUnicode } from '../../utils/sanitization.js'
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { homedir, platform } from 'os'
|
|||||||
import { dirname, join } from 'path'
|
import { dirname, join } from 'path'
|
||||||
import type { ThemeName } from 'src/utils/theme.js'
|
import type { ThemeName } from 'src/utils/theme.js'
|
||||||
import { pathToFileURL } from 'url'
|
import { pathToFileURL } from 'url'
|
||||||
import { supportsHyperlinks } from '@anthropic/ink'
|
import { supportsHyperlinks } from '../../ink/supports-hyperlinks.js'
|
||||||
import { color } from '@anthropic/ink'
|
import { color } from '../../ink.js'
|
||||||
import { maybeMarkProjectOnboardingComplete } from '../../projectOnboardingState.js'
|
import { maybeMarkProjectOnboardingComplete } from '../../projectOnboardingState.js'
|
||||||
import type { ToolUseContext } from '../../Tool.js'
|
import type { ToolUseContext } from '../../Tool.js'
|
||||||
import type {
|
import type {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import type { CommandResultDisplay } from '../../commands.js'
|
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 { ThemePicker } from '../../components/ThemePicker.js'
|
||||||
import { useTheme } from '@anthropic/ink'
|
import { useTheme } from '../../ink.js'
|
||||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import * as React from 'react'
|
|||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import type { CommandResultDisplay } from '../../commands.js'
|
import type { CommandResultDisplay } from '../../commands.js'
|
||||||
import { Select } from '../../components/CustomSelect/select.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 { 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 { enablePluginOp } from '../../services/plugins/pluginOperations.js'
|
||||||
import { logForDebugging } from '../../utils/debug.js'
|
import { logForDebugging } from '../../utils/debug.js'
|
||||||
import { isENOENT, toError } from '../../utils/errors.js'
|
import { isENOENT, toError } from '../../utils/errors.js'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { Box, Text } from '@anthropic/ink'
|
import { Box, Text } from '../ink.js'
|
||||||
import { formatNumber } from '../utils/format.js'
|
import { formatNumber } from '../utils/format.js'
|
||||||
import type { Theme } from '../utils/theme.js'
|
import type { Theme } from '../utils/theme.js'
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Text, Dialog } from '@anthropic/ink'
|
import { Text } from '../ink.js'
|
||||||
import { saveGlobalConfig } from '../utils/config.js'
|
import { saveGlobalConfig } from '../utils/config.js'
|
||||||
import { Select } from './CustomSelect/index.js'
|
import { Select } from './CustomSelect/index.js'
|
||||||
|
import { Dialog } from './design-system/Dialog.js'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
customApiKeyTruncated: string
|
customApiKeyTruncated: string
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { logEvent } from 'src/services/analytics/index.js'
|
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 { updateSettingsForSource } from '../utils/settings/settings.js'
|
||||||
import { Select } from './CustomSelect/index.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.
|
// NOTE: This copy is legally reviewed — do not modify without Legal team approval.
|
||||||
export const AUTO_MODE_DESCRIPTION =
|
export const AUTO_MODE_DESCRIPTION =
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
} from 'src/services/analytics/index.js'
|
} from 'src/services/analytics/index.js'
|
||||||
import { useInterval } from 'usehooks-ts'
|
import { useInterval } from 'usehooks-ts'
|
||||||
import { useUpdateNotification } from '../hooks/useUpdateNotification.js'
|
import { useUpdateNotification } from '../hooks/useUpdateNotification.js'
|
||||||
import { Box, Text } from '@anthropic/ink'
|
import { Box, Text } from '../ink.js'
|
||||||
import {
|
import {
|
||||||
type AutoUpdaterResult,
|
type AutoUpdaterResult,
|
||||||
getLatestVersion,
|
getLatestVersion,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { Box, Link, Text } from '@anthropic/ink'
|
import { Box, Link, Text } from '../ink.js'
|
||||||
import {
|
import {
|
||||||
type AwsAuthStatus,
|
type AwsAuthStatus,
|
||||||
AwsAuthStatusManager,
|
AwsAuthStatusManager,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { renderPlaceholder } from '../hooks/renderPlaceholder.js'
|
import { renderPlaceholder } from '../hooks/renderPlaceholder.js'
|
||||||
import { usePasteHandler } from '../hooks/usePasteHandler.js'
|
import { usePasteHandler } from '../hooks/usePasteHandler.js'
|
||||||
import { useDeclaredCursor } from '@anthropic/ink'
|
import { useDeclaredCursor } from '../ink/hooks/use-declared-cursor.js'
|
||||||
import { Ansi, Box, Text, useInput } from '@anthropic/ink'
|
import { Ansi, Box, Text, useInput } from '../ink.js'
|
||||||
import type {
|
import type {
|
||||||
BaseInputState,
|
BaseInputState,
|
||||||
BaseTextInputProps,
|
BaseTextInputProps,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Box } from '@anthropic/ink'
|
import { Box } from '../ink.js'
|
||||||
import { BashTool } from '../tools/BashTool/BashTool.js'
|
import { BashTool } from '../tools/BashTool/BashTool.js'
|
||||||
import type { ShellProgress } from '../types/tools.js'
|
import type { ShellProgress } from '../types/tools.js'
|
||||||
import { UserBashInputMessage } from './messages/UserBashInputMessage.js'
|
import { UserBashInputMessage } from './messages/UserBashInputMessage.js'
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ import {
|
|||||||
} from '../constants/figures.js'
|
} from '../constants/figures.js'
|
||||||
import { useRegisterOverlay } from '../context/overlayContext.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
|
// 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 { useKeybindings } from '../keybindings/useKeybinding.js'
|
||||||
import { useAppState, useSetAppState } from '../state/AppState.js'
|
import { useAppState, useSetAppState } from '../state/AppState.js'
|
||||||
import { saveGlobalConfig } from '../utils/config.js'
|
import { saveGlobalConfig } from '../utils/config.js'
|
||||||
import { getBranch } from '../utils/git.js'
|
import { getBranch } from '../utils/git.js'
|
||||||
import { Dialog } from '@anthropic/ink'
|
import { Dialog } from './design-system/Dialog.js'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onDone: () => void
|
onDone: () => void
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { formatCost } from '../cost-tracker.js';
|
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 { formatTokens } from '../utils/format.js';
|
||||||
|
import { ProgressBar } from './design-system/ProgressBar.js';
|
||||||
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
||||||
|
|
||||||
type RateLimitBucket = {
|
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