diff --git a/.gitignore b/.gitignore index 8b5e47a0e..9813a5d12 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ src/utils/vendor/ # Python bytecode __pycache__/ *.pyc +logs diff --git a/bun.lock b/bun.lock index af8cd232e..06021cca8 100644 --- a/bun.lock +++ b/bun.lock @@ -17,6 +17,7 @@ "@anthropic-ai/sandbox-runtime": "^0.0.44", "@anthropic-ai/sdk": "^0.80.0", "@anthropic-ai/vertex-sdk": "^0.14.4", + "@anthropic/ink": "workspace:*", "@aws-sdk/client-bedrock": "^3.1020.0", "@aws-sdk/client-bedrock-runtime": "^3.1020.0", "@aws-sdk/client-sts": "^3.1020.0", @@ -138,6 +139,29 @@ "name": "@ant/computer-use-swift", "version": "1.0.0", }, + "packages/@ant/ink": { + "name": "@anthropic/ink", + "version": "1.0.0", + "dependencies": { + "auto-bind": "^5.0.1", + "bidi-js": "^1.0.3", + "chalk": "^5.6.2", + "cli-boxes": "^4.0.1", + "emoji-regex": "^10.6.0", + "figures": "^6.1.0", + "get-east-asian-width": "^1.5.0", + "indent-string": "^5.0.0", + "lodash-es": "^4.17.23", + "react": "^19.2.4", + "react-reconciler": "^0.33.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^7.2.0", + "supports-hyperlinks": "^4.4.0", + "type-fest": "^5.5.0", + "usehooks-ts": "^3.1.1", + "wrap-ansi": "^10.0.0", + }, + }, "packages/audio-capture-napi": { "name": "audio-capture-napi", "version": "1.0.0", @@ -190,6 +214,8 @@ "@anthropic-ai/vertex-sdk": ["@anthropic-ai/vertex-sdk@0.14.4", "https://registry.npmmirror.com/@anthropic-ai/vertex-sdk/-/vertex-sdk-0.14.4.tgz", { "dependencies": { "@anthropic-ai/sdk": ">=0.50.3 <1", "google-auth-library": "^9.4.2" } }, "sha512-BZUPRWghZxfSFtAxU563wH+jfWBPoedAwsVxG35FhmNsjeV8tyfN+lFriWhCpcZApxA4NdT6Soov+PzfnxxD5g=="], + "@anthropic/ink": ["@anthropic/ink@workspace:packages/@ant/ink"], + "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "https://registry.npmmirror.com/@aws-crypto/crc32/-/crc32-5.2.0.tgz", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], "@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "https://registry.npmmirror.com/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="], @@ -1198,7 +1224,7 @@ "open": ["open@10.2.0", "https://registry.npmmirror.com/open/-/open-10.2.0.tgz", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], - "openai": ["openai@6.33.0", "", { "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", "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=="], "os-tmpdir": ["os-tmpdir@1.0.2", "https://registry.npmmirror.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="], diff --git a/package.json b/package.json index 6d066da69..586b78fbe 100644 --- a/package.json +++ b/package.json @@ -131,6 +131,7 @@ "highlight.js": "^11.11.1", "https-proxy-agent": "^8.0.0", "ignore": "^7.0.5", + "@anthropic/ink": "workspace:*", "image-processor-napi": "workspace:*", "indent-string": "^5.0.0", "jsonc-parser": "^3.3.1", diff --git a/packages/@ant/ink/package.json b/packages/@ant/ink/package.json new file mode 100644 index 000000000..aec1f4eeb --- /dev/null +++ b/packages/@ant/ink/package.json @@ -0,0 +1,30 @@ +{ + "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" + } +} diff --git a/src/ink/components/AlternateScreen.tsx b/packages/@ant/ink/src/components/AlternateScreen.tsx similarity index 95% rename from src/ink/components/AlternateScreen.tsx rename to packages/@ant/ink/src/components/AlternateScreen.tsx index eeeb1152e..2e07a4cc7 100644 --- a/src/ink/components/AlternateScreen.tsx +++ b/packages/@ant/ink/src/components/AlternateScreen.tsx @@ -3,14 +3,14 @@ import React, { useContext, useInsertionEffect, } from 'react' -import instances from '../instances.js' +import instances from '../core/instances.js' import { DISABLE_MOUSE_TRACKING, ENABLE_MOUSE_TRACKING, ENTER_ALT_SCREEN, EXIT_ALT_SCREEN, -} from '../termio/dec.js' -import { TerminalWriteContext } from '../useTerminalNotification.js' +} from '../core/termio/dec.js' +import { TerminalWriteContext } from '../hooks/useTerminalNotification.js' import Box from './Box.js' import { TerminalSizeContext } from './TerminalSizeContext.js' diff --git a/src/ink/components/App.tsx b/packages/@ant/ink/src/components/App.tsx similarity index 89% rename from src/ink/components/App.tsx rename to packages/@ant/ink/src/components/App.tsx index 9bbb0c06a..8b7f5bdaa 100644 --- a/src/ink/components/App.tsx +++ b/packages/@ant/ink/src/components/App.tsx @@ -1,37 +1,62 @@ import React, { PureComponent, type ReactNode } from 'react' -import { updateLastInteractionTime } from '../../bootstrap/state.js' -import { logForDebugging } from '../../utils/debug.js' -import { stopCapturingEarlyInput } from '../../utils/earlyInput.js' -import { isEnvTruthy } from '../../utils/envUtils.js' -import { isMouseClicksDisabled } from '../../utils/fullscreen.js' -import { logError } from '../../utils/log.js' -import { EventEmitter } from '../events/emitter.js' -import { InputEvent } from '../events/input-event.js' -import { TerminalFocusEvent } from '../events/terminal-focus-event.js' +// Business-layer callbacks — replaced with inline defaults so this package +// has zero dependencies on business code. The business layer can inject +// implementations via AppCallbacks when needed. +type AppCallbacks = { + updateLastInteractionTime?: () => void + stopCapturingEarlyInput?: () => void + isMouseClicksDisabled?: () => boolean + logError?: (error: unknown) => void + logForDebugging?: (message: string, opts?: { level?: string }) => void +} + +/** Default no-op / safe-default implementations */ +const defaultCallbacks: Required = { + updateLastInteractionTime: () => {}, + stopCapturingEarlyInput: () => {}, + isMouseClicksDisabled: () => false, + logError: (error: unknown) => console.error(error), + logForDebugging: (_message: string, _opts?: { level?: string }) => {}, +} + +/** + * Override the default no-op callbacks. Call this from the business layer + * (e.g. src/ink.tsx) before mounting . + */ +export function setAppCallbacks(cb: AppCallbacks): void { + Object.assign(defaultCallbacks, cb) +} + +function isEnvTruthy(value: string | undefined): boolean { + return value === '1' || value === 'true' +} +import { EventEmitter } from '../core/events/emitter.js' +import { InputEvent } from '../core/events/input-event.js' +import { TerminalFocusEvent } from '../core/events/terminal-focus-event.js' import { INITIAL_STATE, type ParsedInput, type ParsedKey, type ParsedMouse, parseMultipleKeypresses, -} from '../parse-keypress.js' -import reconciler from '../reconciler.js' +} from '../core/parse-keypress.js' +import reconciler from '../core/reconciler.js' import { finishSelection, hasSelection, type SelectionState, startSelection, -} from '../selection.js' +} from '../core/selection.js' import { isXtermJs, setXtversionName, supportsExtendedKeys, -} from '../terminal.js' +} from '../core/terminal.js' import { getTerminalFocused, setTerminalFocused, -} from '../terminal-focus-state.js' -import { TerminalQuerier, xtversion } from '../terminal-querier.js' +} from '../core/terminal-focus-state.js' +import { TerminalQuerier, xtversion } from '../core/terminal-querier.js' import { DISABLE_KITTY_KEYBOARD, DISABLE_MODIFY_OTHER_KEYS, @@ -39,7 +64,7 @@ import { ENABLE_MODIFY_OTHER_KEYS, FOCUS_IN, FOCUS_OUT, -} from '../termio/csi.js' +} from '../core/termio/csi.js' import { DBP, DFE, @@ -48,7 +73,7 @@ import { EFE, HIDE_CURSOR, SHOW_CURSOR, -} from '../termio/dec.js' +} from '../core/termio/dec.js' import AppContext from './AppContext.js' import { ClockProvider } from './ClockContext.js' import CursorDeclarationContext, { @@ -290,9 +315,21 @@ export default class App extends PureComponent { if (this.rawModeEnabledCount === 0) { // Stop early input capture right before we add our own readable handler. // Both use the same stdin 'readable' + read() pattern, so they can't - // coexist -- our handler would drain stdin before Ink's can see it. - // The buffered text is preserved for REPL.tsx via consumeEarlyInput(). - stopCapturingEarlyInput() + // coexist -- the early capture handler would drain stdin before ours + // can see it. The buffered text is preserved for REPL.tsx via consumeEarlyInput(). + defaultCallbacks.stopCapturingEarlyInput() + + // Safety net: remove any pre-existing readable listeners that aren't + // ours. In builds where setAppCallbacks() was never called, the early + // input capture's readableHandler remains attached and would consume + // all stdin data before our handleReadable sees it. + const existingListeners = stdin.listeners('readable') + for (const listener of existingListeners) { + if (listener !== this.handleReadable) { + stdin.removeListener('readable', listener as any) + } + } + stdin.ref() stdin.setRawMode(true) stdin.addListener('readable', this.handleReadable) @@ -324,9 +361,9 @@ export default class App extends PureComponent { ]).then(([r]) => { if (r) { setXtversionName(r.name) - logForDebugging(`XTVERSION: terminal identified as "${r.name}"`) + defaultCallbacks.logForDebugging(`XTVERSION: terminal identified as "${r.name}"`) } else { - logForDebugging('XTVERSION: no reply (terminal ignored query)') + defaultCallbacks.logForDebugging('XTVERSION: no reply (terminal ignored query)') } }) }) @@ -338,6 +375,17 @@ export default class App extends PureComponent { // Disable raw mode only when no components left that are using it if (--this.rawModeEnabledCount === 0) { + // Guard: React 19 runs new useLayoutEffect setup before old cleanup when + // replacing the tree (e.g., showSetupDialog → launchResumeChooser). + // If the old tree had more useInput hooks than the new tree, the old + // cleanup over-decrements the count to 0 even though the new tree has + // active listeners. Detect this and fix the count instead of disabling. + const activeListeners = this.internal_eventEmitter.listenerCount('input') + if (activeListeners > 0) { + this.rawModeEnabledCount = activeListeners + return + } + this.props.stdout.write(DISABLE_MODIFY_OTHER_KEYS) this.props.stdout.write(DISABLE_KITTY_KEYBOARD) // Disable terminal focus reporting (DECSET 1004) @@ -436,7 +484,7 @@ export default class App extends PureComponent { // permanently wedge the stream: data stays buffered and 'readable' // never re-emits. Catching here ensures the stream stays healthy so // subsequent keystrokes are still delivered. - logError(error) + defaultCallbacks.logError(error) // Re-attach the listener in case the exception detached it. // Bun may remove the listener after an error; without this, @@ -446,7 +494,7 @@ export default class App extends PureComponent { this.rawModeEnabledCount > 0 && !stdin.listeners('readable').includes(this.handleReadable) ) { - logForDebugging( + defaultCallbacks.logForDebugging( 'handleReadable: re-attaching stdin readable listener after error recovery', { level: 'warn' }, ) @@ -556,7 +604,7 @@ function processKeysInBatch( !((i.button & 0x20) !== 0 && (i.button & 0x03) === 3)), ) ) { - updateLastInteractionTime() + defaultCallbacks.updateLastInteractionTime() } for (const item of items) { @@ -625,7 +673,7 @@ function processKeysInBatch( export function handleMouseEvent(app: App, m: ParsedMouse): void { // Allow disabling click handling while keeping wheel scroll (which goes // through the keybinding system as 'wheelup'/'wheeldown', not here). - if (isMouseClicksDisabled()) return + if (defaultCallbacks.isMouseClicksDisabled()) return const sel = app.props.selection // Terminal coords are 1-indexed; screen buffer is 0-indexed diff --git a/src/ink/components/AppContext.ts b/packages/@ant/ink/src/components/AppContext.ts similarity index 100% rename from src/ink/components/AppContext.ts rename to packages/@ant/ink/src/components/AppContext.ts diff --git a/src/ink/components/Box.tsx b/packages/@ant/ink/src/components/Box.tsx similarity index 91% rename from src/ink/components/Box.tsx rename to packages/@ant/ink/src/components/Box.tsx index 42785f523..d07a2dd3d 100644 --- a/src/ink/components/Box.tsx +++ b/packages/@ant/ink/src/components/Box.tsx @@ -1,11 +1,11 @@ import React, { type PropsWithChildren, type Ref } from 'react' import type { Except } from 'type-fest' -import type { DOMElement } from '../dom.js' -import type { ClickEvent } from '../events/click-event.js' -import type { FocusEvent } from '../events/focus-event.js' -import type { KeyboardEvent } from '../events/keyboard-event.js' -import type { Styles } from '../styles.js' -import * as warn from '../warn.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 { Styles } from '../core/styles.js' +import * as warn from '../core/warn.js' export type Props = Except & { ref?: Ref diff --git a/src/ink/components/Button.tsx b/packages/@ant/ink/src/components/Button.tsx similarity index 90% rename from src/ink/components/Button.tsx rename to packages/@ant/ink/src/components/Button.tsx index 0095d9c59..487c38e13 100644 --- a/src/ink/components/Button.tsx +++ b/packages/@ant/ink/src/components/Button.tsx @@ -6,11 +6,11 @@ import React, { useState, } from 'react' import type { Except } from 'type-fest' -import type { DOMElement } from '../dom.js' -import type { ClickEvent } from '../events/click-event.js' -import type { FocusEvent } from '../events/focus-event.js' -import type { KeyboardEvent } from '../events/keyboard-event.js' -import type { Styles } from '../styles.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 { Styles } from '../core/styles.js' import Box from './Box.js' type ButtonState = { diff --git a/src/ink/components/ClockContext.tsx b/packages/@ant/ink/src/components/ClockContext.tsx similarity index 98% rename from src/ink/components/ClockContext.tsx rename to packages/@ant/ink/src/components/ClockContext.tsx index 32a8b9a28..2822a84ba 100644 --- a/src/ink/components/ClockContext.tsx +++ b/packages/@ant/ink/src/components/ClockContext.tsx @@ -1,5 +1,5 @@ import React, { createContext, useEffect, useState } from 'react' -import { FRAME_INTERVAL_MS } from '../constants.js' +import { FRAME_INTERVAL_MS } from '../core/constants.js' import { useTerminalFocus } from '../hooks/use-terminal-focus.js' export type Clock = { diff --git a/src/ink/components/CursorDeclarationContext.ts b/packages/@ant/ink/src/components/CursorDeclarationContext.ts similarity index 95% rename from src/ink/components/CursorDeclarationContext.ts rename to packages/@ant/ink/src/components/CursorDeclarationContext.ts index 358c80409..eb5b4d0f9 100644 --- a/src/ink/components/CursorDeclarationContext.ts +++ b/packages/@ant/ink/src/components/CursorDeclarationContext.ts @@ -1,5 +1,5 @@ import { createContext } from 'react' -import type { DOMElement } from '../dom.js' +import type { DOMElement } from '../core/dom.js' export type CursorDeclaration = { /** Display column (terminal cell width) within the declared node */ diff --git a/src/ink/components/ErrorOverview.tsx b/packages/@ant/ink/src/components/ErrorOverview.tsx similarity index 100% rename from src/ink/components/ErrorOverview.tsx rename to packages/@ant/ink/src/components/ErrorOverview.tsx diff --git a/src/ink/components/Link.tsx b/packages/@ant/ink/src/components/Link.tsx similarity index 90% rename from src/ink/components/Link.tsx rename to packages/@ant/ink/src/components/Link.tsx index ee7f04d14..c3ad1e2f3 100644 --- a/src/ink/components/Link.tsx +++ b/packages/@ant/ink/src/components/Link.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from 'react' import React from 'react' -import { supportsHyperlinks } from '../supports-hyperlinks.js' +import { supportsHyperlinks } from '../core/supports-hyperlinks.js' import Text from './Text.js' export type Props = { diff --git a/src/ink/components/Newline.tsx b/packages/@ant/ink/src/components/Newline.tsx similarity index 100% rename from src/ink/components/Newline.tsx rename to packages/@ant/ink/src/components/Newline.tsx diff --git a/src/ink/components/NoSelect.tsx b/packages/@ant/ink/src/components/NoSelect.tsx similarity index 100% rename from src/ink/components/NoSelect.tsx rename to packages/@ant/ink/src/components/NoSelect.tsx diff --git a/src/ink/components/RawAnsi.tsx b/packages/@ant/ink/src/components/RawAnsi.tsx similarity index 100% rename from src/ink/components/RawAnsi.tsx rename to packages/@ant/ink/src/components/RawAnsi.tsx diff --git a/src/ink/components/ScrollBox.tsx b/packages/@ant/ink/src/components/ScrollBox.tsx similarity index 97% rename from src/ink/components/ScrollBox.tsx rename to packages/@ant/ink/src/components/ScrollBox.tsx index c2d432be2..371ab0ab0 100644 --- a/src/ink/components/ScrollBox.tsx +++ b/packages/@ant/ink/src/components/ScrollBox.tsx @@ -6,11 +6,10 @@ import React, { useState, } from 'react' import type { Except } from 'type-fest' -import { markScrollActivity } from '../../bootstrap/state.js' -import type { DOMElement } from '../dom.js' -import { markDirty, scheduleRenderFrom } from '../dom.js' -import { markCommitStart } from '../reconciler.js' -import type { Styles } from '../styles.js' +import type { DOMElement } from '../core/dom.js' +import { markDirty, scheduleRenderFrom } from '../core/dom.js' +import { markCommitStart } from '../core/reconciler.js' +import type { Styles } from '../core/styles.js' import Box from './Box.js' export type ScrollBoxHandle = { @@ -116,7 +115,7 @@ function ScrollBox({ // Signal background intervals (IDE poll, LSP poll, GCS fetch, orphan // check) to skip their next tick — they compete for the event loop and // contributed to 1402ms max frame gaps during scroll drain. - markScrollActivity() + // noop — injected by business layer via onScrollActivity callback markDirty(el) markCommitStart() notify() diff --git a/src/ink/components/Spacer.tsx b/packages/@ant/ink/src/components/Spacer.tsx similarity index 100% rename from src/ink/components/Spacer.tsx rename to packages/@ant/ink/src/components/Spacer.tsx diff --git a/src/ink/components/StdinContext.ts b/packages/@ant/ink/src/components/StdinContext.ts similarity index 92% rename from src/ink/components/StdinContext.ts rename to packages/@ant/ink/src/components/StdinContext.ts index 0b1a49717..34de48e5e 100644 --- a/src/ink/components/StdinContext.ts +++ b/packages/@ant/ink/src/components/StdinContext.ts @@ -1,6 +1,6 @@ import { createContext } from 'react' -import { EventEmitter } from '../events/emitter.js' -import type { TerminalQuerier } from '../terminal-querier.js' +import { EventEmitter } from '../core/events/emitter.js' +import type { TerminalQuerier } from '../core/terminal-querier.js' export type Props = { /** diff --git a/src/ink/components/TerminalFocusContext.tsx b/packages/@ant/ink/src/components/TerminalFocusContext.tsx similarity index 97% rename from src/ink/components/TerminalFocusContext.tsx rename to packages/@ant/ink/src/components/TerminalFocusContext.tsx index 81dbaf60b..e1fca2563 100644 --- a/src/ink/components/TerminalFocusContext.tsx +++ b/packages/@ant/ink/src/components/TerminalFocusContext.tsx @@ -4,7 +4,7 @@ import { getTerminalFocusState, subscribeTerminalFocus, type TerminalFocusState, -} from '../terminal-focus-state.js' +} from '../core/terminal-focus-state.js' export type { TerminalFocusState } diff --git a/src/ink/components/TerminalSizeContext.tsx b/packages/@ant/ink/src/components/TerminalSizeContext.tsx similarity index 100% rename from src/ink/components/TerminalSizeContext.tsx rename to packages/@ant/ink/src/components/TerminalSizeContext.tsx diff --git a/src/ink/components/Text.tsx b/packages/@ant/ink/src/components/Text.tsx similarity index 97% rename from src/ink/components/Text.tsx rename to packages/@ant/ink/src/components/Text.tsx index f2e2bdb77..620881450 100644 --- a/src/ink/components/Text.tsx +++ b/packages/@ant/ink/src/components/Text.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from 'react' import React from 'react' -import type { Color, Styles, TextStyles } from '../styles.js' +import type { Color, Styles, TextStyles } from '../core/styles.js' type BaseProps = { /** diff --git a/src/ink/Ansi.tsx b/packages/@ant/ink/src/core/Ansi.tsx similarity index 98% rename from src/ink/Ansi.tsx rename to packages/@ant/ink/src/core/Ansi.tsx index f6ff7f7de..a8a0999a0 100644 --- a/src/ink/Ansi.tsx +++ b/packages/@ant/ink/src/core/Ansi.tsx @@ -1,6 +1,6 @@ import React from 'react' -import Link from './components/Link.js' -import Text from './components/Text.js' +import Link from '../components/Link.js' +import Text from '../components/Text.js' import type { Color } from './styles.js' import { type NamedColor, diff --git a/src/ink/bidi.ts b/packages/@ant/ink/src/core/bidi.ts similarity index 100% rename from src/ink/bidi.ts rename to packages/@ant/ink/src/core/bidi.ts diff --git a/src/ink/clearTerminal.ts b/packages/@ant/ink/src/core/clearTerminal.ts similarity index 100% rename from src/ink/clearTerminal.ts rename to packages/@ant/ink/src/core/clearTerminal.ts diff --git a/src/ink/colorize.ts b/packages/@ant/ink/src/core/colorize.ts similarity index 100% rename from src/ink/colorize.ts rename to packages/@ant/ink/src/core/colorize.ts diff --git a/src/ink/constants.ts b/packages/@ant/ink/src/core/constants.ts similarity index 100% rename from src/ink/constants.ts rename to packages/@ant/ink/src/core/constants.ts diff --git a/src/ink/cursor.ts b/packages/@ant/ink/src/core/cursor.ts similarity index 100% rename from src/ink/cursor.ts rename to packages/@ant/ink/src/core/cursor.ts diff --git a/src/ink/devtools.ts b/packages/@ant/ink/src/core/devtools.ts similarity index 100% rename from src/ink/devtools.ts rename to packages/@ant/ink/src/core/devtools.ts diff --git a/src/ink/dom.ts b/packages/@ant/ink/src/core/dom.ts similarity index 100% rename from src/ink/dom.ts rename to packages/@ant/ink/src/core/dom.ts diff --git a/src/ink/events/click-event.ts b/packages/@ant/ink/src/core/events/click-event.ts similarity index 100% rename from src/ink/events/click-event.ts rename to packages/@ant/ink/src/core/events/click-event.ts diff --git a/src/ink/events/dispatcher.ts b/packages/@ant/ink/src/core/events/dispatcher.ts similarity index 97% rename from src/ink/events/dispatcher.ts rename to packages/@ant/ink/src/core/events/dispatcher.ts index a310d389e..46466e88f 100644 --- a/src/ink/events/dispatcher.ts +++ b/packages/@ant/ink/src/core/events/dispatcher.ts @@ -4,10 +4,14 @@ import { DiscreteEventPriority, NoEventPriority, } from 'react-reconciler/constants.js' -import { logError } from '../../utils/log.js' import { HANDLER_FOR_EVENT } from './event-handlers.js' import type { EventTarget, TerminalEvent } from './terminal-event.js' +// logError stub — replaced from business dependency; use injected logger in production +const logError = (error: unknown): void => { + console.error(error) +} + // -- type DispatchListener = { diff --git a/src/ink/events/emitter.ts b/packages/@ant/ink/src/core/events/emitter.ts similarity index 100% rename from src/ink/events/emitter.ts rename to packages/@ant/ink/src/core/events/emitter.ts diff --git a/src/ink/events/event-handlers.ts b/packages/@ant/ink/src/core/events/event-handlers.ts similarity index 100% rename from src/ink/events/event-handlers.ts rename to packages/@ant/ink/src/core/events/event-handlers.ts diff --git a/src/ink/events/event.ts b/packages/@ant/ink/src/core/events/event.ts similarity index 100% rename from src/ink/events/event.ts rename to packages/@ant/ink/src/core/events/event.ts diff --git a/src/ink/events/focus-event.ts b/packages/@ant/ink/src/core/events/focus-event.ts similarity index 100% rename from src/ink/events/focus-event.ts rename to packages/@ant/ink/src/core/events/focus-event.ts diff --git a/src/ink/events/input-event.ts b/packages/@ant/ink/src/core/events/input-event.ts similarity index 100% rename from src/ink/events/input-event.ts rename to packages/@ant/ink/src/core/events/input-event.ts diff --git a/src/ink/events/keyboard-event.ts b/packages/@ant/ink/src/core/events/keyboard-event.ts similarity index 100% rename from src/ink/events/keyboard-event.ts rename to packages/@ant/ink/src/core/events/keyboard-event.ts diff --git a/src/ink/events/paste-event.ts b/packages/@ant/ink/src/core/events/paste-event.ts similarity index 100% rename from src/ink/events/paste-event.ts rename to packages/@ant/ink/src/core/events/paste-event.ts diff --git a/src/ink/events/resize-event.ts b/packages/@ant/ink/src/core/events/resize-event.ts similarity index 100% rename from src/ink/events/resize-event.ts rename to packages/@ant/ink/src/core/events/resize-event.ts diff --git a/src/ink/events/terminal-event.ts b/packages/@ant/ink/src/core/events/terminal-event.ts similarity index 100% rename from src/ink/events/terminal-event.ts rename to packages/@ant/ink/src/core/events/terminal-event.ts diff --git a/src/ink/events/terminal-focus-event.ts b/packages/@ant/ink/src/core/events/terminal-focus-event.ts similarity index 100% rename from src/ink/events/terminal-focus-event.ts rename to packages/@ant/ink/src/core/events/terminal-focus-event.ts diff --git a/src/ink/focus.ts b/packages/@ant/ink/src/core/focus.ts similarity index 100% rename from src/ink/focus.ts rename to packages/@ant/ink/src/core/focus.ts diff --git a/src/ink/frame.ts b/packages/@ant/ink/src/core/frame.ts similarity index 100% rename from src/ink/frame.ts rename to packages/@ant/ink/src/core/frame.ts diff --git a/src/ink/get-max-width.ts b/packages/@ant/ink/src/core/get-max-width.ts similarity index 100% rename from src/ink/get-max-width.ts rename to packages/@ant/ink/src/core/get-max-width.ts diff --git a/src/ink/hit-test.ts b/packages/@ant/ink/src/core/hit-test.ts similarity index 100% rename from src/ink/hit-test.ts rename to packages/@ant/ink/src/core/hit-test.ts diff --git a/src/ink/ink.tsx b/packages/@ant/ink/src/core/ink.tsx similarity index 98% rename from src/ink/ink.tsx rename to packages/@ant/ink/src/core/ink.tsx index 65bf32bd3..f18f0ddec 100644 --- a/src/ink/ink.tsx +++ b/packages/@ant/ink/src/core/ink.tsx @@ -12,17 +12,14 @@ import React, { type ReactNode } from 'react' import type { FiberRoot } from 'react-reconciler' import { ConcurrentRoot } from 'react-reconciler/constants.js' import { onExit } from 'signal-exit' -import { flushInteractionTime } from 'src/bootstrap/state.js' -import { getYogaCounters } from 'src/native-ts/yoga-layout/index.js' -import { logForDebugging } from 'src/utils/debug.js' -import { logError } from 'src/utils/log.js' +import { getYogaCounters } from './yoga-layout/index.js' import { format } from 'util' import { colorize } from './colorize.js' -import App from './components/App.js' +import App from '../components/App.js' import type { CursorDeclaration, CursorDeclarationSetter, -} from './components/CursorDeclarationContext.js' +} from '../components/CursorDeclarationContext.js' import { FRAME_INTERVAL_MS } from './constants.js' import * as dom from './dom.js' import { KeyboardEvent } from './events/keyboard-event.js' @@ -116,7 +113,7 @@ import { supportsTabStatus, wrapForMultiplexer, } from './termio/osc.js' -import { TerminalWriteProvider } from './useTerminalNotification.js' +import { TerminalWriteProvider } from '../hooks/useTerminalNotification.js' // Alt-screen: renderer.ts sets cursor.visible = !isTTY || screen.height===0, // which is always false in alt-screen (TTY + content fills screen). @@ -140,6 +137,11 @@ function makeAltScreenParkPatch(terminalRows: number) { }) } +export type Logger = { + debug(message: string, options?: { level?: string }): void + error(error: Error | unknown): void +} + export type Options = { stdout: NodeJS.WriteStream stdin: NodeJS.ReadStream @@ -148,6 +150,16 @@ export type Options = { patchConsole: boolean waitUntilExit?: () => Promise onFrame?: (event: FrameEvent) => void + /** Called before each render cycle. Replaces flushInteractionTime(). */ + onBeforeRender?: () => void + /** Injected logger. Replaces logForDebugging / logError imports. */ + logger?: Logger +} + +/** No-op logger used when no logger is injected. */ +const noopLogger: Logger = { + debug() {}, + error() {}, } export default class Ink { @@ -240,9 +252,11 @@ export default class Ink { // for log-update's relative-move invariants). Alt-screen doesn't need // this — every frame begins with CSI H. null = no move emitted last frame. private displayCursor: { x: number; y: number } | null = null + private readonly logger: Logger constructor(private readonly options: Options) { autoBind(this) + this.logger = options.logger ?? noopLogger if (this.options.patchConsole) { this.restoreConsole = this.patchConsole() @@ -533,7 +547,7 @@ export default class Ink { // Date.now() at most once per frame instead of once per keypress. // Done before the render to avoid dirtying state that would trigger // an extra React re-render cycle. - flushInteractionTime() + this.options.onBeforeRender?.() const renderStart = performance.now() const terminalWidth = this.options.stdout.columns || 80 @@ -754,7 +768,7 @@ export default class Ink { this.rootNode, patch.debug.triggerY, ) - logForDebugging( + this.logger.debug( `[REPAINT] full reset · ${patch.reason} · row ${patch.debug.triggerY}\n` + ` prev: "${patch.debug.prevLine}"\n` + ` next: "${patch.debug.nextLine}"\n` + @@ -1274,7 +1288,7 @@ export default class Ink { // correctly. One extra paint of this message, but correct > fast. dom.markDirty(el) const positions = scanPositions(rendered, this.searchHighlightQuery) - logForDebugging( + this.logger.debug( `scanElementSubtree: q='${this.searchHighlightQuery}' ` + `el=${width}x${height}@(${elLeft},${elTop}) n=${positions.length} ` + `[${positions @@ -1580,7 +1594,7 @@ export default class Ink { // Store and remove all 'readable' event listeners temporarily // This prevents Ink from consuming stdin while the editor is active const readableListeners = stdin.listeners('readable') - logForDebugging( + this.logger.debug( `[stdin] suspendStdin: removing ${readableListeners.length} readable listener(s), wasRawMode=${(stdin as NodeJS.ReadStream & { isRaw?: boolean }).isRaw ?? false}`, ) readableListeners.forEach(listener => { @@ -1610,12 +1624,12 @@ export default class Ink { // Re-attach all the stored listeners if (this.stdinListeners.length === 0 && !this.wasRawMode) { - logForDebugging( + this.logger.debug( '[stdin] resumeStdin: called with no stored listeners and wasRawMode=false (possible desync)', { level: 'warn' }, ) } - logForDebugging( + this.logger.debug( `[stdin] resumeStdin: re-attaching ${this.stdinListeners.length} listener(s), wasRawMode=${this.wasRawMode}`, ) this.stdinListeners.forEach(({ event, listener }) => { @@ -1833,9 +1847,9 @@ export default class Ink { const con = console const originals: Partial> = {} const toDebug = (...args: unknown[]) => - logForDebugging(`console.log: ${format(...args)}`) + this.logger.debug(`console.log: ${format(...args)}`) const toError = (...args: unknown[]) => - logError(new Error(`console.error: ${format(...args)}`)) + this.logger.error(new Error(`console.error: ${format(...args)}`)) for (const m of CONSOLE_STDOUT_METHODS) { originals[m] = con[m] con[m] = toDebug @@ -1873,7 +1887,7 @@ export default class Ink { cb?: (err?: Error) => void, ): boolean => { const callback = typeof encodingOrCb === 'function' ? encodingOrCb : cb - // Reentrancy guard: logForDebugging → writeToStderr → here. Pass + // Reentrancy guard: logger.debug → writeToStderr → here. Pass // through to the original so --debug-to-stderr still works and we // don't stack-overflow. if (reentered) { @@ -1887,7 +1901,7 @@ export default class Ink { typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8') - logForDebugging(`[stderr] ${text}`, { level: 'warn' }) + this.logger.debug(`[stderr] ${text}`, { level: 'warn' }) if (this.altScreenActive && !this.isUnmounted && !this.isPaused) { this.prevFrameContaminated = true this.scheduleRender() diff --git a/src/ink/instances.ts b/packages/@ant/ink/src/core/instances.ts similarity index 100% rename from src/ink/instances.ts rename to packages/@ant/ink/src/core/instances.ts diff --git a/src/ink/layout/engine.ts b/packages/@ant/ink/src/core/layout/engine.ts similarity index 100% rename from src/ink/layout/engine.ts rename to packages/@ant/ink/src/core/layout/engine.ts diff --git a/src/ink/layout/geometry.ts b/packages/@ant/ink/src/core/layout/geometry.ts similarity index 100% rename from src/ink/layout/geometry.ts rename to packages/@ant/ink/src/core/layout/geometry.ts diff --git a/src/ink/layout/node.ts b/packages/@ant/ink/src/core/layout/node.ts similarity index 100% rename from src/ink/layout/node.ts rename to packages/@ant/ink/src/core/layout/node.ts diff --git a/src/ink/layout/yoga.ts b/packages/@ant/ink/src/core/layout/yoga.ts similarity index 99% rename from src/ink/layout/yoga.ts rename to packages/@ant/ink/src/core/layout/yoga.ts index 58f2646fb..1d5729e8c 100644 --- a/src/ink/layout/yoga.ts +++ b/packages/@ant/ink/src/core/layout/yoga.ts @@ -11,7 +11,7 @@ import Yoga, { PositionType, Wrap, type Node as YogaNode, -} from 'src/native-ts/yoga-layout/index.js' +} from '../yoga-layout/index.js' import { type LayoutAlign, LayoutDisplay, diff --git a/src/ink/line-width-cache.ts b/packages/@ant/ink/src/core/line-width-cache.ts similarity index 100% rename from src/ink/line-width-cache.ts rename to packages/@ant/ink/src/core/line-width-cache.ts diff --git a/src/ink/log-update.ts b/packages/@ant/ink/src/core/log-update.ts similarity index 99% rename from src/ink/log-update.ts rename to packages/@ant/ink/src/core/log-update.ts index 4434b9418..210b1e313 100644 --- a/src/ink/log-update.ts +++ b/packages/@ant/ink/src/core/log-update.ts @@ -3,7 +3,8 @@ import { ansiCodesToString, diffAnsiCodes, } from '@alcalzone/ansi-tokenize' -import { logForDebugging } from '../utils/debug.js' +/** Debug logger — no-op placeholder until proper logger injection is added */ +const logForDebugging = (_message: string) => {} import type { Diff, FlickerReason, Frame } from './frame.js' import type { Point } from './layout/geometry.js' import { diff --git a/src/ink/measure-element.ts b/packages/@ant/ink/src/core/measure-element.ts similarity index 100% rename from src/ink/measure-element.ts rename to packages/@ant/ink/src/core/measure-element.ts diff --git a/src/ink/measure-text.ts b/packages/@ant/ink/src/core/measure-text.ts similarity index 100% rename from src/ink/measure-text.ts rename to packages/@ant/ink/src/core/measure-text.ts diff --git a/src/ink/node-cache.ts b/packages/@ant/ink/src/core/node-cache.ts similarity index 100% rename from src/ink/node-cache.ts rename to packages/@ant/ink/src/core/node-cache.ts diff --git a/src/ink/optimizer.ts b/packages/@ant/ink/src/core/optimizer.ts similarity index 100% rename from src/ink/optimizer.ts rename to packages/@ant/ink/src/core/optimizer.ts diff --git a/src/ink/output.ts b/packages/@ant/ink/src/core/output.ts similarity index 99% rename from src/ink/output.ts rename to packages/@ant/ink/src/core/output.ts index 16b5ae27f..edee080c9 100644 --- a/src/ink/output.ts +++ b/packages/@ant/ink/src/core/output.ts @@ -4,9 +4,11 @@ import { styledCharsFromTokens, tokenize, } from '@alcalzone/ansi-tokenize' -import { logForDebugging } from '../utils/debug.js' -import { getGraphemeSegmenter } from '../utils/intl.js' -import sliceAnsi from '../utils/sliceAnsi.js' +import { getGraphemeSegmenter } from './utils/grapheme.js' +import sliceAnsi from './utils/sliceAnsi.js' + +/** Debug logger — no-op placeholder until proper logger injection is added */ +const logForDebugging = (_message: string) => {} import { reorderBidi } from './bidi.js' import { type Rectangle, unionRect } from './layout/geometry.js' import { diff --git a/src/ink/parse-keypress.ts b/packages/@ant/ink/src/core/parse-keypress.ts similarity index 100% rename from src/ink/parse-keypress.ts rename to packages/@ant/ink/src/core/parse-keypress.ts diff --git a/src/ink/reconciler.ts b/packages/@ant/ink/src/core/reconciler.ts similarity index 93% rename from src/ink/reconciler.ts rename to packages/@ant/ink/src/core/reconciler.ts index 831366f7a..4d75923b8 100644 --- a/src/ink/reconciler.ts +++ b/packages/@ant/ink/src/core/reconciler.ts @@ -1,9 +1,7 @@ /* eslint-disable custom-rules/no-top-level-side-effects */ -import { appendFileSync } from 'fs' import createReconciler from 'react-reconciler' -import { getYogaCounters } from 'src/native-ts/yoga-layout/index.js' -import { isEnvTruthy } from '../utils/envUtils.js' +import { getYogaCounters } from './yoga-layout/index.js' import { appendChildNode, clearYogaNodeReferences, @@ -179,15 +177,16 @@ export function getOwnerChain(fiber: unknown): string[] { let debugRepaints: boolean | undefined export function isDebugRepaintsEnabled(): boolean { if (debugRepaints === undefined) { - debugRepaints = isEnvTruthy(process.env.CLAUDE_CODE_DEBUG_REPAINTS) + debugRepaints = process.env.CLAUDE_CODE_DEBUG_REPAINTS === '1' } return debugRepaints } export const dispatcher = new Dispatcher() -// --- COMMIT INSTRUMENTATION (temp debugging) --- -// eslint-disable-next-line custom-rules/no-process-env-top-level -- debug instrumentation, read-once is fine +// --- COMMIT INSTRUMENTATION (debug logging) --- +// Uses console.warn instead of fs.appendFileSync to avoid filesystem dependencies. +// Set CLAUDE_CODE_COMMIT_LOG=1 to enable debug logging to stderr. const COMMIT_LOG = process.env.CLAUDE_CODE_COMMIT_LOG let _commits = 0 let _lastLog = 0 @@ -195,6 +194,12 @@ let _lastCommitAt = 0 let _maxGapMs = 0 let _createCount = 0 let _prepareAt = 0 + +/** Debug log helper — replaces fs.appendFileSync with console.warn. */ +function debugLog(message: string): void { + // biome-ignore lint/suspicious/noConsole: debug instrumentation + console.warn(`[ink-commit] ${message}`) +} // --- END --- // --- SCROLL PROFILING (bench/scroll-e2e.sh reads via getLastYogaMs) --- @@ -255,18 +260,14 @@ const reconciler = createReconciler< _lastCommitAt = now const reconcileMs = _prepareAt > 0 ? now - _prepareAt : 0 if (gap > 30 || reconcileMs > 20 || _createCount > 50) { - // eslint-disable-next-line custom-rules/no-sync-fs -- debug instrumentation - appendFileSync( - COMMIT_LOG, - `${now.toFixed(1)} gap=${gap.toFixed(1)}ms reconcile=${reconcileMs.toFixed(1)}ms creates=${_createCount}\n`, + debugLog( + `${now.toFixed(1)} gap=${gap.toFixed(1)}ms reconcile=${reconcileMs.toFixed(1)}ms creates=${_createCount}`, ) } _createCount = 0 if (now - _lastLog > 1000) { - // eslint-disable-next-line custom-rules/no-sync-fs -- debug instrumentation - appendFileSync( - COMMIT_LOG, - `${now.toFixed(1)} commits=${_commits}/s maxGap=${_maxGapMs.toFixed(1)}ms\n`, + debugLog( + `${now.toFixed(1)} commits=${_commits}/s maxGap=${_maxGapMs.toFixed(1)}ms`, ) _commits = 0 _maxGapMs = 0 @@ -281,10 +282,8 @@ const reconciler = createReconciler< const layoutMs = performance.now() - _t0 if (layoutMs > 20) { const c = getYogaCounters() - // eslint-disable-next-line custom-rules/no-sync-fs -- debug instrumentation - appendFileSync( - COMMIT_LOG, - `${_t0.toFixed(1)} SLOW_YOGA ${layoutMs.toFixed(1)}ms visited=${c.visited} measured=${c.measured} hits=${c.cacheHits} live=${c.live}\n`, + debugLog( + `${_t0.toFixed(1)} SLOW_YOGA ${layoutMs.toFixed(1)}ms visited=${c.visited} measured=${c.measured} hits=${c.cacheHits} live=${c.live}`, ) } } @@ -305,10 +304,8 @@ const reconciler = createReconciler< if (COMMIT_LOG) { const renderMs = performance.now() - _tr if (renderMs > 10) { - // eslint-disable-next-line custom-rules/no-sync-fs -- debug instrumentation - appendFileSync( - COMMIT_LOG, - `${_tr.toFixed(1)} SLOW_PAINT ${renderMs.toFixed(1)}ms\n`, + debugLog( + `${_tr.toFixed(1)} SLOW_PAINT ${renderMs.toFixed(1)}ms`, ) } } diff --git a/src/ink/render-border.ts b/packages/@ant/ink/src/core/render-border.ts similarity index 100% rename from src/ink/render-border.ts rename to packages/@ant/ink/src/core/render-border.ts diff --git a/src/ink/render-node-to-output.ts b/packages/@ant/ink/src/core/render-node-to-output.ts similarity index 100% rename from src/ink/render-node-to-output.ts rename to packages/@ant/ink/src/core/render-node-to-output.ts diff --git a/src/ink/render-to-screen.ts b/packages/@ant/ink/src/core/render-to-screen.ts similarity index 98% rename from src/ink/render-to-screen.ts rename to packages/@ant/ink/src/core/render-to-screen.ts index 0992dc946..dcf6407c6 100644 --- a/src/ink/render-to-screen.ts +++ b/packages/@ant/ink/src/core/render-to-screen.ts @@ -1,7 +1,8 @@ import noop from 'lodash-es/noop.js' import type { ReactElement } from 'react' import { LegacyRoot } from 'react-reconciler/constants.js' -import { logForDebugging } from '../utils/debug.js' +/** Debug logger — no-op placeholder until proper logger injection is added */ +const logForDebugging = (_message: string) => {} import { createNode, type DOMElement } from './dom.js' import { FocusManager } from './focus.js' import Output from './output.js' diff --git a/src/ink/renderer.ts b/packages/@ant/ink/src/core/renderer.ts similarity index 97% rename from src/ink/renderer.ts rename to packages/@ant/ink/src/core/renderer.ts index d87fb3db9..aa2cdeaa3 100644 --- a/src/ink/renderer.ts +++ b/packages/@ant/ink/src/core/renderer.ts @@ -1,4 +1,5 @@ -import { logForDebugging } from 'src/utils/debug.js' +/** Debug logger — no-op placeholder until proper logger injection is added */ +const logForDebugging = (_message: string, _opts?: { level?: string }) => {} import { type DOMElement, markDirty } from './dom.js' import type { Frame } from './frame.js' import { consumeAbsoluteRemovedFlag } from './node-cache.js' diff --git a/src/ink/root.ts b/packages/@ant/ink/src/core/root.ts similarity index 94% rename from src/ink/root.ts rename to packages/@ant/ink/src/core/root.ts index 067bbd496..36df98c70 100644 --- a/src/ink/root.ts +++ b/packages/@ant/ink/src/core/root.ts @@ -1,5 +1,4 @@ import type { ReactNode } from 'react' -import { logForDebugging } from 'src/utils/debug.js' import { Stream } from 'stream' import type { FrameEvent } from './frame.js' import Ink, { type Options as InkOptions } from './ink.js' @@ -114,9 +113,12 @@ const wrappedRender = async ( // write overwrites scrollback instead of appending below the logo. await Promise.resolve() const instance = renderSync(node, options) - logForDebugging( - `[render] first ink render: ${Math.round(process.uptime() * 1000)}ms since process start`, - ) + if (process.env.CLAUDE_CODE_DEBUG_REPAINTS === '1') { + // biome-ignore lint/suspicious/noConsole: debug instrumentation + console.warn( + `[render] first ink render: ${Math.round(process.uptime() * 1000)}ms since process start`, + ) + } return instance } diff --git a/src/ink/screen.ts b/packages/@ant/ink/src/core/screen.ts similarity index 100% rename from src/ink/screen.ts rename to packages/@ant/ink/src/core/screen.ts diff --git a/src/ink/searchHighlight.ts b/packages/@ant/ink/src/core/searchHighlight.ts similarity index 100% rename from src/ink/searchHighlight.ts rename to packages/@ant/ink/src/core/searchHighlight.ts diff --git a/src/ink/selection.ts b/packages/@ant/ink/src/core/selection.ts similarity index 100% rename from src/ink/selection.ts rename to packages/@ant/ink/src/core/selection.ts diff --git a/src/ink/squash-text-nodes.ts b/packages/@ant/ink/src/core/squash-text-nodes.ts similarity index 100% rename from src/ink/squash-text-nodes.ts rename to packages/@ant/ink/src/core/squash-text-nodes.ts diff --git a/src/ink/stringWidth.ts b/packages/@ant/ink/src/core/stringWidth.ts similarity index 99% rename from src/ink/stringWidth.ts rename to packages/@ant/ink/src/core/stringWidth.ts index 83f7bcb5e..2c3fa13b8 100644 --- a/src/ink/stringWidth.ts +++ b/packages/@ant/ink/src/core/stringWidth.ts @@ -1,7 +1,7 @@ import emojiRegex from 'emoji-regex' import { eastAsianWidth } from 'get-east-asian-width' import stripAnsi from 'strip-ansi' -import { getGraphemeSegmenter } from '../utils/intl.js' +import { getGraphemeSegmenter } from './utils/grapheme.js' const EMOJI_REGEX = emojiRegex() diff --git a/src/ink/styles.ts b/packages/@ant/ink/src/core/styles.ts similarity index 100% rename from src/ink/styles.ts rename to packages/@ant/ink/src/core/styles.ts diff --git a/src/ink/supports-hyperlinks.ts b/packages/@ant/ink/src/core/supports-hyperlinks.ts similarity index 100% rename from src/ink/supports-hyperlinks.ts rename to packages/@ant/ink/src/core/supports-hyperlinks.ts diff --git a/src/ink/tabstops.ts b/packages/@ant/ink/src/core/tabstops.ts similarity index 100% rename from src/ink/tabstops.ts rename to packages/@ant/ink/src/core/tabstops.ts diff --git a/src/ink/terminal-focus-state.ts b/packages/@ant/ink/src/core/terminal-focus-state.ts similarity index 100% rename from src/ink/terminal-focus-state.ts rename to packages/@ant/ink/src/core/terminal-focus-state.ts diff --git a/src/ink/terminal-querier.ts b/packages/@ant/ink/src/core/terminal-querier.ts similarity index 100% rename from src/ink/terminal-querier.ts rename to packages/@ant/ink/src/core/terminal-querier.ts diff --git a/src/ink/terminal.ts b/packages/@ant/ink/src/core/terminal.ts similarity index 97% rename from src/ink/terminal.ts rename to packages/@ant/ink/src/core/terminal.ts index 2aad947d7..1331376c3 100644 --- a/src/ink/terminal.ts +++ b/packages/@ant/ink/src/core/terminal.ts @@ -1,7 +1,5 @@ -import { coerce } from 'semver' +import { coerce, gte } from 'semver' import type { Writable } from 'stream' -import { env } from '../utils/env.js' -import { gte } from '../utils/semver.js' import { getClearTerminalSequence } from './clearTerminal.js' import type { Diff } from './frame.js' import { cursorMove, cursorTo, eraseLines } from './termio/csi.js' @@ -165,7 +163,7 @@ const EXTENDED_KEYS_TERMINALS = [ /** True if this terminal correctly handles extended key reporting * (Kitty keyboard protocol + xterm modifyOtherKeys). */ export function supportsExtendedKeys(): boolean { - return EXTENDED_KEYS_TERMINALS.includes(env.terminal ?? '') + return EXTENDED_KEYS_TERMINALS.includes(process.env.TERM_PROGRAM ?? '') } /** True if the terminal scrolls the viewport when it receives cursor-up diff --git a/src/ink/termio.ts b/packages/@ant/ink/src/core/termio.ts similarity index 100% rename from src/ink/termio.ts rename to packages/@ant/ink/src/core/termio.ts diff --git a/src/ink/termio/ansi.ts b/packages/@ant/ink/src/core/termio/ansi.ts similarity index 100% rename from src/ink/termio/ansi.ts rename to packages/@ant/ink/src/core/termio/ansi.ts diff --git a/src/ink/termio/csi.ts b/packages/@ant/ink/src/core/termio/csi.ts similarity index 100% rename from src/ink/termio/csi.ts rename to packages/@ant/ink/src/core/termio/csi.ts diff --git a/src/ink/termio/dec.ts b/packages/@ant/ink/src/core/termio/dec.ts similarity index 100% rename from src/ink/termio/dec.ts rename to packages/@ant/ink/src/core/termio/dec.ts diff --git a/src/ink/termio/esc.ts b/packages/@ant/ink/src/core/termio/esc.ts similarity index 100% rename from src/ink/termio/esc.ts rename to packages/@ant/ink/src/core/termio/esc.ts diff --git a/src/ink/termio/osc.ts b/packages/@ant/ink/src/core/termio/osc.ts similarity index 95% rename from src/ink/termio/osc.ts rename to packages/@ant/ink/src/core/termio/osc.ts index 9bef51532..f5e6f3712 100644 --- a/src/ink/termio/osc.ts +++ b/packages/@ant/ink/src/core/termio/osc.ts @@ -3,9 +3,26 @@ */ import { Buffer } from 'buffer' -import { env } from '../../utils/env.js' -import { execFileNoThrow } from '../../utils/execFileNoThrow.js' +import { execFile as nodeExecFile } from 'child_process' import { BEL, ESC, ESC_TYPE, SEP } from './ansi.js' + +/** Promise-based wrapper around child_process.execFile */ +function execFileNoThrow( + command: string, + args: string[], + options: { input?: string; useCwd?: boolean; timeout?: number } = {}, +): Promise<{ code: number; stdout: string; stderr: string }> { + return new Promise(resolve => { + const { input, timeout } = options + const proc = nodeExecFile(command, args, { timeout }, (error, stdout, stderr) => { + resolve({ code: error ? 1 : 0, stdout: stdout ?? '', stderr: stderr ?? '' }) + }) + if (input && proc.stdin) { + proc.stdin.write(input) + proc.stdin.end() + } + }) +} import type { Action, Color, TabStatusAction } from './types.js' export const OSC_PREFIX = ESC + String.fromCharCode(ESC_TYPE.OSC) @@ -16,7 +33,7 @@ export const ST = ESC + '\\' /** Generate an OSC sequence: ESC ] p1;p2;...;pN * Uses ST terminator for Kitty (avoids beeps), BEL for others */ export function osc(...parts: (string | number)[]): string { - const terminator = env.terminal === 'kitty' ? ST : BEL + const terminator = process.env.TERM_PROGRAM === 'kitty' ? ST : BEL return `${OSC_PREFIX}${parts.join(SEP)}${terminator}` } diff --git a/src/ink/termio/parser.ts b/packages/@ant/ink/src/core/termio/parser.ts similarity index 99% rename from src/ink/termio/parser.ts rename to packages/@ant/ink/src/core/termio/parser.ts index 301f14c5a..0ae9e7fa5 100644 --- a/src/ink/termio/parser.ts +++ b/packages/@ant/ink/src/core/termio/parser.ts @@ -11,7 +11,7 @@ * - Style tracking: maintains current text style state */ -import { getGraphemeSegmenter } from '../../utils/intl.js' +import { getGraphemeSegmenter } from '../utils/grapheme.js' import { C0 } from './ansi.js' import { CSI, CURSOR_STYLES, ERASE_DISPLAY, ERASE_LINE_REGION } from './csi.js' import { DEC } from './dec.js' diff --git a/src/ink/termio/sgr.ts b/packages/@ant/ink/src/core/termio/sgr.ts similarity index 100% rename from src/ink/termio/sgr.ts rename to packages/@ant/ink/src/core/termio/sgr.ts diff --git a/src/ink/termio/tokenize.ts b/packages/@ant/ink/src/core/termio/tokenize.ts similarity index 100% rename from src/ink/termio/tokenize.ts rename to packages/@ant/ink/src/core/termio/tokenize.ts diff --git a/src/ink/termio/types.ts b/packages/@ant/ink/src/core/termio/types.ts similarity index 100% rename from src/ink/termio/types.ts rename to packages/@ant/ink/src/core/termio/types.ts diff --git a/packages/@ant/ink/src/core/utils/grapheme.ts b/packages/@ant/ink/src/core/utils/grapheme.ts new file mode 100644 index 000000000..e010dc664 --- /dev/null +++ b/packages/@ant/ink/src/core/utils/grapheme.ts @@ -0,0 +1,53 @@ +/** + * 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 +} diff --git a/packages/@ant/ink/src/core/utils/sliceAnsi.ts b/packages/@ant/ink/src/core/utils/sliceAnsi.ts new file mode 100644 index 000000000..42abe7ac7 --- /dev/null +++ b/packages/@ant/ink/src/core/utils/sliceAnsi.ts @@ -0,0 +1,97 @@ +/** + * 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 +} diff --git a/src/ink/warn.ts b/packages/@ant/ink/src/core/warn.ts similarity index 100% rename from src/ink/warn.ts rename to packages/@ant/ink/src/core/warn.ts diff --git a/src/ink/widest-line.ts b/packages/@ant/ink/src/core/widest-line.ts similarity index 100% rename from src/ink/widest-line.ts rename to packages/@ant/ink/src/core/widest-line.ts diff --git a/src/ink/wrap-text.ts b/packages/@ant/ink/src/core/wrap-text.ts similarity index 97% rename from src/ink/wrap-text.ts rename to packages/@ant/ink/src/core/wrap-text.ts index 434412cc9..b97f64235 100644 --- a/src/ink/wrap-text.ts +++ b/packages/@ant/ink/src/core/wrap-text.ts @@ -1,4 +1,4 @@ -import sliceAnsi from '../utils/sliceAnsi.js' +import sliceAnsi from './utils/sliceAnsi.js' import { stringWidth } from './stringWidth.js' import type { Styles } from './styles.js' import { wrapAnsi } from './wrapAnsi.js' diff --git a/src/ink/wrapAnsi.ts b/packages/@ant/ink/src/core/wrapAnsi.ts similarity index 100% rename from src/ink/wrapAnsi.ts rename to packages/@ant/ink/src/core/wrapAnsi.ts diff --git a/packages/@ant/ink/src/core/yoga-layout/enums.ts b/packages/@ant/ink/src/core/yoga-layout/enums.ts new file mode 100644 index 000000000..8cbb6ecff --- /dev/null +++ b/packages/@ant/ink/src/core/yoga-layout/enums.ts @@ -0,0 +1,134 @@ +/** + * 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] diff --git a/packages/@ant/ink/src/core/yoga-layout/index.ts b/packages/@ant/ink/src/core/yoga-layout/index.ts new file mode 100644 index 000000000..49b9602be --- /dev/null +++ b/packages/@ant/ink/src/core/yoga-layout/index.ts @@ -0,0 +1,2578 @@ +/** + * Pure-TypeScript port of yoga-layout (Meta's flexbox engine). + * + * This matches the `yoga-layout/load` API surface used by src/ink/layout/yoga.ts. + * The upstream C++ source is ~2500 lines in CalculateLayout.cpp alone; this port + * is a simplified single-pass flexbox implementation that covers the subset of + * features Ink actually uses: + * - flex-direction (row/column + reverse) + * - flex-grow / flex-shrink / flex-basis + * - align-items / align-self (stretch, flex-start, center, flex-end) + * - justify-content (all six values) + * - margin / padding / border / gap + * - width / height / min / max (point, percent, auto) + * - position: relative / absolute + * - display: flex / none + * - measure functions (for text nodes) + * + * Also implemented for spec parity (not used by Ink): + * - margin: auto (main + cross axis, overrides justify/align) + * - multi-pass flex clamping when children hit min/max constraints + * - flex-grow/shrink against container min/max when size is indefinite + * + * Also implemented for spec parity (not used by Ink): + * - flex-wrap: wrap / wrap-reverse (multi-line flex) + * - align-content (positions wrapped lines on cross axis) + * + * Also implemented for spec parity (not used by Ink): + * - display: contents (children lifted to grandparent, box removed) + * + * Also implemented for spec parity (not used by Ink): + * - baseline alignment (align-items/align-self: baseline) + * + * Not implemented (not used by Ink): + * - aspect-ratio + * - box-sizing: content-box + * - RTL direction (Ink always passes Direction.LTR) + * + * Upstream: https://github.com/facebook/yoga + */ + +import { + Align, + BoxSizing, + Dimension, + Direction, + Display, + Edge, + Errata, + ExperimentalFeature, + FlexDirection, + Gutter, + Justify, + MeasureMode, + Overflow, + PositionType, + Unit, + Wrap, +} from './enums.js' + +export { + Align, + BoxSizing, + Dimension, + Direction, + Display, + Edge, + Errata, + ExperimentalFeature, + FlexDirection, + Gutter, + Justify, + MeasureMode, + Overflow, + PositionType, + Unit, + Wrap, +} + +// -- +// Value types + +export type Value = { + unit: Unit + value: number +} + +const UNDEFINED_VALUE: Value = { unit: Unit.Undefined, value: NaN } +const AUTO_VALUE: Value = { unit: Unit.Auto, value: NaN } + +function pointValue(v: number): Value { + return { unit: Unit.Point, value: v } +} +function percentValue(v: number): Value { + return { unit: Unit.Percent, value: v } +} + +function resolveValue(v: Value, ownerSize: number): number { + switch (v.unit) { + case Unit.Point: + return v.value + case Unit.Percent: + return isNaN(ownerSize) ? NaN : (v.value * ownerSize) / 100 + default: + return NaN + } +} + +function isDefined(n: number): boolean { + return !isNaN(n) +} + +// NaN-safe equality for layout-cache input comparison +function sameFloat(a: number, b: number): boolean { + return a === b || (a !== a && b !== b) +} + +// -- +// Layout result (computed values) + +type Layout = { + left: number + top: number + width: number + height: number + // Computed per-edge values (resolved to physical edges) + border: [number, number, number, number] // left, top, right, bottom + padding: [number, number, number, number] + margin: [number, number, number, number] +} + +// -- +// Style (input values) + +type Style = { + direction: Direction + flexDirection: FlexDirection + justifyContent: Justify + alignItems: Align + alignSelf: Align + alignContent: Align + flexWrap: Wrap + overflow: Overflow + display: Display + positionType: PositionType + + flexGrow: number + flexShrink: number + flexBasis: Value + + // 9-edge arrays indexed by Edge enum + margin: Value[] + padding: Value[] + border: Value[] + position: Value[] + + // 3-gutter array indexed by Gutter enum + gap: Value[] + + width: Value + height: Value + minWidth: Value + minHeight: Value + maxWidth: Value + maxHeight: Value +} + +function defaultStyle(): Style { + return { + direction: Direction.Inherit, + flexDirection: FlexDirection.Column, + justifyContent: Justify.FlexStart, + alignItems: Align.Stretch, + alignSelf: Align.Auto, + alignContent: Align.FlexStart, + flexWrap: Wrap.NoWrap, + overflow: Overflow.Visible, + display: Display.Flex, + positionType: PositionType.Relative, + flexGrow: 0, + flexShrink: 0, + flexBasis: AUTO_VALUE, + margin: new Array(9).fill(UNDEFINED_VALUE), + padding: new Array(9).fill(UNDEFINED_VALUE), + border: new Array(9).fill(UNDEFINED_VALUE), + position: new Array(9).fill(UNDEFINED_VALUE), + gap: new Array(3).fill(UNDEFINED_VALUE), + width: AUTO_VALUE, + height: AUTO_VALUE, + minWidth: UNDEFINED_VALUE, + minHeight: UNDEFINED_VALUE, + maxWidth: UNDEFINED_VALUE, + maxHeight: UNDEFINED_VALUE, + } +} + +// -- +// Edge resolution — yoga's 9-edge model collapsed to 4 physical edges + +const EDGE_LEFT = 0 +const EDGE_TOP = 1 +const EDGE_RIGHT = 2 +const EDGE_BOTTOM = 3 + +function resolveEdge( + edges: Value[], + physicalEdge: number, + ownerSize: number, + // For margin/position we allow auto; for padding/border auto resolves to 0 + allowAuto = false, +): number { + // Precedence: specific edge > horizontal/vertical > all + let v = edges[physicalEdge]! + if (v.unit === Unit.Undefined) { + if (physicalEdge === EDGE_LEFT || physicalEdge === EDGE_RIGHT) { + v = edges[Edge.Horizontal]! + } else { + v = edges[Edge.Vertical]! + } + } + if (v.unit === Unit.Undefined) { + v = edges[Edge.All]! + } + // Start/End map to Left/Right for LTR (Ink is always LTR) + if (v.unit === Unit.Undefined) { + if (physicalEdge === EDGE_LEFT) v = edges[Edge.Start]! + if (physicalEdge === EDGE_RIGHT) v = edges[Edge.End]! + } + if (v.unit === Unit.Undefined) return 0 + if (v.unit === Unit.Auto) return allowAuto ? NaN : 0 + return resolveValue(v, ownerSize) +} + +function resolveEdgeRaw(edges: Value[], physicalEdge: number): Value { + let v = edges[physicalEdge]! + if (v.unit === Unit.Undefined) { + if (physicalEdge === EDGE_LEFT || physicalEdge === EDGE_RIGHT) { + v = edges[Edge.Horizontal]! + } else { + v = edges[Edge.Vertical]! + } + } + if (v.unit === Unit.Undefined) v = edges[Edge.All]! + if (v.unit === Unit.Undefined) { + if (physicalEdge === EDGE_LEFT) v = edges[Edge.Start]! + if (physicalEdge === EDGE_RIGHT) v = edges[Edge.End]! + } + return v +} + +function isMarginAuto(edges: Value[], physicalEdge: number): boolean { + return resolveEdgeRaw(edges, physicalEdge).unit === Unit.Auto +} + +// Setter helpers for the _hasAutoMargin / _hasPosition fast-path flags. +// Unit.Undefined = 0, Unit.Auto = 3. +function hasAnyAutoEdge(edges: Value[]): boolean { + for (let i = 0; i < 9; i++) if (edges[i]!.unit === 3) return true + return false +} +function hasAnyDefinedEdge(edges: Value[]): boolean { + for (let i = 0; i < 9; i++) if (edges[i]!.unit !== 0) return true + return false +} + +// Hot path: resolve all 4 physical edges in one pass, writing into `out`. +// Equivalent to calling resolveEdge() 4× with allowAuto=false, but hoists the +// shared fallback lookups (Horizontal/Vertical/All/Start/End) and avoids +// allocating a fresh 4-array on every layoutNode() call. +function resolveEdges4Into( + edges: Value[], + ownerSize: number, + out: [number, number, number, number], +): void { + // Hoist fallbacks once — the 4 per-edge chains share these reads. + const eH = edges[6]! // Edge.Horizontal + const eV = edges[7]! // Edge.Vertical + const eA = edges[8]! // Edge.All + const eS = edges[4]! // Edge.Start + const eE = edges[5]! // Edge.End + const pctDenom = isNaN(ownerSize) ? NaN : ownerSize / 100 + + // Left: edges[0] → Horizontal → All → Start + let v = edges[0]! + if (v.unit === 0) v = eH + if (v.unit === 0) v = eA + if (v.unit === 0) v = eS + out[0] = v.unit === 1 ? v.value : v.unit === 2 ? v.value * pctDenom : 0 + + // Top: edges[1] → Vertical → All + v = edges[1]! + if (v.unit === 0) v = eV + if (v.unit === 0) v = eA + out[1] = v.unit === 1 ? v.value : v.unit === 2 ? v.value * pctDenom : 0 + + // Right: edges[2] → Horizontal → All → End + v = edges[2]! + if (v.unit === 0) v = eH + if (v.unit === 0) v = eA + if (v.unit === 0) v = eE + out[2] = v.unit === 1 ? v.value : v.unit === 2 ? v.value * pctDenom : 0 + + // Bottom: edges[3] → Vertical → All + v = edges[3]! + if (v.unit === 0) v = eV + if (v.unit === 0) v = eA + out[3] = v.unit === 1 ? v.value : v.unit === 2 ? v.value * pctDenom : 0 +} + +// -- +// Axis helpers + +function isRow(dir: FlexDirection): boolean { + return dir === FlexDirection.Row || dir === FlexDirection.RowReverse +} +function isReverse(dir: FlexDirection): boolean { + return dir === FlexDirection.RowReverse || dir === FlexDirection.ColumnReverse +} +function crossAxis(dir: FlexDirection): FlexDirection { + return isRow(dir) ? FlexDirection.Column : FlexDirection.Row +} +function leadingEdge(dir: FlexDirection): number { + switch (dir) { + case FlexDirection.Row: + return EDGE_LEFT + case FlexDirection.RowReverse: + return EDGE_RIGHT + case FlexDirection.Column: + return EDGE_TOP + case FlexDirection.ColumnReverse: + return EDGE_BOTTOM + } +} +function trailingEdge(dir: FlexDirection): number { + switch (dir) { + case FlexDirection.Row: + return EDGE_RIGHT + case FlexDirection.RowReverse: + return EDGE_LEFT + case FlexDirection.Column: + return EDGE_BOTTOM + case FlexDirection.ColumnReverse: + return EDGE_TOP + } +} + +// -- +// Public types + +export type MeasureFunction = ( + width: number, + widthMode: MeasureMode, + height: number, + heightMode: MeasureMode, +) => { width: number; height: number } + +export type Size = { width: number; height: number } + +// -- +// Config + +export type Config = { + pointScaleFactor: number + errata: Errata + useWebDefaults: boolean + free(): void + isExperimentalFeatureEnabled(_: ExperimentalFeature): boolean + setExperimentalFeatureEnabled(_: ExperimentalFeature, __: boolean): void + setPointScaleFactor(factor: number): void + getErrata(): Errata + setErrata(errata: Errata): void + setUseWebDefaults(v: boolean): void +} + +function createConfig(): Config { + const config: Config = { + pointScaleFactor: 1, + errata: Errata.None, + useWebDefaults: false, + free() {}, + isExperimentalFeatureEnabled() { + return false + }, + setExperimentalFeatureEnabled() {}, + setPointScaleFactor(f) { + config.pointScaleFactor = f + }, + getErrata() { + return config.errata + }, + setErrata(e) { + config.errata = e + }, + setUseWebDefaults(v) { + config.useWebDefaults = v + }, + } + return config +} + +// -- +// Node implementation + +export class Node { + style: Style + layout: Layout + parent: Node | null + children: Node[] + measureFunc: MeasureFunction | null + config: Config + isDirty_: boolean + isReferenceBaseline_: boolean + + // Per-layout scratch (not public API) + _flexBasis = 0 + _mainSize = 0 + _crossSize = 0 + _lineIndex = 0 + // Fast-path flags maintained by style setters. Per CPU profile, the + // positioning loop calls isMarginAuto 6× and resolveEdgeRaw(position) 4× + // per child per layout pass — ~11k calls for the 1000-node bench, nearly + // all of which return false/undefined since most nodes have no auto + // margins and no position insets. These flags let us skip straight to + // the common case with a single branch. + _hasAutoMargin = false + _hasPosition = false + // Same pattern for the 3× resolveEdges4Into calls at the top of every + // layoutNode(). In the 1000-node bench ~67% of those calls operate on + // all-undefined edge arrays (most nodes have no border; only cols have + // padding; only leaf cells have margin) — a single-branch skip beats + // ~20 property reads + ~15 compares + 4 writes of zeros. + _hasPadding = false + _hasBorder = false + _hasMargin = false + // -- Dirty-flag layout cache. Mirrors upstream CalculateLayout.cpp's + // layoutNodeInternal: skip a subtree entirely when it's clean and we're + // asking the same question we cached the answer to. Two slots since + // each node typically sees a measure call (performLayout=false, from + // computeFlexBasis) followed by a layout call (performLayout=true) with + // different inputs per parent pass — a single slot thrashes. Re-layout + // bench (dirty one leaf, recompute root) went 2.7x→1.1x with this: + // clean siblings skip straight through, only the dirty chain recomputes. + _lW = NaN + _lH = NaN + _lWM: MeasureMode = 0 + _lHM: MeasureMode = 0 + _lOW = NaN + _lOH = NaN + _lFW = false + _lFH = false + // _hasL stores INPUTS early (before compute) but layout.width/height are + // mutated by the multi-entry cache and by subsequent compute calls with + // different inputs. Without storing OUTPUTS, a _hasL hit returns whatever + // layout.width/height happened to be left by the last call — the scrollbox + // vpH=33→2624 bug. Store + restore outputs like the multi-entry cache does. + _lOutW = NaN + _lOutH = NaN + _hasL = false + _mW = NaN + _mH = NaN + _mWM: MeasureMode = 0 + _mHM: MeasureMode = 0 + _mOW = NaN + _mOH = NaN + _mOutW = NaN + _mOutH = NaN + _hasM = false + // Cached computeFlexBasis result. For clean children, basis only depends + // on the container's inner dimensions — if those haven't changed, skip the + // layoutNode(performLayout=false) recursion entirely. This is the hot path + // for scroll: 500-message content container is dirty, its 499 clean + // children each get measured ~20× as the dirty chain's measure/layout + // passes cascade. Basis cache short-circuits at the child boundary. + _fbBasis = NaN + _fbOwnerW = NaN + _fbOwnerH = NaN + _fbAvailMain = NaN + _fbAvailCross = NaN + _fbCrossMode: MeasureMode = 0 + // Generation at which _fbBasis was written. Dirty nodes from a PREVIOUS + // generation have stale cache (subtree changed), but within the SAME + // generation the cache is fresh — the dirty chain's measure→layout + // cascade invokes computeFlexBasis ≥2^depth times per calculateLayout on + // fresh-mounted items, and the subtree doesn't change between calls. + // Gating on generation instead of isDirty_ lets fresh mounts (virtual + // scroll) cache-hit after first compute: 105k visits → ~10k. + _fbGen = -1 + // Multi-entry layout cache — stores (inputs → computed w,h) so hits with + // different inputs than _hasL can restore the right dimensions. Upstream + // yoga uses 16; 4 covers Ink's dirty-chain depth. Packed as flat arrays + // to avoid per-entry object allocs. Slot i uses indices [i*8, i*8+8) in + // _cIn (aW,aH,wM,hM,oW,oH,fW,fH) and [i*2, i*2+2) in _cOut (w,h). + _cIn: Float64Array | null = null + _cOut: Float64Array | null = null + _cGen = -1 + _cN = 0 + _cWr = 0 + + constructor(config?: Config) { + this.style = defaultStyle() + this.layout = { + left: 0, + top: 0, + width: 0, + height: 0, + border: [0, 0, 0, 0], + padding: [0, 0, 0, 0], + margin: [0, 0, 0, 0], + } + this.parent = null + this.children = [] + this.measureFunc = null + this.config = config ?? DEFAULT_CONFIG + this.isDirty_ = true + this.isReferenceBaseline_ = false + _yogaLiveNodes++ + } + + // -- Tree + + insertChild(child: Node, index: number): void { + child.parent = this + this.children.splice(index, 0, child) + this.markDirty() + } + removeChild(child: Node): void { + const idx = this.children.indexOf(child) + if (idx >= 0) { + this.children.splice(idx, 1) + child.parent = null + this.markDirty() + } + } + getChild(index: number): Node { + return this.children[index]! + } + getChildCount(): number { + return this.children.length + } + getParent(): Node | null { + return this.parent + } + + // -- Lifecycle + + free(): void { + this.parent = null + this.children = [] + this.measureFunc = null + this._cIn = null + this._cOut = null + _yogaLiveNodes-- + } + freeRecursive(): void { + for (const c of this.children) c.freeRecursive() + this.free() + } + reset(): void { + this.style = defaultStyle() + this.children = [] + this.parent = null + this.measureFunc = null + this.isDirty_ = true + this._hasAutoMargin = false + this._hasPosition = false + this._hasPadding = false + this._hasBorder = false + this._hasMargin = false + this._hasL = false + this._hasM = false + this._cN = 0 + this._cWr = 0 + this._fbBasis = NaN + } + + // -- Dirty tracking + + markDirty(): void { + this.isDirty_ = true + if (this.parent && !this.parent.isDirty_) this.parent.markDirty() + } + isDirty(): boolean { + return this.isDirty_ + } + hasNewLayout(): boolean { + return true + } + markLayoutSeen(): void {} + + // -- Measure function + + setMeasureFunc(fn: MeasureFunction | null): void { + this.measureFunc = fn + this.markDirty() + } + unsetMeasureFunc(): void { + this.measureFunc = null + this.markDirty() + } + + // -- Computed layout getters + + getComputedLeft(): number { + return this.layout.left + } + getComputedTop(): number { + return this.layout.top + } + getComputedWidth(): number { + return this.layout.width + } + getComputedHeight(): number { + return this.layout.height + } + getComputedRight(): number { + const p = this.parent + return p ? p.layout.width - this.layout.left - this.layout.width : 0 + } + getComputedBottom(): number { + const p = this.parent + return p ? p.layout.height - this.layout.top - this.layout.height : 0 + } + getComputedLayout(): { + left: number + top: number + right: number + bottom: number + width: number + height: number + } { + return { + left: this.layout.left, + top: this.layout.top, + right: this.getComputedRight(), + bottom: this.getComputedBottom(), + width: this.layout.width, + height: this.layout.height, + } + } + getComputedBorder(edge: Edge): number { + return this.layout.border[physicalEdge(edge)]! + } + getComputedPadding(edge: Edge): number { + return this.layout.padding[physicalEdge(edge)]! + } + getComputedMargin(edge: Edge): number { + return this.layout.margin[physicalEdge(edge)]! + } + + // -- Style setters: dimensions + + setWidth(v: number | 'auto' | string | undefined): void { + this.style.width = parseDimension(v) + this.markDirty() + } + setWidthPercent(v: number): void { + this.style.width = percentValue(v) + this.markDirty() + } + setWidthAuto(): void { + this.style.width = AUTO_VALUE + this.markDirty() + } + setHeight(v: number | 'auto' | string | undefined): void { + this.style.height = parseDimension(v) + this.markDirty() + } + setHeightPercent(v: number): void { + this.style.height = percentValue(v) + this.markDirty() + } + setHeightAuto(): void { + this.style.height = AUTO_VALUE + this.markDirty() + } + setMinWidth(v: number | string | undefined): void { + this.style.minWidth = parseDimension(v) + this.markDirty() + } + setMinWidthPercent(v: number): void { + this.style.minWidth = percentValue(v) + this.markDirty() + } + setMinHeight(v: number | string | undefined): void { + this.style.minHeight = parseDimension(v) + this.markDirty() + } + setMinHeightPercent(v: number): void { + this.style.minHeight = percentValue(v) + this.markDirty() + } + setMaxWidth(v: number | string | undefined): void { + this.style.maxWidth = parseDimension(v) + this.markDirty() + } + setMaxWidthPercent(v: number): void { + this.style.maxWidth = percentValue(v) + this.markDirty() + } + setMaxHeight(v: number | string | undefined): void { + this.style.maxHeight = parseDimension(v) + this.markDirty() + } + setMaxHeightPercent(v: number): void { + this.style.maxHeight = percentValue(v) + this.markDirty() + } + + // -- Style setters: flex + + setFlexDirection(dir: FlexDirection): void { + this.style.flexDirection = dir + this.markDirty() + } + setFlexGrow(v: number | undefined): void { + this.style.flexGrow = v ?? 0 + this.markDirty() + } + setFlexShrink(v: number | undefined): void { + this.style.flexShrink = v ?? 0 + this.markDirty() + } + setFlex(v: number | undefined): void { + if (v === undefined || isNaN(v)) { + this.style.flexGrow = 0 + this.style.flexShrink = 0 + } else if (v > 0) { + this.style.flexGrow = v + this.style.flexShrink = 1 + this.style.flexBasis = pointValue(0) + } else if (v < 0) { + this.style.flexGrow = 0 + this.style.flexShrink = -v + } else { + this.style.flexGrow = 0 + this.style.flexShrink = 0 + } + this.markDirty() + } + setFlexBasis(v: number | 'auto' | string | undefined): void { + this.style.flexBasis = parseDimension(v) + this.markDirty() + } + setFlexBasisPercent(v: number): void { + this.style.flexBasis = percentValue(v) + this.markDirty() + } + setFlexBasisAuto(): void { + this.style.flexBasis = AUTO_VALUE + this.markDirty() + } + setFlexWrap(wrap: Wrap): void { + this.style.flexWrap = wrap + this.markDirty() + } + + // -- Style setters: alignment + + setAlignItems(a: Align): void { + this.style.alignItems = a + this.markDirty() + } + setAlignSelf(a: Align): void { + this.style.alignSelf = a + this.markDirty() + } + setAlignContent(a: Align): void { + this.style.alignContent = a + this.markDirty() + } + setJustifyContent(j: Justify): void { + this.style.justifyContent = j + this.markDirty() + } + + // -- Style setters: display / position / overflow + + setDisplay(d: Display): void { + this.style.display = d + this.markDirty() + } + getDisplay(): Display { + return this.style.display + } + setPositionType(t: PositionType): void { + this.style.positionType = t + this.markDirty() + } + setPosition(edge: Edge, v: number | string | undefined): void { + this.style.position[edge] = parseDimension(v) + this._hasPosition = hasAnyDefinedEdge(this.style.position) + this.markDirty() + } + setPositionPercent(edge: Edge, v: number): void { + this.style.position[edge] = percentValue(v) + this._hasPosition = true + this.markDirty() + } + setPositionAuto(edge: Edge): void { + this.style.position[edge] = AUTO_VALUE + this._hasPosition = true + this.markDirty() + } + setOverflow(o: Overflow): void { + this.style.overflow = o + this.markDirty() + } + setDirection(d: Direction): void { + this.style.direction = d + this.markDirty() + } + setBoxSizing(_: BoxSizing): void { + // Not implemented — Ink doesn't use content-box + } + + // -- Style setters: spacing + + setMargin(edge: Edge, v: number | 'auto' | string | undefined): void { + const val = parseDimension(v) + this.style.margin[edge] = val + if (val.unit === Unit.Auto) this._hasAutoMargin = true + else this._hasAutoMargin = hasAnyAutoEdge(this.style.margin) + this._hasMargin = + this._hasAutoMargin || hasAnyDefinedEdge(this.style.margin) + this.markDirty() + } + setMarginPercent(edge: Edge, v: number): void { + this.style.margin[edge] = percentValue(v) + this._hasAutoMargin = hasAnyAutoEdge(this.style.margin) + this._hasMargin = true + this.markDirty() + } + setMarginAuto(edge: Edge): void { + this.style.margin[edge] = AUTO_VALUE + this._hasAutoMargin = true + this._hasMargin = true + this.markDirty() + } + setPadding(edge: Edge, v: number | string | undefined): void { + this.style.padding[edge] = parseDimension(v) + this._hasPadding = hasAnyDefinedEdge(this.style.padding) + this.markDirty() + } + setPaddingPercent(edge: Edge, v: number): void { + this.style.padding[edge] = percentValue(v) + this._hasPadding = true + this.markDirty() + } + setBorder(edge: Edge, v: number | undefined): void { + this.style.border[edge] = v === undefined ? UNDEFINED_VALUE : pointValue(v) + this._hasBorder = hasAnyDefinedEdge(this.style.border) + this.markDirty() + } + setGap(gutter: Gutter, v: number | string | undefined): void { + this.style.gap[gutter] = parseDimension(v) + this.markDirty() + } + setGapPercent(gutter: Gutter, v: number): void { + this.style.gap[gutter] = percentValue(v) + this.markDirty() + } + + // -- Style getters (partial — only what tests need) + + getFlexDirection(): FlexDirection { + return this.style.flexDirection + } + getJustifyContent(): Justify { + return this.style.justifyContent + } + getAlignItems(): Align { + return this.style.alignItems + } + getAlignSelf(): Align { + return this.style.alignSelf + } + getAlignContent(): Align { + return this.style.alignContent + } + getFlexGrow(): number { + return this.style.flexGrow + } + getFlexShrink(): number { + return this.style.flexShrink + } + getFlexBasis(): Value { + return this.style.flexBasis + } + getFlexWrap(): Wrap { + return this.style.flexWrap + } + getWidth(): Value { + return this.style.width + } + getHeight(): Value { + return this.style.height + } + getOverflow(): Overflow { + return this.style.overflow + } + getPositionType(): PositionType { + return this.style.positionType + } + getDirection(): Direction { + return this.style.direction + } + + // -- Unused API stubs (present for API parity) + + copyStyle(_: Node): void {} + setDirtiedFunc(_: unknown): void {} + unsetDirtiedFunc(): void {} + setIsReferenceBaseline(v: boolean): void { + this.isReferenceBaseline_ = v + this.markDirty() + } + isReferenceBaseline(): boolean { + return this.isReferenceBaseline_ + } + setAspectRatio(_: number | undefined): void {} + getAspectRatio(): number { + return NaN + } + setAlwaysFormsContainingBlock(_: boolean): void {} + + // -- Layout entry point + + calculateLayout( + ownerWidth: number | undefined, + ownerHeight: number | undefined, + _direction?: Direction, + ): void { + _yogaNodesVisited = 0 + _yogaMeasureCalls = 0 + _yogaCacheHits = 0 + _generation++ + const w = ownerWidth === undefined ? NaN : ownerWidth + const h = ownerHeight === undefined ? NaN : ownerHeight + layoutNode( + this, + w, + h, + isDefined(w) ? MeasureMode.Exactly : MeasureMode.Undefined, + isDefined(h) ? MeasureMode.Exactly : MeasureMode.Undefined, + w, + h, + true, + ) + // Root's own position = margin + position insets (yoga applies position + // to the root even without a parent container; this matters for rounding + // since the root's abs top/left seeds the pixel-grid walk). + const mar = this.layout.margin + const posL = resolveValue( + resolveEdgeRaw(this.style.position, EDGE_LEFT), + isDefined(w) ? w : 0, + ) + const posT = resolveValue( + resolveEdgeRaw(this.style.position, EDGE_TOP), + isDefined(w) ? w : 0, + ) + this.layout.left = mar[EDGE_LEFT] + (isDefined(posL) ? posL : 0) + this.layout.top = mar[EDGE_TOP] + (isDefined(posT) ? posT : 0) + roundLayout(this, this.config.pointScaleFactor, 0, 0) + } +} + +const DEFAULT_CONFIG = createConfig() + +const CACHE_SLOTS = 4 +function cacheWrite( + node: Node, + aW: number, + aH: number, + wM: MeasureMode, + hM: MeasureMode, + oW: number, + oH: number, + fW: boolean, + fH: boolean, + wasDirty: boolean, +): void { + if (!node._cIn) { + node._cIn = new Float64Array(CACHE_SLOTS * 8) + node._cOut = new Float64Array(CACHE_SLOTS * 2) + } + // First write after a dirty clears stale entries from before the dirty. + // _cGen < _generation means entries are from a previous calculateLayout; + // if wasDirty, the subtree changed since then → old dimensions invalid. + // Clean nodes' old entries stay — same subtree → same result for same + // inputs, so cross-generation caching works (the scroll hot path where + // 499 clean messages cache-hit while one dirty leaf recomputes). + if (wasDirty && node._cGen !== _generation) { + node._cN = 0 + node._cWr = 0 + } + // LRU write index wraps; _cN stays at CACHE_SLOTS so the read scan always + // checks all populated slots (not just those since last wrap). + const i = node._cWr++ % CACHE_SLOTS + if (node._cN < CACHE_SLOTS) node._cN = node._cWr + const o = i * 8 + const cIn = node._cIn + cIn[o] = aW + cIn[o + 1] = aH + cIn[o + 2] = wM + cIn[o + 3] = hM + cIn[o + 4] = oW + cIn[o + 5] = oH + cIn[o + 6] = fW ? 1 : 0 + cIn[o + 7] = fH ? 1 : 0 + node._cOut![i * 2] = node.layout.width + node._cOut![i * 2 + 1] = node.layout.height + node._cGen = _generation +} + +// Store computed layout.width/height into the single-slot cache output fields. +// _hasL/_hasM inputs are committed at the TOP of layoutNode (before compute); +// outputs must be committed HERE (after compute) so a cache hit can restore +// the correct dimensions. Without this, a _hasL hit returns whatever +// layout.width/height was left by the last call — which may be the intrinsic +// content height from a heightMode=Undefined measure pass rather than the +// constrained viewport height from the layout pass. That's the scrollbox +// vpH=33→2624 bug: scrollTop clamps to 0, viewport goes blank. +function commitCacheOutputs(node: Node, performLayout: boolean): void { + if (performLayout) { + node._lOutW = node.layout.width + node._lOutH = node.layout.height + } else { + node._mOutW = node.layout.width + node._mOutH = node.layout.height + } +} + +// -- +// Core flexbox algorithm + +// Profiling counters — reset per calculateLayout, read via getYogaCounters. +// Incremented on each calculateLayout(). Nodes stamp _fbGen/_cGen when +// their cache is written; a cache entry with gen === _generation was +// computed THIS pass and is fresh regardless of isDirty_ state. +let _generation = 0 +let _yogaNodesVisited = 0 +let _yogaMeasureCalls = 0 +let _yogaCacheHits = 0 +let _yogaLiveNodes = 0 +export function getYogaCounters(): { + visited: number + measured: number + cacheHits: number + live: number +} { + return { + visited: _yogaNodesVisited, + measured: _yogaMeasureCalls, + cacheHits: _yogaCacheHits, + live: _yogaLiveNodes, + } +} + +function layoutNode( + node: Node, + availableWidth: number, + availableHeight: number, + widthMode: MeasureMode, + heightMode: MeasureMode, + ownerWidth: number, + ownerHeight: number, + performLayout: boolean, + // When true, ignore style dimension on this axis — the flex container + // has already determined the main size (flex-basis + grow/shrink result). + forceWidth = false, + forceHeight = false, +): void { + _yogaNodesVisited++ + const style = node.style + const layout = node.layout + + // Dirty-flag skip: clean subtree + matching inputs → layout object already + // holds the answer. A cached layout result also satisfies a measure request + // (positions are a superset of dimensions); the reverse does not hold. + // Same-generation entries are fresh regardless of isDirty_ — they were + // computed THIS calculateLayout, the subtree hasn't changed since. + // Previous-generation entries need !isDirty_ (a dirty node's cache from + // before the dirty is stale). + // sameGen bypass only for MEASURE calls — a layout-pass cache hit would + // skip the child-positioning recursion (STEP 5), leaving children at + // stale positions. Measure calls only need w/h which the cache stores. + const sameGen = node._cGen === _generation && !performLayout + if (!node.isDirty_ || sameGen) { + if ( + !node.isDirty_ && + node._hasL && + node._lWM === widthMode && + node._lHM === heightMode && + node._lFW === forceWidth && + node._lFH === forceHeight && + sameFloat(node._lW, availableWidth) && + sameFloat(node._lH, availableHeight) && + sameFloat(node._lOW, ownerWidth) && + sameFloat(node._lOH, ownerHeight) + ) { + _yogaCacheHits++ + layout.width = node._lOutW + layout.height = node._lOutH + return + } + // Multi-entry cache: scan for matching inputs, restore cached w/h on hit. + // Covers the scroll case where a dirty ancestor's measure→layout cascade + // produces N>1 distinct input combos per clean child — the single _hasL + // slot thrashed, forcing full subtree recursion. With 500-message + // scrollbox and one dirty leaf, this took dirty-leaf relayout from + // 76k layoutNode calls (21.7×nodes) to 4k (1.2×nodes), 6.86ms → 550µs. + // Same-generation check covers fresh-mounted (dirty) nodes during + // virtual scroll — the dirty chain invokes them ≥2^depth times, first + // call writes cache, rest hit: 105k visits → ~10k for 1593-node tree. + if (node._cN > 0 && (sameGen || !node.isDirty_)) { + const cIn = node._cIn! + for (let i = 0; i < node._cN; i++) { + const o = i * 8 + if ( + cIn[o + 2] === widthMode && + cIn[o + 3] === heightMode && + cIn[o + 6] === (forceWidth ? 1 : 0) && + cIn[o + 7] === (forceHeight ? 1 : 0) && + sameFloat(cIn[o]!, availableWidth) && + sameFloat(cIn[o + 1]!, availableHeight) && + sameFloat(cIn[o + 4]!, ownerWidth) && + sameFloat(cIn[o + 5]!, ownerHeight) + ) { + layout.width = node._cOut![i * 2]! + layout.height = node._cOut![i * 2 + 1]! + _yogaCacheHits++ + return + } + } + } + if ( + !node.isDirty_ && + !performLayout && + node._hasM && + node._mWM === widthMode && + node._mHM === heightMode && + sameFloat(node._mW, availableWidth) && + sameFloat(node._mH, availableHeight) && + sameFloat(node._mOW, ownerWidth) && + sameFloat(node._mOH, ownerHeight) + ) { + layout.width = node._mOutW + layout.height = node._mOutH + _yogaCacheHits++ + return + } + } + // Commit cache inputs up front so every return path leaves a valid entry. + // Only clear isDirty_ on the LAYOUT pass — the measure pass (computeFlexBasis + // → layoutNode(performLayout=false)) runs before the layout pass in the same + // calculateLayout call. Clearing dirty during measure lets the subsequent + // layout pass hit the STALE _hasL cache from the previous calculateLayout + // (before children were inserted), so ScrollBox content height never grows + // and sticky-scroll never follows new content. A dirty node's _hasL entry is + // stale by definition — invalidate it so the layout pass recomputes. + const wasDirty = node.isDirty_ + if (performLayout) { + node._lW = availableWidth + node._lH = availableHeight + node._lWM = widthMode + node._lHM = heightMode + node._lOW = ownerWidth + node._lOH = ownerHeight + node._lFW = forceWidth + node._lFH = forceHeight + node._hasL = true + node.isDirty_ = false + // Previous approach cleared _cN here to prevent stale pre-dirty entries + // from hitting (long-continuous blank-screen bug). Now replaced by + // generation stamping: the cache check requires sameGen || !isDirty_, so + // previous-generation entries from a dirty node can't hit. Clearing here + // would wipe fresh same-generation entries from an earlier measure call, + // forcing recompute on the layout call. + if (wasDirty) node._hasM = false + } else { + node._mW = availableWidth + node._mH = availableHeight + node._mWM = widthMode + node._mHM = heightMode + node._mOW = ownerWidth + node._mOH = ownerHeight + node._hasM = true + // Don't clear isDirty_. For DIRTY nodes, invalidate _hasL so the upcoming + // performLayout=true call recomputes with the new child set (otherwise + // sticky-scroll never follows new content — the bug from 4557bc9f9c). + // Clean nodes keep _hasL: their layout from the previous generation is + // still valid, they're only here because an ancestor is dirty and called + // with different inputs than cached. + if (wasDirty) node._hasL = false + } + + // Resolve padding/border/margin against ownerWidth (yoga uses ownerWidth for %) + // Write directly into the pre-allocated layout arrays — avoids 3 allocs per + // layoutNode call and 12 resolveEdge calls (was the #1 hotspot per CPU profile). + // Skip entirely when no edges are set — the 4-write zero is cheaper than + // the ~20 reads + ~15 compares resolveEdges4Into does to produce zeros. + const pad = layout.padding + const bor = layout.border + const mar = layout.margin + if (node._hasPadding) resolveEdges4Into(style.padding, ownerWidth, pad) + else pad[0] = pad[1] = pad[2] = pad[3] = 0 + if (node._hasBorder) resolveEdges4Into(style.border, ownerWidth, bor) + else bor[0] = bor[1] = bor[2] = bor[3] = 0 + if (node._hasMargin) resolveEdges4Into(style.margin, ownerWidth, mar) + else mar[0] = mar[1] = mar[2] = mar[3] = 0 + + const paddingBorderWidth = pad[0] + pad[2] + bor[0] + bor[2] + const paddingBorderHeight = pad[1] + pad[3] + bor[1] + bor[3] + + // Resolve style dimensions + const styleWidth = forceWidth ? NaN : resolveValue(style.width, ownerWidth) + const styleHeight = forceHeight + ? NaN + : resolveValue(style.height, ownerHeight) + + // If style dimension is defined, it overrides the available size + let width = availableWidth + let height = availableHeight + let wMode = widthMode + let hMode = heightMode + if (isDefined(styleWidth)) { + width = styleWidth + wMode = MeasureMode.Exactly + } + if (isDefined(styleHeight)) { + height = styleHeight + hMode = MeasureMode.Exactly + } + + // Apply min/max constraints to the node's own dimensions + width = boundAxis(style, true, width, ownerWidth, ownerHeight) + height = boundAxis(style, false, height, ownerWidth, ownerHeight) + + // Measure-func leaf node + if (node.measureFunc && node.children.length === 0) { + const innerW = + wMode === MeasureMode.Undefined + ? NaN + : Math.max(0, width - paddingBorderWidth) + const innerH = + hMode === MeasureMode.Undefined + ? NaN + : Math.max(0, height - paddingBorderHeight) + _yogaMeasureCalls++ + const measured = node.measureFunc(innerW, wMode, innerH, hMode) + node.layout.width = + wMode === MeasureMode.Exactly + ? width + : boundAxis( + style, + true, + (measured.width ?? 0) + paddingBorderWidth, + ownerWidth, + ownerHeight, + ) + node.layout.height = + hMode === MeasureMode.Exactly + ? height + : boundAxis( + style, + false, + (measured.height ?? 0) + paddingBorderHeight, + ownerWidth, + ownerHeight, + ) + commitCacheOutputs(node, performLayout) + // Write cache even for dirty nodes — fresh-mounted items during virtual + // scroll are dirty on first layout, but the dirty chain's measure→layout + // cascade invokes them ≥2^depth times per calculateLayout. Writing here + // lets the 2nd+ calls hit cache (isDirty_ was cleared in the layout pass + // above). Measured: 105k visits → 10k for a 1593-node fresh-mount tree. + cacheWrite( + node, + availableWidth, + availableHeight, + widthMode, + heightMode, + ownerWidth, + ownerHeight, + forceWidth, + forceHeight, + wasDirty, + ) + return + } + + // Leaf node with no children and no measure func + if (node.children.length === 0) { + node.layout.width = + wMode === MeasureMode.Exactly + ? width + : boundAxis(style, true, paddingBorderWidth, ownerWidth, ownerHeight) + node.layout.height = + hMode === MeasureMode.Exactly + ? height + : boundAxis(style, false, paddingBorderHeight, ownerWidth, ownerHeight) + commitCacheOutputs(node, performLayout) + // Write cache even for dirty nodes — fresh-mounted items during virtual + // scroll are dirty on first layout, but the dirty chain's measure→layout + // cascade invokes them ≥2^depth times per calculateLayout. Writing here + // lets the 2nd+ calls hit cache (isDirty_ was cleared in the layout pass + // above). Measured: 105k visits → 10k for a 1593-node fresh-mount tree. + cacheWrite( + node, + availableWidth, + availableHeight, + widthMode, + heightMode, + ownerWidth, + ownerHeight, + forceWidth, + forceHeight, + wasDirty, + ) + return + } + + // Container with children — run flexbox algorithm + const mainAxis = style.flexDirection + const crossAx = crossAxis(mainAxis) + const isMainRow = isRow(mainAxis) + + const mainSize = isMainRow ? width : height + const crossSize = isMainRow ? height : width + const mainMode = isMainRow ? wMode : hMode + const crossMode = isMainRow ? hMode : wMode + const mainPadBorder = isMainRow ? paddingBorderWidth : paddingBorderHeight + const crossPadBorder = isMainRow ? paddingBorderHeight : paddingBorderWidth + + const innerMainSize = isDefined(mainSize) + ? Math.max(0, mainSize - mainPadBorder) + : NaN + const innerCrossSize = isDefined(crossSize) + ? Math.max(0, crossSize - crossPadBorder) + : NaN + + // Resolve gap + const gapMain = resolveGap( + style, + isMainRow ? Gutter.Column : Gutter.Row, + innerMainSize, + ) + + // Partition children into flow vs absolute. display:contents nodes are + // transparent — their children are lifted into the grandparent's child list + // (recursively), and the contents node itself gets zero layout. + const flowChildren: Node[] = [] + const absChildren: Node[] = [] + collectLayoutChildren(node, flowChildren, absChildren) + + // ownerW/H are the reference sizes for resolving children's percentage + // values. Per CSS, a % width resolves against the parent's content-box + // width. If this node's width is indefinite, children's % widths are also + // indefinite — do NOT fall through to the grandparent's size. + const ownerW = isDefined(width) ? width : NaN + const ownerH = isDefined(height) ? height : NaN + const isWrap = style.flexWrap !== Wrap.NoWrap + const gapCross = resolveGap( + style, + isMainRow ? Gutter.Row : Gutter.Column, + innerCrossSize, + ) + + // STEP 1: Compute flex-basis for each flow child and break into lines. + // Single-line (NoWrap) containers always get one line; multi-line containers + // break when accumulated basis+margin+gap exceeds innerMainSize. + for (const c of flowChildren) { + c._flexBasis = computeFlexBasis( + c, + mainAxis, + innerMainSize, + innerCrossSize, + crossMode, + ownerW, + ownerH, + ) + } + const lines: Node[][] = [] + if (!isWrap || !isDefined(innerMainSize) || flowChildren.length === 0) { + for (const c of flowChildren) c._lineIndex = 0 + lines.push(flowChildren) + } else { + // Line-break decisions use the min/max-clamped basis (flexbox spec §9.3.5: + // "hypothetical main size"), not the raw flex-basis. + let lineStart = 0 + let lineLen = 0 + for (let i = 0; i < flowChildren.length; i++) { + const c = flowChildren[i]! + const hypo = boundAxis(c.style, isMainRow, c._flexBasis, ownerW, ownerH) + const outer = Math.max(0, hypo) + childMarginForAxis(c, mainAxis, ownerW) + const withGap = i > lineStart ? gapMain : 0 + if (i > lineStart && lineLen + withGap + outer > innerMainSize) { + lines.push(flowChildren.slice(lineStart, i)) + lineStart = i + lineLen = outer + } else { + lineLen += withGap + outer + } + c._lineIndex = lines.length + } + lines.push(flowChildren.slice(lineStart)) + } + const lineCount = lines.length + const isBaseline = isBaselineLayout(node, flowChildren) + + // STEP 2+3: For each line, resolve flexible lengths and lay out children to + // measure cross sizes. Track per-line consumed main and max cross. + const lineConsumedMain: number[] = new Array(lineCount) + const lineCrossSizes: number[] = new Array(lineCount) + // Baseline layout tracks max ascent (baseline + leading margin) per line so + // baseline-aligned items can be positioned at maxAscent - childBaseline. + const lineMaxAscent: number[] = isBaseline ? new Array(lineCount).fill(0) : [] + let maxLineMain = 0 + let totalLinesCross = 0 + for (let li = 0; li < lineCount; li++) { + const line = lines[li]! + const lineGap = line.length > 1 ? gapMain * (line.length - 1) : 0 + let lineBasis = lineGap + for (const c of line) { + lineBasis += c._flexBasis + childMarginForAxis(c, mainAxis, ownerW) + } + // Resolve flexible lengths against available inner main. For indefinite + // containers with min/max, flex against the clamped size. + let availMain = innerMainSize + if (!isDefined(availMain)) { + const mainOwner = isMainRow ? ownerWidth : ownerHeight + const minM = resolveValue( + isMainRow ? style.minWidth : style.minHeight, + mainOwner, + ) + const maxM = resolveValue( + isMainRow ? style.maxWidth : style.maxHeight, + mainOwner, + ) + if (isDefined(maxM) && lineBasis > maxM - mainPadBorder) { + availMain = Math.max(0, maxM - mainPadBorder) + } else if (isDefined(minM) && lineBasis < minM - mainPadBorder) { + availMain = Math.max(0, minM - mainPadBorder) + } + } + resolveFlexibleLengths( + line, + availMain, + lineBasis, + isMainRow, + ownerW, + ownerH, + ) + + // Lay out each child in this line to measure cross + let lineCross = 0 + for (const c of line) { + const cStyle = c.style + const childAlign = + cStyle.alignSelf === Align.Auto ? style.alignItems : cStyle.alignSelf + const cMarginCross = childMarginForAxis(c, crossAx, ownerW) + let childCrossSize = NaN + let childCrossMode: MeasureMode = MeasureMode.Undefined + const resolvedCrossStyle = resolveValue( + isMainRow ? cStyle.height : cStyle.width, + isMainRow ? ownerH : ownerW, + ) + const crossLeadE = isMainRow ? EDGE_TOP : EDGE_LEFT + const crossTrailE = isMainRow ? EDGE_BOTTOM : EDGE_RIGHT + const hasCrossAutoMargin = + c._hasAutoMargin && + (isMarginAuto(cStyle.margin, crossLeadE) || + isMarginAuto(cStyle.margin, crossTrailE)) + // Single-line stretch goes directly to the container cross size. + // Multi-line wrap measures intrinsic cross (Undefined mode) so + // flex-grow grandchildren don't expand to the container — the line + // cross size is determined first, then items are re-stretched. + if (isDefined(resolvedCrossStyle)) { + childCrossSize = resolvedCrossStyle + childCrossMode = MeasureMode.Exactly + } else if ( + childAlign === Align.Stretch && + !hasCrossAutoMargin && + !isWrap && + isDefined(innerCrossSize) && + crossMode === MeasureMode.Exactly + ) { + childCrossSize = Math.max(0, innerCrossSize - cMarginCross) + childCrossMode = MeasureMode.Exactly + } else if (!isWrap && isDefined(innerCrossSize)) { + childCrossSize = Math.max(0, innerCrossSize - cMarginCross) + childCrossMode = MeasureMode.AtMost + } + const cw = isMainRow ? c._mainSize : childCrossSize + const ch = isMainRow ? childCrossSize : c._mainSize + layoutNode( + c, + cw, + ch, + isMainRow ? MeasureMode.Exactly : childCrossMode, + isMainRow ? childCrossMode : MeasureMode.Exactly, + ownerW, + ownerH, + performLayout, + isMainRow, + !isMainRow, + ) + c._crossSize = isMainRow ? c.layout.height : c.layout.width + lineCross = Math.max(lineCross, c._crossSize + cMarginCross) + } + // Baseline layout: line cross size must fit maxAscent + maxDescent of + // baseline-aligned children (yoga STEP 8). Only applies to row direction. + if (isBaseline) { + let maxAscent = 0 + let maxDescent = 0 + for (const c of line) { + if (resolveChildAlign(node, c) !== Align.Baseline) continue + const mTop = resolveEdge(c.style.margin, EDGE_TOP, ownerW) + const mBot = resolveEdge(c.style.margin, EDGE_BOTTOM, ownerW) + const ascent = calculateBaseline(c) + mTop + const descent = c.layout.height + mTop + mBot - ascent + if (ascent > maxAscent) maxAscent = ascent + if (descent > maxDescent) maxDescent = descent + } + lineMaxAscent[li] = maxAscent + if (maxAscent + maxDescent > lineCross) { + lineCross = maxAscent + maxDescent + } + } + // layoutNode(c) at line ~1117 above already resolved c.layout.margin[] via + // resolveEdges4Into with the same ownerW — read directly instead of + // re-resolving through childMarginForAxis → 2× resolveEdge. + const mainLead = leadingEdge(mainAxis) + const mainTrail = trailingEdge(mainAxis) + let consumed = lineGap + for (const c of line) { + const cm = c.layout.margin + consumed += c._mainSize + cm[mainLead]! + cm[mainTrail]! + } + lineConsumedMain[li] = consumed + lineCrossSizes[li] = lineCross + maxLineMain = Math.max(maxLineMain, consumed) + totalLinesCross += lineCross + } + const totalCrossGap = lineCount > 1 ? gapCross * (lineCount - 1) : 0 + totalLinesCross += totalCrossGap + + // STEP 4: Determine container dimensions. Per yoga's STEP 9, for both + // AtMost (FitContent) and Undefined (MaxContent) the node sizes to its + // content — AtMost is NOT a hard clamp, items may overflow the available + // space (CSS "fit-content" behavior). Only Scroll overflow clamps to the + // available size. Wrap containers that broke into multiple lines under + // AtMost fill the available main size since they wrapped at that boundary. + const isScroll = style.overflow === Overflow.Scroll + const contentMain = maxLineMain + mainPadBorder + const finalMainSize = + mainMode === MeasureMode.Exactly + ? mainSize + : mainMode === MeasureMode.AtMost && isScroll + ? Math.max(Math.min(mainSize, contentMain), mainPadBorder) + : isWrap && lineCount > 1 && mainMode === MeasureMode.AtMost + ? mainSize + : contentMain + const contentCross = totalLinesCross + crossPadBorder + const finalCrossSize = + crossMode === MeasureMode.Exactly + ? crossSize + : crossMode === MeasureMode.AtMost && isScroll + ? Math.max(Math.min(crossSize, contentCross), crossPadBorder) + : contentCross + node.layout.width = boundAxis( + style, + true, + isMainRow ? finalMainSize : finalCrossSize, + ownerWidth, + ownerHeight, + ) + node.layout.height = boundAxis( + style, + false, + isMainRow ? finalCrossSize : finalMainSize, + ownerWidth, + ownerHeight, + ) + commitCacheOutputs(node, performLayout) + // Write cache even for dirty nodes — fresh-mounted items during virtual scroll + cacheWrite( + node, + availableWidth, + availableHeight, + widthMode, + heightMode, + ownerWidth, + ownerHeight, + forceWidth, + forceHeight, + wasDirty, + ) + + if (!performLayout) return + + // STEP 5: Position lines (align-content) and children (justify-content + + // align-items + auto margins). + const actualInnerMain = + (isMainRow ? node.layout.width : node.layout.height) - mainPadBorder + const actualInnerCross = + (isMainRow ? node.layout.height : node.layout.width) - crossPadBorder + const mainLeadEdgePhys = leadingEdge(mainAxis) + const mainTrailEdgePhys = trailingEdge(mainAxis) + const crossLeadEdgePhys = isMainRow ? EDGE_TOP : EDGE_LEFT + const crossTrailEdgePhys = isMainRow ? EDGE_BOTTOM : EDGE_RIGHT + const reversed = isReverse(mainAxis) + const mainContainerSize = isMainRow ? node.layout.width : node.layout.height + const crossLead = pad[crossLeadEdgePhys]! + bor[crossLeadEdgePhys]! + + // Align-content: distribute free cross space among lines. Single-line + // containers use the full cross size for the one line (align-items handles + // positioning within it). + let lineCrossOffset = crossLead + let betweenLines = gapCross + const freeCross = actualInnerCross - totalLinesCross + if (lineCount === 1 && !isWrap && !isBaseline) { + lineCrossSizes[0] = actualInnerCross + } else { + const remCross = Math.max(0, freeCross) + switch (style.alignContent) { + case Align.FlexStart: + break + case Align.Center: + lineCrossOffset += freeCross / 2 + break + case Align.FlexEnd: + lineCrossOffset += freeCross + break + case Align.Stretch: + if (lineCount > 0 && remCross > 0) { + const add = remCross / lineCount + for (let i = 0; i < lineCount; i++) lineCrossSizes[i]! += add + } + break + case Align.SpaceBetween: + if (lineCount > 1) betweenLines += remCross / (lineCount - 1) + break + case Align.SpaceAround: + if (lineCount > 0) { + betweenLines += remCross / lineCount + lineCrossOffset += remCross / lineCount / 2 + } + break + case Align.SpaceEvenly: + if (lineCount > 0) { + betweenLines += remCross / (lineCount + 1) + lineCrossOffset += remCross / (lineCount + 1) + } + break + default: + break + } + } + + // For wrap-reverse, lines stack from the trailing cross edge. Walk lines in + // order but flip the cross position within the container. + const wrapReverse = style.flexWrap === Wrap.WrapReverse + const crossContainerSize = isMainRow ? node.layout.height : node.layout.width + let lineCrossPos = lineCrossOffset + for (let li = 0; li < lineCount; li++) { + const line = lines[li]! + const lineCross = lineCrossSizes[li]! + const consumedMain = lineConsumedMain[li]! + const n = line.length + + // Re-stretch children whose cross is auto and align is stretch, now that + // the line cross size is known. Needed for multi-line wrap (line cross + // wasn't known during initial measure) AND single-line when the container + // cross was not Exactly (initial stretch at ~line 1250 was skipped because + // innerCrossSize wasn't defined — the container sized to max child cross). + if (isWrap || crossMode !== MeasureMode.Exactly) { + for (const c of line) { + const cStyle = c.style + const childAlign = + cStyle.alignSelf === Align.Auto ? style.alignItems : cStyle.alignSelf + const crossStyleDef = isDefined( + resolveValue( + isMainRow ? cStyle.height : cStyle.width, + isMainRow ? ownerH : ownerW, + ), + ) + const hasCrossAutoMargin = + c._hasAutoMargin && + (isMarginAuto(cStyle.margin, crossLeadEdgePhys) || + isMarginAuto(cStyle.margin, crossTrailEdgePhys)) + if ( + childAlign === Align.Stretch && + !crossStyleDef && + !hasCrossAutoMargin + ) { + const cMarginCross = childMarginForAxis(c, crossAx, ownerW) + const target = Math.max(0, lineCross - cMarginCross) + if (c._crossSize !== target) { + const cw = isMainRow ? c._mainSize : target + const ch = isMainRow ? target : c._mainSize + layoutNode( + c, + cw, + ch, + MeasureMode.Exactly, + MeasureMode.Exactly, + ownerW, + ownerH, + performLayout, + isMainRow, + !isMainRow, + ) + c._crossSize = target + } + } + } + } + + // Justify-content + auto margins for this line + let mainOffset = pad[mainLeadEdgePhys]! + bor[mainLeadEdgePhys]! + let betweenMain = gapMain + let numAutoMarginsMain = 0 + for (const c of line) { + if (!c._hasAutoMargin) continue + if (isMarginAuto(c.style.margin, mainLeadEdgePhys)) numAutoMarginsMain++ + if (isMarginAuto(c.style.margin, mainTrailEdgePhys)) numAutoMarginsMain++ + } + const freeMain = actualInnerMain - consumedMain + const remainingMain = Math.max(0, freeMain) + const autoMarginMainSize = + numAutoMarginsMain > 0 && remainingMain > 0 + ? remainingMain / numAutoMarginsMain + : 0 + if (numAutoMarginsMain === 0) { + switch (style.justifyContent) { + case Justify.FlexStart: + break + case Justify.Center: + mainOffset += freeMain / 2 + break + case Justify.FlexEnd: + mainOffset += freeMain + break + case Justify.SpaceBetween: + if (n > 1) betweenMain += remainingMain / (n - 1) + break + case Justify.SpaceAround: + if (n > 0) { + betweenMain += remainingMain / n + mainOffset += remainingMain / n / 2 + } + break + case Justify.SpaceEvenly: + if (n > 0) { + betweenMain += remainingMain / (n + 1) + mainOffset += remainingMain / (n + 1) + } + break + } + } + + const effectiveLineCrossPos = wrapReverse + ? crossContainerSize - lineCrossPos - lineCross + : lineCrossPos + + let pos = mainOffset + for (const c of line) { + const cMargin = c.style.margin + // c.layout.margin[] was populated by resolveEdges4Into inside the + // layoutNode(c) call above (same ownerW). Read resolved values directly + // instead of re-running the edge fallback chain 4× via resolveEdge. + // Auto margins resolve to 0 in layout.margin, so autoMarginMainSize + // substitution still uses the isMarginAuto check against style. + const cLayoutMargin = c.layout.margin + let autoMainLead = false + let autoMainTrail = false + let autoCrossLead = false + let autoCrossTrail = false + let mMainLead: number + let mMainTrail: number + let mCrossLead: number + let mCrossTrail: number + if (c._hasAutoMargin) { + autoMainLead = isMarginAuto(cMargin, mainLeadEdgePhys) + autoMainTrail = isMarginAuto(cMargin, mainTrailEdgePhys) + autoCrossLead = isMarginAuto(cMargin, crossLeadEdgePhys) + autoCrossTrail = isMarginAuto(cMargin, crossTrailEdgePhys) + mMainLead = autoMainLead + ? autoMarginMainSize + : cLayoutMargin[mainLeadEdgePhys]! + mMainTrail = autoMainTrail + ? autoMarginMainSize + : cLayoutMargin[mainTrailEdgePhys]! + mCrossLead = autoCrossLead ? 0 : cLayoutMargin[crossLeadEdgePhys]! + mCrossTrail = autoCrossTrail ? 0 : cLayoutMargin[crossTrailEdgePhys]! + } else { + // Fast path: no auto margins — read resolved values directly. + mMainLead = cLayoutMargin[mainLeadEdgePhys]! + mMainTrail = cLayoutMargin[mainTrailEdgePhys]! + mCrossLead = cLayoutMargin[crossLeadEdgePhys]! + mCrossTrail = cLayoutMargin[crossTrailEdgePhys]! + } + + const mainPos = reversed + ? mainContainerSize - (pos + mMainLead) - c._mainSize + : pos + mMainLead + + const childAlign = + c.style.alignSelf === Align.Auto ? style.alignItems : c.style.alignSelf + let crossPos = effectiveLineCrossPos + mCrossLead + const crossFree = lineCross - c._crossSize - mCrossLead - mCrossTrail + if (autoCrossLead && autoCrossTrail) { + crossPos += Math.max(0, crossFree) / 2 + } else if (autoCrossLead) { + crossPos += Math.max(0, crossFree) + } else if (autoCrossTrail) { + // stays at leading + } else { + switch (childAlign) { + case Align.FlexStart: + case Align.Stretch: + if (wrapReverse) crossPos += crossFree + break + case Align.Center: + crossPos += crossFree / 2 + break + case Align.FlexEnd: + if (!wrapReverse) crossPos += crossFree + break + case Align.Baseline: + // Row direction only (isBaselineLayout checked this). Position so + // the child's baseline aligns with the line's max ascent. Per + // yoga: top = currentLead + maxAscent - childBaseline + leadingPosition. + if (isBaseline) { + crossPos = + effectiveLineCrossPos + + lineMaxAscent[li]! - + calculateBaseline(c) + } + break + default: + break + } + } + + // Relative position offsets. Fast path: no position insets set → + // skip 4× resolveEdgeRaw + 4× resolveValue + 4× isDefined. + let relX = 0 + let relY = 0 + if (c._hasPosition) { + const relLeft = resolveValue( + resolveEdgeRaw(c.style.position, EDGE_LEFT), + ownerW, + ) + const relRight = resolveValue( + resolveEdgeRaw(c.style.position, EDGE_RIGHT), + ownerW, + ) + const relTop = resolveValue( + resolveEdgeRaw(c.style.position, EDGE_TOP), + ownerW, + ) + const relBottom = resolveValue( + resolveEdgeRaw(c.style.position, EDGE_BOTTOM), + ownerW, + ) + relX = isDefined(relLeft) + ? relLeft + : isDefined(relRight) + ? -relRight + : 0 + relY = isDefined(relTop) + ? relTop + : isDefined(relBottom) + ? -relBottom + : 0 + } + + if (isMainRow) { + c.layout.left = mainPos + relX + c.layout.top = crossPos + relY + } else { + c.layout.left = crossPos + relX + c.layout.top = mainPos + relY + } + pos += c._mainSize + mMainLead + mMainTrail + betweenMain + } + lineCrossPos += lineCross + betweenLines + } + + // STEP 6: Absolute-positioned children + for (const c of absChildren) { + layoutAbsoluteChild( + node, + c, + node.layout.width, + node.layout.height, + pad, + bor, + ) + } +} + +function layoutAbsoluteChild( + parent: Node, + child: Node, + parentWidth: number, + parentHeight: number, + pad: [number, number, number, number], + bor: [number, number, number, number], +): void { + const cs = child.style + const posLeft = resolveEdgeRaw(cs.position, EDGE_LEFT) + const posRight = resolveEdgeRaw(cs.position, EDGE_RIGHT) + const posTop = resolveEdgeRaw(cs.position, EDGE_TOP) + const posBottom = resolveEdgeRaw(cs.position, EDGE_BOTTOM) + + const rLeft = resolveValue(posLeft, parentWidth) + const rRight = resolveValue(posRight, parentWidth) + const rTop = resolveValue(posTop, parentHeight) + const rBottom = resolveValue(posBottom, parentHeight) + + // Absolute children's percentage dimensions resolve against the containing + // block's padding-box (parent size minus border), per CSS §10.1. + const paddingBoxW = parentWidth - bor[0] - bor[2] + const paddingBoxH = parentHeight - bor[1] - bor[3] + let cw = resolveValue(cs.width, paddingBoxW) + let ch = resolveValue(cs.height, paddingBoxH) + + // If both left+right defined and width not, derive width + if (!isDefined(cw) && isDefined(rLeft) && isDefined(rRight)) { + cw = paddingBoxW - rLeft - rRight + } + if (!isDefined(ch) && isDefined(rTop) && isDefined(rBottom)) { + ch = paddingBoxH - rTop - rBottom + } + + layoutNode( + child, + cw, + ch, + isDefined(cw) ? MeasureMode.Exactly : MeasureMode.Undefined, + isDefined(ch) ? MeasureMode.Exactly : MeasureMode.Undefined, + paddingBoxW, + paddingBoxH, + true, + ) + + // Margin of absolute child (applied in addition to insets) + const mL = resolveEdge(cs.margin, EDGE_LEFT, parentWidth) + const mT = resolveEdge(cs.margin, EDGE_TOP, parentWidth) + const mR = resolveEdge(cs.margin, EDGE_RIGHT, parentWidth) + const mB = resolveEdge(cs.margin, EDGE_BOTTOM, parentWidth) + + const mainAxis = parent.style.flexDirection + const reversed = isReverse(mainAxis) + const mainRow = isRow(mainAxis) + const wrapReverse = parent.style.flexWrap === Wrap.WrapReverse + // alignSelf overrides alignItems for absolute children (same as flow items) + const alignment = + cs.alignSelf === Align.Auto ? parent.style.alignItems : cs.alignSelf + + // Position + let left: number + if (isDefined(rLeft)) { + left = bor[0] + rLeft + mL + } else if (isDefined(rRight)) { + left = parentWidth - bor[2] - rRight - child.layout.width - mR + } else if (mainRow) { + // Main axis — justify-content, flipped for reversed + const lead = pad[0] + bor[0] + const trail = parentWidth - pad[2] - bor[2] + left = reversed + ? trail - child.layout.width - mR + : justifyAbsolute( + parent.style.justifyContent, + lead, + trail, + child.layout.width, + ) + mL + } else { + left = + alignAbsolute( + alignment, + pad[0] + bor[0], + parentWidth - pad[2] - bor[2], + child.layout.width, + wrapReverse, + ) + mL + } + + let top: number + if (isDefined(rTop)) { + top = bor[1] + rTop + mT + } else if (isDefined(rBottom)) { + top = parentHeight - bor[3] - rBottom - child.layout.height - mB + } else if (mainRow) { + top = + alignAbsolute( + alignment, + pad[1] + bor[1], + parentHeight - pad[3] - bor[3], + child.layout.height, + wrapReverse, + ) + mT + } else { + const lead = pad[1] + bor[1] + const trail = parentHeight - pad[3] - bor[3] + top = reversed + ? trail - child.layout.height - mB + : justifyAbsolute( + parent.style.justifyContent, + lead, + trail, + child.layout.height, + ) + mT + } + + child.layout.left = left + child.layout.top = top +} + +function justifyAbsolute( + justify: Justify, + leadEdge: number, + trailEdge: number, + childSize: number, +): number { + switch (justify) { + case Justify.Center: + return leadEdge + (trailEdge - leadEdge - childSize) / 2 + case Justify.FlexEnd: + return trailEdge - childSize + default: + return leadEdge + } +} + +function alignAbsolute( + align: Align, + leadEdge: number, + trailEdge: number, + childSize: number, + wrapReverse: boolean, +): number { + // Wrap-reverse flips the cross axis: flex-start/stretch go to trailing, + // flex-end goes to leading (yoga's absoluteLayoutChild flips the align value + // when the containing block has wrap-reverse). + switch (align) { + case Align.Center: + return leadEdge + (trailEdge - leadEdge - childSize) / 2 + case Align.FlexEnd: + return wrapReverse ? leadEdge : trailEdge - childSize + default: + return wrapReverse ? trailEdge - childSize : leadEdge + } +} + +function computeFlexBasis( + child: Node, + mainAxis: FlexDirection, + availableMain: number, + availableCross: number, + crossMode: MeasureMode, + ownerWidth: number, + ownerHeight: number, +): number { + // Same-generation cache hit: basis was computed THIS calculateLayout, so + // it's fresh regardless of isDirty_. Covers both clean children (scrolling + // past unchanged messages) AND fresh-mounted dirty children (virtual + // scroll mounts new items — the dirty chain's measure→layout cascade + // invokes this ≥2^depth times, but the child's subtree doesn't change + // between calls within one calculateLayout). For clean children with + // cache from a PREVIOUS generation, also hit if inputs match — isDirty_ + // gates since a dirty child's previous-gen cache is stale. + const sameGen = child._fbGen === _generation + if ( + (sameGen || !child.isDirty_) && + child._fbCrossMode === crossMode && + sameFloat(child._fbOwnerW, ownerWidth) && + sameFloat(child._fbOwnerH, ownerHeight) && + sameFloat(child._fbAvailMain, availableMain) && + sameFloat(child._fbAvailCross, availableCross) + ) { + return child._fbBasis + } + const cs = child.style + const isMainRow = isRow(mainAxis) + + // Explicit flex-basis + const basis = resolveValue(cs.flexBasis, availableMain) + if (isDefined(basis)) { + const b = Math.max(0, basis) + child._fbBasis = b + child._fbOwnerW = ownerWidth + child._fbOwnerH = ownerHeight + child._fbAvailMain = availableMain + child._fbAvailCross = availableCross + child._fbCrossMode = crossMode + child._fbGen = _generation + return b + } + + // Style dimension on main axis + const mainStyleDim = isMainRow ? cs.width : cs.height + const mainOwner = isMainRow ? ownerWidth : ownerHeight + const resolved = resolveValue(mainStyleDim, mainOwner) + if (isDefined(resolved)) { + const b = Math.max(0, resolved) + child._fbBasis = b + child._fbOwnerW = ownerWidth + child._fbOwnerH = ownerHeight + child._fbAvailMain = availableMain + child._fbAvailCross = availableCross + child._fbCrossMode = crossMode + child._fbGen = _generation + return b + } + + // Need to measure the child to get its natural size + const crossStyleDim = isMainRow ? cs.height : cs.width + const crossOwner = isMainRow ? ownerHeight : ownerWidth + let crossConstraint = resolveValue(crossStyleDim, crossOwner) + let crossConstraintMode: MeasureMode = isDefined(crossConstraint) + ? MeasureMode.Exactly + : MeasureMode.Undefined + if (!isDefined(crossConstraint) && isDefined(availableCross)) { + crossConstraint = availableCross + crossConstraintMode = + crossMode === MeasureMode.Exactly && isStretchAlign(child) + ? MeasureMode.Exactly + : MeasureMode.AtMost + } + + // Upstream yoga (YGNodeComputeFlexBasisForChild) passes the available inner + // width with mode AtMost when the subtree will call a measure-func — so text + // nodes don't report unconstrained intrinsic width as flex-basis, which + // would force siblings to shrink and the text to wrap at the wrong width. + // Passing Undefined here made Ink's inside get + // width = intrinsic instead of available, dropping chars at wrap boundaries. + // + // Two constraints on when this applies: + // - Width only. Height is never constrained during basis measurement — + // column containers must measure children at natural height so + // scrollable content can overflow (constraining height clips ScrollBox). + // - Subtree has a measure-func. Pure layout subtrees (no measure-func) + // with flex-grow children would grow into the AtMost constraint, + // inflating the basis (breaks YGMinMaxDimensionTest flex_grow_in_at_most + // where a flexGrow:1 child should stay at basis 0, not grow to 100). + let mainConstraint = NaN + let mainConstraintMode: MeasureMode = MeasureMode.Undefined + if (isMainRow && isDefined(availableMain) && hasMeasureFuncInSubtree(child)) { + mainConstraint = availableMain + mainConstraintMode = MeasureMode.AtMost + } + + const mw = isMainRow ? mainConstraint : crossConstraint + const mh = isMainRow ? crossConstraint : mainConstraint + const mwMode = isMainRow ? mainConstraintMode : crossConstraintMode + const mhMode = isMainRow ? crossConstraintMode : mainConstraintMode + + layoutNode(child, mw, mh, mwMode, mhMode, ownerWidth, ownerHeight, false) + const b = isMainRow ? child.layout.width : child.layout.height + child._fbBasis = b + child._fbOwnerW = ownerWidth + child._fbOwnerH = ownerHeight + child._fbAvailMain = availableMain + child._fbAvailCross = availableCross + child._fbCrossMode = crossMode + child._fbGen = _generation + return b +} + +function hasMeasureFuncInSubtree(node: Node): boolean { + if (node.measureFunc) return true + for (const c of node.children) { + if (hasMeasureFuncInSubtree(c)) return true + } + return false +} + +function resolveFlexibleLengths( + children: Node[], + availableInnerMain: number, + totalFlexBasis: number, + isMainRow: boolean, + ownerW: number, + ownerH: number, +): void { + // Multi-pass flex distribution per CSS flexbox spec §9.7 "Resolving Flexible + // Lengths": distribute free space, detect min/max violations, freeze all + // violators, redistribute among unfrozen children. Repeat until stable. + const n = children.length + const frozen: boolean[] = new Array(n).fill(false) + const initialFree = isDefined(availableInnerMain) + ? availableInnerMain - totalFlexBasis + : 0 + // Freeze inflexible items at their clamped basis + for (let i = 0; i < n; i++) { + const c = children[i]! + const clamped = boundAxis(c.style, isMainRow, c._flexBasis, ownerW, ownerH) + const inflexible = + !isDefined(availableInnerMain) || + (initialFree >= 0 ? c.style.flexGrow === 0 : c.style.flexShrink === 0) + if (inflexible) { + c._mainSize = Math.max(0, clamped) + frozen[i] = true + } else { + c._mainSize = c._flexBasis + } + } + // Iteratively distribute until no violations. Free space is recomputed each + // pass: initial free space minus the delta frozen children consumed beyond + // (or below) their basis. + const unclamped: number[] = new Array(n) + for (let iter = 0; iter <= n; iter++) { + let frozenDelta = 0 + let totalGrow = 0 + let totalShrinkScaled = 0 + let unfrozenCount = 0 + for (let i = 0; i < n; i++) { + const c = children[i]! + if (frozen[i]) { + frozenDelta += c._mainSize - c._flexBasis + } else { + totalGrow += c.style.flexGrow + totalShrinkScaled += c.style.flexShrink * c._flexBasis + unfrozenCount++ + } + } + if (unfrozenCount === 0) break + let remaining = initialFree - frozenDelta + // Spec §9.7 step 4c: if sum of flex factors < 1, only distribute + // initialFree × sum, not the full remaining space (partial flex). + if (remaining > 0 && totalGrow > 0 && totalGrow < 1) { + const scaled = initialFree * totalGrow + if (scaled < remaining) remaining = scaled + } else if (remaining < 0 && totalShrinkScaled > 0) { + let totalShrink = 0 + for (let i = 0; i < n; i++) { + if (!frozen[i]) totalShrink += children[i]!.style.flexShrink + } + if (totalShrink < 1) { + const scaled = initialFree * totalShrink + if (scaled > remaining) remaining = scaled + } + } + // Compute targets + violations for all unfrozen children + let totalViolation = 0 + for (let i = 0; i < n; i++) { + if (frozen[i]) continue + const c = children[i]! + let t = c._flexBasis + if (remaining > 0 && totalGrow > 0) { + t += (remaining * c.style.flexGrow) / totalGrow + } else if (remaining < 0 && totalShrinkScaled > 0) { + t += + (remaining * (c.style.flexShrink * c._flexBasis)) / totalShrinkScaled + } + unclamped[i] = t + const clamped = Math.max( + 0, + boundAxis(c.style, isMainRow, t, ownerW, ownerH), + ) + c._mainSize = clamped + totalViolation += clamped - t + } + // Freeze per spec §9.7 step 5: if totalViolation is zero freeze all; if + // positive freeze min-violators; if negative freeze max-violators. + if (totalViolation === 0) break + let anyFrozen = false + for (let i = 0; i < n; i++) { + if (frozen[i]) continue + const v = children[i]!._mainSize - unclamped[i]! + if ((totalViolation > 0 && v > 0) || (totalViolation < 0 && v < 0)) { + frozen[i] = true + anyFrozen = true + } + } + if (!anyFrozen) break + } +} + +function isStretchAlign(child: Node): boolean { + const p = child.parent + if (!p) return false + const align = + child.style.alignSelf === Align.Auto + ? p.style.alignItems + : child.style.alignSelf + return align === Align.Stretch +} + +function resolveChildAlign(parent: Node, child: Node): Align { + return child.style.alignSelf === Align.Auto + ? parent.style.alignItems + : child.style.alignSelf +} + +// Baseline of a node per CSS Flexbox §8.5 / yoga's YGBaseline. Leaf nodes +// (no children) use their own height. Containers recurse into the first +// baseline-aligned child on the first line (or the first flow child if none +// are baseline-aligned), returning that child's baseline + its top offset. +function calculateBaseline(node: Node): number { + let baselineChild: Node | null = null + for (const c of node.children) { + if (c._lineIndex > 0) break + if (c.style.positionType === PositionType.Absolute) continue + if (c.style.display === Display.None) continue + if ( + resolveChildAlign(node, c) === Align.Baseline || + c.isReferenceBaseline_ + ) { + baselineChild = c + break + } + if (baselineChild === null) baselineChild = c + } + if (baselineChild === null) return node.layout.height + return calculateBaseline(baselineChild) + baselineChild.layout.top +} + +// A container uses baseline layout only for row direction, when either +// align-items is baseline or any flow child has align-self: baseline. +function isBaselineLayout(node: Node, flowChildren: Node[]): boolean { + if (!isRow(node.style.flexDirection)) return false + if (node.style.alignItems === Align.Baseline) return true + for (const c of flowChildren) { + if (c.style.alignSelf === Align.Baseline) return true + } + return false +} + +function childMarginForAxis( + child: Node, + axis: FlexDirection, + ownerWidth: number, +): number { + if (!child._hasMargin) return 0 + const lead = resolveEdge(child.style.margin, leadingEdge(axis), ownerWidth) + const trail = resolveEdge(child.style.margin, trailingEdge(axis), ownerWidth) + return lead + trail +} + +function resolveGap(style: Style, gutter: Gutter, ownerSize: number): number { + let v = style.gap[gutter]! + if (v.unit === Unit.Undefined) v = style.gap[Gutter.All]! + const r = resolveValue(v, ownerSize) + return isDefined(r) ? Math.max(0, r) : 0 +} + +function boundAxis( + style: Style, + isWidth: boolean, + value: number, + ownerWidth: number, + ownerHeight: number, +): number { + const minV = isWidth ? style.minWidth : style.minHeight + const maxV = isWidth ? style.maxWidth : style.maxHeight + const minU = minV.unit + const maxU = maxV.unit + // Fast path: no min/max constraints set. Per CPU profile this is the + // overwhelmingly common case (~32k calls/layout on the 1000-node bench, + // nearly all with undefined min/max) — skipping 2× resolveValue + 2× isNaN + // that always no-op. Unit.Undefined = 0. + if (minU === 0 && maxU === 0) return value + const owner = isWidth ? ownerWidth : ownerHeight + let v = value + // Inlined resolveValue: Unit.Point=1, Unit.Percent=2. `m === m` is !isNaN. + if (maxU === 1) { + if (v > maxV.value) v = maxV.value + } else if (maxU === 2) { + const m = (maxV.value * owner) / 100 + if (m === m && v > m) v = m + } + if (minU === 1) { + if (v < minV.value) v = minV.value + } else if (minU === 2) { + const m = (minV.value * owner) / 100 + if (m === m && v < m) v = m + } + return v +} + +function zeroLayoutRecursive(node: Node): void { + for (const c of node.children) { + c.layout.left = 0 + c.layout.top = 0 + c.layout.width = 0 + c.layout.height = 0 + // Invalidate layout cache — without this, unhide → calculateLayout finds + // the child clean (!isDirty_) with _hasL intact, hits the cache at line + // ~1086, restores stale _lOutW/_lOutH, and returns early — skipping the + // child-positioning recursion. Grandchildren stay at (0,0,0,0) from the + // zeroing above and render invisible. isDirty_=true also gates _cN and + // _fbBasis via their (sameGen || !isDirty_) checks — _cGen/_fbGen freeze + // during hide so sameGen is false on unhide. + c.isDirty_ = true + c._hasL = false + c._hasM = false + zeroLayoutRecursive(c) + } +} + +function collectLayoutChildren(node: Node, flow: Node[], abs: Node[]): void { + // Partition a node's children into flow and absolute lists, flattening + // display:contents subtrees so their children are laid out as direct + // children of this node (per CSS display:contents spec — the box is removed + // from the layout tree but its children remain, lifted to the grandparent). + for (const c of node.children) { + const disp = c.style.display + if (disp === Display.None) { + c.layout.left = 0 + c.layout.top = 0 + c.layout.width = 0 + c.layout.height = 0 + zeroLayoutRecursive(c) + } else if (disp === Display.Contents) { + c.layout.left = 0 + c.layout.top = 0 + c.layout.width = 0 + c.layout.height = 0 + // Recurse — nested display:contents lifts all the way up. The contents + // node's own margin/padding/position/dimensions are ignored. + collectLayoutChildren(c, flow, abs) + } else if (c.style.positionType === PositionType.Absolute) { + abs.push(c) + } else { + flow.push(c) + } + } +} + +function roundLayout( + node: Node, + scale: number, + absLeft: number, + absTop: number, +): void { + if (scale === 0) return + const l = node.layout + const nodeLeft = l.left + const nodeTop = l.top + const nodeWidth = l.width + const nodeHeight = l.height + + const absNodeLeft = absLeft + nodeLeft + const absNodeTop = absTop + nodeTop + + // Upstream YGRoundValueToPixelGrid: text nodes (has measureFunc) floor their + // positions so wrapped text never starts past its allocated column. Width + // uses ceil-if-fractional to avoid clipping the last glyph. Non-text nodes + // use standard round. Matches yoga's PixelGrid.cpp — without this, justify + // center/space-evenly positions are off-by-one vs WASM and flex-shrink + // overflow places siblings at the wrong column. + const isText = node.measureFunc !== null + l.left = roundValue(nodeLeft, scale, false, isText) + l.top = roundValue(nodeTop, scale, false, isText) + + // Width/height rounded via absolute edges to avoid cumulative drift + const absRight = absNodeLeft + nodeWidth + const absBottom = absNodeTop + nodeHeight + const hasFracW = !isWholeNumber(nodeWidth * scale) + const hasFracH = !isWholeNumber(nodeHeight * scale) + l.width = + roundValue(absRight, scale, isText && hasFracW, isText && !hasFracW) - + roundValue(absNodeLeft, scale, false, isText) + l.height = + roundValue(absBottom, scale, isText && hasFracH, isText && !hasFracH) - + roundValue(absNodeTop, scale, false, isText) + + for (const c of node.children) { + roundLayout(c, scale, absNodeLeft, absNodeTop) + } +} + +function isWholeNumber(v: number): boolean { + const frac = v - Math.floor(v) + return frac < 0.0001 || frac > 0.9999 +} + +function roundValue( + v: number, + scale: number, + forceCeil: boolean, + forceFloor: boolean, +): number { + let scaled = v * scale + let frac = scaled - Math.floor(scaled) + if (frac < 0) frac += 1 + // Float-epsilon tolerance matches upstream YGDoubleEqual (1e-4) + if (frac < 0.0001) { + scaled = Math.floor(scaled) + } else if (frac > 0.9999) { + scaled = Math.ceil(scaled) + } else if (forceCeil) { + scaled = Math.ceil(scaled) + } else if (forceFloor) { + scaled = Math.floor(scaled) + } else { + // Round half-up (>= 0.5 goes up), per upstream + scaled = Math.floor(scaled) + (frac >= 0.4999 ? 1 : 0) + } + return scaled / scale +} + +// -- +// Helpers + +function parseDimension(v: number | string | undefined): Value { + if (v === undefined) return UNDEFINED_VALUE + if (v === 'auto') return AUTO_VALUE + if (typeof v === 'number') { + // WASM yoga's YGFloatIsUndefined treats NaN and ±Infinity as undefined. + // Ink passes height={Infinity} (e.g. LogSelector maxHeight default) and + // expects it to mean "unconstrained" — storing it as a literal point value + // makes the node height Infinity and breaks all downstream layout. + return Number.isFinite(v) ? pointValue(v) : UNDEFINED_VALUE + } + if (typeof v === 'string' && v.endsWith('%')) { + return percentValue(parseFloat(v)) + } + const n = parseFloat(v) + return isNaN(n) ? UNDEFINED_VALUE : pointValue(n) +} + +function physicalEdge(edge: Edge): number { + switch (edge) { + case Edge.Left: + case Edge.Start: + return EDGE_LEFT + case Edge.Top: + return EDGE_TOP + case Edge.Right: + case Edge.End: + return EDGE_RIGHT + case Edge.Bottom: + return EDGE_BOTTOM + default: + return EDGE_LEFT + } +} + +// -- +// Module API matching yoga-layout/load + +export type Yoga = { + Config: { + create(): Config + destroy(config: Config): void + } + Node: { + create(config?: Config): Node + createDefault(): Node + createWithConfig(config: Config): Node + destroy(node: Node): void + } +} + +const YOGA_INSTANCE: Yoga = { + Config: { + create: createConfig, + destroy() {}, + }, + Node: { + create: (config?: Config) => new Node(config), + createDefault: () => new Node(), + createWithConfig: (config: Config) => new Node(config), + destroy() {}, + }, +} + +export function loadYoga(): Promise { + return Promise.resolve(YOGA_INSTANCE) +} + +export default YOGA_INSTANCE diff --git a/src/ink/hooks/use-animation-frame.ts b/packages/@ant/ink/src/hooks/use-animation-frame.ts similarity index 97% rename from src/ink/hooks/use-animation-frame.ts rename to packages/@ant/ink/src/hooks/use-animation-frame.ts index d4dd38a16..a6ef6b4e2 100644 --- a/src/ink/hooks/use-animation-frame.ts +++ b/packages/@ant/ink/src/hooks/use-animation-frame.ts @@ -1,6 +1,6 @@ import { useContext, useEffect, useState } from 'react' import { ClockContext } from '../components/ClockContext.js' -import type { DOMElement } from '../dom.js' +import type { DOMElement } from '../core/dom.js' import { useTerminalViewport } from './use-terminal-viewport.js' /** diff --git a/src/ink/hooks/use-app.ts b/packages/@ant/ink/src/hooks/use-app.ts similarity index 100% rename from src/ink/hooks/use-app.ts rename to packages/@ant/ink/src/hooks/use-app.ts diff --git a/src/ink/hooks/use-declared-cursor.ts b/packages/@ant/ink/src/hooks/use-declared-cursor.ts similarity index 98% rename from src/ink/hooks/use-declared-cursor.ts rename to packages/@ant/ink/src/hooks/use-declared-cursor.ts index e49668b38..d1aea014c 100644 --- a/src/ink/hooks/use-declared-cursor.ts +++ b/packages/@ant/ink/src/hooks/use-declared-cursor.ts @@ -1,6 +1,6 @@ import { useCallback, useContext, useLayoutEffect, useRef } from 'react' import CursorDeclarationContext from '../components/CursorDeclarationContext.js' -import type { DOMElement } from '../dom.js' +import type { DOMElement } from '../core/dom.js' /** * Declares where the terminal cursor should be parked after each frame. diff --git a/src/ink/hooks/use-input.ts b/packages/@ant/ink/src/hooks/use-input.ts similarity index 97% rename from src/ink/hooks/use-input.ts rename to packages/@ant/ink/src/hooks/use-input.ts index 7cf75b311..0d5cd55b7 100644 --- a/src/ink/hooks/use-input.ts +++ b/packages/@ant/ink/src/hooks/use-input.ts @@ -1,6 +1,6 @@ import { useEffect, useLayoutEffect } from 'react' import { useEventCallback } from 'usehooks-ts' -import type { InputEvent, Key } from '../events/input-event.js' +import type { InputEvent, Key } from '../core/events/input-event.js' import useStdin from './use-stdin.js' type Handler = (input: string, key: Key, event: InputEvent) => void diff --git a/src/ink/hooks/use-interval.ts b/packages/@ant/ink/src/hooks/use-interval.ts similarity index 100% rename from src/ink/hooks/use-interval.ts rename to packages/@ant/ink/src/hooks/use-interval.ts diff --git a/src/ink/hooks/use-search-highlight.ts b/packages/@ant/ink/src/hooks/use-search-highlight.ts similarity index 92% rename from src/ink/hooks/use-search-highlight.ts rename to packages/@ant/ink/src/hooks/use-search-highlight.ts index ce9fc364d..6c79d5ebe 100644 --- a/src/ink/hooks/use-search-highlight.ts +++ b/packages/@ant/ink/src/hooks/use-search-highlight.ts @@ -1,8 +1,8 @@ import { useContext, useMemo } from 'react' import StdinContext from '../components/StdinContext.js' -import type { DOMElement } from '../dom.js' -import instances from '../instances.js' -import type { MatchPosition } from '../render-to-screen.js' +import type { DOMElement } from '../core/dom.js' +import instances from '../core/instances.js' +import type { MatchPosition } from '../core/render-to-screen.js' /** * Set the search highlight query on the Ink instance. Non-empty → all diff --git a/src/ink/hooks/use-selection.ts b/packages/@ant/ink/src/hooks/use-selection.ts similarity index 98% rename from src/ink/hooks/use-selection.ts rename to packages/@ant/ink/src/hooks/use-selection.ts index f7e1d4571..1a29e384d 100644 --- a/src/ink/hooks/use-selection.ts +++ b/packages/@ant/ink/src/hooks/use-selection.ts @@ -1,11 +1,11 @@ import { useContext, useMemo, useSyncExternalStore } from 'react' import StdinContext from '../components/StdinContext.js' -import instances from '../instances.js' +import instances from '../core/instances.js' import { type FocusMove, type SelectionState, shiftAnchor, -} from '../selection.js' +} from '../core/selection.js' /** * Access to text selection operations on the Ink instance (fullscreen only). diff --git a/src/ink/hooks/use-stdin.ts b/packages/@ant/ink/src/hooks/use-stdin.ts similarity index 100% rename from src/ink/hooks/use-stdin.ts rename to packages/@ant/ink/src/hooks/use-stdin.ts diff --git a/src/ink/hooks/use-tab-status.ts b/packages/@ant/ink/src/hooks/use-tab-status.ts similarity index 93% rename from src/ink/hooks/use-tab-status.ts rename to packages/@ant/ink/src/hooks/use-tab-status.ts index be6014204..cfd32e253 100644 --- a/src/ink/hooks/use-tab-status.ts +++ b/packages/@ant/ink/src/hooks/use-tab-status.ts @@ -4,9 +4,9 @@ import { supportsTabStatus, tabStatus, wrapForMultiplexer, -} from '../termio/osc.js' -import type { Color } from '../termio/types.js' -import { TerminalWriteContext } from '../useTerminalNotification.js' +} from '../core/termio/osc.js' +import type { Color } from '../core/termio/types.js' +import { TerminalWriteContext } from './useTerminalNotification.js' export type TabStatusKind = 'idle' | 'busy' | 'waiting' diff --git a/src/ink/hooks/use-terminal-focus.ts b/packages/@ant/ink/src/hooks/use-terminal-focus.ts similarity index 100% rename from src/ink/hooks/use-terminal-focus.ts rename to packages/@ant/ink/src/hooks/use-terminal-focus.ts diff --git a/src/ink/hooks/use-terminal-title.ts b/packages/@ant/ink/src/hooks/use-terminal-title.ts similarity index 88% rename from src/ink/hooks/use-terminal-title.ts rename to packages/@ant/ink/src/hooks/use-terminal-title.ts index d820cd7ae..4645179a7 100644 --- a/src/ink/hooks/use-terminal-title.ts +++ b/packages/@ant/ink/src/hooks/use-terminal-title.ts @@ -1,7 +1,7 @@ import { useContext, useEffect } from 'react' import stripAnsi from 'strip-ansi' -import { OSC, osc } from '../termio/osc.js' -import { TerminalWriteContext } from '../useTerminalNotification.js' +import { OSC, osc } from '../core/termio/osc.js' +import { TerminalWriteContext } from './useTerminalNotification.js' /** * Declaratively set the terminal tab/window title. diff --git a/src/ink/hooks/use-terminal-viewport.ts b/packages/@ant/ink/src/hooks/use-terminal-viewport.ts similarity index 98% rename from src/ink/hooks/use-terminal-viewport.ts rename to packages/@ant/ink/src/hooks/use-terminal-viewport.ts index 91193bf73..82d96f341 100644 --- a/src/ink/hooks/use-terminal-viewport.ts +++ b/packages/@ant/ink/src/hooks/use-terminal-viewport.ts @@ -1,6 +1,6 @@ import { useCallback, useContext, useLayoutEffect, useRef } from 'react' import { TerminalSizeContext } from '../components/TerminalSizeContext.js' -import type { DOMElement } from '../dom.js' +import type { DOMElement } from '../core/dom.js' type ViewportEntry = { /** diff --git a/packages/@ant/ink/src/hooks/useDoublePress.ts b/packages/@ant/ink/src/hooks/useDoublePress.ts new file mode 100644 index 000000000..7844fbd66 --- /dev/null +++ b/packages/@ant/ink/src/hooks/useDoublePress.ts @@ -0,0 +1,62 @@ +// Creates a function that calls one function on the first call and another +// function on the second call within a certain timeout + +import { useCallback, useEffect, useRef } from 'react' + +export const DOUBLE_PRESS_TIMEOUT_MS = 800 + +export function useDoublePress( + setPending: (pending: boolean) => void, + onDoublePress: () => void, + onFirstPress?: () => void, +): () => void { + const lastPressRef = useRef(0) + const timeoutRef = useRef(undefined) + + const clearTimeoutSafe = useCallback(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + timeoutRef.current = undefined + } + }, []) + + // Cleanup timeout on unmount + useEffect(() => { + return () => { + clearTimeoutSafe() + } + }, [clearTimeoutSafe]) + + return useCallback(() => { + const now = Date.now() + const timeSinceLastPress = now - lastPressRef.current + const isDoublePress = + timeSinceLastPress <= DOUBLE_PRESS_TIMEOUT_MS && + timeoutRef.current !== undefined + + if (isDoublePress) { + // Double press detected + clearTimeoutSafe() + setPending(false) + onDoublePress() + } else { + // First press + onFirstPress?.() + setPending(true) + + // Clear any existing timeout and set new one + clearTimeoutSafe() + timeoutRef.current = setTimeout( + (setPending, timeoutRef) => { + setPending(false) + timeoutRef.current = undefined + }, + DOUBLE_PRESS_TIMEOUT_MS, + setPending, + timeoutRef, + ) + } + + lastPressRef.current = now + }, [setPending, onDoublePress, onFirstPress, clearTimeoutSafe]) +} diff --git a/packages/@ant/ink/src/hooks/useExitOnCtrlCD.ts b/packages/@ant/ink/src/hooks/useExitOnCtrlCD.ts new file mode 100644 index 000000000..442d4fad3 --- /dev/null +++ b/packages/@ant/ink/src/hooks/useExitOnCtrlCD.ts @@ -0,0 +1,95 @@ +/** + * 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 | 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({ + 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 +} diff --git a/packages/@ant/ink/src/hooks/useMinDisplayTime.ts b/packages/@ant/ink/src/hooks/useMinDisplayTime.ts new file mode 100644 index 000000000..587b96938 --- /dev/null +++ b/packages/@ant/ink/src/hooks/useMinDisplayTime.ts @@ -0,0 +1,35 @@ +import { useEffect, useRef, useState } from 'react' + +/** + * Throttles a value so each distinct value stays visible for at least `minMs`. + * Prevents fast-cycling progress text from flickering past before it's readable. + * + * Unlike debounce (wait for quiet) or throttle (limit rate), this guarantees + * each value gets its minimum screen time before being replaced. + */ +export function useMinDisplayTime(value: T, minMs: number): T { + const [displayed, setDisplayed] = useState(value) + const lastShownAtRef = useRef(0) + + useEffect(() => { + const elapsed = Date.now() - lastShownAtRef.current + if (elapsed >= minMs) { + lastShownAtRef.current = Date.now() + setDisplayed(value) + return + } + const timer = setTimeout( + (shownAtRef, setFn, v) => { + shownAtRef.current = Date.now() + setFn(v) + }, + minMs - elapsed, + lastShownAtRef, + setDisplayed, + value, + ) + return () => clearTimeout(timer) + }, [value, minMs]) + + return displayed +} diff --git a/packages/@ant/ink/src/hooks/useSearchInput.ts b/packages/@ant/ink/src/hooks/useSearchInput.ts new file mode 100644 index 000000000..9935256c7 --- /dev/null +++ b/packages/@ant/ink/src/hooks/useSearchInput.ts @@ -0,0 +1,222 @@ +/** + * 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 } +} diff --git a/src/ink/useTerminalNotification.ts b/packages/@ant/ink/src/hooks/useTerminalNotification.ts similarity index 96% rename from src/ink/useTerminalNotification.ts rename to packages/@ant/ink/src/hooks/useTerminalNotification.ts index 90e53eb63..083a94ad0 100644 --- a/src/ink/useTerminalNotification.ts +++ b/packages/@ant/ink/src/hooks/useTerminalNotification.ts @@ -1,7 +1,7 @@ import { createContext, useCallback, useContext, useMemo } from 'react' -import { isProgressReportingAvailable, type Progress } from './terminal.js' -import { BEL } from './termio/ansi.js' -import { ITERM2, OSC, osc, PROGRESS, wrapForMultiplexer } from './termio/osc.js' +import { isProgressReportingAvailable, type Progress } from '../core/terminal.js' +import { BEL } from '../core/termio/ansi.js' +import { ITERM2, OSC, osc, PROGRESS, wrapForMultiplexer } from '../core/termio/osc.js' type WriteRaw = (data: string) => void diff --git a/packages/@ant/ink/src/hooks/useTerminalSize.ts b/packages/@ant/ink/src/hooks/useTerminalSize.ts new file mode 100644 index 000000000..bccc5bef6 --- /dev/null +++ b/packages/@ant/ink/src/hooks/useTerminalSize.ts @@ -0,0 +1,15 @@ +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 +} diff --git a/packages/@ant/ink/src/hooks/useTimeout.ts b/packages/@ant/ink/src/hooks/useTimeout.ts new file mode 100644 index 000000000..faed236af --- /dev/null +++ b/packages/@ant/ink/src/hooks/useTimeout.ts @@ -0,0 +1,14 @@ +import { useEffect, useState } from 'react' + +export function useTimeout(delay: number, resetTrigger?: number): boolean { + const [isElapsed, setIsElapsed] = useState(false) + + useEffect(() => { + setIsElapsed(false) + const timer = setTimeout(setIsElapsed, delay, true) + + return () => clearTimeout(timer) + }, [delay, resetTrigger]) + + return isElapsed +} diff --git a/packages/@ant/ink/src/index.ts b/packages/@ant/ink/src/index.ts new file mode 100644 index 000000000..9d9453002 --- /dev/null +++ b/packages/@ant/ink/src/index.ts @@ -0,0 +1,180 @@ +/** + * @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' + + +// ============================================================ +// Keybindings +// ============================================================ +export { useKeybinding, useKeybindings } from './keybindings/useKeybinding.js' +export { + KeybindingProvider, + useKeybindingContext, + useOptionalKeybindingContext, + useRegisterKeybindingContext, +} from './keybindings/KeybindingContext.js' +export { + resolveKey, + resolveKeyWithChordState, + getBindingDisplayText, + keystrokesEqual, + type ResolveResult, + type ChordResolveResult, +} from './keybindings/resolver.js' +export { + parseKeystroke, + parseChord, + keystrokeToString, + chordToString, + keystrokeToDisplayString, + chordToDisplayString, + parseBindings, +} from './keybindings/parser.js' +export { + getKeyName, + matchesKeystroke, + matchesBinding, +} from './keybindings/match.js' +export { + KeybindingSetup, + type KeybindingSetupProps, +} from './keybindings/KeybindingSetup.js' +export type { + ParsedBinding, + ParsedKeystroke, + KeybindingContextName, + KeybindingBlock, + Chord, + KeybindingAction, + KeybindingWarningType, + KeybindingWarning, + KeybindingsLoadResult, +} from './keybindings/types.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 { useTerminalSize } from './hooks/useTerminalSize.js' +export { useTimeout } from './hooks/useTimeout.js' +export { useMinDisplayTime } from './hooks/useMinDisplayTime.js' +export { useDoublePress, DOUBLE_PRESS_TIMEOUT_MS } from './hooks/useDoublePress.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, TextHoverColorContext } from './theme/ThemedText.js' +export type { Props as TextProps } from './theme/ThemedText.js' +export { color } from './theme/color.js' + +// Theme sub-components +export { SearchBox } from './theme/SearchBox.js' +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, Tab, useTabsWidth, useTabHeaderFocus } from './theme/Tabs.js' +export { Byline } from './theme/Byline.js' +export { KeyboardShortcutHint } from './theme/KeyboardShortcutHint.js' diff --git a/packages/@ant/ink/src/keybindings/KeybindingContext.tsx b/packages/@ant/ink/src/keybindings/KeybindingContext.tsx new file mode 100644 index 000000000..8cd56a408 --- /dev/null +++ b/packages/@ant/ink/src/keybindings/KeybindingContext.tsx @@ -0,0 +1,225 @@ +import React, { + createContext, + type RefObject, + useContext, + useLayoutEffect, + useMemo, +} from 'react' +import type { Key } from '../core/events/input-event.js' +import { + type ChordResolveResult, + getBindingDisplayText, + resolveKeyWithChordState, +} from './resolver.js' +import type { + KeybindingContextName, + ParsedBinding, + ParsedKeystroke, +} from './types.js' + +/** Handler registration for action callbacks */ +type HandlerRegistration = { + action: string + context: KeybindingContextName + handler: () => void +} + +type KeybindingContextValue = { + /** Resolve a key input to an action name (with chord support) */ + resolve: ( + input: string, + key: Key, + activeContexts: KeybindingContextName[], + ) => ChordResolveResult + + /** Update the pending chord state */ + setPendingChord: (pending: ParsedKeystroke[] | null) => void + + /** Get display text for an action (e.g., "ctrl+t") */ + getDisplayText: ( + action: string, + context: KeybindingContextName, + ) => string | undefined + + /** All parsed bindings (for help display) */ + bindings: ParsedBinding[] + + /** Current pending chord keystrokes (null if not in a chord) */ + pendingChord: ParsedKeystroke[] | null + + /** Currently active keybinding contexts (for priority resolution) */ + activeContexts: Set + + /** Register a context as active (call on mount) */ + registerActiveContext: (context: KeybindingContextName) => void + + /** Unregister a context (call on unmount) */ + unregisterActiveContext: (context: KeybindingContextName) => void + + /** Register a handler for an action (used by useKeybinding) */ + registerHandler: (registration: HandlerRegistration) => () => void + + /** Invoke all handlers for an action (used by ChordInterceptor) */ + invokeAction: (action: string) => boolean +} + +const KeybindingContext = createContext(null) + +type ProviderProps = { + bindings: ParsedBinding[] + /** Ref for immediate access to pending chord (avoids React state delay) */ + pendingChordRef: RefObject + /** State value for re-renders (UI updates) */ + pendingChord: ParsedKeystroke[] | null + setPendingChord: (pending: ParsedKeystroke[] | null) => void + activeContexts: Set + registerActiveContext: (context: KeybindingContextName) => void + unregisterActiveContext: (context: KeybindingContextName) => void + /** Ref to handler registry (used by ChordInterceptor) */ + handlerRegistryRef: RefObject>> + children: React.ReactNode +} + +export function KeybindingProvider({ + bindings, + pendingChordRef, + pendingChord, + setPendingChord, + activeContexts, + registerActiveContext, + unregisterActiveContext, + handlerRegistryRef, + children, +}: ProviderProps): React.ReactNode { + const value = useMemo(() => { + const getDisplay = (action: string, context: KeybindingContextName) => + getBindingDisplayText(action, context, bindings) + + // Register a handler for an action + const registerHandler = (registration: HandlerRegistration) => { + const registry = handlerRegistryRef.current + if (!registry) return () => {} + + if (!registry.has(registration.action)) { + registry.set(registration.action, new Set()) + } + registry.get(registration.action)!.add(registration) + + // Return unregister function + return () => { + const handlers = registry.get(registration.action) + if (handlers) { + handlers.delete(registration) + if (handlers.size === 0) { + registry.delete(registration.action) + } + } + } + } + + // Invoke all handlers for an action + const invokeAction = (action: string): boolean => { + const registry = handlerRegistryRef.current + if (!registry) return false + + const handlers = registry.get(action) + if (!handlers || handlers.size === 0) return false + + // Find handlers whose context is active + for (const registration of handlers) { + if (activeContexts.has(registration.context)) { + registration.handler() + return true + } + } + return false + } + + return { + // Use ref for immediate access to pending chord, avoiding React state delay + // This is critical for chord sequences where the second key might be pressed + // before React re-renders with the updated pendingChord state + resolve: (input, key, contexts) => + resolveKeyWithChordState( + input, + key, + contexts, + bindings, + pendingChordRef.current, + ), + setPendingChord, + getDisplayText: getDisplay, + bindings, + pendingChord, + activeContexts, + registerActiveContext, + unregisterActiveContext, + registerHandler, + invokeAction, + } + }, [ + bindings, + pendingChordRef, + pendingChord, + setPendingChord, + activeContexts, + registerActiveContext, + unregisterActiveContext, + handlerRegistryRef, + ]) + + return ( + + {children} + + ) +} + +export function useKeybindingContext(): KeybindingContextValue { + const ctx = useContext(KeybindingContext) + if (!ctx) { + throw new Error( + 'useKeybindingContext must be used within KeybindingProvider', + ) + } + return ctx +} + +/** + * Optional hook that returns undefined outside of KeybindingProvider. + * Useful for components that may render before provider is available. + */ +export function useOptionalKeybindingContext(): KeybindingContextValue | null { + return useContext(KeybindingContext) +} + +/** + * Hook to register a keybinding context as active while the component is mounted. + * + * When a context is registered, its keybindings take precedence over Global bindings. + * This allows context-specific bindings (like ThemePicker's ctrl+t) to override + * global bindings (like the todo toggle) when the context is active. + * + * @example + * ```tsx + * function ThemePicker() { + * useRegisterKeybindingContext('ThemePicker') + * // Now ThemePicker's ctrl+t binding takes precedence over Global + * } + * ``` + */ +export function useRegisterKeybindingContext( + context: KeybindingContextName, + isActive: boolean = true, +): void { + const keybindingContext = useOptionalKeybindingContext() + + useLayoutEffect(() => { + if (!keybindingContext || !isActive) return + + keybindingContext.registerActiveContext(context) + return () => { + keybindingContext.unregisterActiveContext(context) + } + }, [context, keybindingContext, isActive]) +} diff --git a/packages/@ant/ink/src/keybindings/KeybindingSetup.tsx b/packages/@ant/ink/src/keybindings/KeybindingSetup.tsx new file mode 100644 index 000000000..36acd9154 --- /dev/null +++ b/packages/@ant/ink/src/keybindings/KeybindingSetup.tsx @@ -0,0 +1,320 @@ +/** + * Generic keybinding setup component for integrating KeybindingProvider into an app. + * + * Provides chord state management, a ChordInterceptor, and the KeybindingProvider + * wrapper. App-specific dependencies (binding loading, change subscription, + * warning display, debug logging) are injected via props. + */ +import React, { useCallback, useEffect, useRef, useState } from 'react' +import type { InputEvent } from '../core/events/input-event.js' +// ChordInterceptor intentionally uses useInput to intercept all keystrokes before +// other handlers process them - this is required for chord sequence support +// eslint-disable-next-line custom-rules/prefer-use-keybindings +import useInput, { type Key } from '../hooks/use-input.js' +import { KeybindingProvider } from './KeybindingContext.js' +import { resolveKeyWithChordState } from './resolver.js' +import type { + KeybindingContextName, + KeybindingsLoadResult, + ParsedBinding, + ParsedKeystroke, + KeybindingWarning, +} from './types.js' + +/** + * Timeout for chord sequences in milliseconds. + * If the user doesn't complete the chord within this time, it's cancelled. + */ +const CHORD_TIMEOUT_MS = 1000 + +export type KeybindingSetupProps = { + children: React.ReactNode + + /** Load bindings synchronously for initial render */ + loadBindings: () => KeybindingsLoadResult + + /** Subscribe to binding changes; return an unsubscribe function */ + subscribeToChanges: ( + callback: (result: KeybindingsLoadResult) => void, + ) => () => void + + /** Initialize any file watcher (idempotent). Called once on mount. */ + initWatcher?: () => void | Promise + + /** Optional callback when warnings are emitted (initial load or reload) */ + onWarnings?: (warnings: KeybindingWarning[], isReload: boolean) => void + + /** Optional debug logger */ + onDebugLog?: (message: string) => void +} + +export function KeybindingSetup({ + children, + loadBindings, + subscribeToChanges, + initWatcher, + onWarnings, + onDebugLog, +}: KeybindingSetupProps): React.ReactNode { + // Load bindings synchronously for initial render + const [loadResult, setLoadResult] = useState(() => { + const result = loadBindings() + onDebugLog?.( + `[keybindings] KeybindingSetup initialized with ${result.bindings.length} bindings, ${result.warnings.length} warnings`, + ) + return result + }) + + const { bindings, warnings } = loadResult + + // Track if this is a reload (not initial load) + const [isReload, setIsReload] = useState(false) + + // Notify about warnings + useEffect(() => { + onWarnings?.(warnings, isReload) + }, [warnings, isReload, onWarnings]) + + // Chord state management - use ref for immediate access, state for re-renders + const pendingChordRef = useRef(null) + const [pendingChord, setPendingChordState] = useState< + ParsedKeystroke[] | null + >(null) + const chordTimeoutRef = useRef(null) + + // Handler registry for action callbacks (used by ChordInterceptor to invoke handlers) + const handlerRegistryRef = useRef( + new Map< + string, + Set<{ + action: string + context: KeybindingContextName + handler: () => void + }> + >(), + ) + + // Active context tracking for keybinding priority resolution + const activeContextsRef = useRef>(new Set()) + + const registerActiveContext = useCallback( + (context: KeybindingContextName) => { + activeContextsRef.current.add(context) + }, + [], + ) + + const unregisterActiveContext = useCallback( + (context: KeybindingContextName) => { + activeContextsRef.current.delete(context) + }, + [], + ) + + // Clear chord timeout when component unmounts or chord changes + const clearChordTimeout = useCallback(() => { + if (chordTimeoutRef.current) { + clearTimeout(chordTimeoutRef.current) + chordTimeoutRef.current = null + } + }, []) + + // Wrapper for setPendingChord that manages timeout and syncs ref+state + const setPendingChord = useCallback( + (pending: ParsedKeystroke[] | null) => { + clearChordTimeout() + + if (pending !== null) { + // Set timeout to cancel chord if not completed + chordTimeoutRef.current = setTimeout( + (pendingChordRef, setPendingChordState) => { + onDebugLog?.('[keybindings] Chord timeout - cancelling') + pendingChordRef.current = null + setPendingChordState(null) + }, + CHORD_TIMEOUT_MS, + pendingChordRef, + setPendingChordState, + ) + } + + // Update ref immediately for synchronous access in resolve() + pendingChordRef.current = pending + // Update state to trigger re-renders for UI updates + setPendingChordState(pending) + }, + [clearChordTimeout, onDebugLog], + ) + + useEffect(() => { + // Initialize file watcher (idempotent - only runs once) + void initWatcher?.() + + // Subscribe to changes + const unsubscribe = subscribeToChanges(result => { + // Any callback invocation is a reload since initial load happens + // synchronously in useState, not via this subscription + setIsReload(true) + + setLoadResult(result) + onDebugLog?.( + `[keybindings] Reloaded: ${result.bindings.length} bindings, ${result.warnings.length} warnings`, + ) + }) + + return () => { + unsubscribe() + clearChordTimeout() + } + }, [subscribeToChanges, initWatcher, clearChordTimeout, onDebugLog]) + + return ( + + + {children} + + ) +} + +/** + * Global chord interceptor that registers useInput FIRST (before children). + * + * This component intercepts keystrokes that are part of chord sequences and + * stops propagation before other handlers (like PromptInput) can see them. + * + * Without this, the second key of a chord (e.g., 'r' in "ctrl+c r") would be + * captured by PromptInput and added to the input field before the keybinding + * system could recognize it as completing a chord. + */ +type HandlerRegistration = { + action: string + context: KeybindingContextName + handler: () => void +} + +function ChordInterceptor({ + bindings, + pendingChordRef, + setPendingChord, + activeContexts, + handlerRegistryRef, +}: { + bindings: ParsedBinding[] + pendingChordRef: React.RefObject + setPendingChord: (pending: ParsedKeystroke[] | null) => void + activeContexts: Set + handlerRegistryRef: React.RefObject>> +}): null { + const handleInput = useCallback( + (input: string, key: Key, event: InputEvent) => { + // Wheel events can never start chord sequences — scroll:lineUp/Down are + // single-key bindings handled by per-component useKeybindings hooks, not + // here. Skip the registry scan. Mid-chord wheel still falls through so + // scrolling cancels the pending chord like any other non-matching key. + if ((key.wheelUp || key.wheelDown) && pendingChordRef.current === null) { + return + } + + // Build context list from registered handlers + activeContexts + Global + const registry = handlerRegistryRef.current + const handlerContexts = new Set() + if (registry) { + for (const handlers of registry.values()) { + for (const registration of handlers) { + handlerContexts.add(registration.context) + } + } + } + const contexts: KeybindingContextName[] = [ + ...handlerContexts, + ...activeContexts, + 'Global', + ] + + // Track whether we're completing a chord (pending was non-null) + const wasInChord = pendingChordRef.current !== null + + // Check if this keystroke is part of a chord sequence + const result = resolveKeyWithChordState( + input, + key, + contexts, + bindings, + pendingChordRef.current, + ) + + switch (result.type) { + case 'chord_started': + // This key starts a chord - store pending state and stop propagation + setPendingChord(result.pending) + event.stopImmediatePropagation() + break + + case 'match': { + // Clear pending state + setPendingChord(null) + + // Only invoke handlers and stop propagation for chord completions + // (multi-keystroke sequences). Single-keystroke matches should propagate + // to per-hook handlers to avoid interfering with other input handling. + if (wasInChord) { + const contextsSet = new Set(contexts) + if (registry) { + const handlers = registry.get(result.action) + if (handlers && handlers.size > 0) { + for (const registration of handlers) { + if (contextsSet.has(registration.context)) { + registration.handler() + event.stopImmediatePropagation() + break + } + } + } + } + } + break + } + + case 'chord_cancelled': + setPendingChord(null) + event.stopImmediatePropagation() + break + + case 'unbound': + setPendingChord(null) + event.stopImmediatePropagation() + break + + case 'none': + // No chord involvement - let other handlers process + break + } + }, + [ + bindings, + pendingChordRef, + setPendingChord, + activeContexts, + handlerRegistryRef, + ], + ) + + useInput(handleInput) + + return null +} diff --git a/packages/@ant/ink/src/keybindings/match.ts b/packages/@ant/ink/src/keybindings/match.ts new file mode 100644 index 000000000..55e787b9f --- /dev/null +++ b/packages/@ant/ink/src/keybindings/match.ts @@ -0,0 +1,120 @@ +import type { Key } from '../core/events/input-event.js' +import type { ParsedBinding, ParsedKeystroke } from './types.js' + +/** + * Modifier keys from Ink's Key type that we care about for matching. + * Note: `fn` from Key is intentionally excluded as it's rarely used and + * not commonly configurable in terminal applications. + */ +type InkModifiers = Pick + +/** + * Extract modifiers from an Ink Key object. + * This function ensures we're explicitly extracting the modifiers we care about. + */ +function getInkModifiers(key: Key): InkModifiers { + return { + ctrl: key.ctrl, + shift: key.shift, + meta: key.meta, + super: key.super, + } +} + +/** + * Extract the normalized key name from Ink's Key + input. + * Maps Ink's boolean flags (key.escape, key.return, etc.) to string names + * that match our ParsedKeystroke.key format. + */ +export function getKeyName(input: string, key: Key): string | null { + if (key.escape) return 'escape' + if (key.return) return 'enter' + if (key.tab) return 'tab' + if (key.backspace) return 'backspace' + if (key.delete) return 'delete' + if (key.upArrow) return 'up' + if (key.downArrow) return 'down' + if (key.leftArrow) return 'left' + if (key.rightArrow) return 'right' + if (key.pageUp) return 'pageup' + if (key.pageDown) return 'pagedown' + if (key.wheelUp) return 'wheelup' + if (key.wheelDown) return 'wheeldown' + if (key.home) return 'home' + if (key.end) return 'end' + if (input.length === 1) return input.toLowerCase() + return null +} + +/** + * Check if all modifiers match between Ink Key and ParsedKeystroke. + * + * Alt and Meta: Ink historically set `key.meta` for Alt/Option. A `meta` + * modifier in config is treated as an alias for `alt` — both match when + * `key.meta` is true. + * + * Super (Cmd/Win): distinct from alt/meta. Only arrives via the kitty + * keyboard protocol on supporting terminals. A `cmd`/`super` binding will + * simply never fire on terminals that don't send it. + */ +function modifiersMatch( + inkMods: InkModifiers, + target: ParsedKeystroke, +): boolean { + // Check ctrl modifier + if (inkMods.ctrl !== target.ctrl) return false + + // Check shift modifier + if (inkMods.shift !== target.shift) return false + + // Alt and meta both map to key.meta in Ink (terminal limitation) + // So we check if EITHER alt OR meta is required in target + const targetNeedsMeta = target.alt || target.meta + if (inkMods.meta !== targetNeedsMeta) return false + + // Super (cmd/win) is a distinct modifier from alt/meta + if (inkMods.super !== target.super) return false + + return true +} + +/** + * Check if a ParsedKeystroke matches the given Ink input + Key. + * + * The display text will show platform-appropriate names (opt on macOS, alt elsewhere). + */ +export function matchesKeystroke( + input: string, + key: Key, + target: ParsedKeystroke, +): boolean { + const keyName = getKeyName(input, key) + if (keyName !== target.key) return false + + const inkMods = getInkModifiers(key) + + // QUIRK: Ink sets key.meta=true when escape is pressed (see input-event.ts). + // This is a legacy behavior from how escape sequences work in terminals. + // We need to ignore the meta modifier when matching the escape key itself, + // otherwise bindings like "escape" (without modifiers) would never match. + if (key.escape) { + return modifiersMatch({ ...inkMods, meta: false }, target) + } + + return modifiersMatch(inkMods, target) +} + +/** + * Check if Ink's Key + input matches a parsed binding's first keystroke. + * For single-keystroke bindings only (Phase 1). + */ +export function matchesBinding( + input: string, + key: Key, + binding: ParsedBinding, +): boolean { + if (binding.chord.length !== 1) return false + const keystroke = binding.chord[0] + if (!keystroke) return false + return matchesKeystroke(input, key, keystroke) +} diff --git a/packages/@ant/ink/src/keybindings/parser.ts b/packages/@ant/ink/src/keybindings/parser.ts new file mode 100644 index 000000000..ead1a1a8f --- /dev/null +++ b/packages/@ant/ink/src/keybindings/parser.ts @@ -0,0 +1,203 @@ +import type { + Chord, + KeybindingBlock, + ParsedBinding, + ParsedKeystroke, +} from './types.js' + +/** + * Parse a keystroke string like "ctrl+shift+k" into a ParsedKeystroke. + * Supports various modifier aliases (ctrl/control, alt/opt/option/meta, + * cmd/command/super/win). + */ +export function parseKeystroke(input: string): ParsedKeystroke { + const parts = input.split('+') + const keystroke: ParsedKeystroke = { + key: '', + ctrl: false, + alt: false, + shift: false, + meta: false, + super: false, + } + for (const part of parts) { + const lower = part.toLowerCase() + switch (lower) { + case 'ctrl': + case 'control': + keystroke.ctrl = true + break + case 'alt': + case 'opt': + case 'option': + keystroke.alt = true + break + case 'shift': + keystroke.shift = true + break + case 'meta': + keystroke.meta = true + break + case 'cmd': + case 'command': + case 'super': + case 'win': + keystroke.super = true + break + case 'esc': + keystroke.key = 'escape' + break + case 'return': + keystroke.key = 'enter' + break + case 'space': + keystroke.key = ' ' + break + case '↑': + keystroke.key = 'up' + break + case '↓': + keystroke.key = 'down' + break + case '←': + keystroke.key = 'left' + break + case '→': + keystroke.key = 'right' + break + default: + keystroke.key = lower + break + } + } + + return keystroke +} + +/** + * Parse a chord string like "ctrl+k ctrl+s" into an array of ParsedKeystrokes. + */ +export function parseChord(input: string): Chord { + // A lone space character IS the space key binding, not a separator + if (input === ' ') return [parseKeystroke('space')] + return input.trim().split(/\s+/).map(parseKeystroke) +} + +/** + * Convert a ParsedKeystroke to its canonical string representation for display. + */ +export function keystrokeToString(ks: ParsedKeystroke): string { + const parts: string[] = [] + if (ks.ctrl) parts.push('ctrl') + if (ks.alt) parts.push('alt') + if (ks.shift) parts.push('shift') + if (ks.meta) parts.push('meta') + if (ks.super) parts.push('cmd') + // Use readable names for display + const displayKey = keyToDisplayName(ks.key) + parts.push(displayKey) + return parts.join('+') +} + +/** + * Map internal key names to human-readable display names. + */ +function keyToDisplayName(key: string): string { + switch (key) { + case 'escape': + return 'Esc' + case ' ': + return 'Space' + case 'tab': + return 'tab' + case 'enter': + return 'Enter' + case 'backspace': + return 'Backspace' + case 'delete': + return 'Delete' + case 'up': + return '↑' + case 'down': + return '↓' + case 'left': + return '←' + case 'right': + return '→' + case 'pageup': + return 'PageUp' + case 'pagedown': + return 'PageDown' + case 'home': + return 'Home' + case 'end': + return 'End' + default: + return key + } +} + +/** + * Convert a Chord to its canonical string representation for display. + */ +export function chordToString(chord: Chord): string { + return chord.map(keystrokeToString).join(' ') +} + +/** + * Display platform type - a subset of Platform that we care about for display. + * WSL and unknown are treated as linux for display purposes. + */ +type DisplayPlatform = 'macos' | 'windows' | 'linux' | 'wsl' | 'unknown' + +/** + * Convert a ParsedKeystroke to a platform-appropriate display string. + * Uses "opt" for alt on macOS, "alt" elsewhere. + */ +export function keystrokeToDisplayString( + ks: ParsedKeystroke, + platform: DisplayPlatform = 'linux', +): string { + const parts: string[] = [] + if (ks.ctrl) parts.push('ctrl') + // Alt/meta are equivalent in terminals, show platform-appropriate name + if (ks.alt || ks.meta) { + // Only macOS uses "opt", all other platforms use "alt" + parts.push(platform === 'macos' ? 'opt' : 'alt') + } + if (ks.shift) parts.push('shift') + if (ks.super) { + parts.push(platform === 'macos' ? 'cmd' : 'super') + } + // Use readable names for display + const displayKey = keyToDisplayName(ks.key) + parts.push(displayKey) + return parts.join('+') +} + +/** + * Convert a Chord to a platform-appropriate display string. + */ +export function chordToDisplayString( + chord: Chord, + platform: DisplayPlatform = 'linux', +): string { + return chord.map(ks => keystrokeToDisplayString(ks, platform)).join(' ') +} + +/** + * Parse keybinding blocks (from JSON config) into a flat list of ParsedBindings. + */ +export function parseBindings(blocks: KeybindingBlock[]): ParsedBinding[] { + const bindings: ParsedBinding[] = [] + for (const block of blocks) { + for (const [key, action] of Object.entries(block.bindings)) { + bindings.push({ + chord: parseChord(key), + action, + context: block.context, + }) + } + } + return bindings +} diff --git a/packages/@ant/ink/src/keybindings/resolver.ts b/packages/@ant/ink/src/keybindings/resolver.ts new file mode 100644 index 000000000..5c930d68b --- /dev/null +++ b/packages/@ant/ink/src/keybindings/resolver.ts @@ -0,0 +1,244 @@ +import type { Key } from '../core/events/input-event.js' +import { getKeyName, matchesBinding } from './match.js' +import { chordToString } from './parser.js' +import type { + KeybindingContextName, + ParsedBinding, + ParsedKeystroke, +} from './types.js' + +export type ResolveResult = + | { type: 'match'; action: string } + | { type: 'none' } + | { type: 'unbound' } + +export type ChordResolveResult = + | { type: 'match'; action: string } + | { type: 'none' } + | { type: 'unbound' } + | { type: 'chord_started'; pending: ParsedKeystroke[] } + | { type: 'chord_cancelled' } + +/** + * Resolve a key input to an action. + * Pure function - no state, no side effects, just matching logic. + * + * @param input - The character input from Ink + * @param key - The Key object from Ink with modifier flags + * @param activeContexts - Array of currently active contexts (e.g., ['Chat', 'Global']) + * @param bindings - All parsed bindings to search through + * @returns The resolution result + */ +export function resolveKey( + input: string, + key: Key, + activeContexts: KeybindingContextName[], + bindings: ParsedBinding[], +): ResolveResult { + // Find matching bindings (last one wins for user overrides) + let match: ParsedBinding | undefined + const ctxSet = new Set(activeContexts) + + for (const binding of bindings) { + // Phase 1: Only single-keystroke bindings + if (binding.chord.length !== 1) continue + if (!ctxSet.has(binding.context)) continue + + if (matchesBinding(input, key, binding)) { + match = binding + } + } + + if (!match) { + return { type: 'none' } + } + + if (match.action === null) { + return { type: 'unbound' } + } + + return { type: 'match', action: match.action } +} + +/** + * Get display text for an action from bindings (e.g., "ctrl+t" for "app:toggleTodos"). + * Searches in reverse order so user overrides take precedence. + */ +export function getBindingDisplayText( + action: string, + context: KeybindingContextName, + bindings: ParsedBinding[], +): string | undefined { + // Find the last binding for this action in this context + const binding = bindings.findLast( + b => b.action === action && b.context === context, + ) + return binding ? chordToString(binding.chord) : undefined +} + +/** + * Build a ParsedKeystroke from Ink's input/key. + */ +function buildKeystroke(input: string, key: Key): ParsedKeystroke | null { + const keyName = getKeyName(input, key) + if (!keyName) return null + + // QUIRK: Ink sets key.meta=true when escape is pressed (see input-event.ts). + // This is legacy terminal behavior - we should NOT record this as a modifier + // for the escape key itself, otherwise chord matching will fail. + const effectiveMeta = key.escape ? false : key.meta + + return { + key: keyName, + ctrl: key.ctrl, + alt: effectiveMeta, + shift: key.shift, + meta: effectiveMeta, + super: key.super, + } +} + +/** + * Compare two ParsedKeystrokes for equality. Collapses alt/meta into + * one logical modifier — legacy terminals can't distinguish them (see + * match.ts modifiersMatch), so "alt+k" and "meta+k" are the same key. + * Super (cmd/win) is distinct — only arrives via kitty keyboard protocol. + */ +export function keystrokesEqual( + a: ParsedKeystroke, + b: ParsedKeystroke, +): boolean { + return ( + a.key === b.key && + a.ctrl === b.ctrl && + a.shift === b.shift && + (a.alt || a.meta) === (b.alt || b.meta) && + a.super === b.super + ) +} + +/** + * Check if a chord prefix matches the beginning of a binding's chord. + */ +function chordPrefixMatches( + prefix: ParsedKeystroke[], + binding: ParsedBinding, +): boolean { + if (prefix.length >= binding.chord.length) return false + for (let i = 0; i < prefix.length; i++) { + const prefixKey = prefix[i] + const bindingKey = binding.chord[i] + if (!prefixKey || !bindingKey) return false + if (!keystrokesEqual(prefixKey, bindingKey)) return false + } + return true +} + +/** + * Check if a full chord matches a binding's chord. + */ +function chordExactlyMatches( + chord: ParsedKeystroke[], + binding: ParsedBinding, +): boolean { + if (chord.length !== binding.chord.length) return false + for (let i = 0; i < chord.length; i++) { + const chordKey = chord[i] + const bindingKey = binding.chord[i] + if (!chordKey || !bindingKey) return false + if (!keystrokesEqual(chordKey, bindingKey)) return false + } + return true +} + +/** + * Resolve a key with chord state support. + * + * This function handles multi-keystroke chord bindings like "ctrl+k ctrl+s". + * + * @param input - The character input from Ink + * @param key - The Key object from Ink with modifier flags + * @param activeContexts - Array of currently active contexts + * @param bindings - All parsed bindings + * @param pending - Current chord state (null if not in a chord) + * @returns Resolution result with chord state + */ +export function resolveKeyWithChordState( + input: string, + key: Key, + activeContexts: KeybindingContextName[], + bindings: ParsedBinding[], + pending: ParsedKeystroke[] | null, +): ChordResolveResult { + // Cancel chord on escape + if (key.escape && pending !== null) { + return { type: 'chord_cancelled' } + } + + // Build current keystroke + const currentKeystroke = buildKeystroke(input, key) + if (!currentKeystroke) { + if (pending !== null) { + return { type: 'chord_cancelled' } + } + return { type: 'none' } + } + + // Build the full chord sequence to test + const testChord = pending + ? [...pending, currentKeystroke] + : [currentKeystroke] + + // Filter bindings by active contexts (Set lookup: O(n) instead of O(n·m)) + const ctxSet = new Set(activeContexts) + const contextBindings = bindings.filter(b => ctxSet.has(b.context)) + + // Check if this could be a prefix for longer chords. Group by chord + // string so a later null-override shadows the default it unbinds — + // otherwise null-unbinding `ctrl+x ctrl+k` still makes `ctrl+x` enter + // chord-wait and the single-key binding on the prefix never fires. + const chordWinners = new Map() + for (const binding of contextBindings) { + if ( + binding.chord.length > testChord.length && + chordPrefixMatches(testChord, binding) + ) { + chordWinners.set(chordToString(binding.chord), binding.action) + } + } + let hasLongerChords = false + for (const action of chordWinners.values()) { + if (action !== null) { + hasLongerChords = true + break + } + } + + // If this keystroke could start a longer chord, prefer that + // (even if there's an exact single-key match) + if (hasLongerChords) { + return { type: 'chord_started', pending: testChord } + } + + // Check for exact matches (last one wins) + let exactMatch: ParsedBinding | undefined + for (const binding of contextBindings) { + if (chordExactlyMatches(testChord, binding)) { + exactMatch = binding + } + } + + if (exactMatch) { + if (exactMatch.action === null) { + return { type: 'unbound' } + } + return { type: 'match', action: exactMatch.action } + } + + // No match and no potential longer chords + if (pending !== null) { + return { type: 'chord_cancelled' } + } + + return { type: 'none' } +} diff --git a/packages/@ant/ink/src/keybindings/types.ts b/packages/@ant/ink/src/keybindings/types.ts new file mode 100644 index 000000000..0482ca118 --- /dev/null +++ b/packages/@ant/ink/src/keybindings/types.ts @@ -0,0 +1,54 @@ +// Keybinding type definitions +export type ParsedBinding = { + chord: ParsedKeystroke[] + action: string | null + context: KeybindingContextName +} + +export type ParsedKeystroke = { + key: string + ctrl: boolean + alt: boolean + shift: boolean + meta: boolean + super: boolean +} + +export type KeybindingContextName = string +export type KeybindingBlock = { + context: KeybindingContextName + bindings: Record +} +export type Chord = ParsedKeystroke[] +export type KeybindingAction = string + +/** + * Types of validation issues that can occur with keybindings. + */ +export type KeybindingWarningType = + | 'parse_error' + | 'duplicate' + | 'reserved' + | 'invalid_context' + | 'invalid_action' + +/** + * A warning or error about a keybinding configuration issue. + */ +export type KeybindingWarning = { + type: KeybindingWarningType + severity: 'error' | 'warning' + message: string + key?: string + context?: string + action?: string + suggestion?: string +} + +/** + * Result of loading keybindings, including any validation warnings. + */ +export type KeybindingsLoadResult = { + bindings: ParsedBinding[] + warnings: KeybindingWarning[] +} diff --git a/packages/@ant/ink/src/keybindings/useKeybinding.ts b/packages/@ant/ink/src/keybindings/useKeybinding.ts new file mode 100644 index 000000000..ea822300d --- /dev/null +++ b/packages/@ant/ink/src/keybindings/useKeybinding.ts @@ -0,0 +1,197 @@ +import { useCallback, useEffect } from 'react' +import type { InputEvent } from '../core/events/input-event.js' +import { type Key } from '../core/events/input-event.js' +import useInput from '../hooks/use-input.js' +import { useOptionalKeybindingContext } from './KeybindingContext.js' +import type { KeybindingContextName } from './types.js' + +type Options = { + /** Which context this binding belongs to (default: 'Global') */ + context?: KeybindingContextName + /** Only handle when active (like useInput's isActive) */ + isActive?: boolean +} + +/** + * Ink-native hook for handling a keybinding. + * + * The handler stays in the component (React way). + * The binding (keystroke → action) comes from config. + * + * Supports chord sequences (e.g., "ctrl+k ctrl+s"). When a chord is started, + * the hook will manage the pending state automatically. + * + * Uses stopImmediatePropagation() to prevent other handlers from firing + * once this binding is handled. + * + * @example + * ```tsx + * useKeybinding('app:toggleTodos', () => { + * setShowTodos(prev => !prev) + * }, { context: 'Global' }) + * ``` + */ +export function useKeybinding( + action: string, + handler: () => void | false | Promise, + options: Options = {}, +): void { + const { context = 'Global', isActive = true } = options + const keybindingContext = useOptionalKeybindingContext() + + // Register handler with the context for ChordInterceptor to invoke + useEffect(() => { + if (!keybindingContext || !isActive) return + return keybindingContext.registerHandler({ action, context, handler }) + }, [action, context, handler, keybindingContext, isActive]) + + const handleInput = useCallback( + (input: string, key: Key, event: InputEvent) => { + // If no keybinding context available, skip resolution + if (!keybindingContext) return + + // Build context list: registered active contexts + this context + Global + // More specific contexts (registered ones) take precedence over Global + const contextsToCheck: KeybindingContextName[] = [ + ...keybindingContext.activeContexts, + context, + 'Global', + ] + // Deduplicate while preserving order (first occurrence wins for priority) + const uniqueContexts = [...new Set(contextsToCheck)] + + const result = keybindingContext.resolve(input, key, uniqueContexts) + + switch (result.type) { + case 'match': + // Chord completed (if any) - clear pending state + keybindingContext.setPendingChord(null) + if (result.action === action) { + if (handler() !== false) { + event.stopImmediatePropagation() + } + } + break + case 'chord_started': + // User started a chord sequence - update pending state + keybindingContext.setPendingChord(result.pending) + event.stopImmediatePropagation() + break + case 'chord_cancelled': + // Chord was cancelled (escape or invalid key) + keybindingContext.setPendingChord(null) + break + case 'unbound': + // Explicitly unbound - clear any pending chord + keybindingContext.setPendingChord(null) + event.stopImmediatePropagation() + break + case 'none': + // No match - let other handlers try + break + } + }, + [action, context, handler, keybindingContext], + ) + + useInput(handleInput, { isActive }) +} + +/** + * Handle multiple keybindings in one hook (reduces useInput calls). + * + * Supports chord sequences. When a chord is started, the hook will + * manage the pending state automatically. + * + * @example + * ```tsx + * useKeybindings({ + * 'chat:submit': () => handleSubmit(), + * 'chat:cancel': () => handleCancel(), + * }, { context: 'Chat' }) + * ``` + */ +export function useKeybindings( + // Handler returning `false` means "not consumed" — the event propagates + // to later useInput/useKeybindings handlers. Useful for fall-through: + // e.g. ScrollKeybindingHandler's scroll:line* returns false when the + // ScrollBox content fits (scroll is a no-op), letting a child component's + // handler take the wheel event for list navigation instead. Promise + // is allowed for fire-and-forget async handlers (the `!== false` check + // only skips propagation for a sync `false`, not a pending Promise). + handlers: Record void | false | Promise>, + options: Options = {}, +): void { + const { context = 'Global', isActive = true } = options + const keybindingContext = useOptionalKeybindingContext() + + // Register all handlers with the context for ChordInterceptor to invoke + useEffect(() => { + if (!keybindingContext || !isActive) return + + const unregisterFns: Array<() => void> = [] + for (const [action, handler] of Object.entries(handlers)) { + unregisterFns.push( + keybindingContext.registerHandler({ action, context, handler }), + ) + } + + return () => { + for (const unregister of unregisterFns) { + unregister() + } + } + }, [context, handlers, keybindingContext, isActive]) + + const handleInput = useCallback( + (input: string, key: Key, event: InputEvent) => { + // If no keybinding context available, skip resolution + if (!keybindingContext) return + + // Build context list: registered active contexts + this context + Global + // More specific contexts (registered ones) take precedence over Global + const contextsToCheck: KeybindingContextName[] = [ + ...keybindingContext.activeContexts, + context, + 'Global', + ] + // Deduplicate while preserving order (first occurrence wins for priority) + const uniqueContexts = [...new Set(contextsToCheck)] + + const result = keybindingContext.resolve(input, key, uniqueContexts) + + switch (result.type) { + case 'match': + // Chord completed (if any) - clear pending state + keybindingContext.setPendingChord(null) + if (result.action in handlers) { + const handler = handlers[result.action] + if (handler && handler() !== false) { + event.stopImmediatePropagation() + } + } + break + case 'chord_started': + // User started a chord sequence - update pending state + keybindingContext.setPendingChord(result.pending) + event.stopImmediatePropagation() + break + case 'chord_cancelled': + // Chord was cancelled (escape or invalid key) + keybindingContext.setPendingChord(null) + break + case 'unbound': + // Explicitly unbound - clear any pending chord + keybindingContext.setPendingChord(null) + event.stopImmediatePropagation() + break + case 'none': + // No match - let other handlers try + break + } + }, + [context, handlers, keybindingContext], + ) + + useInput(handleInput, { isActive }) +} diff --git a/packages/@ant/ink/src/theme/Byline.tsx b/packages/@ant/ink/src/theme/Byline.tsx new file mode 100644 index 000000000..bb34e52fc --- /dev/null +++ b/packages/@ant/ink/src/theme/Byline.tsx @@ -0,0 +1,57 @@ +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" + * + * + * + * + * + * + * + * @example + * // With conditional children: "Esc to cancel" (only one item shown) + * + * + * {showEnter && } + * + * + * + * + */ +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) => ( + + {index > 0 && · } + {child} + + ))} + + ) +} diff --git a/packages/@ant/ink/src/theme/ConfigurableShortcutHint.tsx b/packages/@ant/ink/src/theme/ConfigurableShortcutHint.tsx new file mode 100644 index 000000000..85911d387 --- /dev/null +++ b/packages/@ant/ink/src/theme/ConfigurableShortcutHint.tsx @@ -0,0 +1,35 @@ +/** + * 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 ( + + ) +} diff --git a/packages/@ant/ink/src/theme/Dialog.tsx b/packages/@ant/ink/src/theme/Dialog.tsx new file mode 100644 index 000000000..caf6166a0 --- /dev/null +++ b/packages/@ant/ink/src/theme/Dialog.tsx @@ -0,0 +1,100 @@ +import React from 'react' +import { + type ExitState, + useExitOnCtrlCDWithKeybindings, +} from '../hooks/useExitOnCtrlCD.js' +import { Box, Text } from '../index.js' +import { useKeybinding } from '../keybindings/useKeybinding.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 ? ( + Press {exitState.keyName} again to exit + ) : ( + + + + + ) + + const content = ( + <> + + + + {title} + + {subtitle && {subtitle}} + + {children} + + {!hideInputGuide && ( + + + {inputGuide ? inputGuide(exitState) : defaultInputGuide} + + + )} + + ) + + if (hideBorder) { + return content + } + + return {content} +} diff --git a/packages/@ant/ink/src/theme/Divider.tsx b/packages/@ant/ink/src/theme/Divider.tsx new file mode 100644 index 000000000..077546a75 --- /dev/null +++ b/packages/@ant/ink/src/theme/Divider.tsx @@ -0,0 +1,97 @@ +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 ─────────── + * + */ + title?: string +} + +/** + * A horizontal divider line. + * + * @example + * // Full-width dimmed divider + * + * + * @example + * // Colored divider + * + * + * @example + * // Fixed width + * + * + * @example + * // Full width minus padding (for indented content) + * + * + * @example + * // With centered title + * + */ +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 ( + + {char.repeat(leftWidth)}{' '} + + {title} + {' '} + {char.repeat(rightWidth)} + + ) + } + + return ( + + {char.repeat(effectiveWidth)} + + ) +} diff --git a/packages/@ant/ink/src/theme/FuzzyPicker.tsx b/packages/@ant/ink/src/theme/FuzzyPicker.tsx new file mode 100644 index 000000000..642c09e7d --- /dev/null +++ b/packages/@ant/ink/src/theme/FuzzyPicker.tsx @@ -0,0 +1,350 @@ +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 = { + /** Hint label shown in the byline, e.g. "mention" → "Tab to mention". */ + action: string + handler: (item: T) => void +} + +type Props = { + 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 + /** Shift+Tab key. Gets its own hint. */ + onShiftTab?: PickerAction + /** + * 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({ + 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): 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 = ( + + ) + + const listBlock = ( + + ) + + const preview = + renderPreview && focused ? ( + + {renderPreview(focused)} + + ) : 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' ? ( + + + {listBlock} + {matchLabel && {matchLabel}} + + {preview ?? } + + ) : ( + // 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'. + + {listBlock} + {matchLabel && {matchLabel}} + {preview} + + ) + + const inputAbove = direction !== 'up' + return ( + + + + {title} + + {inputAbove && searchBox} + {listGroup} + {!inputAbove && searchBox} + + + + + {onTab && ( + + )} + {onShiftTab && !compact && ( + + )} + + {extraHints} + + + + + ) +} + +type ListProps = Pick< + Props, + 'visibleCount' | 'direction' | 'getKey' | 'renderItem' +> & { + visible: readonly T[] + windowStart: number + total: number + focusedIndex: number + emptyText: string +} + +function List({ + visible, + windowStart, + visibleCount, + total, + focusedIndex, + direction, + getKey, + renderItem, + emptyText, +}: ListProps): React.ReactNode { + if (visible.length === 0) { + return ( + + {emptyText} + + ) + } + + 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 ( + + {renderItem(item, isFocused)} + + ) + }) + + return ( + + {rows} + + ) +} + +function firstWord(s: string): string { + const i = s.indexOf(' ') + return i === -1 ? s : s.slice(0, i) +} diff --git a/packages/@ant/ink/src/theme/KeyboardShortcutHint.tsx b/packages/@ant/ink/src/theme/KeyboardShortcutHint.tsx new file mode 100644 index 000000000..30d4d2ccc --- /dev/null +++ b/packages/@ant/ink/src/theme/KeyboardShortcutHint.tsx @@ -0,0 +1,58 @@ +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 for the common dim styling. + * + * @example + * // Simple hint wrapped in dim Text + * + * + * // With parentheses: "(ctrl+o to expand)" + * + * + * // With bold shortcut: "Enter to confirm" (Enter is bold) + * + * + * // Multiple hints with middot separator - use Byline + * + * + * + * + * + * + */ +export function KeyboardShortcutHint({ + shortcut, + action, + parens = false, + bold = false, +}: Props): React.ReactNode { + const shortcutText = bold ? {shortcut} : shortcut + + if (parens) { + return ( + + ({shortcutText} to {action}) + + ) + } + return ( + + {shortcutText} to {action} + + ) +} diff --git a/packages/@ant/ink/src/theme/ListItem.tsx b/packages/@ant/ink/src/theme/ListItem.tsx new file mode 100644 index 000000000..b74619de1 --- /dev/null +++ b/packages/@ant/ink/src/theme/ListItem.tsx @@ -0,0 +1,188 @@ +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) => ( + * + * {option.label} + * + * ))} + * + * @example + * // With scroll indicators + * First visible item + * ... + * Last visible item + * + * @example + * // With description + * + * Primary text + * + * + * @example + * // Custom children styling (styled=false) + * + * Custom styled content + * + */ +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 + } + + if (isFocused) { + return {figures.pointer} + } + + if (showScrollDown) { + return {figures.arrowDown} + } + + if (showScrollUp) { + return {figures.arrowUp} + } + + return + } + + // 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 ( + + + {renderIndicator()} + {styled ? ( + + {children} + + ) : ( + children + )} + {isSelected && !disabled && {figures.tick}} + + {description && ( + + {description} + + )} + + ) +} diff --git a/packages/@ant/ink/src/theme/LoadingState.tsx b/packages/@ant/ink/src/theme/LoadingState.tsx new file mode 100644 index 000000000..ec1459cee --- /dev/null +++ b/packages/@ant/ink/src/theme/LoadingState.tsx @@ -0,0 +1,66 @@ +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 + * + * + * @example + * // Bold loading message + * + * + * @example + * // With subtitle + * + */ +export function LoadingState({ + message, + bold = false, + dimColor = false, + subtitle, +}: LoadingStateProps): React.ReactNode { + return ( + + + + + {' '} + {message} + + + {subtitle && {subtitle}} + + ) +} diff --git a/packages/@ant/ink/src/theme/Pane.tsx b/packages/@ant/ink/src/theme/Pane.tsx new file mode 100644 index 000000000..d6868faec --- /dev/null +++ b/packages/@ant/ink/src/theme/Pane.tsx @@ -0,0 +1,57 @@ +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 + * `` instead — it registers its own keybindings. For a full + * rounded-border card, use ``. + * + * Submenus rendered inside a Pane should use `hideBorder` on their Dialog + * so the Pane's border remains the single frame. + * + * @example + * + * ... + * + */ +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 ( + + {children} + + ) + } + return ( + + + + {children} + + + ) +} diff --git a/packages/@ant/ink/src/theme/ProgressBar.tsx b/packages/@ant/ink/src/theme/ProgressBar.tsx new file mode 100644 index 000000000..1d5c1f674 --- /dev/null +++ b/packages/@ant/ink/src/theme/ProgressBar.tsx @@ -0,0 +1,54 @@ +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 ( + + {segments.join('')} + + ) +} diff --git a/packages/@ant/ink/src/theme/Ratchet.tsx b/packages/@ant/ink/src/theme/Ratchet.tsx new file mode 100644 index 000000000..dbb80c3f4 --- /dev/null +++ b/packages/@ant/ink/src/theme/Ratchet.tsx @@ -0,0 +1,45 @@ +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(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 ( + + + {children} + + + ) +} diff --git a/packages/@ant/ink/src/theme/SearchBox.tsx b/packages/@ant/ink/src/theme/SearchBox.tsx new file mode 100644 index 000000000..bf716be9e --- /dev/null +++ b/packages/@ant/ink/src/theme/SearchBox.tsx @@ -0,0 +1,71 @@ +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 ( + + + {prefix}{' '} + {isFocused ? ( + <> + {query ? ( + isTerminalFocused ? ( + <> + {query.slice(0, offset)} + + {offset < query.length ? query[offset] : ' '} + + {offset < query.length && ( + {query.slice(offset + 1)} + )} + + ) : ( + {query} + ) + ) : isTerminalFocused ? ( + <> + {placeholder.charAt(0)} + {placeholder.slice(1)} + + ) : ( + {placeholder} + )} + + ) : query ? ( + {query} + ) : ( + {placeholder} + )} + + + ) +} diff --git a/packages/@ant/ink/src/theme/Spinner.tsx b/packages/@ant/ink/src/theme/Spinner.tsx new file mode 100644 index 000000000..2d85ec08c --- /dev/null +++ b/packages/@ant/ink/src/theme/Spinner.tsx @@ -0,0 +1,20 @@ +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 {FRAMES[frame]} +} diff --git a/packages/@ant/ink/src/theme/StatusIcon.tsx b/packages/@ant/ink/src/theme/StatusIcon.tsx new file mode 100644 index 000000000..943d6be25 --- /dev/null +++ b/packages/@ant/ink/src/theme/StatusIcon.tsx @@ -0,0 +1,71 @@ +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 + * + * + * @example + * // Error with trailing space for text + * Failed to connect + * + * @example + * // Status line pattern + * + * + * Waiting for response + * + */ +export function StatusIcon({ + status, + withSpace = false, +}: Props): React.ReactNode { + const config = STATUS_CONFIG[status] + + return ( + + {config.icon} + {withSpace && ' '} + + ) +} diff --git a/packages/@ant/ink/src/theme/Tabs.tsx b/packages/@ant/ink/src/theme/Tabs.tsx new file mode 100644 index 000000000..df49f4619 --- /dev/null +++ b/packages/@ant/ink/src/theme/Tabs.tsx @@ -0,0 +1,339 @@ +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/useKeybinding.js' +import type { Theme } from './theme-types.js' + +type TabsProps = { + children: Array> + 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({ + 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 ( + + + {!hidden && ( + + {title !== undefined && ( + + {title} + + )} + {tabs.map(([id, title], i) => { + const isCurrent = selectedTabIndex === i + const hasColorCursor = color && isCurrent && headerFocused + return ( + + {' '} + {title}{' '} + + ) + })} + {spacerWidth > 0 && {' '.repeat(spacerWidth)}} + + )} + {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. + + + {children} + + + ) : ( + + {children} + + )} + + + ) +} + +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 ( + + {children} + + ) +} + +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 } +} diff --git a/packages/@ant/ink/src/theme/ThemeProvider.tsx b/packages/@ant/ink/src/theme/ThemeProvider.tsx new file mode 100644 index 000000000..bfd6a4307 --- /dev/null +++ b/packages/@ant/ink/src/theme/ThemeProvider.tsx @@ -0,0 +1,172 @@ +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({ + 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(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(() => + (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( + () => ({ + 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 {children} +} + +/** + * 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 } +} diff --git a/packages/@ant/ink/src/theme/ThemedBox.tsx b/packages/@ant/ink/src/theme/ThemedBox.tsx new file mode 100644 index 000000000..46aadeab9 --- /dev/null +++ b/packages/@ant/ink/src/theme/ThemedBox.tsx @@ -0,0 +1,112 @@ +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 + 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): 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 ( + + {children} + + ) +} + +export default ThemedBox diff --git a/packages/@ant/ink/src/theme/ThemedText.tsx b/packages/@ant/ink/src/theme/ThemedText.tsx new file mode 100644 index 000000000..35e884ca9 --- /dev/null +++ b/packages/@ant/ink/src/theme/ThemedText.tsx @@ -0,0 +1,132 @@ +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 ( + + {children} + + ) +} diff --git a/packages/@ant/ink/src/theme/color.ts b/packages/@ant/ink/src/theme/color.ts new file mode 100644 index 000000000..f8b567d4d --- /dev/null +++ b/packages/@ant/ink/src/theme/color.ts @@ -0,0 +1,30 @@ +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) + } +} diff --git a/packages/@ant/ink/src/theme/modalContext.ts b/packages/@ant/ink/src/theme/modalContext.ts new file mode 100644 index 000000000..919abe9d6 --- /dev/null +++ b/packages/@ant/ink/src/theme/modalContext.ts @@ -0,0 +1,25 @@ +/** + * 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 | null +} + +export const ModalContext = createContext(null) + +export function useIsInsideModal(): boolean { + return useContext(ModalContext) !== null +} + +export function useModalScrollRef(): RefObject | null { + return useContext(ModalContext)?.scrollRef ?? null +} diff --git a/packages/@ant/ink/src/theme/systemTheme.ts b/packages/@ant/ink/src/theme/systemTheme.ts new file mode 100644 index 000000000..2ac3da5b6 --- /dev/null +++ b/packages/@ant/ink/src/theme/systemTheme.ts @@ -0,0 +1,40 @@ +/** + * 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 +} diff --git a/packages/@ant/ink/src/theme/theme-types.ts b/packages/@ant/ink/src/theme/theme-types.ts new file mode 100644 index 000000000..7e59f4059 --- /dev/null +++ b/packages/@ant/ink/src/theme/theme-types.ts @@ -0,0 +1,639 @@ +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' +} diff --git a/packages/@ant/ink/src/theme/types.ts b/packages/@ant/ink/src/theme/types.ts new file mode 100644 index 000000000..561da3b2e --- /dev/null +++ b/packages/@ant/ink/src/theme/types.ts @@ -0,0 +1,11 @@ +/** + * 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' diff --git a/packages/@ant/ink/src/types/ink-elements.d.ts b/packages/@ant/ink/src/types/ink-elements.d.ts new file mode 100644 index 000000000..e439ae56b --- /dev/null +++ b/packages/@ant/ink/src/types/ink-elements.d.ts @@ -0,0 +1,49 @@ +// 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; + 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 {}; diff --git a/packages/@ant/ink/src/types/ink-jsx.d.ts b/packages/@ant/ink/src/types/ink-jsx.d.ts new file mode 100644 index 000000000..456be9399 --- /dev/null +++ b/packages/@ant/ink/src/types/ink-jsx.d.ts @@ -0,0 +1,54 @@ +/** + * 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; + 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; + }; + } + } +} diff --git a/packages/@ant/ink/src/utils/debug.ts b/packages/@ant/ink/src/utils/debug.ts new file mode 100644 index 000000000..2d3ae198e --- /dev/null +++ b/packages/@ant/ink/src/utils/debug.ts @@ -0,0 +1,2 @@ +// Stub debug logger for package independence +export function logForDebugging(..._args: unknown[]): void {} diff --git a/src/bridge/bridgeStatusUtil.ts b/src/bridge/bridgeStatusUtil.ts index 90de462e8..d9c285d70 100644 --- a/src/bridge/bridgeStatusUtil.ts +++ b/src/bridge/bridgeStatusUtil.ts @@ -2,7 +2,7 @@ import { getClaudeAiBaseUrl, getRemoteSessionUrl, } from '../constants/product.js' -import { stringWidth } from '../ink/stringWidth.js' +import { stringWidth } from '@anthropic/ink' import { formatDuration, truncateToWidth } from '../utils/format.js' import { getGraphemeSegmenter } from '../utils/intl.js' diff --git a/src/bridge/bridgeUI.ts b/src/bridge/bridgeUI.ts index 5149839ea..67a5132d2 100644 --- a/src/bridge/bridgeUI.ts +++ b/src/bridge/bridgeUI.ts @@ -5,7 +5,7 @@ import { BRIDGE_READY_INDICATOR, BRIDGE_SPINNER_FRAMES, } from '../constants/figures.js' -import { stringWidth } from '../ink/stringWidth.js' +import { stringWidth } from '@anthropic/ink' import { logForDebugging } from '../utils/debug.js' import { buildActiveFooterText, diff --git a/src/buddy/CompanionCard.tsx b/src/buddy/CompanionCard.tsx index f9264acf3..1f571605c 100644 --- a/src/buddy/CompanionCard.tsx +++ b/src/buddy/CompanionCard.tsx @@ -3,8 +3,8 @@ * Mirrors official vc8 component: bordered box with sprite, stats, last reaction. */ import React from 'react'; -import { Box, Text } from '../ink.js'; -import { useInput } from '../ink.js'; +import { Box, Text } from '@anthropic/ink'; +import { useInput } from '@anthropic/ink'; import { renderSprite } from './sprites.js'; import { RARITY_COLORS, RARITY_STARS, STAT_NAMES, type Companion } from './types.js'; diff --git a/src/buddy/CompanionSprite.tsx b/src/buddy/CompanionSprite.tsx index d8c7ae473..22dbeb643 100644 --- a/src/buddy/CompanionSprite.tsx +++ b/src/buddy/CompanionSprite.tsx @@ -2,8 +2,7 @@ import { feature } from 'bun:bundle' import figures from 'figures' import React, { useEffect, useRef, useState } from 'react' import { useTerminalSize } from '../hooks/useTerminalSize.js' -import { stringWidth } from '../ink/stringWidth.js' -import { Box, Text } from '../ink.js' +import { Box, Text, stringWidth } from '@anthropic/ink' import { useAppState, useSetAppState } from '../state/AppState.js' import type { AppState } from '../state/AppStateStore.js' import { getGlobalConfig } from '../utils/config.js' diff --git a/src/buddy/useBuddyNotification.tsx b/src/buddy/useBuddyNotification.tsx index 62d61f4cf..2df078e79 100644 --- a/src/buddy/useBuddyNotification.tsx +++ b/src/buddy/useBuddyNotification.tsx @@ -1,7 +1,7 @@ import { feature } from 'bun:bundle' import React, { useEffect } from 'react' import { useNotifications } from '../context/notifications.js' -import { Text } from '../ink.js' +import { Text } from '@anthropic/ink' import { getGlobalConfig } from '../utils/config.js' import { getRainbowColor } from '../utils/thinking.js' diff --git a/src/cli/handlers/mcp.tsx b/src/cli/handlers/mcp.tsx index 134918c75..b9c030da9 100644 --- a/src/cli/handlers/mcp.tsx +++ b/src/cli/handlers/mcp.tsx @@ -8,7 +8,7 @@ import pMap from 'p-map' import { cwd } from 'process' import React from 'react' import { MCPServerDesktopImportDialog } from '../../components/MCPServerDesktopImportDialog.js' -import { render } from '../../ink.js' +import { wrappedRender as render } from '@anthropic/ink' import { KeybindingSetup } from '../../keybindings/KeybindingProviderSetup.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, diff --git a/src/cli/handlers/util.tsx b/src/cli/handlers/util.tsx index c86b31737..b1a8cc0b9 100644 --- a/src/cli/handlers/util.tsx +++ b/src/cli/handlers/util.tsx @@ -8,8 +8,8 @@ import { cwd } from 'process' import React from 'react' import { WelcomeV2 } from '../../components/LogoV2/WelcomeV2.js' import { useManagePlugins } from '../../hooks/useManagePlugins.js' -import type { Root } from '../../ink.js' -import { Box, Text } from '../../ink.js' +import type { Root } from '@anthropic/ink' +import { Box, Text } from '@anthropic/ink' import { KeybindingSetup } from '../../keybindings/KeybindingProviderSetup.js' import { logEvent } from '../../services/analytics/index.js' import { MCPConnectionManager } from '../../services/mcp/MCPConnectionManager.js' diff --git a/src/commands/add-dir/add-dir.tsx b/src/commands/add-dir/add-dir.tsx index cfb6c6687..91180cd73 100644 --- a/src/commands/add-dir/add-dir.tsx +++ b/src/commands/add-dir/add-dir.tsx @@ -8,7 +8,7 @@ import { import type { LocalJSXCommandContext } from '../../commands.js' import { MessageResponse } from '../../components/MessageResponse.js' import { AddWorkspaceDirectory } from '../../components/permissions/rules/AddWorkspaceDirectory.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import type { LocalJSXCommandOnDone } from '../../types/command.js' import { applyPermissionUpdate, diff --git a/src/commands/bridge/bridge.tsx b/src/commands/bridge/bridge.tsx index 33a681202..78b0341f1 100644 --- a/src/commands/bridge/bridge.tsx +++ b/src/commands/bridge/bridge.tsx @@ -13,11 +13,10 @@ import { BRIDGE_LOGIN_INSTRUCTION, REMOTE_CONTROL_DISCONNECTED_MSG, } from '../../bridge/types.js' -import { Dialog } from '../../components/design-system/Dialog.js' -import { ListItem } from '../../components/design-system/ListItem.js' +import { Dialog, ListItem } from '@anthropic/ink' import { shouldShowRemoteCallout } from '../../components/RemoteCallout.js' import { useRegisterOverlay } from '../../context/overlayContext.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { useKeybindings } from '../../keybindings/useKeybinding.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, diff --git a/src/commands/btw/btw.tsx b/src/commands/btw/btw.tsx index 28a83946b..753b5821e 100644 --- a/src/commands/btw/btw.tsx +++ b/src/commands/btw/btw.tsx @@ -9,11 +9,8 @@ import { getSystemPrompt } from '../../constants/prompts.js' import { useModalOrTerminalSize } from '../../context/modalContext.js' import { getSystemContext, getUserContext } from '../../context.js' import { useTerminalSize } from '../../hooks/useTerminalSize.js' -import ScrollBox, { - type ScrollBoxHandle, -} from '../../ink/components/ScrollBox.js' -import type { KeyboardEvent } from '../../ink/events/keyboard-event.js' -import { Box, Text } from '../../ink.js' +import { type KeyboardEvent, type ScrollBoxHandle, ScrollBox } from '@anthropic/ink' +import { Box, Text } from '@anthropic/ink' import type { LocalJSXCommandOnDone } from '../../types/command.js' import type { Message } from '../../types/message.js' import { createAbortController } from '../../utils/abortController.js' diff --git a/src/commands/chrome/chrome.tsx b/src/commands/chrome/chrome.tsx index 3fd0dbca3..1fe1b1470 100644 --- a/src/commands/chrome/chrome.tsx +++ b/src/commands/chrome/chrome.tsx @@ -3,8 +3,8 @@ import { type OptionWithDescription, Select, } from '../../components/CustomSelect/select.js' -import { Dialog } from '../../components/design-system/Dialog.js' -import { Box, Text } from '../../ink.js' +import { Dialog } from '@anthropic/ink' +import { Box, Text } from '@anthropic/ink' import { useAppState } from '../../state/AppState.js' import { isClaudeAISubscriber } from '../../utils/auth.js' import { openBrowser } from '../../utils/browser.js' diff --git a/src/commands/copy/copy.tsx b/src/commands/copy/copy.tsx index d5196de20..e54171a63 100644 --- a/src/commands/copy/copy.tsx +++ b/src/commands/copy/copy.tsx @@ -6,13 +6,8 @@ import React, { useRef } from 'react' import type { CommandResultDisplay } from '../../commands.js' import type { OptionWithDescription } from '../../components/CustomSelect/select.js' import { Select } from '../../components/CustomSelect/select.js' -import { Byline } from '../../components/design-system/Byline.js' -import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js' -import { Pane } from '../../components/design-system/Pane.js' -import type { KeyboardEvent } from '../../ink/events/keyboard-event.js' -import { stringWidth } from '../../ink/stringWidth.js' -import { setClipboard } from '../../ink/termio/osc.js' -import { Box, Text } from '../../ink.js' +import { Byline, KeyboardShortcutHint, Pane } from '@anthropic/ink' +import { Box, setClipboard, Text, stringWidth, type KeyboardEvent } from '@anthropic/ink' import { logEvent } from '../../services/analytics/index.js' import type { LocalJSXCommandCall } from '../../types/command.js' import type { AssistantMessage, Message } from '../../types/message.js' diff --git a/src/commands/fast/fast.tsx b/src/commands/fast/fast.tsx index a959a909a..2ab17db78 100644 --- a/src/commands/fast/fast.tsx +++ b/src/commands/fast/fast.tsx @@ -4,9 +4,9 @@ import type { CommandResultDisplay, LocalJSXCommandContext, } from '../../commands.js' -import { Dialog } from '../../components/design-system/Dialog.js' +import { Dialog } from '@anthropic/ink' import { FastIcon, getFastIconString } from '../../components/FastIcon.js' -import { Box, Link, Text } from '../../ink.js' +import { Box, Link, Text } from '@anthropic/ink' import { useKeybindings } from '../../keybindings/useKeybinding.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, diff --git a/src/commands/ide/ide.tsx b/src/commands/ide/ide.tsx index f22c16e6a..d5944636d 100644 --- a/src/commands/ide/ide.tsx +++ b/src/commands/ide/ide.tsx @@ -7,14 +7,14 @@ import type { LocalJSXCommandContext, } from '../../commands.js' import { Select } from '../../components/CustomSelect/index.js' -import { Dialog } from '../../components/design-system/Dialog.js' +import { Dialog } from '@anthropic/ink' import { IdeAutoConnectDialog, IdeDisableAutoConnectDialog, shouldShowAutoConnectDialog, shouldShowDisableAutoConnectDialog, } from '../../components/IdeAutoConnectDialog.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { clearServerCache } from '../../services/mcp/client.js' import type { ScopedMcpServerConfig } from '../../services/mcp/types.js' import { useAppState, useSetAppState } from '../../state/AppState.js' diff --git a/src/commands/install-github-app/ApiKeyStep.tsx b/src/commands/install-github-app/ApiKeyStep.tsx index 3b88a94f3..942bc662a 100644 --- a/src/commands/install-github-app/ApiKeyStep.tsx +++ b/src/commands/install-github-app/ApiKeyStep.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useState } from 'react' import TextInput from '../../components/TextInput.js' import { useTerminalSize } from '../../hooks/useTerminalSize.js' -import { Box, color, Text, useTheme } from '../../ink.js' +import { Box, color, Text, useTheme } from '@anthropic/ink' import { useKeybindings } from '../../keybindings/useKeybinding.js' interface ApiKeyStepProps { diff --git a/src/commands/install-github-app/CheckExistingSecretStep.tsx b/src/commands/install-github-app/CheckExistingSecretStep.tsx index b00b682c2..de7f4b9a7 100644 --- a/src/commands/install-github-app/CheckExistingSecretStep.tsx +++ b/src/commands/install-github-app/CheckExistingSecretStep.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useState } from 'react' import TextInput from '../../components/TextInput.js' import { useTerminalSize } from '../../hooks/useTerminalSize.js' -import { Box, color, Text, useTheme } from '../../ink.js' +import { Box, color, Text, useTheme } from '@anthropic/ink' import { useKeybindings } from '../../keybindings/useKeybinding.js' interface CheckExistingSecretStepProps { diff --git a/src/commands/install-github-app/CheckGitHubStep.tsx b/src/commands/install-github-app/CheckGitHubStep.tsx index 16f4d7e8a..a43be6c6c 100644 --- a/src/commands/install-github-app/CheckGitHubStep.tsx +++ b/src/commands/install-github-app/CheckGitHubStep.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Text } from '../../ink.js' +import { Text } from '@anthropic/ink' export function CheckGitHubStep() { return Checking GitHub CLI installation… diff --git a/src/commands/install-github-app/ChooseRepoStep.tsx b/src/commands/install-github-app/ChooseRepoStep.tsx index b0d4c63b0..67e921834 100644 --- a/src/commands/install-github-app/ChooseRepoStep.tsx +++ b/src/commands/install-github-app/ChooseRepoStep.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useState } from 'react' import TextInput from '../../components/TextInput.js' import { useTerminalSize } from '../../hooks/useTerminalSize.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { useKeybindings } from '../../keybindings/useKeybinding.js' interface ChooseRepoStepProps { diff --git a/src/commands/install-github-app/CreatingStep.tsx b/src/commands/install-github-app/CreatingStep.tsx index 1861571ed..c021d0bcb 100644 --- a/src/commands/install-github-app/CreatingStep.tsx +++ b/src/commands/install-github-app/CreatingStep.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import type { Workflow } from './types.js' interface CreatingStepProps { diff --git a/src/commands/install-github-app/ErrorStep.tsx b/src/commands/install-github-app/ErrorStep.tsx index a8333f395..5864a0659 100644 --- a/src/commands/install-github-app/ErrorStep.tsx +++ b/src/commands/install-github-app/ErrorStep.tsx @@ -1,6 +1,6 @@ import React from 'react' import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' interface ErrorStepProps { error: string | undefined diff --git a/src/commands/install-github-app/ExistingWorkflowStep.tsx b/src/commands/install-github-app/ExistingWorkflowStep.tsx index 645edb742..11b0a1bb2 100644 --- a/src/commands/install-github-app/ExistingWorkflowStep.tsx +++ b/src/commands/install-github-app/ExistingWorkflowStep.tsx @@ -1,6 +1,6 @@ import React from 'react' import { Select } from 'src/components/CustomSelect/index.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' interface ExistingWorkflowStepProps { repoName: string diff --git a/src/commands/install-github-app/InstallAppStep.tsx b/src/commands/install-github-app/InstallAppStep.tsx index 98a699945..e966578c1 100644 --- a/src/commands/install-github-app/InstallAppStep.tsx +++ b/src/commands/install-github-app/InstallAppStep.tsx @@ -1,7 +1,7 @@ import figures from 'figures' import React from 'react' import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { useKeybinding } from '../../keybindings/useKeybinding.js' interface InstallAppStepProps { diff --git a/src/commands/install-github-app/OAuthFlowStep.tsx b/src/commands/install-github-app/OAuthFlowStep.tsx index b8fd96a49..b13493f18 100644 --- a/src/commands/install-github-app/OAuthFlowStep.tsx +++ b/src/commands/install-github-app/OAuthFlowStep.tsx @@ -3,13 +3,11 @@ import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, } from 'src/services/analytics/index.js' -import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js' +import { KeyboardShortcutHint } from '@anthropic/ink' import { Spinner } from '../../components/Spinner.js' import TextInput from '../../components/TextInput.js' import { useTerminalSize } from '../../hooks/useTerminalSize.js' -import type { KeyboardEvent } from '../../ink/events/keyboard-event.js' -import { setClipboard } from '../../ink/termio/osc.js' -import { Box, Link, Text } from '../../ink.js' +import { type KeyboardEvent, setClipboard, Box, Link, Text } from '@anthropic/ink' import { OAuthService } from '../../services/oauth/index.js' import { saveOAuthTokensIfNeeded } from '../../utils/auth.js' import { logError } from '../../utils/log.js' diff --git a/src/commands/install-github-app/SuccessStep.tsx b/src/commands/install-github-app/SuccessStep.tsx index a04b98ac7..2080f3dc7 100644 --- a/src/commands/install-github-app/SuccessStep.tsx +++ b/src/commands/install-github-app/SuccessStep.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' type SuccessStepProps = { secretExists: boolean diff --git a/src/commands/install-github-app/WarningsStep.tsx b/src/commands/install-github-app/WarningsStep.tsx index 122cdeac3..c3d347798 100644 --- a/src/commands/install-github-app/WarningsStep.tsx +++ b/src/commands/install-github-app/WarningsStep.tsx @@ -1,7 +1,7 @@ import figures from 'figures' import React from 'react' import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { useKeybinding } from '../../keybindings/useKeybinding.js' import type { Warning } from './types.js' diff --git a/src/commands/install-github-app/install-github-app.tsx b/src/commands/install-github-app/install-github-app.tsx index 3a78ae106..c57be920c 100644 --- a/src/commands/install-github-app/install-github-app.tsx +++ b/src/commands/install-github-app/install-github-app.tsx @@ -7,8 +7,7 @@ import { import { WorkflowMultiselectDialog } from '../../components/WorkflowMultiselectDialog.js' import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js' import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js' -import type { KeyboardEvent } from '../../ink/events/keyboard-event.js' -import { Box } from '../../ink.js' +import { type KeyboardEvent, Box } from '@anthropic/ink' import type { LocalJSXCommandOnDone } from '../../types/command.js' import { getAnthropicApiKey, isAnthropicAuthEnabled } from '../../utils/auth.js' import { openBrowser } from '../../utils/browser.js' diff --git a/src/commands/install.tsx b/src/commands/install.tsx index 15eddd575..bb72ad4a8 100644 --- a/src/commands/install.tsx +++ b/src/commands/install.tsx @@ -3,8 +3,8 @@ import { join } from 'node:path' import React, { useEffect, useState } from 'react' import type { CommandResultDisplay } from 'src/commands.js' import { logEvent } from 'src/services/analytics/index.js' -import { StatusIcon } from '../components/design-system/StatusIcon.js' -import { Box, render, Text } from '../ink.js' +import { StatusIcon } from '@anthropic/ink' +import { Box, wrappedRender as render, Text } from '@anthropic/ink' import { logForDebugging } from '../utils/debug.js' import { env } from '../utils/env.js' import { errorMessage } from '../utils/errors.js' diff --git a/src/commands/login/login.tsx b/src/commands/login/login.tsx index 912ad61f9..b4329fe62 100644 --- a/src/commands/login/login.tsx +++ b/src/commands/login/login.tsx @@ -8,9 +8,9 @@ import { import type { LocalJSXCommandContext } from '../../commands.js' import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js' import { ConsoleOAuthFlow } from '../../components/ConsoleOAuthFlow.js' -import { Dialog } from '../../components/design-system/Dialog.js' +import { Dialog } from '@anthropic/ink' import { useMainLoopModel } from '../../hooks/useMainLoopModel.js' -import { Text } from '../../ink.js' +import { Text } from '@anthropic/ink' import { refreshGrowthBookAfterAuthChange } from '../../services/analytics/growthbook.js' import { refreshPolicyLimits } from '../../services/policyLimits/index.js' import { refreshRemoteManagedSettings } from '../../services/remoteManagedSettings/index.js' diff --git a/src/commands/logout/logout.tsx b/src/commands/logout/logout.tsx index b8eb13b87..4223feff4 100644 --- a/src/commands/logout/logout.tsx +++ b/src/commands/logout/logout.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { clearTrustedDeviceTokenCache } from '../../bridge/trustedDevice.js' -import { Text } from '../../ink.js' +import { Text } from '@anthropic/ink' import { refreshGrowthBookAfterAuthChange } from '../../services/analytics/growthbook.js' import { getGroveNoticeConfig, diff --git a/src/commands/memory/memory.tsx b/src/commands/memory/memory.tsx index 945b34a60..885ab57dd 100644 --- a/src/commands/memory/memory.tsx +++ b/src/commands/memory/memory.tsx @@ -1,10 +1,10 @@ import { mkdir, writeFile } from 'fs/promises' import * as React from 'react' import type { CommandResultDisplay } from '../../commands.js' -import { Dialog } from '../../components/design-system/Dialog.js' +import { Dialog } from '@anthropic/ink' import { MemoryFileSelector } from '../../components/memory/MemoryFileSelector.js' import { getRelativeMemoryPath } from '../../components/memory/MemoryUpdateNotification.js' -import { Box, Link, Text } from '../../ink.js' +import { Box, Link, Text } from '@anthropic/ink' import type { LocalJSXCommandCall } from '../../types/command.js' import { clearMemoryFileCaches, getMemoryFiles } from '../../utils/claudemd.js' import { getClaudeConfigHomeDir } from '../../utils/envUtils.js' diff --git a/src/commands/mobile/mobile.tsx b/src/commands/mobile/mobile.tsx index 0467919bb..6c1d8f828 100644 --- a/src/commands/mobile/mobile.tsx +++ b/src/commands/mobile/mobile.tsx @@ -1,9 +1,8 @@ import { toString as qrToString } from 'qrcode' import * as React from 'react' import { useCallback, useEffect, useState } from 'react' -import { Pane } from '../../components/design-system/Pane.js' -import type { KeyboardEvent } from '../../ink/events/keyboard-event.js' -import { Box, Text } from '../../ink.js' +import { Pane } from '@anthropic/ink' +import { type KeyboardEvent, Box, Text } from '@anthropic/ink' import { useKeybinding } from '../../keybindings/useKeybinding.js' import type { LocalJSXCommandOnDone } from '../../types/command.js' diff --git a/src/commands/plan/plan.tsx b/src/commands/plan/plan.tsx index 8c3f328a7..d7694182c 100644 --- a/src/commands/plan/plan.tsx +++ b/src/commands/plan/plan.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { handlePlanModeTransition } from '../../bootstrap/state.js' import type { LocalJSXCommandContext } from '../../commands.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import type { LocalJSXCommandOnDone } from '../../types/command.js' import { getExternalEditor } from '../../utils/editor.js' import { toIDEDisplayName } from '../../utils/ide.js' diff --git a/src/commands/plugin/AddMarketplace.tsx b/src/commands/plugin/AddMarketplace.tsx index e0a1d4d16..7a9138333 100644 --- a/src/commands/plugin/AddMarketplace.tsx +++ b/src/commands/plugin/AddMarketplace.tsx @@ -5,11 +5,10 @@ import { logEvent, } from 'src/services/analytics/index.js' import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js' -import { Byline } from '../../components/design-system/Byline.js' -import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js' +import { Byline, KeyboardShortcutHint } from '@anthropic/ink' import { Spinner } from '../../components/Spinner.js' import TextInput from '../../components/TextInput.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { toError } from '../../utils/errors.js' import { logError } from '../../utils/log.js' import { clearAllCaches } from '../../utils/plugins/cacheUtils.js' diff --git a/src/commands/plugin/BrowseMarketplace.tsx b/src/commands/plugin/BrowseMarketplace.tsx index e8733052e..8e3205f5e 100644 --- a/src/commands/plugin/BrowseMarketplace.tsx +++ b/src/commands/plugin/BrowseMarketplace.tsx @@ -2,8 +2,7 @@ import figures from 'figures' import * as React from 'react' import { useEffect, useState } from 'react' import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js' -import { Byline } from '../../components/design-system/Byline.js' -import { Box, Text } from '../../ink.js' +import { Box, Byline, Text } from '@anthropic/ink' import { useKeybinding, useKeybindings, diff --git a/src/commands/plugin/DiscoverPlugins.tsx b/src/commands/plugin/DiscoverPlugins.tsx index 442e2686f..4c3435de1 100644 --- a/src/commands/plugin/DiscoverPlugins.tsx +++ b/src/commands/plugin/DiscoverPlugins.tsx @@ -2,12 +2,12 @@ import figures from 'figures' import * as React from 'react' import { useCallback, useEffect, useMemo, useState } from 'react' import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js' -import { Byline } from '../../components/design-system/Byline.js' import { SearchBox } from '../../components/SearchBox.js' +import { Byline } from '@anthropic/ink' import { useSearchInput } from '../../hooks/useSearchInput.js' import { useTerminalSize } from '../../hooks/useTerminalSize.js' // eslint-disable-next-line custom-rules/prefer-use-keybindings -- useInput needed for raw search mode text input -import { Box, Text, useInput, useTerminalFocus } from '../../ink.js' +import { Box, Text, useInput, useTerminalFocus } from '@anthropic/ink' import { useKeybinding, useKeybindings, diff --git a/src/commands/plugin/ManageMarketplaces.tsx b/src/commands/plugin/ManageMarketplaces.tsx index 868f26e32..5ec3dbe80 100644 --- a/src/commands/plugin/ManageMarketplaces.tsx +++ b/src/commands/plugin/ManageMarketplaces.tsx @@ -6,10 +6,9 @@ import { logEvent, } from 'src/services/analytics/index.js' import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js' -import { Byline } from '../../components/design-system/Byline.js' -import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js' +import { Byline, KeyboardShortcutHint } from '@anthropic/ink' // 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 '../../ink.js' +import { Box, Text, useInput } from '@anthropic/ink' import { useKeybinding, useKeybindings, diff --git a/src/commands/plugin/ManagePlugins.tsx b/src/commands/plugin/ManagePlugins.tsx index e1ec554ac..a3524724c 100644 --- a/src/commands/plugin/ManagePlugins.tsx +++ b/src/commands/plugin/ManagePlugins.tsx @@ -5,7 +5,7 @@ import * as path from 'path' import * as React from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js' -import { Byline } from '../../components/design-system/Byline.js' +import { Byline } from '@anthropic/ink' import { MCPRemoteServerMenu } from '../../components/mcp/MCPRemoteServerMenu.js' import { MCPStdioServerMenu } from '../../components/mcp/MCPStdioServerMenu.js' import { MCPToolDetailView } from '../../components/mcp/MCPToolDetailView.js' @@ -20,7 +20,7 @@ import { SearchBox } from '../../components/SearchBox.js' import { useSearchInput } from '../../hooks/useSearchInput.js' import { useTerminalSize } from '../../hooks/useTerminalSize.js' // eslint-disable-next-line custom-rules/prefer-use-keybindings -- useInput needed for raw search mode text input -import { Box, Text, useInput, useTerminalFocus } from '../../ink.js' +import { Box, Text, useInput, useTerminalFocus } from '@anthropic/ink' import { useKeybinding, useKeybindings, diff --git a/src/commands/plugin/PluginOptionsDialog.tsx b/src/commands/plugin/PluginOptionsDialog.tsx index 8ef1f6809..cb09352d3 100644 --- a/src/commands/plugin/PluginOptionsDialog.tsx +++ b/src/commands/plugin/PluginOptionsDialog.tsx @@ -1,9 +1,8 @@ import figures from 'figures' import React, { useCallback, useState } from 'react' -import { Dialog } from '../../components/design-system/Dialog.js' -import { stringWidth } from '../../ink/stringWidth.js' +import { Dialog } from '@anthropic/ink' // eslint-disable-next-line custom-rules/prefer-use-keybindings -- raw text input for config dialog -import { Box, Text, useInput } from '../../ink.js' +import { Box, Text, useInput, stringWidth } from '@anthropic/ink' import { useKeybinding, useKeybindings, diff --git a/src/commands/plugin/PluginSettings.tsx b/src/commands/plugin/PluginSettings.tsx index e0c3d54da..444bf9761 100644 --- a/src/commands/plugin/PluginSettings.tsx +++ b/src/commands/plugin/PluginSettings.tsx @@ -2,11 +2,9 @@ import figures from 'figures' import * as React from 'react' import { useCallback, useEffect, useState } from 'react' import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js' -import { Byline } from '../../components/design-system/Byline.js' -import { Pane } from '../../components/design-system/Pane.js' -import { Tab, Tabs } from '../../components/design-system/Tabs.js' +import { Byline, Pane, Tab, Tabs } from '@anthropic/ink' import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { useKeybinding, useKeybindings, diff --git a/src/commands/plugin/PluginTrustWarning.tsx b/src/commands/plugin/PluginTrustWarning.tsx index 2295db691..3be3dd79f 100644 --- a/src/commands/plugin/PluginTrustWarning.tsx +++ b/src/commands/plugin/PluginTrustWarning.tsx @@ -1,6 +1,6 @@ import figures from 'figures' import * as React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { getPluginTrustMessage } from '../../utils/plugins/marketplaceHelpers.js' export function PluginTrustWarning(): React.ReactNode { diff --git a/src/commands/plugin/UnifiedInstalledCell.tsx b/src/commands/plugin/UnifiedInstalledCell.tsx index 5ce8783de..05e44821f 100644 --- a/src/commands/plugin/UnifiedInstalledCell.tsx +++ b/src/commands/plugin/UnifiedInstalledCell.tsx @@ -1,6 +1,6 @@ import figures from 'figures' import * as React from 'react' -import { Box, color, Text, useTheme } from '../../ink.js' +import { Box, color, Text, useTheme } from '@anthropic/ink' import { plural } from '../../utils/stringUtils.js' import type { UnifiedInstalledItem } from './unifiedTypes.js' diff --git a/src/commands/plugin/ValidatePlugin.tsx b/src/commands/plugin/ValidatePlugin.tsx index 4d8afd8b6..9eb52d4b6 100644 --- a/src/commands/plugin/ValidatePlugin.tsx +++ b/src/commands/plugin/ValidatePlugin.tsx @@ -1,7 +1,7 @@ import figures from 'figures' import * as React from 'react' import { useEffect } from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { errorMessage } from '../../utils/errors.js' import { logError } from '../../utils/log.js' import { validateManifest } from '../../utils/plugins/validatePlugin.js' diff --git a/src/commands/plugin/pluginDetailsHelpers.tsx b/src/commands/plugin/pluginDetailsHelpers.tsx index caec86b46..2a9909e1a 100644 --- a/src/commands/plugin/pluginDetailsHelpers.tsx +++ b/src/commands/plugin/pluginDetailsHelpers.tsx @@ -6,8 +6,7 @@ import * as React from 'react' import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js' -import { Byline } from '../../components/design-system/Byline.js' -import { Box, Text } from '../../ink.js' +import { Box, Byline, Text } from '@anthropic/ink' import type { PluginMarketplaceEntry } from '../../utils/plugins/schemas.js' /** diff --git a/src/commands/rate-limit-options/rate-limit-options.tsx b/src/commands/rate-limit-options/rate-limit-options.tsx index e86eb040d..50198f561 100644 --- a/src/commands/rate-limit-options/rate-limit-options.tsx +++ b/src/commands/rate-limit-options/rate-limit-options.tsx @@ -7,7 +7,7 @@ import { type OptionWithDescription, Select, } from '../../components/CustomSelect/select.js' -import { Dialog } from '../../components/design-system/Dialog.js' +import { Dialog } from '@anthropic/ink' import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' import { logEvent } from '../../services/analytics/index.js' import { useClaudeAiLimits } from '../../services/claudeAiLimitsHook.js' diff --git a/src/commands/remote-setup/remote-setup.tsx b/src/commands/remote-setup/remote-setup.tsx index 05813453d..e511f064b 100644 --- a/src/commands/remote-setup/remote-setup.tsx +++ b/src/commands/remote-setup/remote-setup.tsx @@ -2,9 +2,7 @@ import { execa } from 'execa' import * as React from 'react' import { useEffect, useState } from 'react' import { Select } from '../../components/CustomSelect/index.js' -import { Dialog } from '../../components/design-system/Dialog.js' -import { LoadingState } from '../../components/design-system/LoadingState.js' -import { Box, Text } from '../../ink.js' +import { Box, Dialog, LoadingState, Text } from '@anthropic/ink' import { logEvent, type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS as SafeString, diff --git a/src/commands/resume/resume.tsx b/src/commands/resume/resume.tsx index f66d654c6..795c05077 100644 --- a/src/commands/resume/resume.tsx +++ b/src/commands/resume/resume.tsx @@ -9,8 +9,8 @@ import { MessageResponse } from '../../components/MessageResponse.js' import { Spinner } from '../../components/Spinner.js' import { useIsInsideModal } from '../../context/modalContext.js' import { useTerminalSize } from '../../hooks/useTerminalSize.js' -import { setClipboard } from '../../ink/termio/osc.js' -import { Box, Text } from '../../ink.js' +import { setClipboard } from '@anthropic/ink' +import { Box, Text } from '@anthropic/ink' import type { LocalJSXCommandCall } from '../../types/command.js' import type { LogOption } from '../../types/logs.js' import { agenticSessionSearch } from '../../utils/agenticSessionSearch.js' diff --git a/src/commands/review/UltrareviewOverageDialog.tsx b/src/commands/review/UltrareviewOverageDialog.tsx index 020db57f8..12ea10429 100644 --- a/src/commands/review/UltrareviewOverageDialog.tsx +++ b/src/commands/review/UltrareviewOverageDialog.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useRef, useState } from 'react' import { Select } from '../../components/CustomSelect/select.js' -import { Dialog } from '../../components/design-system/Dialog.js' -import { Box, Text } from '../../ink.js' +import { Box, Dialog, Text } from '@anthropic/ink' type Props = { onProceed: (signal: AbortSignal) => Promise diff --git a/src/commands/sandbox-toggle/sandbox-toggle.tsx b/src/commands/sandbox-toggle/sandbox-toggle.tsx index 157961ad5..5f8d46097 100644 --- a/src/commands/sandbox-toggle/sandbox-toggle.tsx +++ b/src/commands/sandbox-toggle/sandbox-toggle.tsx @@ -2,7 +2,7 @@ import { relative } from 'path' import React from 'react' import { getCwdState } from '../../bootstrap/state.js' import { SandboxSettings } from '../../components/sandbox/SandboxSettings.js' -import { color } from '../../ink.js' +import { color } from '@anthropic/ink' import { getPlatform } from '../../utils/platform.js' import { addToExcludedCommands, diff --git a/src/commands/session/session.tsx b/src/commands/session/session.tsx index 82135a3fa..18312e183 100644 --- a/src/commands/session/session.tsx +++ b/src/commands/session/session.tsx @@ -1,8 +1,7 @@ import { toString as qrToString } from 'qrcode' import * as React from 'react' import { useEffect, useState } from 'react' -import { Pane } from '../../components/design-system/Pane.js' -import { Box, Text } from '../../ink.js' +import { Box, Pane, Text } from '@anthropic/ink' import { useKeybinding } from '../../keybindings/useKeybinding.js' import { useAppState } from '../../state/AppState.js' import type { LocalJSXCommandCall } from '../../types/command.js' diff --git a/src/commands/tag/tag.tsx b/src/commands/tag/tag.tsx index c9d0c6524..ea5813d59 100644 --- a/src/commands/tag/tag.tsx +++ b/src/commands/tag/tag.tsx @@ -4,9 +4,9 @@ import * as React from 'react' import { getSessionId } from '../../bootstrap/state.js' import type { CommandResultDisplay } from '../../commands.js' import { Select } from '../../components/CustomSelect/select.js' -import { Dialog } from '../../components/design-system/Dialog.js' +import { Dialog } from '@anthropic/ink' import { COMMON_HELP_ARGS, COMMON_INFO_ARGS } from '../../constants/xml.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { logEvent } from '../../services/analytics/index.js' import type { LocalJSXCommandOnDone } from '../../types/command.js' import { recursivelySanitizeUnicode } from '../../utils/sanitization.js' diff --git a/src/commands/terminalSetup/terminalSetup.tsx b/src/commands/terminalSetup/terminalSetup.tsx index 52b8cdea2..c7776da46 100644 --- a/src/commands/terminalSetup/terminalSetup.tsx +++ b/src/commands/terminalSetup/terminalSetup.tsx @@ -5,8 +5,8 @@ import { homedir, platform } from 'os' import { dirname, join } from 'path' import type { ThemeName } from 'src/utils/theme.js' import { pathToFileURL } from 'url' -import { supportsHyperlinks } from '../../ink/supports-hyperlinks.js' -import { color } from '../../ink.js' +import { supportsHyperlinks } from '@anthropic/ink' +import { color } from '@anthropic/ink' import { maybeMarkProjectOnboardingComplete } from '../../projectOnboardingState.js' import type { ToolUseContext } from '../../Tool.js' import type { diff --git a/src/commands/theme/theme.tsx b/src/commands/theme/theme.tsx index 03ad2fd13..26d4bd1aa 100644 --- a/src/commands/theme/theme.tsx +++ b/src/commands/theme/theme.tsx @@ -1,8 +1,8 @@ import * as React from 'react' import type { CommandResultDisplay } from '../../commands.js' -import { Pane } from '../../components/design-system/Pane.js' +import { Pane } from '@anthropic/ink' import { ThemePicker } from '../../components/ThemePicker.js' -import { useTheme } from '../../ink.js' +import { useTheme } from '@anthropic/ink' import type { LocalJSXCommandCall } from '../../types/command.js' type Props = { diff --git a/src/commands/thinkback/thinkback.tsx b/src/commands/thinkback/thinkback.tsx index feea58a66..1986cd4ed 100644 --- a/src/commands/thinkback/thinkback.tsx +++ b/src/commands/thinkback/thinkback.tsx @@ -5,10 +5,9 @@ import * as React from 'react' import { useCallback, useEffect, useState } from 'react' import type { CommandResultDisplay } from '../../commands.js' import { Select } from '../../components/CustomSelect/select.js' -import { Dialog } from '../../components/design-system/Dialog.js' +import { Dialog } from '@anthropic/ink' import { Spinner } from '../../components/Spinner.js' -import instances from '../../ink/instances.js' -import { Box, Text } from '../../ink.js' +import { Box, Text, instances } from '@anthropic/ink' import { enablePluginOp } from '../../services/plugins/pluginOperations.js' import { logForDebugging } from '../../utils/debug.js' import { isENOENT, toError } from '../../utils/errors.js' diff --git a/src/components/AgentProgressLine.tsx b/src/components/AgentProgressLine.tsx index 7580160e7..c15a765a4 100644 --- a/src/components/AgentProgressLine.tsx +++ b/src/components/AgentProgressLine.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import { formatNumber } from '../utils/format.js' import type { Theme } from '../utils/theme.js' diff --git a/src/components/ApproveApiKey.tsx b/src/components/ApproveApiKey.tsx index 990f0c14e..545e78f6f 100644 --- a/src/components/ApproveApiKey.tsx +++ b/src/components/ApproveApiKey.tsx @@ -1,8 +1,7 @@ import React from 'react' -import { Text } from '../ink.js' +import { Text, Dialog } from '@anthropic/ink' import { saveGlobalConfig } from '../utils/config.js' import { Select } from './CustomSelect/index.js' -import { Dialog } from './design-system/Dialog.js' type Props = { customApiKeyTruncated: string diff --git a/src/components/AutoModeOptInDialog.tsx b/src/components/AutoModeOptInDialog.tsx index 4aeee28e6..a6d7f6a12 100644 --- a/src/components/AutoModeOptInDialog.tsx +++ b/src/components/AutoModeOptInDialog.tsx @@ -1,9 +1,8 @@ import React from 'react' import { logEvent } from 'src/services/analytics/index.js' -import { Box, Link, Text } from '../ink.js' +import { Box, Dialog, Link, Text } from '@anthropic/ink' import { updateSettingsForSource } from '../utils/settings/settings.js' import { Select } from './CustomSelect/index.js' -import { Dialog } from './design-system/Dialog.js' // NOTE: This copy is legally reviewed — do not modify without Legal team approval. export const AUTO_MODE_DESCRIPTION = diff --git a/src/components/AutoUpdater.tsx b/src/components/AutoUpdater.tsx index f5dc19586..6e9898ada 100644 --- a/src/components/AutoUpdater.tsx +++ b/src/components/AutoUpdater.tsx @@ -6,7 +6,7 @@ import { } from 'src/services/analytics/index.js' import { useInterval } from 'usehooks-ts' import { useUpdateNotification } from '../hooks/useUpdateNotification.js' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import { type AutoUpdaterResult, getLatestVersion, diff --git a/src/components/AwsAuthStatusBox.tsx b/src/components/AwsAuthStatusBox.tsx index ea2d1a5d3..fbe0216d7 100644 --- a/src/components/AwsAuthStatusBox.tsx +++ b/src/components/AwsAuthStatusBox.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react' -import { Box, Link, Text } from '../ink.js' +import { Box, Link, Text } from '@anthropic/ink' import { type AwsAuthStatus, AwsAuthStatusManager, diff --git a/src/components/BaseTextInput.tsx b/src/components/BaseTextInput.tsx index 07d12974b..054e6e2e0 100644 --- a/src/components/BaseTextInput.tsx +++ b/src/components/BaseTextInput.tsx @@ -1,8 +1,8 @@ import React from 'react' import { renderPlaceholder } from '../hooks/renderPlaceholder.js' import { usePasteHandler } from '../hooks/usePasteHandler.js' -import { useDeclaredCursor } from '../ink/hooks/use-declared-cursor.js' -import { Ansi, Box, Text, useInput } from '../ink.js' +import { useDeclaredCursor } from '@anthropic/ink' +import { Ansi, Box, Text, useInput } from '@anthropic/ink' import type { BaseInputState, BaseTextInputProps, diff --git a/src/components/BashModeProgress.tsx b/src/components/BashModeProgress.tsx index 0b6d4b408..c36dd1b32 100644 --- a/src/components/BashModeProgress.tsx +++ b/src/components/BashModeProgress.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Box } from '../ink.js' +import { Box } from '@anthropic/ink' import { BashTool } from '../tools/BashTool/BashTool.js' import type { ShellProgress } from '../types/tools.js' import { UserBashInputMessage } from './messages/UserBashInputMessage.js' diff --git a/src/components/BridgeDialog.tsx b/src/components/BridgeDialog.tsx index 9a23311fb..27707875d 100644 --- a/src/components/BridgeDialog.tsx +++ b/src/components/BridgeDialog.tsx @@ -15,12 +15,12 @@ import { } from '../constants/figures.js' import { useRegisterOverlay } from '../context/overlayContext.js' // eslint-disable-next-line custom-rules/prefer-use-keybindings -- raw 'd' key for disconnect, not a configurable keybinding action -import { Box, Text, useInput } from '../ink.js' +import { Box, Text, useInput } from '@anthropic/ink' import { useKeybindings } from '../keybindings/useKeybinding.js' import { useAppState, useSetAppState } from '../state/AppState.js' import { saveGlobalConfig } from '../utils/config.js' import { getBranch } from '../utils/git.js' -import { Dialog } from './design-system/Dialog.js' +import { Dialog } from '@anthropic/ink' type Props = { onDone: () => void diff --git a/src/components/BuiltinStatusLine.tsx b/src/components/BuiltinStatusLine.tsx index 14b7ec594..fdbf2969d 100644 --- a/src/components/BuiltinStatusLine.tsx +++ b/src/components/BuiltinStatusLine.tsx @@ -1,8 +1,7 @@ import React, { useEffect, useState } from 'react'; import { formatCost } from '../cost-tracker.js'; -import { Box, Text } from '../ink.js'; +import { Box, Text, ProgressBar } from '@anthropic/ink'; import { formatTokens } from '../utils/format.js'; -import { ProgressBar } from './design-system/ProgressBar.js'; import { useTerminalSize } from '../hooks/useTerminalSize.js'; type RateLimitBucket = { diff --git a/src/components/BypassPermissionsModeDialog.tsx b/src/components/BypassPermissionsModeDialog.tsx index adc708c77..8e8e40d88 100644 --- a/src/components/BypassPermissionsModeDialog.tsx +++ b/src/components/BypassPermissionsModeDialog.tsx @@ -1,10 +1,10 @@ import React, { useCallback } from 'react' import { logEvent } from 'src/services/analytics/index.js' -import { Box, Link, Newline, Text } from '../ink.js' +import { Box, Link, Newline, Text } from '@anthropic/ink' import { gracefulShutdownSync } from '../utils/gracefulShutdown.js' import { updateSettingsForSource } from '../utils/settings/settings.js' import { Select } from './CustomSelect/index.js' -import { Dialog } from './design-system/Dialog.js' +import { Dialog } from '@anthropic/ink' type Props = { onAccept(): void diff --git a/src/components/ChannelDowngradeDialog.tsx b/src/components/ChannelDowngradeDialog.tsx index 54db87690..5122dda7b 100644 --- a/src/components/ChannelDowngradeDialog.tsx +++ b/src/components/ChannelDowngradeDialog.tsx @@ -1,7 +1,7 @@ import React from 'react' -import { Text } from '../ink.js' +import { Text } from '@anthropic/ink' import { Select } from './CustomSelect/index.js' -import { Dialog } from './design-system/Dialog.js' +import { Dialog } from '@anthropic/ink' export type ChannelDowngradeChoice = 'downgrade' | 'stay' | 'cancel' diff --git a/src/components/ClaudeCodeHint/PluginHintMenu.tsx b/src/components/ClaudeCodeHint/PluginHintMenu.tsx index 8afd7cda8..e461afb77 100644 --- a/src/components/ClaudeCodeHint/PluginHintMenu.tsx +++ b/src/components/ClaudeCodeHint/PluginHintMenu.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { Select } from '../CustomSelect/select.js' import { PermissionDialog } from '../permissions/PermissionDialog.js' diff --git a/src/components/ClaudeInChromeOnboarding.tsx b/src/components/ClaudeInChromeOnboarding.tsx index 7c420ad43..ad2ca096c 100644 --- a/src/components/ClaudeInChromeOnboarding.tsx +++ b/src/components/ClaudeInChromeOnboarding.tsx @@ -1,10 +1,9 @@ import React from 'react' import { logEvent } from 'src/services/analytics/index.js' // eslint-disable-next-line custom-rules/prefer-use-keybindings -- enter to continue -import { Box, Link, Newline, Text, useInput } from '../ink.js' +import { Box, Dialog, Link, Newline, Text, useInput } from '@anthropic/ink' import { isChromeExtensionInstalled } from '../utils/claudeInChrome/setup.js' import { saveGlobalConfig } from '../utils/config.js' -import { Dialog } from './design-system/Dialog.js' const CHROME_EXTENSION_URL = 'https://claude.ai/chrome' const CHROME_PERMISSIONS_URL = 'https://clau.de/chrome/permissions' diff --git a/src/components/ClaudeMdExternalIncludesDialog.tsx b/src/components/ClaudeMdExternalIncludesDialog.tsx index 1ca6fcd12..2614010ca 100644 --- a/src/components/ClaudeMdExternalIncludesDialog.tsx +++ b/src/components/ClaudeMdExternalIncludesDialog.tsx @@ -1,10 +1,9 @@ import React, { useCallback } from 'react' import { logEvent } from 'src/services/analytics/index.js' -import { Box, Link, Text } from '../ink.js' +import { Box, Dialog, Link, Text } from '@anthropic/ink' import type { ExternalClaudeMdInclude } from '../utils/claudemd.js' import { saveCurrentProjectConfig } from '../utils/config.js' import { Select } from './CustomSelect/index.js' -import { Dialog } from './design-system/Dialog.js' type Props = { onDone(): void diff --git a/src/components/ClickableImageRef.tsx b/src/components/ClickableImageRef.tsx index 51144a720..2aa6485b6 100644 --- a/src/components/ClickableImageRef.tsx +++ b/src/components/ClickableImageRef.tsx @@ -1,8 +1,6 @@ import * as React from 'react' import { pathToFileURL } from 'url' -import Link from '../ink/components/Link.js' -import { supportsHyperlinks } from '../ink/supports-hyperlinks.js' -import { Text } from '../ink.js' +import { Link, supportsHyperlinks, Text } from '@anthropic/ink' import { getStoredImagePath } from '../utils/imageStore.js' import type { Theme } from '../utils/theme.js' diff --git a/src/components/CompactSummary.tsx b/src/components/CompactSummary.tsx index 1cd1687fa..e343a6591 100644 --- a/src/components/CompactSummary.tsx +++ b/src/components/CompactSummary.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { BLACK_CIRCLE } from '../constants/figures.js' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import type { Screen } from '../screens/REPL.js' import type { NormalizedUserMessage } from '../types/message.js' import { getUserMessageText } from '../utils/messages.js' diff --git a/src/components/ConfigurableShortcutHint.tsx b/src/components/ConfigurableShortcutHint.tsx index 82aea15fa..36ec0acd9 100644 --- a/src/components/ConfigurableShortcutHint.tsx +++ b/src/components/ConfigurableShortcutHint.tsx @@ -4,7 +4,7 @@ import type { KeybindingContextName, } from '../keybindings/types.js' import { useShortcutDisplay } from '../keybindings/useShortcutDisplay.js' -import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js' +import { KeyboardShortcutHint } from '@anthropic/ink' type Props = { /** The keybinding action (e.g., 'app:toggleTranscript') */ diff --git a/src/components/ConsoleOAuthFlow.tsx b/src/components/ConsoleOAuthFlow.tsx index 7330be828..5264b32b3 100644 --- a/src/components/ConsoleOAuthFlow.tsx +++ b/src/components/ConsoleOAuthFlow.tsx @@ -5,9 +5,7 @@ import { } from 'src/services/analytics/index.js' import { installOAuthTokens } from '../cli/handlers/auth.js' import { useTerminalSize } from '../hooks/useTerminalSize.js' -import { setClipboard } from '../ink/termio/osc.js' -import { useTerminalNotification } from '../ink/useTerminalNotification.js' -import { Box, Link, Text } from '../ink.js' +import { setClipboard, useTerminalNotification, Box, Link, Text, KeyboardShortcutHint } from '@anthropic/ink' import { useKeybinding } from '../keybindings/useKeybinding.js' import { getSSLErrorHint } from '../services/api/errorUtils.js' import { sendNotification } from '../services/notifier.js' @@ -16,7 +14,6 @@ import { getOauthAccountInfo, validateForceLoginOrg } from '../utils/auth.js' import { logError } from '../utils/log.js' import { getSettings_DEPRECATED, updateSettingsForSource } from '../utils/settings/settings.js' import { Select } from './CustomSelect/select.js' -import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js' import { Spinner } from './Spinner.js' import TextInput from './TextInput.js' import { fi } from 'zod/v4/locales' diff --git a/src/components/ContextSuggestions.tsx b/src/components/ContextSuggestions.tsx index 2eeafafe3..2017d7ab9 100644 --- a/src/components/ContextSuggestions.tsx +++ b/src/components/ContextSuggestions.tsx @@ -1,9 +1,8 @@ import figures from 'figures' import * as React from 'react' -import { Box, Text } from '../ink.js' +import { Box, Text, StatusIcon } from '@anthropic/ink' import type { ContextSuggestion } from '../utils/contextSuggestions.js' import { formatTokens } from '../utils/format.js' -import { StatusIcon } from './design-system/StatusIcon.js' type Props = { suggestions: ContextSuggestion[] diff --git a/src/components/ContextVisualization.tsx b/src/components/ContextVisualization.tsx index e6fb57493..3502db5f8 100644 --- a/src/components/ContextVisualization.tsx +++ b/src/components/ContextVisualization.tsx @@ -1,6 +1,6 @@ import { feature } from 'bun:bundle' import * as React from 'react' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import type { ContextData } from '../utils/analyzeContext.js' import { generateContextSuggestions } from '../utils/contextSuggestions.js' import { getDisplayPath } from '../utils/file.js' diff --git a/src/components/CoordinatorAgentStatus.tsx b/src/components/CoordinatorAgentStatus.tsx index 2aacb7d6a..9609a0662 100644 --- a/src/components/CoordinatorAgentStatus.tsx +++ b/src/components/CoordinatorAgentStatus.tsx @@ -10,8 +10,7 @@ import figures from 'figures' import * as React from 'react' import { BLACK_CIRCLE, PAUSE_ICON, PLAY_ICON } from '../constants/figures.js' import { useTerminalSize } from '../hooks/useTerminalSize.js' -import { stringWidth } from '../ink/stringWidth.js' -import { Box, Text, wrapText } from '../ink.js' +import { Box, Text, stringWidth, wrapText } from '@anthropic/ink' import { type AppState, useAppState, diff --git a/src/components/CostThresholdDialog.tsx b/src/components/CostThresholdDialog.tsx index 584d864a3..283e76c44 100644 --- a/src/components/CostThresholdDialog.tsx +++ b/src/components/CostThresholdDialog.tsx @@ -1,7 +1,6 @@ import React from 'react' -import { Box, Link, Text } from '../ink.js' +import { Box, Dialog, Link, Text } from '@anthropic/ink' import { Select } from './CustomSelect/index.js' -import { Dialog } from './design-system/Dialog.js' type Props = { onDone: () => void diff --git a/src/components/CtrlOToExpand.tsx b/src/components/CtrlOToExpand.tsx index 24b4add81..01ac3b2fb 100644 --- a/src/components/CtrlOToExpand.tsx +++ b/src/components/CtrlOToExpand.tsx @@ -1,9 +1,9 @@ import chalk from 'chalk' import React, { useContext } from 'react' -import { Text } from '../ink.js' +import { Text } from '@anthropic/ink' import { getShortcutDisplay } from '../keybindings/shortcutFormat.js' import { useShortcutDisplay } from '../keybindings/useShortcutDisplay.js' -import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js' +import { KeyboardShortcutHint } from '@anthropic/ink' import { InVirtualListContext } from './messageActions.js' // Context to track if we're inside a sub agent diff --git a/src/components/CustomSelect/SelectMulti.tsx b/src/components/CustomSelect/SelectMulti.tsx index bb43e9e1e..198917487 100644 --- a/src/components/CustomSelect/SelectMulti.tsx +++ b/src/components/CustomSelect/SelectMulti.tsx @@ -1,6 +1,6 @@ import figures from 'figures' import React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import type { PastedContent } from '../../utils/config.js' import type { ImageDimensions } from '../../utils/imageResizer.js' import type { OptionWithDescription } from './select.js' diff --git a/src/components/CustomSelect/select-input-option.tsx b/src/components/CustomSelect/select-input-option.tsx index 0f3f9483f..ccb8a3989 100644 --- a/src/components/CustomSelect/select-input-option.tsx +++ b/src/components/CustomSelect/select-input-option.tsx @@ -1,6 +1,6 @@ import React, { type ReactNode, useEffect, useRef, useState } from 'react' // eslint-disable-next-line custom-rules/prefer-use-keybindings -- UP arrow exit not in Attachments bindings -import { Box, Text, useInput } from '../../ink.js' +import { Box, Text, useInput } from '@anthropic/ink' import { useKeybinding, useKeybindings, @@ -10,7 +10,7 @@ import { getImageFromClipboard } from '../../utils/imagePaste.js' import type { ImageDimensions } from '../../utils/imageResizer.js' import { ClickableImageRef } from '../ClickableImageRef.js' import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js' -import { Byline } from '../design-system/Byline.js' +import { Byline } from '@anthropic/ink' import TextInput from '../TextInput.js' import type { OptionWithDescription } from './select.js' import { SelectOption } from './select-option.js' diff --git a/src/components/CustomSelect/select-option.tsx b/src/components/CustomSelect/select-option.tsx index 2f84affd9..a4c5a386d 100644 --- a/src/components/CustomSelect/select-option.tsx +++ b/src/components/CustomSelect/select-option.tsx @@ -1,5 +1,5 @@ import React, { type ReactNode } from 'react' -import { ListItem } from '../design-system/ListItem.js' +import { ListItem } from '@anthropic/ink' export type SelectOptionProps = { /** diff --git a/src/components/CustomSelect/select.tsx b/src/components/CustomSelect/select.tsx index d3a144772..7d368c2c3 100644 --- a/src/components/CustomSelect/select.tsx +++ b/src/components/CustomSelect/select.tsx @@ -1,8 +1,6 @@ import figures from 'figures' import React, { type ReactNode, useEffect, useRef, useState } from 'react' -import { useDeclaredCursor } from '../../ink/hooks/use-declared-cursor.js' -import { stringWidth } from '../../ink/stringWidth.js' -import { Ansi, Box, Text } from '../../ink.js' +import { Ansi, Box, Text, stringWidth, useDeclaredCursor } from '@anthropic/ink' import { count } from '../../utils/array.js' import type { PastedContent } from '../../utils/config.js' import type { ImageDimensions } from '../../utils/imageResizer.js' diff --git a/src/components/CustomSelect/use-multi-select-state.ts b/src/components/CustomSelect/use-multi-select-state.ts index 16b6c01b2..a089a20d4 100644 --- a/src/components/CustomSelect/use-multi-select-state.ts +++ b/src/components/CustomSelect/use-multi-select-state.ts @@ -1,9 +1,7 @@ import { useCallback, useState } from 'react' import { isDeepStrictEqual } from 'util' import { useRegisterOverlay } from '../../context/overlayContext.js' -import type { InputEvent } from '../../ink/events/input-event.js' -// eslint-disable-next-line custom-rules/prefer-use-keybindings -- raw space/arrow multiselect input -import { useInput } from '../../ink.js' +import { type InputEvent, useInput } from '@anthropic/ink' import { normalizeFullWidthDigits, normalizeFullWidthSpace, diff --git a/src/components/CustomSelect/use-select-input.ts b/src/components/CustomSelect/use-select-input.ts index dcafeb4f9..b289056ee 100644 --- a/src/components/CustomSelect/use-select-input.ts +++ b/src/components/CustomSelect/use-select-input.ts @@ -1,7 +1,6 @@ import { useMemo } from 'react' import { useRegisterOverlay } from '../../context/overlayContext.js' -import type { InputEvent } from '../../ink/events/input-event.js' -import { useInput } from '../../ink.js' +import { type InputEvent, useInput } from '@anthropic/ink' import { useKeybindings } from '../../keybindings/useKeybinding.js' import { normalizeFullWidthDigits, diff --git a/src/components/DesktopHandoff.tsx b/src/components/DesktopHandoff.tsx index 8e0632fd4..8dfe65156 100644 --- a/src/components/DesktopHandoff.tsx +++ b/src/components/DesktopHandoff.tsx @@ -1,16 +1,13 @@ import React, { useEffect, useState } from 'react' import type { CommandResultDisplay } from '../commands.js' // eslint-disable-next-line custom-rules/prefer-use-keybindings -- raw input for "any key" dismiss and y/n prompt -import { Box, Text, useInput } from '../ink.js' +import { Box, Text, useInput, LoadingState } from '@anthropic/ink' +import { getDesktopInstallStatus, openCurrentSessionInDesktop } from '../utils/desktopDeepLink.js' import { openBrowser } from '../utils/browser.js' -import { - getDesktopInstallStatus, - openCurrentSessionInDesktop, -} from '../utils/desktopDeepLink.js' + import { errorMessage } from '../utils/errors.js' import { gracefulShutdown } from '../utils/gracefulShutdown.js' import { flushSessionStorage } from '../utils/sessionStorage.js' -import { LoadingState } from './design-system/LoadingState.js' const DESKTOP_DOCS_URL = 'https://clau.de/desktop' diff --git a/src/components/DesktopUpsell/DesktopUpsellStartup.tsx b/src/components/DesktopUpsell/DesktopUpsellStartup.tsx index 9f5f233a4..f2ca4d46e 100644 --- a/src/components/DesktopUpsell/DesktopUpsellStartup.tsx +++ b/src/components/DesktopUpsell/DesktopUpsellStartup.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useEffect, useState } from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { getDynamicConfig_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' import { logEvent } from '../../services/analytics/index.js' import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js' diff --git a/src/components/DevBar.tsx b/src/components/DevBar.tsx index 6e093840e..c1f8aa9ee 100644 --- a/src/components/DevBar.tsx +++ b/src/components/DevBar.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { useState } from 'react' import { getSlowOperations } from '../bootstrap/state.js' -import { Text, useInterval } from '../ink.js' +import { Text, useInterval } from '@anthropic/ink' // Show DevBar for dev builds or all ants function shouldShowDevBar(): boolean { diff --git a/src/components/DevChannelsDialog.tsx b/src/components/DevChannelsDialog.tsx index 7dfc674cc..f055e4ae6 100644 --- a/src/components/DevChannelsDialog.tsx +++ b/src/components/DevChannelsDialog.tsx @@ -1,9 +1,8 @@ import React, { useCallback } from 'react' import type { ChannelEntry } from '../bootstrap/state.js' -import { Box, Text } from '../ink.js' +import { Box, Text, Dialog } from '@anthropic/ink' import { gracefulShutdownSync } from '../utils/gracefulShutdown.js' import { Select } from './CustomSelect/index.js' -import { Dialog } from './design-system/Dialog.js' type Props = { channels: ChannelEntry[] diff --git a/src/components/DiagnosticsDisplay.tsx b/src/components/DiagnosticsDisplay.tsx index ad01c7756..5a92190b5 100644 --- a/src/components/DiagnosticsDisplay.tsx +++ b/src/components/DiagnosticsDisplay.tsx @@ -1,6 +1,6 @@ import { relative } from 'path' import React from 'react' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import { DiagnosticTrackingService } from '../services/diagnosticTracking.js' import type { Attachment } from '../utils/attachments.js' import { getCwd } from '../utils/cwd.js' diff --git a/src/components/EffortCallout.tsx b/src/components/EffortCallout.tsx index 00feda439..a9acc10a8 100644 --- a/src/components/EffortCallout.tsx +++ b/src/components/EffortCallout.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useRef } from 'react' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import { isMaxSubscriber, isProSubscriber, diff --git a/src/components/ExportDialog.tsx b/src/components/ExportDialog.tsx index f4f1560a4..b217d4c15 100644 --- a/src/components/ExportDialog.tsx +++ b/src/components/ExportDialog.tsx @@ -2,16 +2,12 @@ import { join } from 'path' import React, { useCallback, useState } from 'react' import type { ExitState } from '../hooks/useExitOnCtrlCDWithKeybindings.js' import { useTerminalSize } from '../hooks/useTerminalSize.js' -import { setClipboard } from '../ink/termio/osc.js' -import { Box, Text } from '../ink.js' +import { setClipboard, Box, Text, Byline, Dialog, KeyboardShortcutHint } from '@anthropic/ink' import { useKeybinding } from '../keybindings/useKeybinding.js' import { getCwd } from '../utils/cwd.js' import { writeFileSync_DEPRECATED } from '../utils/slowOperations.js' import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js' import { Select } from './CustomSelect/select.js' -import { Byline } from './design-system/Byline.js' -import { Dialog } from './design-system/Dialog.js' -import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js' import TextInput from './TextInput.js' type ExportDialogProps = { diff --git a/src/components/FallbackToolUseErrorMessage.tsx b/src/components/FallbackToolUseErrorMessage.tsx index d86ac2b7c..a60ad65b9 100644 --- a/src/components/FallbackToolUseErrorMessage.tsx +++ b/src/components/FallbackToolUseErrorMessage.tsx @@ -3,7 +3,7 @@ import * as React from 'react' import { stripUnderlineAnsi } from 'src/components/shell/OutputLine.js' import { extractTag } from 'src/utils/messages.js' import { removeSandboxViolationTags } from 'src/utils/sandbox/sandbox-ui-utils.js' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import { useShortcutDisplay } from '../keybindings/useShortcutDisplay.js' import { countCharInString } from '../utils/stringUtils.js' import { MessageResponse } from './MessageResponse.js' diff --git a/src/components/FastIcon.tsx b/src/components/FastIcon.tsx index 956c6290a..b89283145 100644 --- a/src/components/FastIcon.tsx +++ b/src/components/FastIcon.tsx @@ -1,10 +1,9 @@ import chalk from 'chalk' import * as React from 'react' import { LIGHTNING_BOLT } from '../constants/figures.js' -import { Text } from '../ink.js' +import { Text, color } from '@anthropic/ink' import { getGlobalConfig } from '../utils/config.js' import { resolveThemeSetting } from '../utils/systemTheme.js' -import { color } from './design-system/color.js' type Props = { cooldown?: boolean diff --git a/src/components/Feedback.tsx b/src/components/Feedback.tsx index 5d3a3678f..6298b61b9 100644 --- a/src/components/Feedback.tsx +++ b/src/components/Feedback.tsx @@ -14,7 +14,7 @@ import { } from 'src/utils/messages.js' import type { CommandResultDisplay } from '../commands.js' import { useTerminalSize } from '../hooks/useTerminalSize.js' -import { Box, Text, useInput } from '../ink.js' +import { Box, Text, useInput } from '@anthropic/ink' import { useKeybinding } from '../keybindings/useKeybinding.js' import { queryHaiku } from '../services/api/claude.js' import { startsWithApiErrorPrefix } from '../services/api/errors.js' @@ -36,9 +36,7 @@ import { import { jsonStringify } from '../utils/slowOperations.js' import { asSystemPrompt } from '../utils/systemPromptType.js' import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js' -import { Byline } from './design-system/Byline.js' -import { Dialog } from './design-system/Dialog.js' -import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js' +import { Byline, Dialog, KeyboardShortcutHint } from '@anthropic/ink' import TextInput from './TextInput.js' // This value was determined experimentally by testing the URL length limit diff --git a/src/components/FeedbackSurvey/FeedbackSurvey.tsx b/src/components/FeedbackSurvey/FeedbackSurvey.tsx index 2f9c8e47d..a92213441 100644 --- a/src/components/FeedbackSurvey/FeedbackSurvey.tsx +++ b/src/components/FeedbackSurvey/FeedbackSurvey.tsx @@ -3,7 +3,7 @@ import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, } from 'src/services/analytics/index.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { FeedbackSurveyView, isValidResponseInput, diff --git a/src/components/FeedbackSurvey/FeedbackSurveyView.tsx b/src/components/FeedbackSurvey/FeedbackSurveyView.tsx index a8eadf3ba..5a8721e12 100644 --- a/src/components/FeedbackSurvey/FeedbackSurveyView.tsx +++ b/src/components/FeedbackSurvey/FeedbackSurveyView.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { useDebouncedDigitInput } from './useDebouncedDigitInput.js' import type { FeedbackSurveyResponse } from './utils.js' diff --git a/src/components/FeedbackSurvey/TranscriptSharePrompt.tsx b/src/components/FeedbackSurvey/TranscriptSharePrompt.tsx index ec7a974f5..d3f39d25e 100644 --- a/src/components/FeedbackSurvey/TranscriptSharePrompt.tsx +++ b/src/components/FeedbackSurvey/TranscriptSharePrompt.tsx @@ -1,6 +1,6 @@ import React from 'react' import { BLACK_CIRCLE } from '../../constants/figures.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { useDebouncedDigitInput } from './useDebouncedDigitInput.js' export type TranscriptShareResponse = 'yes' | 'no' | 'dont_ask_again' diff --git a/src/components/FileEditToolDiff.tsx b/src/components/FileEditToolDiff.tsx index a5146d3c8..75efda0f5 100644 --- a/src/components/FileEditToolDiff.tsx +++ b/src/components/FileEditToolDiff.tsx @@ -2,7 +2,7 @@ import type { StructuredPatchHunk } from 'diff' import * as React from 'react' import { Suspense, use, useState } from 'react' import { useTerminalSize } from '../hooks/useTerminalSize.js' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import type { FileEdit } from '../tools/FileEditTool/types.js' import { findActualString, diff --git a/src/components/FileEditToolUpdatedMessage.tsx b/src/components/FileEditToolUpdatedMessage.tsx index 0248583af..5fbc68005 100644 --- a/src/components/FileEditToolUpdatedMessage.tsx +++ b/src/components/FileEditToolUpdatedMessage.tsx @@ -1,7 +1,7 @@ import type { StructuredPatchHunk } from 'diff' import * as React from 'react' import { useTerminalSize } from '../hooks/useTerminalSize.js' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import { count } from '../utils/array.js' import { MessageResponse } from './MessageResponse.js' import { StructuredDiffList } from './StructuredDiffList.js' diff --git a/src/components/FileEditToolUseRejectedMessage.tsx b/src/components/FileEditToolUseRejectedMessage.tsx index 6171b0f65..2d53117d2 100644 --- a/src/components/FileEditToolUseRejectedMessage.tsx +++ b/src/components/FileEditToolUseRejectedMessage.tsx @@ -3,7 +3,7 @@ import { relative } from 'path' import * as React from 'react' import { useTerminalSize } from 'src/hooks/useTerminalSize.js' import { getCwd } from 'src/utils/cwd.js' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import { HighlightedCode } from './HighlightedCode.js' import { MessageResponse } from './MessageResponse.js' import { StructuredDiffList } from './StructuredDiffList.js' diff --git a/src/components/FilePathLink.tsx b/src/components/FilePathLink.tsx index 05a6167a2..9ab7f3cd1 100644 --- a/src/components/FilePathLink.tsx +++ b/src/components/FilePathLink.tsx @@ -1,6 +1,6 @@ import React from 'react' import { pathToFileURL } from 'url' -import Link from '../ink/components/Link.js' +import { Link } from '@anthropic/ink' type Props = { /** The absolute file path */ diff --git a/src/components/FullscreenLayout.tsx b/src/components/FullscreenLayout.tsx index 8502e46de..608c7b1c9 100644 --- a/src/components/FullscreenLayout.tsx +++ b/src/components/FullscreenLayout.tsx @@ -19,9 +19,7 @@ import { usePromptOverlayDialog, } from '../context/promptOverlayContext.js' import { useTerminalSize } from '../hooks/useTerminalSize.js' -import ScrollBox, { type ScrollBoxHandle } from '../ink/components/ScrollBox.js' -import instances from '../ink/instances.js' -import { Box, Text } from '../ink.js' +import { Box, ScrollBox, type ScrollBoxHandle, Text, instances } from '@anthropic/ink' import type { Message } from '../types/message.js' import { openBrowser, openPath } from '../utils/browser.js' import { isFullscreenEnvEnabled } from '../utils/fullscreen.js' diff --git a/src/components/GlobalSearchDialog.tsx b/src/components/GlobalSearchDialog.tsx index 0df3231ce..ee1d2fdf5 100644 --- a/src/components/GlobalSearchDialog.tsx +++ b/src/components/GlobalSearchDialog.tsx @@ -3,7 +3,7 @@ import * as React from 'react' import { useEffect, useRef, useState } from 'react' import { useRegisterOverlay } from '../context/overlayContext.js' import { useTerminalSize } from '../hooks/useTerminalSize.js' -import { Text } from '../ink.js' +import { Text } from '@anthropic/ink' import { logEvent } from '../services/analytics/index.js' import { getCwd } from '../utils/cwd.js' import { openFileInExternalEditor } from '../utils/editor.js' @@ -12,8 +12,7 @@ import { highlightMatch } from '../utils/highlightMatch.js' import { relativePath } from '../utils/permissions/filesystem.js' import { readFileInRange } from '../utils/readFileInRange.js' import { ripGrepStream } from '../utils/ripgrep.js' -import { FuzzyPicker } from './design-system/FuzzyPicker.js' -import { LoadingState } from './design-system/LoadingState.js' +import { FuzzyPicker, LoadingState } from '@anthropic/ink' type Props = { onDone: () => void diff --git a/src/components/HelpV2/Commands.tsx b/src/components/HelpV2/Commands.tsx index 9e34e5539..fcff85f72 100644 --- a/src/components/HelpV2/Commands.tsx +++ b/src/components/HelpV2/Commands.tsx @@ -1,10 +1,7 @@ import * as React from 'react' import { useMemo } from 'react' import { type Command, formatDescriptionWithSource } from '../../commands.js' -import { Box, Text } from '../../ink.js' -import { truncate } from '../../utils/format.js' -import { Select } from '../CustomSelect/select.js' -import { useTabHeaderFocus } from '../design-system/Tabs.js' +import { Box, Text } from '@anthropic/ink' type Props = { commands: Command[] diff --git a/src/components/HelpV2/General.tsx b/src/components/HelpV2/General.tsx index 69b5c6509..4117d9568 100644 --- a/src/components/HelpV2/General.tsx +++ b/src/components/HelpV2/General.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { PromptInputHelpMenu } from '../PromptInput/PromptInputHelpMenu.js' export function General(): React.ReactNode { diff --git a/src/components/HelpV2/HelpV2.tsx b/src/components/HelpV2/HelpV2.tsx index 9e2b0ce27..2a3e0b4e2 100644 --- a/src/components/HelpV2/HelpV2.tsx +++ b/src/components/HelpV2/HelpV2.tsx @@ -9,10 +9,8 @@ import { } from '../../commands.js' import { useIsInsideModal } from '../../context/modalContext.js' import { useTerminalSize } from '../../hooks/useTerminalSize.js' -import { Box, Link, Text } from '../../ink.js' +import { Box, Link, Text, Tab, Tabs, Pane } from '@anthropic/ink' import { useKeybinding } from '../../keybindings/useKeybinding.js' -import { Pane } from '../design-system/Pane.js' -import { Tab, Tabs } from '../design-system/Tabs.js' import { Commands } from './Commands.js' import { General } from './General.js' diff --git a/src/components/HighlightedCode.tsx b/src/components/HighlightedCode.tsx index 47f7271bc..3cfe9a736 100644 --- a/src/components/HighlightedCode.tsx +++ b/src/components/HighlightedCode.tsx @@ -1,15 +1,7 @@ import * as React from 'react' import { memo, useEffect, useMemo, useRef, useState } from 'react' import { useSettings } from '../hooks/useSettings.js' -import { - Ansi, - Box, - type DOMElement, - measureElement, - NoSelect, - Text, - useTheme, -} from '../ink.js' +import { Ansi, Box, type DOMElement, measureElement, NoSelect, Text, useTheme } from '@anthropic/ink' import { isFullscreenEnvEnabled } from '../utils/fullscreen.js' import sliceAnsi from '../utils/sliceAnsi.js' import { countCharInString } from '../utils/stringUtils.js' diff --git a/src/components/HighlightedCode/Fallback.tsx b/src/components/HighlightedCode/Fallback.tsx index 3d1f70112..e81d44f3d 100644 --- a/src/components/HighlightedCode/Fallback.tsx +++ b/src/components/HighlightedCode/Fallback.tsx @@ -1,6 +1,6 @@ import { extname } from 'path' import React, { Suspense, use, useMemo } from 'react' -import { Ansi, Text } from '../../ink.js' +import { Ansi, Text } from '@anthropic/ink' import { getCliHighlightPromise } from '../../utils/cliHighlight.js' import { logForDebugging } from '../../utils/debug.js' import { convertLeadingTabsToSpaces } from '../../utils/file.js' diff --git a/src/components/HistorySearchDialog.tsx b/src/components/HistorySearchDialog.tsx index dd2e02da5..54bf98372 100644 --- a/src/components/HistorySearchDialog.tsx +++ b/src/components/HistorySearchDialog.tsx @@ -6,13 +6,11 @@ import { type TimestampedHistoryEntry, } from '../history.js' import { useTerminalSize } from '../hooks/useTerminalSize.js' -import { stringWidth } from '../ink/stringWidth.js' -import { wrapAnsi } from '../ink/wrapAnsi.js' -import { Box, Text } from '../ink.js' +import { Box, Text, stringWidth, wrapAnsi } from '@anthropic/ink' import { logEvent } from '../services/analytics/index.js' import type { HistoryEntry } from '../utils/config.js' import { formatRelativeTimeAgo, truncateToWidth } from '../utils/format.js' -import { FuzzyPicker } from './design-system/FuzzyPicker.js' +import { FuzzyPicker } from '@anthropic/ink' type Props = { initialQuery?: string diff --git a/src/components/IdeAutoConnectDialog.tsx b/src/components/IdeAutoConnectDialog.tsx index 2377cfb3a..f262cc465 100644 --- a/src/components/IdeAutoConnectDialog.tsx +++ b/src/components/IdeAutoConnectDialog.tsx @@ -1,9 +1,8 @@ import React, { useCallback } from 'react' -import { Text } from '../ink.js' +import { Text, Dialog } from '@anthropic/ink' import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js' import { isSupportedTerminal } from '../utils/ide.js' import { Select } from './CustomSelect/index.js' -import { Dialog } from './design-system/Dialog.js' type IdeAutoConnectDialogProps = { onComplete: () => void diff --git a/src/components/IdeOnboardingDialog.tsx b/src/components/IdeOnboardingDialog.tsx index 86f03018e..aae0b1742 100644 --- a/src/components/IdeOnboardingDialog.tsx +++ b/src/components/IdeOnboardingDialog.tsx @@ -1,6 +1,6 @@ import React from 'react' import { envDynamic } from 'src/utils/envDynamic.js' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import { useKeybindings } from '../keybindings/useKeybinding.js' import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js' import { env } from '../utils/env.js' @@ -10,7 +10,7 @@ import { isJetBrainsIde, toIDEDisplayName, } from '../utils/ide.js' -import { Dialog } from './design-system/Dialog.js' +import { Dialog } from '@anthropic/ink' interface Props { onDone: () => void diff --git a/src/components/IdeStatusIndicator.tsx b/src/components/IdeStatusIndicator.tsx index 13c1846c0..13096b120 100644 --- a/src/components/IdeStatusIndicator.tsx +++ b/src/components/IdeStatusIndicator.tsx @@ -2,7 +2,7 @@ import { basename } from 'path' import * as React from 'react' import { useIdeConnectionStatus } from '../hooks/useIdeConnectionStatus.js' import type { IDESelection } from '../hooks/useIdeSelection.js' -import { Text } from '../ink.js' +import { Text } from '@anthropic/ink' import type { MCPServerConnection } from '../services/mcp/types.js' type IdeStatusIndicatorProps = { diff --git a/src/components/IdleReturnDialog.tsx b/src/components/IdleReturnDialog.tsx index d651cfe38..9ffc8893a 100644 --- a/src/components/IdleReturnDialog.tsx +++ b/src/components/IdleReturnDialog.tsx @@ -1,8 +1,8 @@ import React from 'react' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import { formatTokens } from '../utils/format.js' import { Select } from './CustomSelect/index.js' -import { Dialog } from './design-system/Dialog.js' +import { Dialog } from '@anthropic/ink' type IdleReturnAction = 'continue' | 'clear' | 'dismiss' | 'never' diff --git a/src/components/InterruptedByUser.tsx b/src/components/InterruptedByUser.tsx index 0a77c7153..13c641f0b 100644 --- a/src/components/InterruptedByUser.tsx +++ b/src/components/InterruptedByUser.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Text } from '../ink.js' +import { Text } from '@anthropic/ink' export function InterruptedByUser(): React.ReactNode { return ( diff --git a/src/components/InvalidConfigDialog.tsx b/src/components/InvalidConfigDialog.tsx index 8fa3bba97..7bbc04b14 100644 --- a/src/components/InvalidConfigDialog.tsx +++ b/src/components/InvalidConfigDialog.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Box, render, Text } from '../ink.js' +import { Box, Dialog, wrappedRender as render, Text } from '@anthropic/ink' import { KeybindingSetup } from '../keybindings/KeybindingProviderSetup.js' import { AppStateProvider } from '../state/AppState.js' import type { ConfigParseError } from '../utils/errors.js' @@ -10,7 +10,6 @@ import { } from '../utils/slowOperations.js' import type { ThemeName } from '../utils/theme.js' import { Select } from './CustomSelect/index.js' -import { Dialog } from './design-system/Dialog.js' interface InvalidConfigHandlerProps { error: ConfigParseError diff --git a/src/components/InvalidSettingsDialog.tsx b/src/components/InvalidSettingsDialog.tsx index c1fddf96a..f3b0db018 100644 --- a/src/components/InvalidSettingsDialog.tsx +++ b/src/components/InvalidSettingsDialog.tsx @@ -1,8 +1,7 @@ import React from 'react' -import { Text } from '../ink.js' +import { Text, Dialog } from '@anthropic/ink' import type { ValidationError } from '../utils/settings/validation.js' import { Select } from './CustomSelect/index.js' -import { Dialog } from './design-system/Dialog.js' import { ValidationErrorsList } from './ValidationErrorsList.js' type Props = { diff --git a/src/components/KeybindingWarnings.tsx b/src/components/KeybindingWarnings.tsx index 8f6957c3e..dc1b5a74a 100644 --- a/src/components/KeybindingWarnings.tsx +++ b/src/components/KeybindingWarnings.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import { getCachedKeybindingWarnings, getKeybindingsPath, diff --git a/src/components/LanguagePicker.tsx b/src/components/LanguagePicker.tsx index 53be69d48..ae357ff8e 100644 --- a/src/components/LanguagePicker.tsx +++ b/src/components/LanguagePicker.tsx @@ -1,6 +1,6 @@ import figures from 'figures' import React, { useState } from 'react' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import { useKeybinding } from '../keybindings/useKeybinding.js' import TextInput from './TextInput.js' diff --git a/src/components/LogSelector.tsx b/src/components/LogSelector.tsx index d1fb9f607..806a0082d 100644 --- a/src/components/LogSelector.tsx +++ b/src/components/LogSelector.tsx @@ -6,9 +6,7 @@ import { getOriginalCwd, getSessionId } from '../bootstrap/state.js' import { useExitOnCtrlCDWithKeybindings } from '../hooks/useExitOnCtrlCDWithKeybindings.js' import { useSearchInput } from '../hooks/useSearchInput.js' import { useTerminalSize } from '../hooks/useTerminalSize.js' -import { applyColor } from '../ink/colorize.js' -import type { Color } from '../ink/styles.js' -import { Box, Text, useInput, useTerminalFocus, useTheme } from '../ink.js' +import { applyColor, Box, Text, useInput, useTerminalFocus, useTheme, type Color, Byline, Divider, KeyboardShortcutHint } from '@anthropic/ink' import { useKeybinding } from '../keybindings/useKeybinding.js' import { logEvent } from '../services/analytics/index.js' import type { LogOption, SerializedMessage } from '../types/logs.js' @@ -25,9 +23,6 @@ import { import { getTheme } from '../utils/theme.js' import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js' import { Select } from './CustomSelect/select.js' -import { Byline } from './design-system/Byline.js' -import { Divider } from './design-system/Divider.js' -import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js' import { SearchBox } from './SearchBox.js' import { SessionPreview } from './SessionPreview.js' import { Spinner } from './Spinner.js' diff --git a/src/components/LogoV2/AnimatedAsterisk.tsx b/src/components/LogoV2/AnimatedAsterisk.tsx index 1c5adcf06..9e72baaa2 100644 --- a/src/components/LogoV2/AnimatedAsterisk.tsx +++ b/src/components/LogoV2/AnimatedAsterisk.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { useEffect, useRef, useState } from 'react' import { TEARDROP_ASTERISK } from '../../constants/figures.js' -import { Box, Text, useAnimationFrame } from '../../ink.js' +import { Box, Text, useAnimationFrame } from '@anthropic/ink' import { getInitialSettings } from '../../utils/settings/settings.js' import { hueToRgb, toRGBColor } from '../Spinner/utils.js' diff --git a/src/components/LogoV2/AnimatedClawd.tsx b/src/components/LogoV2/AnimatedClawd.tsx index ed3060066..5ad68babb 100644 --- a/src/components/LogoV2/AnimatedClawd.tsx +++ b/src/components/LogoV2/AnimatedClawd.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useEffect, useRef, useState } from 'react' -import { Box } from '../../ink.js' +import { Box } from '@anthropic/ink' import { getInitialSettings } from '../../utils/settings/settings.js' import { Clawd, type ClawdPose } from './Clawd.js' diff --git a/src/components/LogoV2/ChannelsNotice.tsx b/src/components/LogoV2/ChannelsNotice.tsx index 66f41b303..c58f400c1 100644 --- a/src/components/LogoV2/ChannelsNotice.tsx +++ b/src/components/LogoV2/ChannelsNotice.tsx @@ -11,7 +11,7 @@ import { getAllowedChannels, getHasDevChannels, } from '../../bootstrap/state.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { isChannelsEnabled } from '../../services/mcp/channelAllowlist.js' import { getEffectiveChannelAllowlist } from '../../services/mcp/channelNotification.js' import { getMcpConfigsByScope } from '../../services/mcp/config.js' diff --git a/src/components/LogoV2/Clawd.tsx b/src/components/LogoV2/Clawd.tsx index 8ddc1bf8e..6969466bc 100644 --- a/src/components/LogoV2/Clawd.tsx +++ b/src/components/LogoV2/Clawd.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { env } from '../../utils/env.js' export type ClawdPose = diff --git a/src/components/LogoV2/CondensedLogo.tsx b/src/components/LogoV2/CondensedLogo.tsx index be587bf83..eb048ec2d 100644 --- a/src/components/LogoV2/CondensedLogo.tsx +++ b/src/components/LogoV2/CondensedLogo.tsx @@ -2,8 +2,7 @@ import * as React from 'react' import { type ReactNode, useEffect } from 'react' import { useMainLoopModel } from '../../hooks/useMainLoopModel.js' import { useTerminalSize } from '../../hooks/useTerminalSize.js' -import { stringWidth } from '../../ink/stringWidth.js' -import { Box, Text } from '../../ink.js' +import { Box, Text, stringWidth } from '@anthropic/ink' import { useAppState } from '../../state/AppState.js' import { getEffortSuffix } from '../../utils/effort.js' import { truncate } from '../../utils/format.js' diff --git a/src/components/LogoV2/EmergencyTip.tsx b/src/components/LogoV2/EmergencyTip.tsx index c0a8235ba..33280bcf7 100644 --- a/src/components/LogoV2/EmergencyTip.tsx +++ b/src/components/LogoV2/EmergencyTip.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useEffect, useMemo } from 'react' -import { Box, Text } from 'src/ink.js' +import { Box, Text } from '@anthropic/ink' import { getDynamicConfig_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js' import { getGlobalConfig, saveGlobalConfig } from 'src/utils/config.js' diff --git a/src/components/LogoV2/Feed.tsx b/src/components/LogoV2/Feed.tsx index 15a7d84d6..be849bfd7 100644 --- a/src/components/LogoV2/Feed.tsx +++ b/src/components/LogoV2/Feed.tsx @@ -1,6 +1,5 @@ import * as React from 'react' -import { stringWidth } from '../../ink/stringWidth.js' -import { Box, Text } from '../../ink.js' +import { Box, Text, stringWidth } from '@anthropic/ink' import { truncate } from '../../utils/format.js' export type FeedLine = { diff --git a/src/components/LogoV2/FeedColumn.tsx b/src/components/LogoV2/FeedColumn.tsx index 0b08ec84a..4c6ae84f5 100644 --- a/src/components/LogoV2/FeedColumn.tsx +++ b/src/components/LogoV2/FeedColumn.tsx @@ -1,6 +1,6 @@ import * as React from 'react' -import { Box } from '../../ink.js' -import { Divider } from '../design-system/Divider.js' +import { Box } from '@anthropic/ink' +import { Divider } from '@anthropic/ink' import type { FeedConfig } from './Feed.js' import { calculateFeedWidth, Feed } from './Feed.js' diff --git a/src/components/LogoV2/GuestPassesUpsell.tsx b/src/components/LogoV2/GuestPassesUpsell.tsx index 12796e43b..c1bae01b4 100644 --- a/src/components/LogoV2/GuestPassesUpsell.tsx +++ b/src/components/LogoV2/GuestPassesUpsell.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useState } from 'react' -import { Text } from '../../ink.js' +import { Text } from '@anthropic/ink' import { logEvent } from '../../services/analytics/index.js' import { checkCachedPassesEligibility, diff --git a/src/components/LogoV2/LogoV2.tsx b/src/components/LogoV2/LogoV2.tsx index dd7cf04e8..c7dcf4139 100644 --- a/src/components/LogoV2/LogoV2.tsx +++ b/src/components/LogoV2/LogoV2.tsx @@ -1,8 +1,7 @@ // biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered import * as React from 'react' -import { Box, Text, color } from '../../ink.js' +import { Box, Text, color, stringWidth } from '@anthropic/ink' import { useTerminalSize } from '../../hooks/useTerminalSize.js' -import { stringWidth } from '../../ink/stringWidth.js' import { getLayoutMode, calculateLayoutDimensions, diff --git a/src/components/LogoV2/Opus1mMergeNotice.tsx b/src/components/LogoV2/Opus1mMergeNotice.tsx index 63c42ab66..9bbf84752 100644 --- a/src/components/LogoV2/Opus1mMergeNotice.tsx +++ b/src/components/LogoV2/Opus1mMergeNotice.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { useEffect, useState } from 'react' import { UP_ARROW } from '../../constants/figures.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js' import { isOpus1mMergeEnabled } from '../../utils/model/model.js' import { AnimatedAsterisk } from './AnimatedAsterisk.js' diff --git a/src/components/LogoV2/OverageCreditUpsell.tsx b/src/components/LogoV2/OverageCreditUpsell.tsx index ce006e0d0..c08140645 100644 --- a/src/components/LogoV2/OverageCreditUpsell.tsx +++ b/src/components/LogoV2/OverageCreditUpsell.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useState } from 'react' -import { Text } from '../../ink.js' +import { Text } from '@anthropic/ink' import { logEvent } from '../../services/analytics/index.js' import { formatGrantAmount, diff --git a/src/components/LogoV2/VoiceModeNotice.tsx b/src/components/LogoV2/VoiceModeNotice.tsx index 531460533..b8e74e3c6 100644 --- a/src/components/LogoV2/VoiceModeNotice.tsx +++ b/src/components/LogoV2/VoiceModeNotice.tsx @@ -1,7 +1,7 @@ import { feature } from 'bun:bundle' import * as React from 'react' import { useEffect, useState } from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js' import { getInitialSettings } from '../../utils/settings/settings.js' import { isVoiceModeEnabled } from '../../voice/voiceModeEnabled.js' diff --git a/src/components/LogoV2/WelcomeV2.tsx b/src/components/LogoV2/WelcomeV2.tsx index 354e1182b..ccbbcbf44 100644 --- a/src/components/LogoV2/WelcomeV2.tsx +++ b/src/components/LogoV2/WelcomeV2.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Box, Text, useTheme } from 'src/ink.js' +import { Box, Text, useTheme } from '@anthropic/ink' import { env } from '../../utils/env.js' const WELCOME_V2_WIDTH = 58 diff --git a/src/components/LogoV2/feedConfigs.tsx b/src/components/LogoV2/feedConfigs.tsx index 50ec4575c..8f6652f3c 100644 --- a/src/components/LogoV2/feedConfigs.tsx +++ b/src/components/LogoV2/feedConfigs.tsx @@ -1,7 +1,7 @@ import figures from 'figures' import { homedir } from 'os' import * as React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import type { Step } from '../../projectOnboardingState.js' import { formatCreditAmount, diff --git a/src/components/LspRecommendation/LspRecommendationMenu.tsx b/src/components/LspRecommendation/LspRecommendationMenu.tsx index 7dc41ac39..3ea815069 100644 --- a/src/components/LspRecommendation/LspRecommendationMenu.tsx +++ b/src/components/LspRecommendation/LspRecommendationMenu.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { Select } from '../CustomSelect/select.js' import { PermissionDialog } from '../permissions/PermissionDialog.js' diff --git a/src/components/MCPServerApprovalDialog.tsx b/src/components/MCPServerApprovalDialog.tsx index 5d5e00898..f80d42008 100644 --- a/src/components/MCPServerApprovalDialog.tsx +++ b/src/components/MCPServerApprovalDialog.tsx @@ -8,7 +8,7 @@ import { updateSettingsForSource, } from '../utils/settings/settings.js' import { Select } from './CustomSelect/index.js' -import { Dialog } from './design-system/Dialog.js' +import { Dialog } from '@anthropic/ink' import { MCPServerDialogCopy } from './MCPServerDialogCopy.js' type Props = { diff --git a/src/components/MCPServerDesktopImportDialog.tsx b/src/components/MCPServerDesktopImportDialog.tsx index 50b9ef6d6..aaea61894 100644 --- a/src/components/MCPServerDesktopImportDialog.tsx +++ b/src/components/MCPServerDesktopImportDialog.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useEffect, useState } from 'react' import { gracefulShutdown } from 'src/utils/gracefulShutdown.js' import { writeToStdout } from 'src/utils/process.js' -import { Box, color, Text, useTheme } from '../ink.js' +import { Box, color, Text, useTheme, Byline, Dialog, KeyboardShortcutHint } from '@anthropic/ink' import { addMcpConfig, getAllMcpConfigs } from '../services/mcp/config.js' import type { ConfigScope, @@ -11,9 +11,6 @@ import type { import { plural } from '../utils/stringUtils.js' import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js' import { SelectMulti } from './CustomSelect/SelectMulti.js' -import { Byline } from './design-system/Byline.js' -import { Dialog } from './design-system/Dialog.js' -import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js' type Props = { servers: Record diff --git a/src/components/MCPServerDialogCopy.tsx b/src/components/MCPServerDialogCopy.tsx index 93dce3655..f6a87674f 100644 --- a/src/components/MCPServerDialogCopy.tsx +++ b/src/components/MCPServerDialogCopy.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Link, Text } from '../ink.js' +import { Link, Text } from '@anthropic/ink' export function MCPServerDialogCopy(): React.ReactNode { return ( diff --git a/src/components/MCPServerMultiselectDialog.tsx b/src/components/MCPServerMultiselectDialog.tsx index e14c46d46..fa41da3a7 100644 --- a/src/components/MCPServerMultiselectDialog.tsx +++ b/src/components/MCPServerMultiselectDialog.tsx @@ -1,16 +1,14 @@ import partition from 'lodash-es/partition.js' import React, { useCallback } from 'react' import { logEvent } from 'src/services/analytics/index.js' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import { getSettings_DEPRECATED, updateSettingsForSource, } from '../utils/settings/settings.js' import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js' import { SelectMulti } from './CustomSelect/SelectMulti.js' -import { Byline } from './design-system/Byline.js' -import { Dialog } from './design-system/Dialog.js' -import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js' +import { Byline, Dialog, KeyboardShortcutHint } from '@anthropic/ink' import { MCPServerDialogCopy } from './MCPServerDialogCopy.js' type Props = { diff --git a/src/components/ManagedSettingsSecurityDialog/ManagedSettingsSecurityDialog.tsx b/src/components/ManagedSettingsSecurityDialog/ManagedSettingsSecurityDialog.tsx index 392979770..2d7de2ec7 100644 --- a/src/components/ManagedSettingsSecurityDialog/ManagedSettingsSecurityDialog.tsx +++ b/src/components/ManagedSettingsSecurityDialog/ManagedSettingsSecurityDialog.tsx @@ -1,6 +1,6 @@ import React from 'react' import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { useKeybinding } from '../../keybindings/useKeybinding.js' import type { SettingsJson } from '../../utils/settings/types.js' import { Select } from '../CustomSelect/index.js' diff --git a/src/components/Markdown.tsx b/src/components/Markdown.tsx index 4616f46c2..063404baf 100644 --- a/src/components/Markdown.tsx +++ b/src/components/Markdown.tsx @@ -1,7 +1,7 @@ import { marked, type Token, type Tokens } from 'marked' import React, { Suspense, use, useMemo, useRef } from 'react' import { useSettings } from '../hooks/useSettings.js' -import { Ansi, Box, useTheme } from '../ink.js' +import { Ansi, Box, useTheme } from '@anthropic/ink' import { type CliHighlight, getCliHighlightPromise, diff --git a/src/components/MarkdownTable.tsx b/src/components/MarkdownTable.tsx index c8997d9a1..0b3473d43 100644 --- a/src/components/MarkdownTable.tsx +++ b/src/components/MarkdownTable.tsx @@ -2,9 +2,7 @@ import type { Token, Tokens } from 'marked' import React from 'react' import stripAnsi from 'strip-ansi' import { useTerminalSize } from '../hooks/useTerminalSize.js' -import { stringWidth } from '../ink/stringWidth.js' -import { wrapAnsi } from '../ink/wrapAnsi.js' -import { Ansi, useTheme } from '../ink.js' +import { Ansi, stringWidth, useTheme, wrapAnsi } from '@anthropic/ink' import type { CliHighlight } from '../utils/cliHighlight.js' import { formatToken, padAligned } from '../utils/markdown.js' diff --git a/src/components/MemoryUsageIndicator.tsx b/src/components/MemoryUsageIndicator.tsx index 37c91e778..4004133c2 100644 --- a/src/components/MemoryUsageIndicator.tsx +++ b/src/components/MemoryUsageIndicator.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useMemoryUsage } from '../hooks/useMemoryUsage.js' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import { formatFileSize } from '../utils/format.js' export function MemoryUsageIndicator(): React.ReactNode { diff --git a/src/components/Message.tsx b/src/components/Message.tsx index 79a152682..7f5055465 100644 --- a/src/components/Message.tsx +++ b/src/components/Message.tsx @@ -10,7 +10,7 @@ import type { import * as React from 'react' import type { Command } from '../commands.js' import { useTerminalSize } from '../hooks/useTerminalSize.js' -import { Box } from '../ink.js' +import { Box } from '@anthropic/ink' import type { Tools } from '../Tool.js' import { type ConnectorTextBlock, diff --git a/src/components/MessageModel.tsx b/src/components/MessageModel.tsx index a99f861e6..4afa5bc33 100644 --- a/src/components/MessageModel.tsx +++ b/src/components/MessageModel.tsx @@ -1,6 +1,5 @@ import React from 'react' -import { stringWidth } from '../ink/stringWidth.js' -import { Box, Text } from '../ink.js' +import { Box, Text, stringWidth } from '@anthropic/ink' import type { NormalizedMessage } from '../types/message.js' type Props = { diff --git a/src/components/MessageResponse.tsx b/src/components/MessageResponse.tsx index f71d40ce8..a8dd8613c 100644 --- a/src/components/MessageResponse.tsx +++ b/src/components/MessageResponse.tsx @@ -1,7 +1,6 @@ import * as React from 'react' import { useContext } from 'react' -import { Box, NoSelect, Text } from '../ink.js' -import { Ratchet } from './design-system/Ratchet.js' +import { Box, NoSelect, Text, Ratchet } from '@anthropic/ink' type Props = { children: React.ReactNode diff --git a/src/components/MessageRow.tsx b/src/components/MessageRow.tsx index e42fb9d96..e02a2708f 100644 --- a/src/components/MessageRow.tsx +++ b/src/components/MessageRow.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import type { Command } from '../commands.js' -import { Box } from '../ink.js' +import { Box } from '@anthropic/ink' import type { Screen } from '../screens/REPL.js' import type { Tools } from '../Tool.js' import type { RenderableMessage } from '../types/message.js' diff --git a/src/components/MessageSelector.tsx b/src/components/MessageSelector.tsx index b372a4b5d..ab4e12292 100644 --- a/src/components/MessageSelector.tsx +++ b/src/components/MessageSelector.tsx @@ -19,7 +19,7 @@ import { } from 'src/utils/fileHistory.js' import { logError } from 'src/utils/log.js' import { useExitOnCtrlCDWithKeybindings } from '../hooks/useExitOnCtrlCDWithKeybindings.js' -import { Box, Text } from '../ink.js' +import { Box, Text, Divider } from '@anthropic/ink' import { useKeybinding, useKeybindings } from '../keybindings/useKeybinding.js' import type { Message, @@ -58,8 +58,6 @@ import { import { count } from '../utils/array.js' import { formatRelativeTimeAgo, truncate } from '../utils/format.js' import type { Theme } from '../utils/theme.js' -import { Divider } from './design-system/Divider.js' - type RestoreOption = | 'both' | 'conversation' diff --git a/src/components/MessageTimestamp.tsx b/src/components/MessageTimestamp.tsx index 8eac935e5..3b0e07322 100644 --- a/src/components/MessageTimestamp.tsx +++ b/src/components/MessageTimestamp.tsx @@ -1,6 +1,5 @@ import React from 'react' -import { stringWidth } from '../ink/stringWidth.js' -import { Box, Text } from '../ink.js' +import { Box, Text, stringWidth } from '@anthropic/ink' import type { NormalizedMessage } from '../types/message.js' type Props = { diff --git a/src/components/Messages.tsx b/src/components/Messages.tsx index c946a9fd7..bd165b535 100644 --- a/src/components/Messages.tsx +++ b/src/components/Messages.tsx @@ -9,9 +9,9 @@ import { getIsRemoteMode } from '../bootstrap/state.js' import type { Command } from '../commands.js' import { BLACK_CIRCLE } from '../constants/figures.js' import { useTerminalSize } from '../hooks/useTerminalSize.js' -import type { ScrollBoxHandle } from '../ink/components/ScrollBox.js' -import { useTerminalNotification } from '../ink/useTerminalNotification.js' -import { Box, Text } from '../ink.js' +import type { ScrollBoxHandle } from '@anthropic/ink' +import { useTerminalNotification } from '@anthropic/ink' +import { Box, Text } from '@anthropic/ink' import { useShortcutDisplay } from '../keybindings/useShortcutDisplay.js' import type { Screen } from '../screens/REPL.js' import type { Tools } from '../Tool.js' @@ -49,7 +49,7 @@ import { } from '../utils/messages.js' import { plural } from '../utils/stringUtils.js' import { renderableSearchText } from '../utils/transcriptSearch.js' -import { Divider } from './design-system/Divider.js' +import { Divider } from '@anthropic/ink' import type { UnseenDivider } from './FullscreenLayout.js' import { LogoV2 } from './LogoV2/LogoV2.js' import { StreamingMarkdown } from './Markdown.js' @@ -291,13 +291,13 @@ type Props = { /** Paint an existing DOM subtree to fresh Screen, scan. Element comes * from the main tree (all real providers). Message-relative positions. */ scanElement?: ( - el: import('../ink/dom.js').DOMElement, - ) => import('../ink/render-to-screen.js').MatchPosition[] + el: import('@anthropic/ink').DOMElement, + ) => import('@anthropic/ink').MatchPosition[] /** Position-based CURRENT highlight. positions stable (msg-relative), * rowOffset tracks scroll. null clears. */ setPositions?: ( state: { - positions: import('../ink/render-to-screen.js').MatchPosition[] + positions: import('@anthropic/ink').MatchPosition[] rowOffset: number currentIdx: number } | null, diff --git a/src/components/ModelPicker.tsx b/src/components/ModelPicker.tsx index 6658ad62d..4977dbbcb 100644 --- a/src/components/ModelPicker.tsx +++ b/src/components/ModelPicker.tsx @@ -12,7 +12,7 @@ import { isFastModeCooldown, isFastModeEnabled, } from 'src/utils/fastMode.js' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import { useKeybindings } from '../keybindings/useKeybinding.js' import { useAppState, useSetAppState } from '../state/AppState.js' import { @@ -37,9 +37,7 @@ import { } from '../utils/settings/settings.js' import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js' import { Select } from './CustomSelect/index.js' -import { Byline } from './design-system/Byline.js' -import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js' -import { Pane } from './design-system/Pane.js' +import { Byline, KeyboardShortcutHint, Pane } from '@anthropic/ink' import { effortLevelToSymbol } from './EffortIndicator.js' export type Props = { diff --git a/src/components/NativeAutoUpdater.tsx b/src/components/NativeAutoUpdater.tsx index 0a96bd506..77d84e968 100644 --- a/src/components/NativeAutoUpdater.tsx +++ b/src/components/NativeAutoUpdater.tsx @@ -5,7 +5,7 @@ import { logForDebugging } from 'src/utils/debug.js' import { logError } from 'src/utils/log.js' import { useInterval } from 'usehooks-ts' import { useUpdateNotification } from '../hooks/useUpdateNotification.js' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import type { AutoUpdaterResult } from '../utils/autoUpdater.js' import { getMaxVersion, getMaxVersionMessage } from '../utils/autoUpdater.js' import { isAutoUpdaterDisabled } from '../utils/config.js' diff --git a/src/components/NotebookEditToolUseRejectedMessage.tsx b/src/components/NotebookEditToolUseRejectedMessage.tsx index 4eb3cf887..fc4e4f317 100644 --- a/src/components/NotebookEditToolUseRejectedMessage.tsx +++ b/src/components/NotebookEditToolUseRejectedMessage.tsx @@ -1,7 +1,7 @@ import { relative } from 'path' import * as React from 'react' import { getCwd } from 'src/utils/cwd.js' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import { HighlightedCode } from './HighlightedCode.js' import { MessageResponse } from './MessageResponse.js' diff --git a/src/components/OffscreenFreeze.tsx b/src/components/OffscreenFreeze.tsx index 51595bb6c..8a94b5bd9 100644 --- a/src/components/OffscreenFreeze.tsx +++ b/src/components/OffscreenFreeze.tsx @@ -1,6 +1,5 @@ import React, { useContext, useRef } from 'react' -import { useTerminalViewport } from '../ink/hooks/use-terminal-viewport.js' -import { Box } from '../ink.js' +import { useTerminalViewport, Box } from '@anthropic/ink' import { InVirtualListContext } from './messageActions.js' type Props = { diff --git a/src/components/Onboarding.tsx b/src/components/Onboarding.tsx index 80083ff91..51dc04306 100644 --- a/src/components/Onboarding.tsx +++ b/src/components/Onboarding.tsx @@ -8,7 +8,7 @@ import { shouldOfferTerminalSetup, } from '../commands/terminalSetup/terminalSetup.js' import { useExitOnCtrlCDWithKeybindings } from '../hooks/useExitOnCtrlCDWithKeybindings.js' -import { Box, Link, Newline, Text, useTheme } from '../ink.js' +import { Box, Link, Newline, Text, useTheme } from '@anthropic/ink' import { useKeybindings } from '../keybindings/useKeybinding.js' import { isAnthropicAuthEnabled } from '../utils/auth.js' import { normalizeApiKeyForConfig } from '../utils/authPortable.js' diff --git a/src/components/OutputStylePicker.tsx b/src/components/OutputStylePicker.tsx index 4ff039a82..5717a26e6 100644 --- a/src/components/OutputStylePicker.tsx +++ b/src/components/OutputStylePicker.tsx @@ -5,12 +5,11 @@ import { OUTPUT_STYLE_CONFIG, type OutputStyleConfig, } from '../constants/outputStyles.js' -import { Box, Text } from '../ink.js' +import { Box, Text, Dialog } from '@anthropic/ink' import type { OutputStyle } from '../utils/config.js' import { getCwd } from '../utils/cwd.js' import type { OptionWithDescription } from './CustomSelect/select.js' import { Select } from './CustomSelect/select.js' -import { Dialog } from './design-system/Dialog.js' const DEFAULT_OUTPUT_STYLE_LABEL = 'Default' const DEFAULT_OUTPUT_STYLE_DESCRIPTION = diff --git a/src/components/PackageManagerAutoUpdater.tsx b/src/components/PackageManagerAutoUpdater.tsx index 5aa996018..eeef217e2 100644 --- a/src/components/PackageManagerAutoUpdater.tsx +++ b/src/components/PackageManagerAutoUpdater.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { useState } from 'react' import { useInterval } from 'usehooks-ts' -import { Text } from '../ink.js' +import { Text } from '@anthropic/ink' import { type AutoUpdaterResult, getLatestVersionFromGcs, diff --git a/src/components/Passes/Passes.tsx b/src/components/Passes/Passes.tsx index 69388618a..94b6b2d52 100644 --- a/src/components/Passes/Passes.tsx +++ b/src/components/Passes/Passes.tsx @@ -3,9 +3,9 @@ import { useCallback, useEffect, useState } from 'react' import type { CommandResultDisplay } from '../../commands.js' import { TEARDROP_ASTERISK } from '../../constants/figures.js' import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js' -import { setClipboard } from '../../ink/termio/osc.js' +import { setClipboard } from '@anthropic/ink' // eslint-disable-next-line custom-rules/prefer-use-keybindings -- enter to copy link -import { Box, Link, Text, useInput } from '../../ink.js' +import { Box, Link, Text, useInput } from '@anthropic/ink' import { useKeybinding } from '../../keybindings/useKeybinding.js' import { logEvent } from '../../services/analytics/index.js' import { @@ -19,7 +19,7 @@ import type { } from '../../services/oauth/types.js' import { count } from '../../utils/array.js' import { logError } from '../../utils/log.js' -import { Pane } from '../design-system/Pane.js' +import { Pane } from '@anthropic/ink' type PassStatus = { passNumber: number diff --git a/src/components/PrBadge.tsx b/src/components/PrBadge.tsx index bb0aef9e7..e2f7a2a7b 100644 --- a/src/components/PrBadge.tsx +++ b/src/components/PrBadge.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Link, Text } from '../ink.js' +import { Link, Text } from '@anthropic/ink' import type { PrReviewState } from '../utils/ghPrStatus.js' type Props = { diff --git a/src/components/PressEnterToContinue.tsx b/src/components/PressEnterToContinue.tsx index 662c7af85..49b398b12 100644 --- a/src/components/PressEnterToContinue.tsx +++ b/src/components/PressEnterToContinue.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Text } from '../ink.js' +import { Text } from '@anthropic/ink' export function PressEnterToContinue(): React.ReactNode { return ( diff --git a/src/components/PromptInput/HistorySearchInput.tsx b/src/components/PromptInput/HistorySearchInput.tsx index 22830119d..b8eaf8714 100644 --- a/src/components/PromptInput/HistorySearchInput.tsx +++ b/src/components/PromptInput/HistorySearchInput.tsx @@ -1,6 +1,5 @@ import * as React from 'react' -import { stringWidth } from '../../ink/stringWidth.js' -import { Box, Text } from '../../ink.js' +import { Box, Text, stringWidth } from '@anthropic/ink' import TextInput from '../TextInput.js' type Props = { diff --git a/src/components/PromptInput/IssueFlagBanner.tsx b/src/components/PromptInput/IssueFlagBanner.tsx index 723678eaf..39cb491b9 100644 --- a/src/components/PromptInput/IssueFlagBanner.tsx +++ b/src/components/PromptInput/IssueFlagBanner.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { FLAG_ICON } from '../../constants/figures.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' /** * ANT-ONLY: Banner shown in the transcript that prompts users to report diff --git a/src/components/PromptInput/Notifications.tsx b/src/components/PromptInput/Notifications.tsx index d89d596b3..6ddccb3cc 100644 --- a/src/components/PromptInput/Notifications.tsx +++ b/src/components/PromptInput/Notifications.tsx @@ -13,7 +13,7 @@ import { useIdeConnectionStatus } from '../../hooks/useIdeConnectionStatus.js' import type { IDESelection } from '../../hooks/useIdeSelection.js' import { useMainLoopModel } from '../../hooks/useMainLoopModel.js' import { useVoiceEnabled } from '../../hooks/useVoiceEnabled.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { useClaudeAiLimits } from '../../services/claudeAiLimitsHook.js' import { calculateTokenWarningState } from '../../services/compact/autoCompact.js' import type { MCPServerConnection } from '../../services/mcp/types.js' @@ -339,16 +339,6 @@ function NotificationContent({ {!isBriefOnly && ( )} - {shouldShowAutoUpdater && ( - - )} {feature('VOICE_MODE') ? voiceEnabled && voiceError && ( diff --git a/src/components/PromptInput/PromptInput.tsx b/src/components/PromptInput/PromptInput.tsx index a678d41f6..e62da28de 100644 --- a/src/components/PromptInput/PromptInput.tsx +++ b/src/components/PromptInput/PromptInput.tsx @@ -63,9 +63,7 @@ import { useMainLoopModel } from '../../hooks/useMainLoopModel.js' import { usePromptSuggestion } from '../../hooks/usePromptSuggestion.js' import { useTerminalSize } from '../../hooks/useTerminalSize.js' import { useTypeahead } from '../../hooks/useTypeahead.js' -import type { BorderTextOptions } from '../../ink/render-border.js' -import { stringWidth } from '../../ink/stringWidth.js' -import { Box, type ClickEvent, type Key, Text, useInput } from '../../ink.js' +import { Box, type BorderTextOptions, type ClickEvent, type Key, stringWidth, Text, useInput } from '@anthropic/ink' import { useOptionalKeybindingContext } from '../../keybindings/KeybindingContext.js' import { getShortcutDisplay } from '../../keybindings/shortcutFormat.js' import { diff --git a/src/components/PromptInput/PromptInputFooter.tsx b/src/components/PromptInput/PromptInputFooter.tsx index 652bdf3f0..46b3981f5 100644 --- a/src/components/PromptInput/PromptInputFooter.tsx +++ b/src/components/PromptInput/PromptInputFooter.tsx @@ -8,7 +8,7 @@ import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js' import type { IDESelection } from '../../hooks/useIdeSelection.js' import { useSettings } from '../../hooks/useSettings.js' import { useTerminalSize } from '../../hooks/useTerminalSize.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import type { MCPServerConnection } from '../../services/mcp/types.js' import { useAppState } from '../../state/AppState.js' import type { ToolPermissionContext } from '../../Tool.js' diff --git a/src/components/PromptInput/PromptInputFooterLeftSide.tsx b/src/components/PromptInput/PromptInputFooterLeftSide.tsx index fc1be8124..8130c8ef1 100644 --- a/src/components/PromptInput/PromptInputFooterLeftSide.tsx +++ b/src/components/PromptInput/PromptInputFooterLeftSide.tsx @@ -6,7 +6,7 @@ const coordinatorModule = feature('COORDINATOR_MODE') ? (require('../../coordinator/coordinatorMode.js') as typeof import('../../coordinator/coordinatorMode.js')) : undefined /* eslint-enable @typescript-eslint/no-require-imports */ -import { Box, Text, Link } from '../../ink.js' +import { Box, Text, Link } from '@anthropic/ink' import * as React from 'react' import figures from 'figures' import { @@ -39,8 +39,7 @@ import { useAppState, useAppStateStore } from 'src/state/AppState.js' import { getIsRemoteMode } from '../../bootstrap/state.js' import HistorySearchInput from './HistorySearchInput.js' import { usePrStatus } from '../../hooks/usePrStatus.js' -import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js' -import { Byline } from '../design-system/Byline.js' +import { Byline, KeyboardShortcutHint } from '@anthropic/ink' import { useTerminalSize } from '../../hooks/useTerminalSize.js' import { useTasksV2 } from '../../hooks/useTasksV2.js' import { formatDuration } from '../../utils/format.js' @@ -48,8 +47,7 @@ import { VoiceWarmupHint } from './VoiceIndicator.js' import { useVoiceEnabled } from '../../hooks/useVoiceEnabled.js' import { useVoiceState } from '../../context/voice.js' import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js' -import { isXtermJs } from '../../ink/terminal.js' -import { useHasSelection, useSelection } from '../../ink/hooks/use-selection.js' +import { isXtermJs, useHasSelection, useSelection } from '@anthropic/ink' import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js' import { getPlatform } from '../../utils/platform.js' import { PrBadge } from '../PrBadge.js' diff --git a/src/components/PromptInput/PromptInputFooterSuggestions.tsx b/src/components/PromptInput/PromptInputFooterSuggestions.tsx index 7a5d70260..689123581 100644 --- a/src/components/PromptInput/PromptInputFooterSuggestions.tsx +++ b/src/components/PromptInput/PromptInputFooterSuggestions.tsx @@ -1,8 +1,7 @@ import * as React from 'react' import { memo, type ReactNode } from 'react' import { useTerminalSize } from '../../hooks/useTerminalSize.js' -import { stringWidth } from '../../ink/stringWidth.js' -import { Box, Text } from '../../ink.js' +import { Box, Text, stringWidth } from '@anthropic/ink' import { truncatePathMiddle, truncateToWidth } from '../../utils/format.js' import type { Theme } from '../../utils/theme.js' diff --git a/src/components/PromptInput/PromptInputHelpMenu.tsx b/src/components/PromptInput/PromptInputHelpMenu.tsx index 5f15327d2..88aa5dfc2 100644 --- a/src/components/PromptInput/PromptInputHelpMenu.tsx +++ b/src/components/PromptInput/PromptInputHelpMenu.tsx @@ -1,6 +1,6 @@ import { feature } from 'bun:bundle' import * as React from 'react' -import { Box, Text } from 'src/ink.js' +import { Box, Text } from '@anthropic/ink' import { getPlatform } from 'src/utils/platform.js' import { isKeybindingCustomizationEnabled } from '../../keybindings/loadUserBindings.js' import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js' diff --git a/src/components/PromptInput/PromptInputModeIndicator.tsx b/src/components/PromptInput/PromptInputModeIndicator.tsx index 4aa66bf7b..e493dba2d 100644 --- a/src/components/PromptInput/PromptInputModeIndicator.tsx +++ b/src/components/PromptInput/PromptInputModeIndicator.tsx @@ -1,6 +1,6 @@ import figures from 'figures' import * as React from 'react' -import { Box, Text } from 'src/ink.js' +import { Box, Text } from '@anthropic/ink' import { AGENT_COLOR_TO_THEME_COLOR, AGENT_COLORS, diff --git a/src/components/PromptInput/PromptInputQueuedCommands.tsx b/src/components/PromptInput/PromptInputQueuedCommands.tsx index c4637b803..e96ba3532 100644 --- a/src/components/PromptInput/PromptInputQueuedCommands.tsx +++ b/src/components/PromptInput/PromptInputQueuedCommands.tsx @@ -1,7 +1,7 @@ import { feature } from 'bun:bundle' import * as React from 'react' import { useMemo } from 'react' -import { Box } from 'src/ink.js' +import { Box } from '@anthropic/ink' import { useAppState } from 'src/state/AppState.js' import { STATUS_TAG, diff --git a/src/components/PromptInput/PromptInputStashNotice.tsx b/src/components/PromptInput/PromptInputStashNotice.tsx index 8a44e8607..c33f43190 100644 --- a/src/components/PromptInput/PromptInputStashNotice.tsx +++ b/src/components/PromptInput/PromptInputStashNotice.tsx @@ -1,6 +1,6 @@ import figures from 'figures' import * as React from 'react' -import { Box, Text } from 'src/ink.js' +import { Box, Text } from '@anthropic/ink' type Props = { hasStash: boolean diff --git a/src/components/PromptInput/SandboxPromptFooterHint.tsx b/src/components/PromptInput/SandboxPromptFooterHint.tsx index 1324a9832..e470509d4 100644 --- a/src/components/PromptInput/SandboxPromptFooterHint.tsx +++ b/src/components/PromptInput/SandboxPromptFooterHint.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { type ReactNode, useEffect, useRef, useState } from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js' import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js' diff --git a/src/components/PromptInput/ShimmeredInput.tsx b/src/components/PromptInput/ShimmeredInput.tsx index 11da7ad76..a14afbc9b 100644 --- a/src/components/PromptInput/ShimmeredInput.tsx +++ b/src/components/PromptInput/ShimmeredInput.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Ansi, Box, Text, useAnimationFrame } from '../../ink.js' +import { Ansi, Box, Text, useAnimationFrame } from '@anthropic/ink' import { segmentTextByHighlights, type TextHighlight, diff --git a/src/components/PromptInput/VoiceIndicator.tsx b/src/components/PromptInput/VoiceIndicator.tsx index 6dc73baf2..c6fe0b07f 100644 --- a/src/components/PromptInput/VoiceIndicator.tsx +++ b/src/components/PromptInput/VoiceIndicator.tsx @@ -1,7 +1,7 @@ import { feature } from 'bun:bundle' import * as React from 'react' import { useSettings } from '../../hooks/useSettings.js' -import { Box, Text, useAnimationFrame } from '../../ink.js' +import { Box, Text, useAnimationFrame } from '@anthropic/ink' import { interpolateColor, toRGBColor } from '../Spinner/utils.js' type Props = { diff --git a/src/components/PromptInput/utils.ts b/src/components/PromptInput/utils.ts index eb5cc8128..8b59fdf74 100644 --- a/src/components/PromptInput/utils.ts +++ b/src/components/PromptInput/utils.ts @@ -2,7 +2,7 @@ import { hasUsedBackslashReturn, isShiftEnterKeyBindingInstalled, } from '../../commands/terminalSetup/terminalSetup.js' -import type { Key } from '../../ink.js' +import type { Key } from '@anthropic/ink' import { getGlobalConfig } from '../../utils/config.js' import { env } from '../../utils/env.js' /** diff --git a/src/components/QuickOpenDialog.tsx b/src/components/QuickOpenDialog.tsx index 37b7bb7e1..6945fb682 100644 --- a/src/components/QuickOpenDialog.tsx +++ b/src/components/QuickOpenDialog.tsx @@ -4,15 +4,14 @@ import { useEffect, useRef, useState } from 'react' import { useRegisterOverlay } from '../context/overlayContext.js' import { generateFileSuggestions } from '../hooks/fileSuggestions.js' import { useTerminalSize } from '../hooks/useTerminalSize.js' -import { Text } from '../ink.js' +import { Text } from '@anthropic/ink' import { logEvent } from '../services/analytics/index.js' import { getCwd } from '../utils/cwd.js' import { openFileInExternalEditor } from '../utils/editor.js' import { truncatePathMiddle, truncateToWidth } from '../utils/format.js' import { highlightMatch } from '../utils/highlightMatch.js' import { readFileInRange } from '../utils/readFileInRange.js' -import { FuzzyPicker } from './design-system/FuzzyPicker.js' -import { LoadingState } from './design-system/LoadingState.js' +import { FuzzyPicker, LoadingState } from '@anthropic/ink' type Props = { onDone: () => void diff --git a/src/components/RemoteCallout.tsx b/src/components/RemoteCallout.tsx index d6b0af589..7d9dff991 100644 --- a/src/components/RemoteCallout.tsx +++ b/src/components/RemoteCallout.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useRef } from 'react' import { isBridgeEnabled } from '../bridge/bridgeEnabled.js' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import { getClaudeAIOAuthTokens } from '../utils/auth.js' import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js' import type { OptionWithDescription } from './CustomSelect/select.js' diff --git a/src/components/RemoteEnvironmentDialog.tsx b/src/components/RemoteEnvironmentDialog.tsx index b0f47a60c..436b59583 100644 --- a/src/components/RemoteEnvironmentDialog.tsx +++ b/src/components/RemoteEnvironmentDialog.tsx @@ -2,7 +2,7 @@ import chalk from 'chalk' import figures from 'figures' import * as React from 'react' import { useEffect, useState } from 'react' -import { Text } from '../ink.js' +import { Text } from '@anthropic/ink' import { useKeybinding } from '../keybindings/useKeybinding.js' import { toError } from '../utils/errors.js' import { logError } from '../utils/log.js' @@ -15,10 +15,7 @@ import { getEnvironmentSelectionInfo } from '../utils/teleport/environmentSelect import type { EnvironmentResource } from '../utils/teleport/environments.js' import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js' import { Select } from './CustomSelect/select.js' -import { Byline } from './design-system/Byline.js' -import { Dialog } from './design-system/Dialog.js' -import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js' -import { LoadingState } from './design-system/LoadingState.js' +import { Byline, Dialog, KeyboardShortcutHint, LoadingState } from '@anthropic/ink' const DIALOG_TITLE = 'Select Remote Environment' const SETUP_HINT = `Configure environments at: https://claude.ai/code` diff --git a/src/components/ResumeTask.tsx b/src/components/ResumeTask.tsx index 8b657ab0d..c81ed0291 100644 --- a/src/components/ResumeTask.tsx +++ b/src/components/ResumeTask.tsx @@ -5,7 +5,7 @@ import { fetchCodeSessionsFromSessionsAPI, } from 'src/utils/teleport/api.js' // eslint-disable-next-line custom-rules/prefer-use-keybindings -- raw j/k/arrow list navigation -import { Box, Text, useInput } from '../ink.js' +import { Box, Text, useInput } from '@anthropic/ink' import { useKeybinding } from '../keybindings/useKeybinding.js' import { useShortcutDisplay } from '../keybindings/useShortcutDisplay.js' import { logForDebugging } from '../utils/debug.js' @@ -13,8 +13,7 @@ import { detectCurrentRepository } from '../utils/detectRepository.js' import { formatRelativeTime } from '../utils/format.js' import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js' import { Select } from './CustomSelect/index.js' -import { Byline } from './design-system/Byline.js' -import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js' +import { Byline, KeyboardShortcutHint } from '@anthropic/ink' import { Spinner } from './Spinner.js' import { TeleportError } from './TeleportError.js' diff --git a/src/components/SandboxViolationExpandedView.tsx b/src/components/SandboxViolationExpandedView.tsx index 4b8bbbd7a..93a448c87 100644 --- a/src/components/SandboxViolationExpandedView.tsx +++ b/src/components/SandboxViolationExpandedView.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { type ReactNode, useEffect, useState } from 'react' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import type { SandboxViolationEvent } from '../utils/sandbox/sandbox-adapter.js' import { SandboxManager } from '../utils/sandbox/sandbox-adapter.js' diff --git a/src/components/ScrollKeybindingHandler.tsx b/src/components/ScrollKeybindingHandler.tsx index e51787f9f..cdaac3256 100644 --- a/src/components/ScrollKeybindingHandler.tsx +++ b/src/components/ScrollKeybindingHandler.tsx @@ -4,13 +4,8 @@ import { useCopyOnSelect, useSelectionBgColor, } from '../hooks/useCopyOnSelect.js' -import type { ScrollBoxHandle } from '../ink/components/ScrollBox.js' -import { useSelection } from '../ink/hooks/use-selection.js' -import type { FocusMove, SelectionState } from '../ink/selection.js' -import { isXtermJs } from '../ink/terminal.js' -import { getClipboardPath } from '../ink/termio/osc.js' -// eslint-disable-next-line custom-rules/prefer-use-keybindings -- Esc needs conditional propagation based on selection state -import { type Key, useInput } from '../ink.js' +import type { ScrollBoxHandle, FocusMove, SelectionState } from '@anthropic/ink' +import { useSelection, type Key, useInput, isXtermJs, getClipboardPath } from '@anthropic/ink' import { useKeybindings } from '../keybindings/useKeybinding.js' import { logForDebugging } from '../utils/debug.js' diff --git a/src/components/SearchBox.tsx b/src/components/SearchBox.tsx index d35d67edd..532e18294 100644 --- a/src/components/SearchBox.tsx +++ b/src/components/SearchBox.tsx @@ -1,71 +1,2 @@ -import React from 'react' -import { Box, Text } from '../ink.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 = '⌕', - width, - cursorOffset, - borderless = false, -}: Props): React.ReactNode { - const offset = cursorOffset ?? query.length - - return ( - - - {prefix}{' '} - {isFocused ? ( - <> - {query ? ( - isTerminalFocused ? ( - <> - {query.slice(0, offset)} - - {offset < query.length ? query[offset] : ' '} - - {offset < query.length && ( - {query.slice(offset + 1)} - )} - - ) : ( - {query} - ) - ) : isTerminalFocused ? ( - <> - {placeholder.charAt(0)} - {placeholder.slice(1)} - - ) : ( - {placeholder} - )} - - ) : query ? ( - {query} - ) : ( - {placeholder} - )} - - - ) -} +// Re-export from @anthropic/ink theme module +export { SearchBox } from '@anthropic/ink' diff --git a/src/components/SessionBackgroundHint.tsx b/src/components/SessionBackgroundHint.tsx index a7f5e8f59..aa336afb0 100644 --- a/src/components/SessionBackgroundHint.tsx +++ b/src/components/SessionBackgroundHint.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { useCallback, useState } from 'react' import { useDoublePress } from '../hooks/useDoublePress.js' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import { useKeybinding } from '../keybindings/useKeybinding.js' import { useShortcutDisplay } from '../keybindings/useShortcutDisplay.js' import { @@ -16,7 +16,7 @@ import { import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js' import { env } from '../utils/env.js' import { isEnvTruthy } from '../utils/envUtils.js' -import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js' +import { KeyboardShortcutHint } from '@anthropic/ink' type Props = { onBackgroundSession: () => void diff --git a/src/components/SessionPreview.tsx b/src/components/SessionPreview.tsx index 2d0e10a97..2e0f74b2d 100644 --- a/src/components/SessionPreview.tsx +++ b/src/components/SessionPreview.tsx @@ -1,6 +1,6 @@ import type { UUID } from 'crypto' import React, { useCallback } from 'react' -import { Box, Text } from '../ink.js' +import { Box, Text, Byline, KeyboardShortcutHint, LoadingState } from '@anthropic/ink' import { useKeybinding } from '../keybindings/useKeybinding.js' import { getAllBaseTools } from '../tools.js' import type { LogOption } from '../types/logs.js' @@ -11,9 +11,6 @@ import { loadFullLog, } from '../utils/sessionStorage.js' import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js' -import { Byline } from './design-system/Byline.js' -import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js' -import { LoadingState } from './design-system/LoadingState.js' import { Messages } from './Messages.js' type Props = { diff --git a/src/components/Settings/Config.tsx b/src/components/Settings/Config.tsx index 09f832f0c..79661e237 100644 --- a/src/components/Settings/Config.tsx +++ b/src/components/Settings/Config.tsx @@ -1,13 +1,6 @@ // biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered import { feature } from 'bun:bundle' -import { - Box, - Text, - useTheme, - useThemeSetting, - useTerminalFocus, -} from '../../ink.js' -import type { KeyboardEvent } from '../../ink/events/keyboard-event.js' +import { type KeyboardEvent, Box, Text, useTheme, useThemeSetting, useTerminalFocus } from '@anthropic/ink' import * as React from 'react' import { useState, useCallback } from 'react' import { @@ -67,7 +60,7 @@ import { ChannelDowngradeDialog, type ChannelDowngradeChoice, } from '../ChannelDowngradeDialog.js' -import { Dialog } from '../design-system/Dialog.js' +import { Dialog } from '@anthropic/ink' import { Select } from '../CustomSelect/index.js' import { OutputStylePicker } from '../OutputStylePicker.js' import { LanguagePicker } from '../LanguagePicker.js' @@ -76,10 +69,8 @@ import { getMemoryFiles, hasExternalClaudeMdIncludes, } from 'src/utils/claudemd.js' -import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js' +import { Byline, KeyboardShortcutHint, useTabHeaderFocus } from '@anthropic/ink' import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js' -import { Byline } from '../design-system/Byline.js' -import { useTabHeaderFocus } from '../design-system/Tabs.js' import { useIsInsideModal } from '../../context/modalContext.js' import { SearchBox } from '../SearchBox.js' import { diff --git a/src/components/Settings/Settings.tsx b/src/components/Settings/Settings.tsx index d54f6e758..5ac172891 100644 --- a/src/components/Settings/Settings.tsx +++ b/src/components/Settings/Settings.tsx @@ -8,8 +8,7 @@ import { useIsInsideModal, useModalOrTerminalSize, } from '../../context/modalContext.js' -import { Pane } from '../design-system/Pane.js' -import { Tabs, Tab } from '../design-system/Tabs.js' +import { Pane, Tab, Tabs } from '@anthropic/ink' import { Status, buildDiagnostics } from './Status.js' import { Config } from './Config.js' import { Usage } from './Usage.js' @@ -24,7 +23,7 @@ type Props = { options?: { display?: CommandResultDisplay }, ) => void context: LocalJSXCommandContext - defaultTab: 'Status' | 'Config' | 'Usage' | 'Gates' + defaultTab: 'Status' | 'Config' | 'Usage' } export function Settings({ @@ -37,7 +36,6 @@ export function Settings({ // True while Config's own Esc handler is active (search mode with content // focused). Settings must cede Esc so search can clear/exit first. const [configOwnsEsc, setConfigOwnsEsc] = useState(false) - const [gatesOwnsEsc, setGatesOwnsEsc] = useState(false) // Fixed content height so switching tabs doesn't shift the pane height. // Outside modals cap at min(80% viewport, 30). Inside a Modal the modal's // innerSize.rows IS the ScrollBox viewport — the 0.8 multiplier over- @@ -79,8 +77,7 @@ export function Settings({ context: 'Settings', isActive: !tabsHidden && - !(selectedTab === 'Config' && configOwnsEsc) && - !(selectedTab === 'Gates' && gatesOwnsEsc), + !(selectedTab === 'Config' && configOwnsEsc), }) const tabs = [ @@ -101,16 +98,6 @@ export function Settings({ , - ...(process.env.USER_TYPE === 'ant' - ? [ - - - , - ] - : []), ] return ( @@ -122,10 +109,10 @@ export function Settings({ hidden={tabsHidden} // Config has interactive content — start with header unfocused so // left/right/tab cycle option values instead of switching tabs. - initialHeaderFocused={defaultTab !== 'Config' && defaultTab !== 'Gates'} + initialHeaderFocused={defaultTab !== 'Config'} // Inside a Modal, skip the Tabs-level cap so tall tabs (Status's // MCP list) flow to their natural height for the Modal's ScrollBox - // to scroll. Config/Gates still get contentHeight above — they + // to scroll. Config still gets contentHeight above — it // paginate internally so this only affects Status/Usage. contentHeight={tabsHidden || insideModal ? undefined : contentHeight} > diff --git a/src/components/Settings/Status.tsx b/src/components/Settings/Status.tsx index 1cd4aac14..acadfcc06 100644 --- a/src/components/Settings/Status.tsx +++ b/src/components/Settings/Status.tsx @@ -4,7 +4,7 @@ import { Suspense, use } from 'react' import { getSessionId } from '../../bootstrap/state.js' import type { LocalJSXCommandContext } from '../../commands.js' import { useIsInsideModal } from '../../context/modalContext.js' -import { Box, Text, useTheme } from '../../ink.js' +import { Box, Text, useTheme } from '@anthropic/ink' import { type AppState, useAppState } from '../../state/AppState.js' import { getCwd } from '../../utils/cwd.js' import { getCurrentSessionTitle } from '../../utils/sessionStorage.js' diff --git a/src/components/Settings/Usage.tsx b/src/components/Settings/Usage.tsx index d52d19577..403e050eb 100644 --- a/src/components/Settings/Usage.tsx +++ b/src/components/Settings/Usage.tsx @@ -4,7 +4,7 @@ import { extraUsage as extraUsageCommand } from 'src/commands/extra-usage/index. import { formatCost } from 'src/cost-tracker.js' import { getSubscriptionType } from 'src/utils/auth.js' import { useTerminalSize } from '../../hooks/useTerminalSize.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { useKeybinding } from '../../keybindings/useKeybinding.js' import { type ExtraUsage, @@ -16,8 +16,7 @@ import { formatResetText } from '../../utils/format.js' import { logError } from '../../utils/log.js' import { jsonStringify } from '../../utils/slowOperations.js' import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js' -import { Byline } from '../design-system/Byline.js' -import { ProgressBar } from '../design-system/ProgressBar.js' +import { Byline, ProgressBar } from '@anthropic/ink' import { isEligibleForOverageCreditGrant, OverageCreditUpsell, diff --git a/src/components/ShowInIDEPrompt.tsx b/src/components/ShowInIDEPrompt.tsx index e5ff331a3..e522cc6ae 100644 --- a/src/components/ShowInIDEPrompt.tsx +++ b/src/components/ShowInIDEPrompt.tsx @@ -1,10 +1,9 @@ import { basename, relative } from 'path' import React from 'react' -import { Box, Text } from '../ink.js' +import { Box, Text, Pane } from '@anthropic/ink' import { getCwd } from '../utils/cwd.js' import { isSupportedVSCodeTerminal } from '../utils/ide.js' import { Select } from './CustomSelect/index.js' -import { Pane } from './design-system/Pane.js' import type { PermissionOption, PermissionOptionWithLabel, diff --git a/src/components/SkillImprovementSurvey.tsx b/src/components/SkillImprovementSurvey.tsx index f42e27f23..ad7b09147 100644 --- a/src/components/SkillImprovementSurvey.tsx +++ b/src/components/SkillImprovementSurvey.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef } from 'react' import { BLACK_CIRCLE, BULLET_OPERATOR } from '../constants/figures.js' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import type { SkillUpdate } from '../utils/hooks/skillImprovement.js' import { normalizeFullWidthDigits } from '../utils/stringUtils.js' import { isValidResponseInput } from './FeedbackSurvey/FeedbackSurveyView.js' diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx index dc8692215..73239ce1c 100644 --- a/src/components/Spinner.tsx +++ b/src/components/Spinner.tsx @@ -1,5 +1,5 @@ // biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered -import { Box, Text } from '../ink.js' +import { Box, Text, stringWidth } from '@anthropic/ink' import * as React from 'react' import { useEffect, useMemo, useRef, useState } from 'react' import { @@ -27,7 +27,6 @@ import { useTasksV2 } from '../hooks/useTasksV2.js' import type { Task } from '../utils/tasks.js' import { useAppState } from '../state/AppState.js' import { useTerminalSize } from '../hooks/useTerminalSize.js' -import { stringWidth } from '../ink/stringWidth.js' import { getDefaultCharacters, type SpinnerMode } from './Spinner/index.js' import { SpinnerAnimationRow } from './Spinner/SpinnerAnimationRow.js' import { useSettings } from '../hooks/useSettings.js' @@ -45,7 +44,7 @@ import { } from '../bootstrap/state.js' import { TeammateSpinnerTree } from './Spinner/TeammateSpinnerTree.js' -import { useAnimationFrame } from '../ink.js' +import { useAnimationFrame } from '@anthropic/ink' import { getGlobalConfig } from '../utils/config.js' export type { SpinnerMode } from './Spinner/index.js' @@ -268,19 +267,9 @@ function SpinnerWithVerbInner({ const messageColor = overrideColor ?? defaultColor const shimmerColor = overrideShimmerColor ?? defaultShimmerColor - // Compute TTFT string here (off the 50ms animation clock) and pass to - // SpinnerAnimationRow so it folds into the `(thought for Ns · ...)` status - // line instead of taking a separate row. apiMetricsRef is a ref so this - // doesn't trigger re-renders; we pick up updates on the parent's ~25x/turn - // re-render cadence, same as the old ApiMetricsLine did. + // TTFT display is gated to internal builds — apiMetricsRef was removed from + // props during a refactor, so skip this until it's re-threaded. let ttftText: string | null = null - if ( - process.env.USER_TYPE === 'ant' && - apiMetricsRef?.current && - apiMetricsRef.current.length > 0 - ) { - ttftText = computeTtftText(apiMetricsRef.current) - } // When leader is idle but teammates are running (and we're viewing the leader), // show a static dim idle display instead of the animated spinner — otherwise diff --git a/src/components/Spinner/FlashingChar.tsx b/src/components/Spinner/FlashingChar.tsx index 7f67a47ad..bfb190999 100644 --- a/src/components/Spinner/FlashingChar.tsx +++ b/src/components/Spinner/FlashingChar.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Text, useTheme } from '../../ink.js' +import { Text, useTheme } from '@anthropic/ink' import { getTheme, type Theme } from '../../utils/theme.js' import { interpolateColor, parseRGB, toRGBColor } from './utils.js' diff --git a/src/components/Spinner/GlimmerMessage.tsx b/src/components/Spinner/GlimmerMessage.tsx index 3e488f9a1..9f8ee9265 100644 --- a/src/components/Spinner/GlimmerMessage.tsx +++ b/src/components/Spinner/GlimmerMessage.tsx @@ -1,6 +1,5 @@ import * as React from 'react' -import { stringWidth } from '../../ink/stringWidth.js' -import { Text, useTheme } from '../../ink.js' +import { Text, stringWidth, useTheme } from '@anthropic/ink' import { getGraphemeSegmenter } from '../../utils/intl.js' import { getTheme, type Theme } from '../../utils/theme.js' import type { SpinnerMode } from './types.js' diff --git a/src/components/Spinner/ShimmerChar.tsx b/src/components/Spinner/ShimmerChar.tsx index 038ffb33d..0daa44c67 100644 --- a/src/components/Spinner/ShimmerChar.tsx +++ b/src/components/Spinner/ShimmerChar.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Text } from '../../ink.js' +import { Text } from '@anthropic/ink' import type { Theme } from '../../utils/theme.js' type Props = { diff --git a/src/components/Spinner/SpinnerAnimationRow.tsx b/src/components/Spinner/SpinnerAnimationRow.tsx index 93b2fc64a..57b8358f2 100644 --- a/src/components/Spinner/SpinnerAnimationRow.tsx +++ b/src/components/Spinner/SpinnerAnimationRow.tsx @@ -1,13 +1,13 @@ import figures from 'figures' import * as React from 'react' import { useMemo, useRef } from 'react' -import { stringWidth } from '../../ink/stringWidth.js' -import { Box, Text, useAnimationFrame } from '../../ink.js' +import { Box, Text, useAnimationFrame, stringWidth, Byline } from '@anthropic/ink' +import { toInkColor } from '../../utils/ink.js' import type { InProcessTeammateTaskState } from '../../tasks/InProcessTeammateTask/types.js' import { formatDuration, formatNumber } from '../../utils/format.js' -import { toInkColor } from '../../utils/ink.js' + import type { Theme } from '../../utils/theme.js' -import { Byline } from '../design-system/Byline.js' + import { GlimmerMessage } from './GlimmerMessage.js' import { SpinnerGlyph } from './SpinnerGlyph.js' import type { SpinnerMode } from './types.js' @@ -236,7 +236,6 @@ export function SpinnerAnimationRow({ totalTokens > 0 && availableSpace > usedAfterTimer + tokensWidth - const thinkingOnly = showThinking && thinkingStatus === 'thinking' && diff --git a/src/components/Spinner/SpinnerGlyph.tsx b/src/components/Spinner/SpinnerGlyph.tsx index 242d05971..d7db456db 100644 --- a/src/components/Spinner/SpinnerGlyph.tsx +++ b/src/components/Spinner/SpinnerGlyph.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Box, Text, useTheme } from '../../ink.js' +import { Box, Text, useTheme } from '@anthropic/ink' import { getTheme, type Theme } from '../../utils/theme.js' import { getDefaultCharacters, diff --git a/src/components/Spinner/TeammateSpinnerLine.tsx b/src/components/Spinner/TeammateSpinnerLine.tsx index ee6807f76..736623ce4 100644 --- a/src/components/Spinner/TeammateSpinnerLine.tsx +++ b/src/components/Spinner/TeammateSpinnerLine.tsx @@ -6,8 +6,8 @@ import { getSpinnerVerbs } from '../../constants/spinnerVerbs.js' import { TURN_COMPLETION_VERBS } from '../../constants/turnCompletionVerbs.js' import { useElapsedTime } from '../../hooks/useElapsedTime.js' import { useTerminalSize } from '../../hooks/useTerminalSize.js' -import { stringWidth } from '../../ink/stringWidth.js' -import { Box, Text } from '../../ink.js' +import { Box, Text, stringWidth } from '@anthropic/ink' +import { toInkColor } from '../../utils/ink.js' import type { InProcessTeammateTaskState } from '../../tasks/InProcessTeammateTask/types.js' import { summarizeRecentActivities } from '../../utils/collapseReadSearch.js' import { @@ -15,7 +15,7 @@ import { formatNumber, truncateToWidth, } from '../../utils/format.js' -import { toInkColor } from '../../utils/ink.js' + import { TEAMMATE_SELECT_HINT } from './teammateSelectHint.js' type Props = { diff --git a/src/components/Spinner/TeammateSpinnerTree.tsx b/src/components/Spinner/TeammateSpinnerTree.tsx index 331126f71..8c297ed57 100644 --- a/src/components/Spinner/TeammateSpinnerTree.tsx +++ b/src/components/Spinner/TeammateSpinnerTree.tsx @@ -1,6 +1,6 @@ import figures from 'figures' import * as React from 'react' -import { Box, Text, type TextProps } from '../../ink.js' +import { Box, Text, type TextProps } from '@anthropic/ink' import { useAppState } from '../../state/AppState.js' import { getRunningTeammatesSorted } from '../../tasks/InProcessTeammateTask/InProcessTeammateTask.js' import { formatNumber } from '../../utils/format.js' diff --git a/src/components/Spinner/useShimmerAnimation.ts b/src/components/Spinner/useShimmerAnimation.ts index d1d4ea947..d71d11f71 100644 --- a/src/components/Spinner/useShimmerAnimation.ts +++ b/src/components/Spinner/useShimmerAnimation.ts @@ -1,6 +1,5 @@ import { useMemo } from 'react' -import { stringWidth } from '../../ink/stringWidth.js' -import { type DOMElement, useAnimationFrame } from '../../ink.js' +import { type DOMElement, useAnimationFrame, stringWidth } from '@anthropic/ink' import type { SpinnerMode } from './types.js' export function useShimmerAnimation( diff --git a/src/components/Spinner/utils.ts b/src/components/Spinner/utils.ts index 7c0c54d00..0b90a7a59 100644 --- a/src/components/Spinner/utils.ts +++ b/src/components/Spinner/utils.ts @@ -1,4 +1,4 @@ -import type { RGBColor as RGBColorString } from '../../ink/styles.js' +import type { RGBColor as RGBColorString } from '@anthropic/ink' import type { RGBColor as RGBColorType } from './types.js' export function getDefaultCharacters(): string[] { diff --git a/src/components/Stats.tsx b/src/components/Stats.tsx index b2dc29168..1d20359f1 100644 --- a/src/components/Stats.tsx +++ b/src/components/Stats.tsx @@ -13,11 +13,8 @@ import React, { import stripAnsi from 'strip-ansi' import type { CommandResultDisplay } from '../commands.js' import { useTerminalSize } from '../hooks/useTerminalSize.js' -import { applyColor } from '../ink/colorize.js' -import { stringWidth as getStringWidth } from '../ink/stringWidth.js' -import type { Color } from '../ink/styles.js' // eslint-disable-next-line custom-rules/prefer-use-keybindings -- raw j/k/arrow stats navigation -import { Ansi, Box, Text, useInput } from '../ink.js' +import { Ansi, applyColor, Box, Text, useInput, stringWidth as getStringWidth, type Color, Pane, Tab, Tabs, useTabHeaderFocus } from '@anthropic/ink' import { useKeybinding } from '../keybindings/useKeybinding.js' import { getGlobalConfig } from '../utils/config.js' import { formatDuration, formatNumber } from '../utils/format.js' @@ -32,8 +29,6 @@ import { } from '../utils/stats.js' import { resolveThemeSetting } from '../utils/systemTheme.js' import { getTheme, themeColorToAnsi } from '../utils/theme.js' -import { Pane } from './design-system/Pane.js' -import { Tab, Tabs, useTabHeaderFocus } from './design-system/Tabs.js' import { Spinner } from './Spinner.js' function formatPeakDay(dateStr: string): string { diff --git a/src/components/StatusLine.tsx b/src/components/StatusLine.tsx index ad90655f4..b2f2c7c91 100644 --- a/src/components/StatusLine.tsx +++ b/src/components/StatusLine.tsx @@ -25,7 +25,7 @@ import { } from '../cost-tracker.js' import { useMainLoopModel } from '../hooks/useMainLoopModel.js' import { type ReadonlySettings, useSettings } from '../hooks/useSettings.js' -import { Ansi, Box, Text } from '../ink.js' +import { Ansi, Box, Text } from '@anthropic/ink' import { getRawUtilization } from '../services/claudeAiLimits.js' import type { Message } from '../types/message.js' import type { StatusLineCommandInput } from '../types/statusLine.js' diff --git a/src/components/StatusNotices.tsx b/src/components/StatusNotices.tsx index a62df498e..ae25c5d9f 100644 --- a/src/components/StatusNotices.tsx +++ b/src/components/StatusNotices.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { use } from 'react' -import { Box } from '../ink.js' +import { Box } from '@anthropic/ink' import type { AgentDefinitionsResult } from '../tools/AgentTool/loadAgentsDir.js' import { getMemoryFiles } from '../utils/claudemd.js' import { getGlobalConfig } from '../utils/config.js' diff --git a/src/components/StructuredDiff.tsx b/src/components/StructuredDiff.tsx index 237dfea2b..890977701 100644 --- a/src/components/StructuredDiff.tsx +++ b/src/components/StructuredDiff.tsx @@ -2,7 +2,7 @@ import type { StructuredPatchHunk } from 'diff' import * as React from 'react' import { memo } from 'react' import { useSettings } from '../hooks/useSettings.js' -import { Box, NoSelect, RawAnsi, useTheme } from '../ink.js' +import { Box, NoSelect, RawAnsi, useTheme } from '@anthropic/ink' import { isFullscreenEnvEnabled } from '../utils/fullscreen.js' import sliceAnsi from '../utils/sliceAnsi.js' import { expectColorDiff } from './StructuredDiff/colorDiff.js' diff --git a/src/components/StructuredDiff/Fallback.tsx b/src/components/StructuredDiff/Fallback.tsx index 335391e0a..0331a0ed5 100644 --- a/src/components/StructuredDiff/Fallback.tsx +++ b/src/components/StructuredDiff/Fallback.tsx @@ -2,8 +2,7 @@ import { diffWordsWithSpace, type StructuredPatchHunk } from 'diff' import * as React from 'react' import { useMemo } from 'react' import type { ThemeName } from 'src/utils/theme.js' -import { stringWidth } from '../../ink/stringWidth.js' -import { Box, NoSelect, Text, useTheme, wrapText } from '../../ink.js' +import { Box, NoSelect, Text, stringWidth, useTheme, wrapText } from '@anthropic/ink' /* * StructuredDiffFallback Component: Word-Level Diff Highlighting Example diff --git a/src/components/StructuredDiffList.tsx b/src/components/StructuredDiffList.tsx index af0eeeb02..cfbedaecb 100644 --- a/src/components/StructuredDiffList.tsx +++ b/src/components/StructuredDiffList.tsx @@ -1,6 +1,6 @@ import type { StructuredPatchHunk } from 'diff' import * as React from 'react' -import { Box, NoSelect, Text } from '../ink.js' +import { Box, NoSelect, Text } from '@anthropic/ink' import { intersperse } from '../utils/array.js' import { StructuredDiff } from './StructuredDiff.js' diff --git a/src/components/TagTabs.tsx b/src/components/TagTabs.tsx index 786a1f81d..f974bdad4 100644 --- a/src/components/TagTabs.tsx +++ b/src/components/TagTabs.tsx @@ -1,6 +1,5 @@ import React from 'react' -import { stringWidth } from '../ink/stringWidth.js' -import { Box, Text } from '../ink.js' +import { Box, Text, stringWidth } from '@anthropic/ink' import { truncateToWidth } from '../utils/format.js' // Constants for width calculations - derived from actual rendered strings diff --git a/src/components/TaskListV2.tsx b/src/components/TaskListV2.tsx index d9d4dfa04..6ab77ccbb 100644 --- a/src/components/TaskListV2.tsx +++ b/src/components/TaskListV2.tsx @@ -1,8 +1,7 @@ import figures from 'figures' import * as React from 'react' import { useTerminalSize } from '../hooks/useTerminalSize.js' -import { stringWidth } from '../ink/stringWidth.js' -import { Box, Text } from '../ink.js' +import { Box, Text, stringWidth } from '@anthropic/ink' import { useAppState } from '../state/AppState.js' import { isInProcessTeammateTask } from '../tasks/InProcessTeammateTask/types.js' import { diff --git a/src/components/TeammateViewHeader.tsx b/src/components/TeammateViewHeader.tsx index 01e64571d..1613e7aca 100644 --- a/src/components/TeammateViewHeader.tsx +++ b/src/components/TeammateViewHeader.tsx @@ -1,9 +1,9 @@ import * as React from 'react' -import { Box, Text } from '../ink.js' +import { Box, Text, KeyboardShortcutHint } from '@anthropic/ink' +import { toInkColor } from '../utils/ink.js' import { useAppState } from '../state/AppState.js' import { getViewedTeammateTask } from '../state/selectors.js' -import { toInkColor } from '../utils/ink.js' -import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js' + import { OffscreenFreeze } from './OffscreenFreeze.js' /** diff --git a/src/components/TeleportError.tsx b/src/components/TeleportError.tsx index 343ef7427..260f488d3 100644 --- a/src/components/TeleportError.tsx +++ b/src/components/TeleportError.tsx @@ -4,10 +4,10 @@ import { checkNeedsClaudeAiLogin, } from 'src/utils/background/remote/preconditions.js' import { gracefulShutdownSync } from 'src/utils/gracefulShutdown.js' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import { ConsoleOAuthFlow } from './ConsoleOAuthFlow.js' import { Select } from './CustomSelect/index.js' -import { Dialog } from './design-system/Dialog.js' +import { Dialog } from '@anthropic/ink' import { TeleportStash } from './TeleportStash.js' export type TeleportLocalErrorType = 'needsLogin' | 'needsGitStash' diff --git a/src/components/TeleportProgress.tsx b/src/components/TeleportProgress.tsx index f8ef62110..6ecb4c7cf 100644 --- a/src/components/TeleportProgress.tsx +++ b/src/components/TeleportProgress.tsx @@ -1,8 +1,8 @@ import figures from 'figures' import * as React from 'react' import { useState } from 'react' -import type { Root } from '../ink.js' -import { Box, Text, useAnimationFrame } from '../ink.js' +import type { Root } from '@anthropic/ink' +import { Box, Text, useAnimationFrame } from '@anthropic/ink' import { AppStateProvider } from '../state/AppState.js' import { checkOutTeleportedSessionBranch, diff --git a/src/components/TeleportRepoMismatchDialog.tsx b/src/components/TeleportRepoMismatchDialog.tsx index 126a9c432..83e55bd90 100644 --- a/src/components/TeleportRepoMismatchDialog.tsx +++ b/src/components/TeleportRepoMismatchDialog.tsx @@ -1,12 +1,12 @@ import React, { useCallback, useState } from 'react' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import { getDisplayPath } from '../utils/file.js' import { removePathFromRepo, validateRepoAtPath, } from '../utils/githubRepoPathMapping.js' import { Select } from './CustomSelect/index.js' -import { Dialog } from './design-system/Dialog.js' +import { Dialog } from '@anthropic/ink' import { Spinner } from './Spinner.js' type Props = { diff --git a/src/components/TeleportResumeWrapper.tsx b/src/components/TeleportResumeWrapper.tsx index 60e6c7806..9a2a6261e 100644 --- a/src/components/TeleportResumeWrapper.tsx +++ b/src/components/TeleportResumeWrapper.tsx @@ -9,7 +9,7 @@ import { type TeleportSource, useTeleportResume, } from '../hooks/useTeleportResume.js' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import { useKeybinding } from '../keybindings/useKeybinding.js' import { ResumeTask } from './ResumeTask.js' import { Spinner } from './Spinner.js' diff --git a/src/components/TeleportStash.tsx b/src/components/TeleportStash.tsx index 8baa30580..bb60e45a4 100644 --- a/src/components/TeleportStash.tsx +++ b/src/components/TeleportStash.tsx @@ -1,11 +1,10 @@ import figures from 'figures' import React, { useEffect, useState } from 'react' -import { Box, Text } from '../ink.js' +import { Box, Text, Dialog } from '@anthropic/ink' import { logForDebugging } from '../utils/debug.js' import type { GitFileStatus } from '../utils/git.js' import { getFileStatus, stashToCleanState } from '../utils/git.js' import { Select } from './CustomSelect/index.js' -import { Dialog } from './design-system/Dialog.js' import { Spinner } from './Spinner.js' type TeleportStashProps = { diff --git a/src/components/TextInput.tsx b/src/components/TextInput.tsx index 486c73ef2..0e1308336 100644 --- a/src/components/TextInput.tsx +++ b/src/components/TextInput.tsx @@ -5,13 +5,7 @@ import { useVoiceState } from '../context/voice.js' import { useClipboardImageHint } from '../hooks/useClipboardImageHint.js' import { useSettings } from '../hooks/useSettings.js' import { useTextInput } from '../hooks/useTextInput.js' -import { - Box, - color, - useAnimationFrame, - useTerminalFocus, - useTheme, -} from '../ink.js' +import { Box, color, useAnimationFrame, useTerminalFocus, useTheme } from '@anthropic/ink' import type { BaseTextInputProps } from '../types/textInputTypes.js' import { isEnvTruthy } from '../utils/envUtils.js' import type { TextHighlight } from '../utils/textHighlighting.js' diff --git a/src/components/ThemePicker.tsx b/src/components/ThemePicker.tsx index b14bcfd2c..2c99fc455 100644 --- a/src/components/ThemePicker.tsx +++ b/src/components/ThemePicker.tsx @@ -2,13 +2,7 @@ import { feature } from 'bun:bundle' import * as React from 'react' import { useExitOnCtrlCDWithKeybindings } from '../hooks/useExitOnCtrlCDWithKeybindings.js' import { useTerminalSize } from '../hooks/useTerminalSize.js' -import { - Box, - Text, - usePreviewTheme, - useTheme, - useThemeSetting, -} from '../ink.js' +import { Box, Text, usePreviewTheme, useTheme, useThemeSetting } from '@anthropic/ink' import { useRegisterKeybindingContext } from '../keybindings/KeybindingContext.js' import { useKeybinding } from '../keybindings/useKeybinding.js' import { useShortcutDisplay } from '../keybindings/useShortcutDisplay.js' @@ -17,8 +11,7 @@ import { gracefulShutdown } from '../utils/gracefulShutdown.js' import { updateSettingsForSource } from '../utils/settings/settings.js' import type { ThemeSetting } from '../utils/theme.js' import { Select } from './CustomSelect/index.js' -import { Byline } from './design-system/Byline.js' -import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js' +import { Byline, KeyboardShortcutHint } from '@anthropic/ink' import { getColorModuleUnavailableReason, getSyntaxTheme, diff --git a/src/components/ThinkingToggle.tsx b/src/components/ThinkingToggle.tsx index f17636cde..da2a78687 100644 --- a/src/components/ThinkingToggle.tsx +++ b/src/components/ThinkingToggle.tsx @@ -1,13 +1,11 @@ import * as React from 'react' import { useState } from 'react' import { useExitOnCtrlCDWithKeybindings } from 'src/hooks/useExitOnCtrlCDWithKeybindings.js' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import { useKeybinding } from '../keybindings/useKeybinding.js' import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js' import { Select } from './CustomSelect/index.js' -import { Byline } from './design-system/Byline.js' -import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js' -import { Pane } from './design-system/Pane.js' +import { Byline, KeyboardShortcutHint, Pane } from '@anthropic/ink' export type Props = { currentValue: boolean diff --git a/src/components/TokenWarning.tsx b/src/components/TokenWarning.tsx index 3ccb7235f..fe134c7ec 100644 --- a/src/components/TokenWarning.tsx +++ b/src/components/TokenWarning.tsx @@ -1,7 +1,7 @@ import { feature } from 'bun:bundle' import * as React from 'react' import { useSyncExternalStore } from 'react' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' import { calculateTokenWarningState, diff --git a/src/components/ToolUseLoader.tsx b/src/components/ToolUseLoader.tsx index 52652be99..de8fdb3c2 100644 --- a/src/components/ToolUseLoader.tsx +++ b/src/components/ToolUseLoader.tsx @@ -1,7 +1,8 @@ import React from 'react' import { BLACK_CIRCLE } from '../constants/figures.js' + +import { Box, Text } from '@anthropic/ink' import { useBlink } from '../hooks/useBlink.js' -import { Box, Text } from '../ink.js' type Props = { isError: boolean diff --git a/src/components/TrustDialog/TrustDialog.tsx b/src/components/TrustDialog/TrustDialog.tsx index a57408392..88a0c897f 100644 --- a/src/components/TrustDialog/TrustDialog.tsx +++ b/src/components/TrustDialog/TrustDialog.tsx @@ -4,7 +4,7 @@ import { logEvent } from 'src/services/analytics/index.js' import { setSessionTrustAccepted } from '../../bootstrap/state.js' import type { Command } from '../../commands.js' import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js' -import { Box, Link, Text } from '../../ink.js' +import { Box, Link, Text } from '@anthropic/ink' import { useKeybinding } from '../../keybindings/useKeybinding.js' import { getMcpConfigsByScope } from '../../services/mcp/config.js' import { BASH_TOOL_NAME } from '../../tools/BashTool/toolName.js' diff --git a/src/components/ValidationErrorsList.tsx b/src/components/ValidationErrorsList.tsx index a7eea5400..6620ab2fd 100644 --- a/src/components/ValidationErrorsList.tsx +++ b/src/components/ValidationErrorsList.tsx @@ -1,6 +1,6 @@ import setWith from 'lodash-es/setWith.js' import * as React from 'react' -import { Box, Text, useTheme } from '../ink.js' +import { Box, Text, useTheme } from '@anthropic/ink' import type { ValidationError } from '../utils/settings/validation.js' import { type TreeNode, treeify } from '../utils/treeify.js' diff --git a/src/components/VimTextInput.tsx b/src/components/VimTextInput.tsx index bc8e8211f..42dfa252e 100644 --- a/src/components/VimTextInput.tsx +++ b/src/components/VimTextInput.tsx @@ -2,7 +2,7 @@ import chalk from 'chalk' import React from 'react' import { useClipboardImageHint } from '../hooks/useClipboardImageHint.js' import { useVimInput } from '../hooks/useVimInput.js' -import { Box, color, useTerminalFocus, useTheme } from '../ink.js' +import { Box, color, useTerminalFocus, useTheme } from '@anthropic/ink' import type { VimTextInputProps } from '../types/textInputTypes.js' import type { TextHighlight } from '../utils/textHighlighting.js' import { BaseTextInput } from './BaseTextInput.js' diff --git a/src/components/VirtualMessageList.tsx b/src/components/VirtualMessageList.tsx index bbe4a196c..5797297f6 100644 --- a/src/components/VirtualMessageList.tsx +++ b/src/components/VirtualMessageList.tsx @@ -10,11 +10,7 @@ import { useSyncExternalStore, } from 'react' import { useVirtualScroll } from '../hooks/useVirtualScroll.js' -import type { ScrollBoxHandle } from '../ink/components/ScrollBox.js' -import type { DOMElement } from '../ink/dom.js' -import type { MatchPosition } from '../ink/render-to-screen.js' -import { Box } from '../ink.js' -import type { RenderableMessage } from '../types/message.js' +import { Box, type DOMElement, type ScrollBoxHandle, type MatchPosition } from '@anthropic/ink' import { TextHoverColorContext } from './design-system/ThemedText.js' import { ScrollChromeContext } from './FullscreenLayout.js' diff --git a/src/components/WorkflowMultiselectDialog.tsx b/src/components/WorkflowMultiselectDialog.tsx index e06737514..d5e1d5460 100644 --- a/src/components/WorkflowMultiselectDialog.tsx +++ b/src/components/WorkflowMultiselectDialog.tsx @@ -1,12 +1,9 @@ import React, { useCallback, useState } from 'react' import type { Workflow } from '../commands/install-github-app/types.js' import type { ExitState } from '../hooks/useExitOnCtrlCDWithKeybindings.js' -import { Box, Link, Text } from '../ink.js' +import { Box, Link, Text, Byline, Dialog, KeyboardShortcutHint } from '@anthropic/ink' import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js' import { SelectMulti } from './CustomSelect/SelectMulti.js' -import { Byline } from './design-system/Byline.js' -import { Dialog } from './design-system/Dialog.js' -import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js' type WorkflowOption = { value: Workflow diff --git a/src/components/WorktreeExitDialog.tsx b/src/components/WorktreeExitDialog.tsx index ba5ab0f83..83ea8d554 100644 --- a/src/components/WorktreeExitDialog.tsx +++ b/src/components/WorktreeExitDialog.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react' import type { CommandResultDisplay } from 'src/commands.js' import { logEvent } from 'src/services/analytics/index.js' import { logForDebugging } from 'src/utils/debug.js' -import { Box, Text } from '../ink.js' +import { Box, Text, Dialog } from '@anthropic/ink' import { execFileNoThrow } from '../utils/execFileNoThrow.js' import { getPlansDirectory } from '../utils/plans.js' import { setCwd } from '../utils/Shell.js' @@ -13,7 +13,6 @@ import { killTmuxSession, } from '../utils/worktree.js' import { Select } from './CustomSelect/select.js' -import { Dialog } from './design-system/Dialog.js' import { Spinner } from './Spinner.js' // Inline require breaks the cycle this file would otherwise close: diff --git a/src/components/agents/AgentDetail.tsx b/src/components/agents/AgentDetail.tsx index 4c817b134..88f368dc6 100644 --- a/src/components/agents/AgentDetail.tsx +++ b/src/components/agents/AgentDetail.tsx @@ -1,7 +1,6 @@ import figures from 'figures' import * as React from 'react' -import type { KeyboardEvent } from '../../ink/events/keyboard-event.js' -import { Box, Text } from '../../ink.js' +import { type KeyboardEvent, Box, Text } from '@anthropic/ink' import { useKeybinding } from '../../keybindings/useKeybinding.js' import type { Tools } from '../../Tool.js' import { getAgentColor } from '../../tools/AgentTool/agentColorManager.js' diff --git a/src/components/agents/AgentEditor.tsx b/src/components/agents/AgentEditor.tsx index e5c7b1847..7cbd52b96 100644 --- a/src/components/agents/AgentEditor.tsx +++ b/src/components/agents/AgentEditor.tsx @@ -3,8 +3,7 @@ import figures from 'figures' import * as React from 'react' import { useCallback, useMemo, useState } from 'react' import { useSetAppState } from 'src/state/AppState.js' -import type { KeyboardEvent } from '../../ink/events/keyboard-event.js' -import { Box, Text } from '../../ink.js' +import { type KeyboardEvent, Box, Text } from '@anthropic/ink' import { useKeybinding } from '../../keybindings/useKeybinding.js' import type { Tools } from '../../Tool.js' import { diff --git a/src/components/agents/AgentNavigationFooter.tsx b/src/components/agents/AgentNavigationFooter.tsx index 9c4fa9f76..e52149f6a 100644 --- a/src/components/agents/AgentNavigationFooter.tsx +++ b/src/components/agents/AgentNavigationFooter.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' type Props = { instructions?: string diff --git a/src/components/agents/AgentsList.tsx b/src/components/agents/AgentsList.tsx index 6eadf1ef7..4be51e11b 100644 --- a/src/components/agents/AgentsList.tsx +++ b/src/components/agents/AgentsList.tsx @@ -1,8 +1,7 @@ import figures from 'figures' import * as React from 'react' import type { SettingSource } from 'src/utils/settings/constants.js' -import type { KeyboardEvent } from '../../ink/events/keyboard-event.js' -import { Box, Text } from '../../ink.js' +import { type KeyboardEvent, Box, Text } from '@anthropic/ink' import type { ResolvedAgent } from '../../tools/AgentTool/agentDisplay.js' import { AGENT_SOURCE_GROUPS, @@ -12,8 +11,7 @@ import { } from '../../tools/AgentTool/agentDisplay.js' import type { AgentDefinition } from '../../tools/AgentTool/loadAgentsDir.js' import { count } from '../../utils/array.js' -import { Dialog } from '../design-system/Dialog.js' -import { Divider } from '../design-system/Divider.js' +import { Dialog, Divider } from '@anthropic/ink' import { getAgentSourceDisplayName } from './utils.js' type Props = { diff --git a/src/components/agents/AgentsMenu.tsx b/src/components/agents/AgentsMenu.tsx index 91de932b4..00c3a9ce3 100644 --- a/src/components/agents/AgentsMenu.tsx +++ b/src/components/agents/AgentsMenu.tsx @@ -5,7 +5,7 @@ import type { SettingSource } from 'src/utils/settings/constants.js' import type { CommandResultDisplay } from '../../commands.js' import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js' import { useMergedTools } from '../../hooks/useMergedTools.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { useAppState, useSetAppState } from '../../state/AppState.js' import type { Tools } from '../../Tool.js' import { @@ -19,7 +19,7 @@ import { import { toError } from '../../utils/errors.js' import { logError } from '../../utils/log.js' import { Select } from '../CustomSelect/select.js' -import { Dialog } from '../design-system/Dialog.js' +import { Dialog } from '@anthropic/ink' import { AgentDetail } from './AgentDetail.js' import { AgentEditor } from './AgentEditor.js' import { AgentNavigationFooter } from './AgentNavigationFooter.js' diff --git a/src/components/agents/ColorPicker.tsx b/src/components/agents/ColorPicker.tsx index 8549424cd..7db5ea288 100644 --- a/src/components/agents/ColorPicker.tsx +++ b/src/components/agents/ColorPicker.tsx @@ -1,7 +1,7 @@ import figures from 'figures' import React, { useState } from 'react' -import type { KeyboardEvent } from '../../ink/events/keyboard-event.js' -import { Box, Text } from '../../ink.js' +import type { KeyboardEvent } from '@anthropic/ink' +import { Box, Text } from '@anthropic/ink' import { AGENT_COLOR_TO_THEME_COLOR, AGENT_COLORS, diff --git a/src/components/agents/ModelSelector.tsx b/src/components/agents/ModelSelector.tsx index 4f1b2e8af..c3707d3fd 100644 --- a/src/components/agents/ModelSelector.tsx +++ b/src/components/agents/ModelSelector.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { getAgentModelOptions } from '../../utils/model/agent.js' import { Select } from '../CustomSelect/select.js' diff --git a/src/components/agents/ToolSelector.tsx b/src/components/agents/ToolSelector.tsx index 9bc20b7d8..96b9f4b68 100644 --- a/src/components/agents/ToolSelector.tsx +++ b/src/components/agents/ToolSelector.tsx @@ -21,12 +21,11 @@ import { TodoWriteTool } from 'src/tools/TodoWriteTool/TodoWriteTool.js' import { TungstenTool } from 'src/tools/TungstenTool/TungstenTool.js' import { WebFetchTool } from 'src/tools/WebFetchTool/WebFetchTool.js' import { WebSearchTool } from 'src/tools/WebSearchTool/WebSearchTool.js' -import type { KeyboardEvent } from '../../ink/events/keyboard-event.js' -import { Box, Text } from '../../ink.js' +import { type KeyboardEvent, Box, Text } from '@anthropic/ink' import { useKeybinding } from '../../keybindings/useKeybinding.js' import { count } from '../../utils/array.js' import { plural } from '../../utils/stringUtils.js' -import { Divider } from '../design-system/Divider.js' +import { Divider } from '@anthropic/ink' type Props = { tools: Tools diff --git a/src/components/agents/new-agent-creation/wizard-steps/ColorStep.tsx b/src/components/agents/new-agent-creation/wizard-steps/ColorStep.tsx index adc35e27c..7763daac6 100644 --- a/src/components/agents/new-agent-creation/wizard-steps/ColorStep.tsx +++ b/src/components/agents/new-agent-creation/wizard-steps/ColorStep.tsx @@ -1,10 +1,8 @@ import React, { type ReactNode } from 'react' -import { Box } from '../../../../ink.js' +import { Box, Byline, KeyboardShortcutHint } from '@anthropic/ink' import { useKeybinding } from '../../../../keybindings/useKeybinding.js' import type { AgentColorName } from '../../../../tools/AgentTool/agentColorManager.js' import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js' -import { Byline } from '../../../design-system/Byline.js' -import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js' import { useWizard } from '../../../wizard/index.js' import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js' import { ColorPicker } from '../../ColorPicker.js' diff --git a/src/components/agents/new-agent-creation/wizard-steps/ConfirmStep.tsx b/src/components/agents/new-agent-creation/wizard-steps/ConfirmStep.tsx index bfa035eb5..ef02cbefe 100644 --- a/src/components/agents/new-agent-creation/wizard-steps/ConfirmStep.tsx +++ b/src/components/agents/new-agent-creation/wizard-steps/ConfirmStep.tsx @@ -1,6 +1,5 @@ import React, { type ReactNode } from 'react' -import type { KeyboardEvent } from '../../../../ink/events/keyboard-event.js' -import { Box, Text } from '../../../../ink.js' +import { type KeyboardEvent, Box, Byline, KeyboardShortcutHint, Text } from '@anthropic/ink' import { useKeybinding } from '../../../../keybindings/useKeybinding.js' import { isAutoMemoryEnabled } from '../../../../memdir/paths.js' import type { Tools } from '../../../../Tool.js' @@ -9,8 +8,6 @@ import type { AgentDefinition } from '../../../../tools/AgentTool/loadAgentsDir. import { truncateToWidth } from '../../../../utils/format.js' import { getAgentModelDisplay } from '../../../../utils/model/agent.js' import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js' -import { Byline } from '../../../design-system/Byline.js' -import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js' import { useWizard } from '../../../wizard/index.js' import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js' import { getNewRelativeAgentFilePath } from '../../agentFileUtils.js' diff --git a/src/components/agents/new-agent-creation/wizard-steps/DescriptionStep.tsx b/src/components/agents/new-agent-creation/wizard-steps/DescriptionStep.tsx index 1138cc3d3..50a0d0590 100644 --- a/src/components/agents/new-agent-creation/wizard-steps/DescriptionStep.tsx +++ b/src/components/agents/new-agent-creation/wizard-steps/DescriptionStep.tsx @@ -1,10 +1,8 @@ import React, { type ReactNode, useCallback, useState } from 'react' -import { Box, Text } from '../../../../ink.js' +import { Box, Byline, KeyboardShortcutHint, Text } from '@anthropic/ink' import { useKeybinding } from '../../../../keybindings/useKeybinding.js' import { editPromptInEditor } from '../../../../utils/promptEditor.js' import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js' -import { Byline } from '../../../design-system/Byline.js' -import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js' import TextInput from '../../../TextInput.js' import { useWizard } from '../../../wizard/index.js' import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js' diff --git a/src/components/agents/new-agent-creation/wizard-steps/GenerateStep.tsx b/src/components/agents/new-agent-creation/wizard-steps/GenerateStep.tsx index 1cb7ae69d..1e0b88512 100644 --- a/src/components/agents/new-agent-creation/wizard-steps/GenerateStep.tsx +++ b/src/components/agents/new-agent-creation/wizard-steps/GenerateStep.tsx @@ -1,12 +1,11 @@ import { APIUserAbortError } from '@anthropic-ai/sdk' import React, { type ReactNode, useCallback, useRef, useState } from 'react' import { useMainLoopModel } from '../../../../hooks/useMainLoopModel.js' -import { Box, Text } from '../../../../ink.js' +import { Box, Byline, Text } from '@anthropic/ink' import { useKeybinding } from '../../../../keybindings/useKeybinding.js' import { createAbortController } from '../../../../utils/abortController.js' import { editPromptInEditor } from '../../../../utils/promptEditor.js' import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js' -import { Byline } from '../../../design-system/Byline.js' import { Spinner } from '../../../Spinner.js' import TextInput from '../../../TextInput.js' import { useWizard } from '../../../wizard/index.js' diff --git a/src/components/agents/new-agent-creation/wizard-steps/LocationStep.tsx b/src/components/agents/new-agent-creation/wizard-steps/LocationStep.tsx index a7fd0a2bc..d846cb39b 100644 --- a/src/components/agents/new-agent-creation/wizard-steps/LocationStep.tsx +++ b/src/components/agents/new-agent-creation/wizard-steps/LocationStep.tsx @@ -1,10 +1,8 @@ import React, { type ReactNode } from 'react' -import { Box } from '../../../../ink.js' +import { Box, Byline, KeyboardShortcutHint } from '@anthropic/ink' import type { SettingSource } from '../../../../utils/settings/constants.js' import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js' import { Select } from '../../../CustomSelect/select.js' -import { Byline } from '../../../design-system/Byline.js' -import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js' import { useWizard } from '../../../wizard/index.js' import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js' import type { AgentWizardData } from '../types.js' diff --git a/src/components/agents/new-agent-creation/wizard-steps/MemoryStep.tsx b/src/components/agents/new-agent-creation/wizard-steps/MemoryStep.tsx index 3c987cf77..d9fb92ed5 100644 --- a/src/components/agents/new-agent-creation/wizard-steps/MemoryStep.tsx +++ b/src/components/agents/new-agent-creation/wizard-steps/MemoryStep.tsx @@ -1,5 +1,5 @@ import React, { type ReactNode } from 'react' -import { Box } from '../../../../ink.js' +import { Box, Byline, KeyboardShortcutHint } from '@anthropic/ink' import { useKeybinding } from '../../../../keybindings/useKeybinding.js' import { isAutoMemoryEnabled } from '../../../../memdir/paths.js' import { @@ -8,8 +8,6 @@ import { } from '../../../../tools/AgentTool/agentMemory.js' import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js' import { Select } from '../../../CustomSelect/select.js' -import { Byline } from '../../../design-system/Byline.js' -import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js' import { useWizard } from '../../../wizard/index.js' import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js' import type { AgentWizardData } from '../types.js' diff --git a/src/components/agents/new-agent-creation/wizard-steps/MethodStep.tsx b/src/components/agents/new-agent-creation/wizard-steps/MethodStep.tsx index 8f8252e12..552c20d1c 100644 --- a/src/components/agents/new-agent-creation/wizard-steps/MethodStep.tsx +++ b/src/components/agents/new-agent-creation/wizard-steps/MethodStep.tsx @@ -1,9 +1,7 @@ import React, { type ReactNode } from 'react' -import { Box } from '../../../../ink.js' +import { Box, Byline, KeyboardShortcutHint } from '@anthropic/ink' import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js' import { Select } from '../../../CustomSelect/select.js' -import { Byline } from '../../../design-system/Byline.js' -import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js' import { useWizard } from '../../../wizard/index.js' import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js' import type { AgentWizardData } from '../types.js' diff --git a/src/components/agents/new-agent-creation/wizard-steps/ModelStep.tsx b/src/components/agents/new-agent-creation/wizard-steps/ModelStep.tsx index 586cc6cc8..a92ecde4a 100644 --- a/src/components/agents/new-agent-creation/wizard-steps/ModelStep.tsx +++ b/src/components/agents/new-agent-creation/wizard-steps/ModelStep.tsx @@ -1,7 +1,6 @@ import React, { type ReactNode } from 'react' import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js' -import { Byline } from '../../../design-system/Byline.js' -import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js' +import { Byline, KeyboardShortcutHint } from '@anthropic/ink' import { useWizard } from '../../../wizard/index.js' import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js' import { ModelSelector } from '../../ModelSelector.js' diff --git a/src/components/agents/new-agent-creation/wizard-steps/PromptStep.tsx b/src/components/agents/new-agent-creation/wizard-steps/PromptStep.tsx index 4d6747520..c1e1f5260 100644 --- a/src/components/agents/new-agent-creation/wizard-steps/PromptStep.tsx +++ b/src/components/agents/new-agent-creation/wizard-steps/PromptStep.tsx @@ -1,10 +1,8 @@ import React, { type ReactNode, useCallback, useState } from 'react' -import { Box, Text } from '../../../../ink.js' +import { Box, Byline, KeyboardShortcutHint, Text } from '@anthropic/ink' import { useKeybinding } from '../../../../keybindings/useKeybinding.js' import { editPromptInEditor } from '../../../../utils/promptEditor.js' import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js' -import { Byline } from '../../../design-system/Byline.js' -import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js' import TextInput from '../../../TextInput.js' import { useWizard } from '../../../wizard/index.js' import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js' diff --git a/src/components/agents/new-agent-creation/wizard-steps/ToolsStep.tsx b/src/components/agents/new-agent-creation/wizard-steps/ToolsStep.tsx index 501509ff5..19a8360c4 100644 --- a/src/components/agents/new-agent-creation/wizard-steps/ToolsStep.tsx +++ b/src/components/agents/new-agent-creation/wizard-steps/ToolsStep.tsx @@ -1,8 +1,7 @@ import React, { type ReactNode } from 'react' import type { Tools } from '../../../../Tool.js' +import { Byline, KeyboardShortcutHint } from '@anthropic/ink' import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js' -import { Byline } from '../../../design-system/Byline.js' -import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js' import { useWizard } from '../../../wizard/index.js' import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js' import { ToolSelector } from '../../ToolSelector.js' diff --git a/src/components/agents/new-agent-creation/wizard-steps/TypeStep.tsx b/src/components/agents/new-agent-creation/wizard-steps/TypeStep.tsx index 6ff025492..6c57440c8 100644 --- a/src/components/agents/new-agent-creation/wizard-steps/TypeStep.tsx +++ b/src/components/agents/new-agent-creation/wizard-steps/TypeStep.tsx @@ -1,10 +1,8 @@ import React, { type ReactNode, useState } from 'react' -import { Box, Text } from '../../../../ink.js' +import { Box, Byline, KeyboardShortcutHint, Text } from '@anthropic/ink' import { useKeybinding } from '../../../../keybindings/useKeybinding.js' import type { AgentDefinition } from '../../../../tools/AgentTool/loadAgentsDir.js' import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js' -import { Byline } from '../../../design-system/Byline.js' -import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js' import TextInput from '../../../TextInput.js' import { useWizard } from '../../../wizard/index.js' import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js' diff --git a/src/components/design-system/Byline.tsx b/src/components/design-system/Byline.tsx index b0ddc97f3..eefab18cf 100644 --- a/src/components/design-system/Byline.tsx +++ b/src/components/design-system/Byline.tsx @@ -1,57 +1 @@ -import React, { Children, isValidElement } from 'react' -import { Text } from '../../ink.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" - * - * - * - * - * - * - * - * @example - * // With conditional children: "Esc to cancel" (only one item shown) - * - * - * {showEnter && } - * - * - * - * - */ -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) => ( - - {index > 0 && · } - {child} - - ))} - - ) -} +export { Byline } from '@anthropic/ink' diff --git a/src/components/design-system/Dialog.tsx b/src/components/design-system/Dialog.tsx index 4472bd0d0..bb531183c 100644 --- a/src/components/design-system/Dialog.tsx +++ b/src/components/design-system/Dialog.tsx @@ -1,100 +1 @@ -import React from 'react' -import { - type ExitState, - useExitOnCtrlCDWithKeybindings, -} from '../../hooks/useExitOnCtrlCDWithKeybindings.js' -import { Box, Text } from '../../ink.js' -import { useKeybinding } from '../../keybindings/useKeybinding.js' -import type { Theme } from '../../utils/theme.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 ? ( - Press {exitState.keyName} again to exit - ) : ( - - - - - ) - - const content = ( - <> - - - - {title} - - {subtitle && {subtitle}} - - {children} - - {!hideInputGuide && ( - - - {inputGuide ? inputGuide(exitState) : defaultInputGuide} - - - )} - - ) - - if (hideBorder) { - return content - } - - return {content} -} +export { Dialog } from '@anthropic/ink' diff --git a/src/components/design-system/Divider.tsx b/src/components/design-system/Divider.tsx index a88982be5..a5b7f0e88 100644 --- a/src/components/design-system/Divider.tsx +++ b/src/components/design-system/Divider.tsx @@ -1,97 +1 @@ -import React from 'react' -import { useTerminalSize } from '../../hooks/useTerminalSize.js' -import { stringWidth } from '../../ink/stringWidth.js' -import { Ansi, Text } from '../../ink.js' -import type { Theme } from '../../utils/theme.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 ─────────── - * - */ - title?: string -} - -/** - * A horizontal divider line. - * - * @example - * // Full-width dimmed divider - * - * - * @example - * // Colored divider - * - * - * @example - * // Fixed width - * - * - * @example - * // Full width minus padding (for indented content) - * - * - * @example - * // With centered title - * - */ -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 ( - - {char.repeat(leftWidth)}{' '} - - {title} - {' '} - {char.repeat(rightWidth)} - - ) - } - - return ( - - {char.repeat(effectiveWidth)} - - ) -} +export { Divider } from '@anthropic/ink' diff --git a/src/components/design-system/FuzzyPicker.tsx b/src/components/design-system/FuzzyPicker.tsx index fc1b9fe9e..c034335e9 100644 --- a/src/components/design-system/FuzzyPicker.tsx +++ b/src/components/design-system/FuzzyPicker.tsx @@ -1,350 +1 @@ -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 '../../ink/events/keyboard-event.js' -import { clamp } from '../../ink/layout/geometry.js' -import { Box, Text, useTerminalFocus } from '../../ink.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 = { - /** Hint label shown in the byline, e.g. "mention" → "Tab to mention". */ - action: string - handler: (item: T) => void -} - -type Props = { - 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 - /** Shift+Tab key. Gets its own hint. */ - onShiftTab?: PickerAction - /** - * 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({ - 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): 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 = ( - - ) - - const listBlock = ( - - ) - - const preview = - renderPreview && focused ? ( - - {renderPreview(focused)} - - ) : 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' ? ( - - - {listBlock} - {matchLabel && {matchLabel}} - - {preview ?? } - - ) : ( - // 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'. - - {listBlock} - {matchLabel && {matchLabel}} - {preview} - - ) - - const inputAbove = direction !== 'up' - return ( - - - - {title} - - {inputAbove && searchBox} - {listGroup} - {!inputAbove && searchBox} - - - - - {onTab && ( - - )} - {onShiftTab && !compact && ( - - )} - - {extraHints} - - - - - ) -} - -type ListProps = Pick< - Props, - 'visibleCount' | 'direction' | 'getKey' | 'renderItem' -> & { - visible: readonly T[] - windowStart: number - total: number - focusedIndex: number - emptyText: string -} - -function List({ - visible, - windowStart, - visibleCount, - total, - focusedIndex, - direction, - getKey, - renderItem, - emptyText, -}: ListProps): React.ReactNode { - if (visible.length === 0) { - return ( - - {emptyText} - - ) - } - - 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 ( - - {renderItem(item, isFocused)} - - ) - }) - - return ( - - {rows} - - ) -} - -function firstWord(s: string): string { - const i = s.indexOf(' ') - return i === -1 ? s : s.slice(0, i) -} +export { FuzzyPicker } from '@anthropic/ink' diff --git a/src/components/design-system/KeyboardShortcutHint.tsx b/src/components/design-system/KeyboardShortcutHint.tsx index 7d3c136d1..ad61f636f 100644 --- a/src/components/design-system/KeyboardShortcutHint.tsx +++ b/src/components/design-system/KeyboardShortcutHint.tsx @@ -1,58 +1 @@ -import React from 'react' -import Text from '../../ink/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 for the common dim styling. - * - * @example - * // Simple hint wrapped in dim Text - * - * - * // With parentheses: "(ctrl+o to expand)" - * - * - * // With bold shortcut: "Enter to confirm" (Enter is bold) - * - * - * // Multiple hints with middot separator - use Byline - * - * - * - * - * - * - */ -export function KeyboardShortcutHint({ - shortcut, - action, - parens = false, - bold = false, -}: Props): React.ReactNode { - const shortcutText = bold ? {shortcut} : shortcut - - if (parens) { - return ( - - ({shortcutText} to {action}) - - ) - } - return ( - - {shortcutText} to {action} - - ) -} +export { KeyboardShortcutHint } from '@anthropic/ink' diff --git a/src/components/design-system/ListItem.tsx b/src/components/design-system/ListItem.tsx index 2d142be03..f309c9975 100644 --- a/src/components/design-system/ListItem.tsx +++ b/src/components/design-system/ListItem.tsx @@ -1,188 +1 @@ -import figures from 'figures' -import type { ReactNode } from 'react' -import React from 'react' -import { useDeclaredCursor } from '../../ink/hooks/use-declared-cursor.js' -import { Box, Text } from '../../ink.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) => ( - * - * {option.label} - * - * ))} - * - * @example - * // With scroll indicators - * First visible item - * ... - * Last visible item - * - * @example - * // With description - * - * Primary text - * - * - * @example - * // Custom children styling (styled=false) - * - * Custom styled content - * - */ -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 - } - - if (isFocused) { - return {figures.pointer} - } - - if (showScrollDown) { - return {figures.arrowDown} - } - - if (showScrollUp) { - return {figures.arrowUp} - } - - return - } - - // 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 ( - - - {renderIndicator()} - {styled ? ( - - {children} - - ) : ( - children - )} - {isSelected && !disabled && {figures.tick}} - - {description && ( - - {description} - - )} - - ) -} +export { ListItem } from '@anthropic/ink' diff --git a/src/components/design-system/LoadingState.tsx b/src/components/design-system/LoadingState.tsx index 046f726fa..71dbdaa40 100644 --- a/src/components/design-system/LoadingState.tsx +++ b/src/components/design-system/LoadingState.tsx @@ -1,66 +1 @@ -import React from 'react' -import { Box, Text } from '../../ink.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 - * - * - * @example - * // Bold loading message - * - * - * @example - * // With subtitle - * - */ -export function LoadingState({ - message, - bold = false, - dimColor = false, - subtitle, -}: LoadingStateProps): React.ReactNode { - return ( - - - - - {' '} - {message} - - - {subtitle && {subtitle}} - - ) -} +export { LoadingState } from '@anthropic/ink' diff --git a/src/components/design-system/Pane.tsx b/src/components/design-system/Pane.tsx index 9c10907d3..12eb3ed55 100644 --- a/src/components/design-system/Pane.tsx +++ b/src/components/design-system/Pane.tsx @@ -1,57 +1 @@ -import React from 'react' -import { useIsInsideModal } from '../../context/modalContext.js' -import { Box } from '../../ink.js' -import type { Theme } from '../../utils/theme.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 - * `` instead — it registers its own keybindings. For a full - * rounded-border card, use ``. - * - * Submenus rendered inside a Pane should use `hideBorder` on their Dialog - * so the Pane's border remains the single frame. - * - * @example - * - * ... - * - */ -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 ( - - {children} - - ) - } - return ( - - - - {children} - - - ) -} +export { Pane } from '@anthropic/ink' diff --git a/src/components/design-system/ProgressBar.tsx b/src/components/design-system/ProgressBar.tsx index 590fcd265..6aacc6129 100644 --- a/src/components/design-system/ProgressBar.tsx +++ b/src/components/design-system/ProgressBar.tsx @@ -1,54 +1 @@ -import React from 'react' -import { Text } from '../../ink.js' -import type { Theme } from '../../utils/theme.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 ( - - {segments.join('')} - - ) -} +export { ProgressBar } from '@anthropic/ink' diff --git a/src/components/design-system/Ratchet.tsx b/src/components/design-system/Ratchet.tsx index 91580ff05..5aaab3289 100644 --- a/src/components/design-system/Ratchet.tsx +++ b/src/components/design-system/Ratchet.tsx @@ -1,45 +1 @@ -import React, { useCallback, useLayoutEffect, useRef, useState } from 'react' -import { useTerminalSize } from '../../hooks/useTerminalSize.js' -import { useTerminalViewport } from '../../ink/hooks/use-terminal-viewport.js' -import { Box, type DOMElement, measureElement } from '../../ink.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(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 ( - - - {children} - - - ) -} +export { Ratchet } from '@anthropic/ink' diff --git a/src/components/design-system/StatusIcon.tsx b/src/components/design-system/StatusIcon.tsx index 832c83a9e..962ea1ba6 100644 --- a/src/components/design-system/StatusIcon.tsx +++ b/src/components/design-system/StatusIcon.tsx @@ -1,71 +1 @@ -import figures from 'figures' -import React from 'react' -import { Text } from '../../ink.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 - * - * - * @example - * // Error with trailing space for text - * Failed to connect - * - * @example - * // Status line pattern - * - * - * Waiting for response - * - */ -export function StatusIcon({ - status, - withSpace = false, -}: Props): React.ReactNode { - const config = STATUS_CONFIG[status] - - return ( - - {config.icon} - {withSpace && ' '} - - ) -} +export { StatusIcon } from '@anthropic/ink' diff --git a/src/components/design-system/Tabs.tsx b/src/components/design-system/Tabs.tsx index 40bae7baa..e11de0a6b 100644 --- a/src/components/design-system/Tabs.tsx +++ b/src/components/design-system/Tabs.tsx @@ -1,339 +1 @@ -import React, { - createContext, - useCallback, - useContext, - useEffect, - useState, -} from 'react' -import { - useIsInsideModal, - useModalScrollRef, -} from '../../context/modalContext.js' -import { useTerminalSize } from '../../hooks/useTerminalSize.js' -import ScrollBox from '../../ink/components/ScrollBox.js' -import type { KeyboardEvent } from '../../ink/events/keyboard-event.js' -import { stringWidth } from '../../ink/stringWidth.js' -import { Box, Text } from '../../ink.js' -import { useKeybindings } from '../../keybindings/useKeybinding.js' -import type { Theme } from '../../utils/theme.js' - -type TabsProps = { - children: Array> - 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({ - 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 ( - - - {!hidden && ( - - {title !== undefined && ( - - {title} - - )} - {tabs.map(([id, title], i) => { - const isCurrent = selectedTabIndex === i - const hasColorCursor = color && isCurrent && headerFocused - return ( - - {' '} - {title}{' '} - - ) - })} - {spacerWidth > 0 && {' '.repeat(spacerWidth)}} - - )} - {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. - - - {children} - - - ) : ( - - {children} - - )} - - - ) -} - -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 ( - - {children} - - ) -} - -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 } -} +export { Tab, Tabs, useTabHeaderFocus, useTabsWidth } from '@anthropic/ink' diff --git a/src/components/design-system/ThemeProvider.tsx b/src/components/design-system/ThemeProvider.tsx index ef60d23a1..9d9bc9f66 100644 --- a/src/components/design-system/ThemeProvider.tsx +++ b/src/components/design-system/ThemeProvider.tsx @@ -1,160 +1 @@ -import { feature } from 'bun:bundle' -import React, { - createContext, - useContext, - useEffect, - useMemo, - useState, -} from 'react' -import useStdin from '../../ink/hooks/use-stdin.js' -import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js' -import { - getSystemThemeName, - type SystemTheme, -} from '../../utils/systemTheme.js' -import type { ThemeName, ThemeSetting } from '../../utils/theme.js' - -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({ - 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 getGlobalConfig().theme -} - -function defaultSaveTheme(setting: ThemeSetting): void { - saveGlobalConfig(current => ({ ...current, theme: setting })) -} - -export function ThemeProvider({ - children, - initialState, - onThemeSave = defaultSaveTheme, -}: Props) { - const [themeSetting, setThemeSetting] = useState( - initialState ?? defaultInitialTheme, - ) - const [previewTheme, setPreviewTheme] = useState(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(() => - (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( - () => ({ - 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 {children} -} - -/** - * 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 } -} +export { ThemeProvider, usePreviewTheme, useTheme, useThemeSetting } from '@anthropic/ink' diff --git a/src/components/design-system/ThemedBox.tsx b/src/components/design-system/ThemedBox.tsx index 10fbe9137..30059f318 100644 --- a/src/components/design-system/ThemedBox.tsx +++ b/src/components/design-system/ThemedBox.tsx @@ -1,112 +1 @@ -import React, { type PropsWithChildren, type Ref } from 'react' -import Box from '../../ink/components/Box.js' -import type { DOMElement } from '../../ink/dom.js' -import type { ClickEvent } from '../../ink/events/click-event.js' -import type { FocusEvent } from '../../ink/events/focus-event.js' -import type { KeyboardEvent } from '../../ink/events/keyboard-event.js' -import type { Color, Styles } from '../../ink/styles.js' -import { getTheme, type Theme } from '../../utils/theme.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 - 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): 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 ( - - {children} - - ) -} - -export default ThemedBox +export { Box as default } from '@anthropic/ink' diff --git a/src/components/design-system/ThemedText.tsx b/src/components/design-system/ThemedText.tsx index 3c32b8bc3..792f0da4b 100644 --- a/src/components/design-system/ThemedText.tsx +++ b/src/components/design-system/ThemedText.tsx @@ -1,132 +1 @@ -import type { ReactNode } from 'react' -import React, { useContext } from 'react' -import Text from '../../ink/components/Text.js' -import type { Color, Styles } from '../../ink/styles.js' -import { getTheme, type Theme } from '../../utils/theme.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 ( - - {children} - - ) -} +export { Text as default, TextHoverColorContext } from '@anthropic/ink' diff --git a/src/components/design-system/color.ts b/src/components/design-system/color.ts index d21e51279..56c8612de 100644 --- a/src/components/design-system/color.ts +++ b/src/components/design-system/color.ts @@ -1,5 +1,4 @@ -import { type ColorType, colorize } from '../../ink/colorize.js' -import type { Color } from '../../ink/styles.js' +import { type ColorType, colorize, type Color } from '@anthropic/ink' import { getTheme, type Theme, type ThemeName } from '../../utils/theme.js' /** diff --git a/src/components/diff/DiffDetailView.tsx b/src/components/diff/DiffDetailView.tsx index af79d9365..43fb442fc 100644 --- a/src/components/diff/DiffDetailView.tsx +++ b/src/components/diff/DiffDetailView.tsx @@ -2,10 +2,10 @@ import type { StructuredPatchHunk } from 'diff' import { resolve } from 'path' import React, { useMemo } from 'react' import { useTerminalSize } from '../../hooks/useTerminalSize.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { getCwd } from '../../utils/cwd.js' import { readFileSafe } from '../../utils/file.js' -import { Divider } from '../design-system/Divider.js' +import { Divider } from '@anthropic/ink' import { StructuredDiff } from '../StructuredDiff.js' type Props = { diff --git a/src/components/diff/DiffDialog.tsx b/src/components/diff/DiffDialog.tsx index 8847a25fc..3f6757b66 100644 --- a/src/components/diff/DiffDialog.tsx +++ b/src/components/diff/DiffDialog.tsx @@ -4,13 +4,12 @@ import type { CommandResultDisplay } from '../../commands.js' import { useRegisterOverlay } from '../../context/overlayContext.js' import { type DiffData, useDiffData } from '../../hooks/useDiffData.js' import { type TurnDiff, useTurnDiffs } from '../../hooks/useTurnDiffs.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { useKeybindings } from '../../keybindings/useKeybinding.js' import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js' import type { Message } from '../../types/message.js' import { plural } from '../../utils/stringUtils.js' -import { Byline } from '../design-system/Byline.js' -import { Dialog } from '../design-system/Dialog.js' +import { Byline, Dialog } from '@anthropic/ink' import { DiffDetailView } from './DiffDetailView.js' import { DiffFileList } from './DiffFileList.js' diff --git a/src/components/diff/DiffFileList.tsx b/src/components/diff/DiffFileList.tsx index 76c3040aa..ef498925b 100644 --- a/src/components/diff/DiffFileList.tsx +++ b/src/components/diff/DiffFileList.tsx @@ -2,7 +2,7 @@ import figures from 'figures' import React, { useMemo } from 'react' import type { DiffFile } from '../../hooks/useDiffData.js' import { useTerminalSize } from '../../hooks/useTerminalSize.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { truncateStartToWidth } from '../../utils/format.js' import { plural } from '../../utils/stringUtils.js' diff --git a/src/components/grove/Grove.tsx b/src/components/grove/Grove.tsx index 0998fcf05..938971b99 100644 --- a/src/components/grove/Grove.tsx +++ b/src/components/grove/Grove.tsx @@ -3,7 +3,7 @@ import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, } from 'src/services/analytics/index.js' -import { Box, Link, Text, useInput } from '../../ink.js' +import { Box, Link, Text, useInput } from '@anthropic/ink' import { type AccountSettings, calculateShouldShowGrove, @@ -14,9 +14,7 @@ import { updateGroveSettings, } from '../../services/api/grove.js' import { Select } from '../CustomSelect/index.js' -import { Byline } from '../design-system/Byline.js' -import { Dialog } from '../design-system/Dialog.js' -import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js' +import { Byline, Dialog, KeyboardShortcutHint } from '@anthropic/ink' export type GroveDecision = | 'accept_opt_in' diff --git a/src/components/hooks/HooksConfigMenu.tsx b/src/components/hooks/HooksConfigMenu.tsx index 425af2798..f59ced979 100644 --- a/src/components/hooks/HooksConfigMenu.tsx +++ b/src/components/hooks/HooksConfigMenu.tsx @@ -16,7 +16,7 @@ import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js' import { useAppState, useAppStateStore } from 'src/state/AppState.js' import type { CommandResultDisplay } from '../../commands.js' import { useSettingsChange } from '../../hooks/useSettingsChange.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { useKeybinding } from '../../keybindings/useKeybinding.js' import { getHookEventMetadata, @@ -31,7 +31,7 @@ import { getSettingsForSource, } from '../../utils/settings/settings.js' import { plural } from '../../utils/stringUtils.js' -import { Dialog } from '../design-system/Dialog.js' +import { Dialog } from '@anthropic/ink' import { SelectEventMode } from './SelectEventMode.js' import { SelectHookMode } from './SelectHookMode.js' import { SelectMatcherMode } from './SelectMatcherMode.js' diff --git a/src/components/hooks/PromptDialog.tsx b/src/components/hooks/PromptDialog.tsx index f5566ec46..2bca1ceb4 100644 --- a/src/components/hooks/PromptDialog.tsx +++ b/src/components/hooks/PromptDialog.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { useKeybinding } from '../../keybindings/useKeybinding.js' import type { PromptRequest } from '../../types/hooks.js' import { Select } from '../CustomSelect/select.js' diff --git a/src/components/hooks/SelectEventMode.tsx b/src/components/hooks/SelectEventMode.tsx index a18d01952..c232f745a 100644 --- a/src/components/hooks/SelectEventMode.tsx +++ b/src/components/hooks/SelectEventMode.tsx @@ -11,10 +11,10 @@ import figures from 'figures' import * as React from 'react' import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js' import type { HookEventMetadata } from 'src/utils/hooks/hooksConfigManager.js' -import { Box, Link, Text } from '../../ink.js' +import { Box, Link, Text } from '@anthropic/ink' import { plural } from '../../utils/stringUtils.js' import { Select } from '../CustomSelect/select.js' -import { Dialog } from '../design-system/Dialog.js' +import { Dialog } from '@anthropic/ink' type Props = { hookEventMetadata: Record diff --git a/src/components/hooks/SelectHookMode.tsx b/src/components/hooks/SelectHookMode.tsx index 5764ae9d5..b27195b14 100644 --- a/src/components/hooks/SelectHookMode.tsx +++ b/src/components/hooks/SelectHookMode.tsx @@ -8,14 +8,14 @@ import * as React from 'react' import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js' import type { HookEventMetadata } from 'src/utils/hooks/hooksConfigManager.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { getHookDisplayText, hookSourceHeaderDisplayString, type IndividualHookConfig, } from '../../utils/hooks/hooksSettings.js' import { Select } from '../CustomSelect/select.js' -import { Dialog } from '../design-system/Dialog.js' +import { Dialog } from '@anthropic/ink' type Props = { selectedEvent: HookEvent diff --git a/src/components/hooks/SelectMatcherMode.tsx b/src/components/hooks/SelectMatcherMode.tsx index 6792a47b1..c82987f02 100644 --- a/src/components/hooks/SelectMatcherMode.tsx +++ b/src/components/hooks/SelectMatcherMode.tsx @@ -6,7 +6,7 @@ */ import * as React from 'react' import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { type HookSource, hookSourceInlineDisplayString, @@ -14,7 +14,7 @@ import { } from '../../utils/hooks/hooksSettings.js' import { plural } from '../../utils/stringUtils.js' import { Select } from '../CustomSelect/select.js' -import { Dialog } from '../design-system/Dialog.js' +import { Dialog } from '@anthropic/ink' type MatcherWithSource = { matcher: string diff --git a/src/components/hooks/ViewHookMode.tsx b/src/components/hooks/ViewHookMode.tsx index 5766ead25..b79c09cca 100644 --- a/src/components/hooks/ViewHookMode.tsx +++ b/src/components/hooks/ViewHookMode.tsx @@ -5,12 +5,12 @@ * confirmation screen and directs users to settings.json or Claude for edits. */ import * as React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { hookSourceDescriptionDisplayString, type IndividualHookConfig, } from '../../utils/hooks/hooksSettings.js' -import { Dialog } from '../design-system/Dialog.js' +import { Dialog } from '@anthropic/ink' type Props = { selectedHook: IndividualHookConfig diff --git a/src/components/mcp/CapabilitiesSection.tsx b/src/components/mcp/CapabilitiesSection.tsx index a5f98466a..dd0033382 100644 --- a/src/components/mcp/CapabilitiesSection.tsx +++ b/src/components/mcp/CapabilitiesSection.tsx @@ -1,6 +1,6 @@ import React from 'react' -import { Box, Text } from '../../ink.js' -import { Byline } from '../design-system/Byline.js' +import { Box, Text } from '@anthropic/ink' +import { Byline } from '@anthropic/ink' type Props = { serverToolsCount: number diff --git a/src/components/mcp/ElicitationDialog.tsx b/src/components/mcp/ElicitationDialog.tsx index dbf8f22f9..c93003106 100644 --- a/src/components/mcp/ElicitationDialog.tsx +++ b/src/components/mcp/ElicitationDialog.tsx @@ -10,7 +10,7 @@ import { useRegisterOverlay } from '../../context/overlayContext.js' import { useNotifyAfterTimeout } from '../../hooks/useNotifyAfterTimeout.js' import { useTerminalSize } from '../../hooks/useTerminalSize.js' // eslint-disable-next-line custom-rules/prefer-use-keybindings -- raw text input for elicitation form -import { Box, Text, useInput } from '../../ink.js' +import { Box, Text, useInput } from '@anthropic/ink' import { useKeybinding } from '../../keybindings/useKeybinding.js' import type { ElicitationRequestEvent } from '../../services/mcp/elicitationHandler.js' import { openBrowser } from '../../utils/browser.js' @@ -27,9 +27,7 @@ import { } from '../../utils/mcp/elicitationValidation.js' import { plural } from '../../utils/stringUtils.js' import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js' -import { Byline } from '../design-system/Byline.js' -import { Dialog } from '../design-system/Dialog.js' -import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js' +import { Byline, Dialog, KeyboardShortcutHint } from '@anthropic/ink' import TextInput from '../TextInput.js' type Props = { diff --git a/src/components/mcp/MCPAgentServerMenu.tsx b/src/components/mcp/MCPAgentServerMenu.tsx index b02f79dac..913dee9bb 100644 --- a/src/components/mcp/MCPAgentServerMenu.tsx +++ b/src/components/mcp/MCPAgentServerMenu.tsx @@ -1,7 +1,7 @@ import figures from 'figures' import React, { useCallback, useEffect, useRef, useState } from 'react' import type { CommandResultDisplay } from '../../commands.js' -import { Box, color, Link, Text, useTheme } from '../../ink.js' +import { Box, color, Link, Text, useTheme } from '@anthropic/ink' import { useKeybinding } from '../../keybindings/useKeybinding.js' import { AuthenticationCancelledError, @@ -10,9 +10,7 @@ import { import { capitalize } from '../../utils/stringUtils.js' import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js' import { Select } from '../CustomSelect/index.js' -import { Byline } from '../design-system/Byline.js' -import { Dialog } from '../design-system/Dialog.js' -import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js' +import { Byline, Dialog, KeyboardShortcutHint } from '@anthropic/ink' import { Spinner } from '../Spinner.js' import type { AgentMcpServerInfo } from './types.js' diff --git a/src/components/mcp/MCPListPanel.tsx b/src/components/mcp/MCPListPanel.tsx index af93c5538..365599284 100644 --- a/src/components/mcp/MCPListPanel.tsx +++ b/src/components/mcp/MCPListPanel.tsx @@ -1,16 +1,14 @@ import figures from 'figures' import React, { useCallback, useState } from 'react' import type { CommandResultDisplay } from '../../commands.js' -import { Box, color, Link, Text, useTheme } from '../../ink.js' +import { Box, color, Link, Text, useTheme } from '@anthropic/ink' import { useKeybindings } from '../../keybindings/useKeybinding.js' import type { ConfigScope } from '../../services/mcp/types.js' import { describeMcpConfigFilePath } from '../../services/mcp/utils.js' import { isDebugMode } from '../../utils/debug.js' import { plural } from '../../utils/stringUtils.js' import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js' -import { Byline } from '../design-system/Byline.js' -import { Dialog } from '../design-system/Dialog.js' -import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js' +import { Byline, Dialog, KeyboardShortcutHint } from '@anthropic/ink' import { McpParsingWarnings } from './McpParsingWarnings.js' import type { AgentMcpServerInfo, ServerInfo } from './types.js' diff --git a/src/components/mcp/MCPReconnect.tsx b/src/components/mcp/MCPReconnect.tsx index 209c80541..496edaf2f 100644 --- a/src/components/mcp/MCPReconnect.tsx +++ b/src/components/mcp/MCPReconnect.tsx @@ -1,7 +1,7 @@ import figures from 'figures' import React, { useEffect, useState } from 'react' import type { CommandResultDisplay } from '../../commands.js' -import { Box, color, Text, useTheme } from '../../ink.js' +import { Box, color, Text, useTheme } from '@anthropic/ink' import { useMcpReconnect } from '../../services/mcp/MCPConnectionManager.js' import { useAppStateStore } from '../../state/AppState.js' import { Spinner } from '../Spinner.js' diff --git a/src/components/mcp/MCPRemoteServerMenu.tsx b/src/components/mcp/MCPRemoteServerMenu.tsx index 0a8efb76d..41665860e 100644 --- a/src/components/mcp/MCPRemoteServerMenu.tsx +++ b/src/components/mcp/MCPRemoteServerMenu.tsx @@ -8,9 +8,9 @@ import type { CommandResultDisplay } from '../../commands.js' import { getOauthConfig } from '../../constants/oauth.js' import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js' import { useTerminalSize } from '../../hooks/useTerminalSize.js' -import { setClipboard } from '../../ink/termio/osc.js' +import { setClipboard } from '@anthropic/ink' // eslint-disable-next-line custom-rules/prefer-use-keybindings -- raw j/k/arrow menu navigation -import { Box, color, Link, Text, useInput, useTheme } from '../../ink.js' +import { Box, color, Link, Text, useInput, useTheme } from '@anthropic/ink' import { useKeybinding } from '../../keybindings/useKeybinding.js' import { AuthenticationCancelledError, @@ -37,8 +37,7 @@ import { logMCPDebug } from '../../utils/log.js' import { capitalize } from '../../utils/stringUtils.js' import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js' import { Select } from '../CustomSelect/index.js' -import { Byline } from '../design-system/Byline.js' -import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js' +import { Byline, KeyboardShortcutHint } from '@anthropic/ink' import { Spinner } from '../Spinner.js' import TextInput from '../TextInput.js' import { CapabilitiesSection } from './CapabilitiesSection.js' diff --git a/src/components/mcp/MCPStdioServerMenu.tsx b/src/components/mcp/MCPStdioServerMenu.tsx index 7caba350e..4132cdfe7 100644 --- a/src/components/mcp/MCPStdioServerMenu.tsx +++ b/src/components/mcp/MCPStdioServerMenu.tsx @@ -2,7 +2,7 @@ import figures from 'figures' import React, { useState } from 'react' import type { CommandResultDisplay } from '../../commands.js' import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js' -import { Box, color, Text, useTheme } from '../../ink.js' +import { Box, color, Text, useTheme } from '@anthropic/ink' import { getMcpConfigByName } from '../../services/mcp/config.js' import { useMcpReconnect, @@ -17,8 +17,7 @@ import { errorMessage } from '../../utils/errors.js' import { capitalize } from '../../utils/stringUtils.js' import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js' import { Select } from '../CustomSelect/index.js' -import { Byline } from '../design-system/Byline.js' -import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js' +import { Byline, KeyboardShortcutHint } from '@anthropic/ink' import { Spinner } from '../Spinner.js' import { CapabilitiesSection } from './CapabilitiesSection.js' import type { StdioServerInfo } from './types.js' diff --git a/src/components/mcp/MCPToolDetailView.tsx b/src/components/mcp/MCPToolDetailView.tsx index b1ccb4d73..6b0b60173 100644 --- a/src/components/mcp/MCPToolDetailView.tsx +++ b/src/components/mcp/MCPToolDetailView.tsx @@ -1,12 +1,12 @@ import React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { extractMcpToolDisplayName, getMcpDisplayName, } from '../../services/mcp/mcpStringUtils.js' import type { Tool } from '../../Tool.js' import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js' -import { Dialog } from '../design-system/Dialog.js' +import { Dialog } from '@anthropic/ink' import type { ServerInfo } from './types.js' type Props = { diff --git a/src/components/mcp/MCPToolListView.tsx b/src/components/mcp/MCPToolListView.tsx index 923791187..ad3eb3695 100644 --- a/src/components/mcp/MCPToolListView.tsx +++ b/src/components/mcp/MCPToolListView.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Text } from '../../ink.js' +import { Text } from '@anthropic/ink' import { extractMcpToolDisplayName, getMcpDisplayName, @@ -10,9 +10,7 @@ import type { Tool } from '../../Tool.js' import { plural } from '../../utils/stringUtils.js' import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js' import { Select } from '../CustomSelect/index.js' -import { Byline } from '../design-system/Byline.js' -import { Dialog } from '../design-system/Dialog.js' -import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js' +import { Byline, Dialog, KeyboardShortcutHint } from '@anthropic/ink' import type { ServerInfo } from './types.js' type Props = { diff --git a/src/components/mcp/McpParsingWarnings.tsx b/src/components/mcp/McpParsingWarnings.tsx index 49e8353b9..c50946a91 100644 --- a/src/components/mcp/McpParsingWarnings.tsx +++ b/src/components/mcp/McpParsingWarnings.tsx @@ -6,7 +6,7 @@ import { getScopeLabel, } from 'src/services/mcp/utils.js' import type { ValidationError } from 'src/utils/settings/validation.js' -import { Box, Link, Text } from '../../ink.js' +import { Box, Link, Text } from '@anthropic/ink' function McpConfigErrorSection({ scope, diff --git a/src/components/memory/MemoryFileSelector.tsx b/src/components/memory/MemoryFileSelector.tsx index 2e2bdf623..fcd5834c6 100644 --- a/src/components/memory/MemoryFileSelector.tsx +++ b/src/components/memory/MemoryFileSelector.tsx @@ -6,7 +6,7 @@ import * as React from 'react' import { use, useEffect, useState } from 'react' import { getOriginalCwd } from '../../bootstrap/state.js' import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js' -import { Box, Text } from '../../ink.js' +import { Box, Text, ListItem } from '@anthropic/ink' import { useKeybinding } from '../../keybindings/useKeybinding.js' import { getAutoMemPath, isAutoMemoryEnabled } from '../../memdir/paths.js' import { logEvent } from '../../services/analytics/index.js' @@ -22,7 +22,6 @@ import { formatRelativeTimeAgo } from '../../utils/format.js' import { projectIsInGitRepo } from '../../utils/memory/versions.js' import { updateSettingsForSource } from '../../utils/settings/settings.js' import { Select } from '../CustomSelect/index.js' -import { ListItem } from '../design-system/ListItem.js' /* eslint-disable @typescript-eslint/no-require-imports */ const teamMemPaths = feature('TEAMMEM') diff --git a/src/components/memory/MemoryUpdateNotification.tsx b/src/components/memory/MemoryUpdateNotification.tsx index 890a567b0..5e2db19b7 100644 --- a/src/components/memory/MemoryUpdateNotification.tsx +++ b/src/components/memory/MemoryUpdateNotification.tsx @@ -1,7 +1,7 @@ import { homedir } from 'os' import { relative } from 'path' import React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { getCwd } from '../../utils/cwd.js' export function getRelativeMemoryPath(path: string): string { diff --git a/src/components/messageActions.tsx b/src/components/messageActions.tsx index 3e368c306..2ecc0ab5a 100644 --- a/src/components/messageActions.tsx +++ b/src/components/messageActions.tsx @@ -1,7 +1,7 @@ import figures from 'figures' import type { RefObject } from 'react' import React, { useCallback, useMemo, useRef } from 'react' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import { useKeybindings } from '../keybindings/useKeybinding.js' import { logEvent } from '../services/analytics/index.js' import type { diff --git a/src/components/messages/AdvisorMessage.tsx b/src/components/messages/AdvisorMessage.tsx index 4a77fe7ca..890e52405 100644 --- a/src/components/messages/AdvisorMessage.tsx +++ b/src/components/messages/AdvisorMessage.tsx @@ -1,6 +1,6 @@ import figures from 'figures' import React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import type { AdvisorBlock } from '../../utils/advisor.js' import { renderModelName } from '../../utils/model/model.js' import { jsonStringify } from '../../utils/slowOperations.js' diff --git a/src/components/messages/AssistantRedactedThinkingMessage.tsx b/src/components/messages/AssistantRedactedThinkingMessage.tsx index eb0f66d35..4d825701d 100644 --- a/src/components/messages/AssistantRedactedThinkingMessage.tsx +++ b/src/components/messages/AssistantRedactedThinkingMessage.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' type Props = { addMargin: boolean diff --git a/src/components/messages/AssistantTextMessage.tsx b/src/components/messages/AssistantTextMessage.tsx index 005d2481e..56ffa054e 100644 --- a/src/components/messages/AssistantTextMessage.tsx +++ b/src/components/messages/AssistantTextMessage.tsx @@ -3,7 +3,7 @@ import React, { useContext } from 'react' import { ERROR_MESSAGE_USER_ABORT } from 'src/services/compact/compact.js' import { isRateLimitErrorMessage } from 'src/services/rateLimitMessages.js' import { BLACK_CIRCLE } from '../../constants/figures.js' -import { Box, NoSelect, Text } from '../../ink.js' +import { Box, NoSelect, Text } from '@anthropic/ink' import { API_ERROR_MESSAGE_PREFIX, API_TIMEOUT_ERROR_MESSAGE, diff --git a/src/components/messages/AssistantThinkingMessage.tsx b/src/components/messages/AssistantThinkingMessage.tsx index 2fc88512d..979f75637 100644 --- a/src/components/messages/AssistantThinkingMessage.tsx +++ b/src/components/messages/AssistantThinkingMessage.tsx @@ -3,7 +3,7 @@ import type { ThinkingBlockParam, } from '@anthropic-ai/sdk/resources/index.mjs' import React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { CtrlOToExpand } from '../CtrlOToExpand.js' import { Markdown } from '../Markdown.js' diff --git a/src/components/messages/AssistantToolUseMessage.tsx b/src/components/messages/AssistantToolUseMessage.tsx index 65a92aad6..8f2dd965b 100644 --- a/src/components/messages/AssistantToolUseMessage.tsx +++ b/src/components/messages/AssistantToolUseMessage.tsx @@ -4,8 +4,7 @@ import { useTerminalSize } from 'src/hooks/useTerminalSize.js' import type { ThemeName } from 'src/utils/theme.js' import type { Command } from '../../commands.js' import { BLACK_CIRCLE } from '../../constants/figures.js' -import { stringWidth } from '../../ink/stringWidth.js' -import { Box, Text, useTheme } from '../../ink.js' +import { Box, Text, stringWidth, useTheme } from '@anthropic/ink' import { useAppStateMaybeOutsideOfProvider } from '../../state/AppState.js' import { findToolByName, diff --git a/src/components/messages/AttachmentMessage.tsx b/src/components/messages/AttachmentMessage.tsx index 51f9ea67d..77363371d 100644 --- a/src/components/messages/AttachmentMessage.tsx +++ b/src/components/messages/AttachmentMessage.tsx @@ -1,6 +1,8 @@ // biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered import React, { useMemo } from 'react' -import { Ansi, Box, Text } from '../../ink.js' +import { Ansi, Box, Text } from '@anthropic/ink' +import { FilePathLink } from '../FilePathLink.js' +import { toInkColor } from '../../utils/ink.js' import type { Attachment } from 'src/utils/attachments.js' import type { NullRenderingAttachmentType } from './nullRenderingAttachments.js' import { useAppState } from '../../state/AppState.js' @@ -13,7 +15,7 @@ import { DiagnosticsDisplay } from '../DiagnosticsDisplay.js' import { getContentText } from 'src/utils/messages.js' import type { Theme } from 'src/utils/theme.js' import { UserImageMessage } from './UserImageMessage.js' -import { toInkColor } from '../../utils/ink.js' + import { jsonParse } from '../../utils/slowOperations.js' import { plural } from '../../utils/stringUtils.js' import { isEnvTruthy } from '../../utils/envUtils.js' @@ -26,7 +28,7 @@ import { BLACK_CIRCLE } from '../../constants/figures.js' import { TeammateMessageContent } from './UserTeammateMessage.js' import { isShutdownApproved } from '../../utils/teammateMailbox.js' import { CtrlOToExpand } from '../CtrlOToExpand.js' -import { FilePathLink } from '../FilePathLink.js' + import { feature } from 'bun:bundle' import { useSelectedMessageBg } from '../messageActions.js' diff --git a/src/components/messages/CollapsedReadSearchContent.tsx b/src/components/messages/CollapsedReadSearchContent.tsx index d8df34f69..5a0f1eb04 100644 --- a/src/components/messages/CollapsedReadSearchContent.tsx +++ b/src/components/messages/CollapsedReadSearchContent.tsx @@ -2,7 +2,7 @@ import { feature } from 'bun:bundle' import { basename } from 'path' import React, { useRef } from 'react' import { useMinDisplayTime } from '../../hooks/useMinDisplayTime.js' -import { Ansi, Box, Text, useTheme } from '../../ink.js' +import { Ansi, Box, Text, useTheme } from '@anthropic/ink' import { findToolByName, type Tools } from '../../Tool.js' import { getReplPrimitiveTools } from '../../tools/REPLTool/primitiveTools.js' import type { diff --git a/src/components/messages/CompactBoundaryMessage.tsx b/src/components/messages/CompactBoundaryMessage.tsx index 7c4e87af1..54033b23e 100644 --- a/src/components/messages/CompactBoundaryMessage.tsx +++ b/src/components/messages/CompactBoundaryMessage.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js' export function CompactBoundaryMessage(): React.ReactNode { diff --git a/src/components/messages/HighlightedThinkingText.tsx b/src/components/messages/HighlightedThinkingText.tsx index 1b4fd0c3c..14c61ff09 100644 --- a/src/components/messages/HighlightedThinkingText.tsx +++ b/src/components/messages/HighlightedThinkingText.tsx @@ -2,7 +2,7 @@ import figures from 'figures' import * as React from 'react' import { useContext } from 'react' import { useQueuedMessage } from '../../context/QueuedMessageContext.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { formatBriefTimestamp } from '../../utils/formatBriefTimestamp.js' import { findThinkingTriggerPositions, diff --git a/src/components/messages/HookProgressMessage.tsx b/src/components/messages/HookProgressMessage.tsx index 61bfddf96..68b1bd2d1 100644 --- a/src/components/messages/HookProgressMessage.tsx +++ b/src/components/messages/HookProgressMessage.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js' import type { buildMessageLookups } from 'src/utils/messages.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { MessageResponse } from '../MessageResponse.js' type Props = { diff --git a/src/components/messages/PlanApprovalMessage.tsx b/src/components/messages/PlanApprovalMessage.tsx index a7fbced71..bb0a582dd 100644 --- a/src/components/messages/PlanApprovalMessage.tsx +++ b/src/components/messages/PlanApprovalMessage.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { Markdown } from '../../components/Markdown.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { jsonParse } from '../../utils/slowOperations.js' import { type IdleNotificationMessage, diff --git a/src/components/messages/RateLimitMessage.tsx b/src/components/messages/RateLimitMessage.tsx index c9a42815b..8ec18da7e 100644 --- a/src/components/messages/RateLimitMessage.tsx +++ b/src/components/messages/RateLimitMessage.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useMemo, useState } from 'react' import { extraUsage } from 'src/commands/extra-usage/index.js' -import { Box, Text } from 'src/ink.js' +import { Box, Text } from '@anthropic/ink' import { useClaudeAiLimits } from 'src/services/claudeAiLimitsHook.js' import { shouldProcessMockLimits } from 'src/services/rateLimitMocking.js' // Used for /mock-limits command import { diff --git a/src/components/messages/ShutdownMessage.tsx b/src/components/messages/ShutdownMessage.tsx index 82e0d59e1..6f0dcbd97 100644 --- a/src/components/messages/ShutdownMessage.tsx +++ b/src/components/messages/ShutdownMessage.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { isShutdownApproved, isShutdownRejected, diff --git a/src/components/messages/SystemAPIErrorMessage.tsx b/src/components/messages/SystemAPIErrorMessage.tsx index c87dec717..30c5a2495 100644 --- a/src/components/messages/SystemAPIErrorMessage.tsx +++ b/src/components/messages/SystemAPIErrorMessage.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useState } from 'react' -import { Box, Text } from 'src/ink.js' +import { Box, Text } from '@anthropic/ink' import { formatAPIError } from 'src/services/api/errorUtils.js' import type { SystemAPIErrorMessage } from 'src/types/message.js' import { useInterval } from 'usehooks-ts' diff --git a/src/components/messages/SystemTextMessage.tsx b/src/components/messages/SystemTextMessage.tsx index 7d05f054a..a592c79de 100644 --- a/src/components/messages/SystemTextMessage.tsx +++ b/src/components/messages/SystemTextMessage.tsx @@ -1,5 +1,6 @@ // biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered -import { Box, Text, type TextProps } from '../../ink.js' +import { Box, Link, Text, type TextProps } from '@anthropic/ink' +import { FilePathLink } from '../FilePathLink.js' import { feature } from 'bun:bundle' import * as React from 'react' import { useState } from 'react' @@ -12,7 +13,7 @@ import { import figures from 'figures' import { basename } from 'path' import { MessageResponse } from '../MessageResponse.js' -import { FilePathLink } from '../FilePathLink.js' + import { openPath } from '../../utils/browser.js' /* eslint-disable @typescript-eslint/no-require-imports */ const teamMemSaved = feature('TEAMMEM') @@ -36,7 +37,6 @@ import { formatSecondsShort, } from '../../utils/format.js' import { getGlobalConfig } from '../../utils/config.js' -import Link from '../../ink/components/Link.js' import ThemedText from '../design-system/ThemedText.js' import { CtrlOToExpand } from '../CtrlOToExpand.js' import { useAppStateStore } from '../../state/AppState.js' @@ -108,7 +108,6 @@ export function SystemTextMessage({ return null } - if (message.subtype === 'bridge_status') { return } diff --git a/src/components/messages/TaskAssignmentMessage.tsx b/src/components/messages/TaskAssignmentMessage.tsx index 1f7797873..146068a4f 100644 --- a/src/components/messages/TaskAssignmentMessage.tsx +++ b/src/components/messages/TaskAssignmentMessage.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { isTaskAssignment, type TaskAssignmentMessage, diff --git a/src/components/messages/UserAgentNotificationMessage.tsx b/src/components/messages/UserAgentNotificationMessage.tsx index 7e19c34d7..8bf906bea 100644 --- a/src/components/messages/UserAgentNotificationMessage.tsx +++ b/src/components/messages/UserAgentNotificationMessage.tsx @@ -1,7 +1,7 @@ import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs' import * as React from 'react' import { BLACK_CIRCLE } from '../../constants/figures.js' -import { Box, Text, type TextProps } from '../../ink.js' +import { Box, Text, type TextProps } from '@anthropic/ink' import { extractTag } from '../../utils/messages.js' type Props = { diff --git a/src/components/messages/UserBashInputMessage.tsx b/src/components/messages/UserBashInputMessage.tsx index c78fafea1..42e40df75 100644 --- a/src/components/messages/UserBashInputMessage.tsx +++ b/src/components/messages/UserBashInputMessage.tsx @@ -1,6 +1,6 @@ import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs' import * as React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { extractTag } from '../../utils/messages.js' type Props = { diff --git a/src/components/messages/UserChannelMessage.tsx b/src/components/messages/UserChannelMessage.tsx index 8e7101b1a..a37f05c8c 100644 --- a/src/components/messages/UserChannelMessage.tsx +++ b/src/components/messages/UserChannelMessage.tsx @@ -2,7 +2,7 @@ import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs' import * as React from 'react' import { CHANNEL_ARROW } from '../../constants/figures.js' import { CHANNEL_TAG } from '../../constants/xml.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { truncateToWidth } from '../../utils/format.js' type Props = { diff --git a/src/components/messages/UserCommandMessage.tsx b/src/components/messages/UserCommandMessage.tsx index 31f6b2871..1c9502c32 100644 --- a/src/components/messages/UserCommandMessage.tsx +++ b/src/components/messages/UserCommandMessage.tsx @@ -2,7 +2,7 @@ import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs' import figures from 'figures' import * as React from 'react' import { COMMAND_MESSAGE_TAG } from '../../constants/xml.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { extractTag } from '../../utils/messages.js' type Props = { diff --git a/src/components/messages/UserImageMessage.tsx b/src/components/messages/UserImageMessage.tsx index 3f542dfb6..ca960d59c 100644 --- a/src/components/messages/UserImageMessage.tsx +++ b/src/components/messages/UserImageMessage.tsx @@ -1,8 +1,6 @@ import * as React from 'react' import { pathToFileURL } from 'url' -import Link from '../../ink/components/Link.js' -import { supportsHyperlinks } from '../../ink/supports-hyperlinks.js' -import { Box, Text } from '../../ink.js' +import { Box, Link, supportsHyperlinks, Text } from '@anthropic/ink' import { getStoredImagePath } from '../../utils/imageStore.js' import { MessageResponse } from '../MessageResponse.js' diff --git a/src/components/messages/UserLocalCommandOutputMessage.tsx b/src/components/messages/UserLocalCommandOutputMessage.tsx index b1c95616a..3467686b4 100644 --- a/src/components/messages/UserLocalCommandOutputMessage.tsx +++ b/src/components/messages/UserLocalCommandOutputMessage.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js' import { NO_CONTENT_MESSAGE } from '../../constants/messages.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { extractTag } from '../../utils/messages.js' import { Markdown } from '../Markdown.js' import { MessageResponse } from '../MessageResponse.js' diff --git a/src/components/messages/UserMemoryInputMessage.tsx b/src/components/messages/UserMemoryInputMessage.tsx index 25a8d7a1c..cbdfdcd5d 100644 --- a/src/components/messages/UserMemoryInputMessage.tsx +++ b/src/components/messages/UserMemoryInputMessage.tsx @@ -1,7 +1,7 @@ import sample from 'lodash-es/sample.js' import * as React from 'react' import { useMemo } from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { extractTag } from '../../utils/messages.js' import { MessageResponse } from '../MessageResponse.js' diff --git a/src/components/messages/UserPlanMessage.tsx b/src/components/messages/UserPlanMessage.tsx index 5ef8fa89a..30f2b369b 100644 --- a/src/components/messages/UserPlanMessage.tsx +++ b/src/components/messages/UserPlanMessage.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { Markdown } from '../Markdown.js' type Props = { diff --git a/src/components/messages/UserPromptMessage.tsx b/src/components/messages/UserPromptMessage.tsx index 090cac272..00f201368 100644 --- a/src/components/messages/UserPromptMessage.tsx +++ b/src/components/messages/UserPromptMessage.tsx @@ -2,7 +2,7 @@ import { feature } from 'bun:bundle' import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs' import React, { useContext, useMemo } from 'react' import { getKairosActive, getUserMsgOptIn } from '../../bootstrap/state.js' -import { Box } from '../../ink.js' +import { Box } from '@anthropic/ink' import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' import { useAppState } from '../../state/AppState.js' import { isEnvTruthy } from '../../utils/envUtils.js' diff --git a/src/components/messages/UserResourceUpdateMessage.tsx b/src/components/messages/UserResourceUpdateMessage.tsx index ce1f4f5d5..ec0f5a2ac 100644 --- a/src/components/messages/UserResourceUpdateMessage.tsx +++ b/src/components/messages/UserResourceUpdateMessage.tsx @@ -1,7 +1,7 @@ import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs' import * as React from 'react' import { REFRESH_ARROW } from '../../constants/figures.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' type Props = { addMargin: boolean diff --git a/src/components/messages/UserTeammateMessage.tsx b/src/components/messages/UserTeammateMessage.tsx index 4c174ff1c..e75256abc 100644 --- a/src/components/messages/UserTeammateMessage.tsx +++ b/src/components/messages/UserTeammateMessage.tsx @@ -2,8 +2,9 @@ import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs' import figures from 'figures' import * as React from 'react' import { TEAMMATE_MESSAGE_TAG } from '../../constants/xml.js' -import { Ansi, Box, Text, type TextProps } from '../../ink.js' +import { Ansi, Box, Text, type TextProps } from '@anthropic/ink' import { toInkColor } from '../../utils/ink.js' + import { jsonParse } from '../../utils/slowOperations.js' import { isShutdownApproved } from '../../utils/teammateMailbox.js' import { MessageResponse } from '../MessageResponse.js' diff --git a/src/components/messages/UserToolResultMessage/RejectedPlanMessage.tsx b/src/components/messages/UserToolResultMessage/RejectedPlanMessage.tsx index bee8d5c3a..ec2130230 100644 --- a/src/components/messages/UserToolResultMessage/RejectedPlanMessage.tsx +++ b/src/components/messages/UserToolResultMessage/RejectedPlanMessage.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { Markdown } from 'src/components/Markdown.js' import { MessageResponse } from 'src/components/MessageResponse.js' -import { Box, Text } from '../../../ink.js' +import { Box, Text } from '@anthropic/ink' type Props = { plan: string diff --git a/src/components/messages/UserToolResultMessage/RejectedToolUseMessage.tsx b/src/components/messages/UserToolResultMessage/RejectedToolUseMessage.tsx index b387b0fea..5764a4e17 100644 --- a/src/components/messages/UserToolResultMessage/RejectedToolUseMessage.tsx +++ b/src/components/messages/UserToolResultMessage/RejectedToolUseMessage.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Text } from '../../../ink.js' +import { Text } from '@anthropic/ink' import { MessageResponse } from '../../MessageResponse.js' export function RejectedToolUseMessage(): React.ReactNode { diff --git a/src/components/messages/UserToolResultMessage/UserToolErrorMessage.tsx b/src/components/messages/UserToolResultMessage/UserToolErrorMessage.tsx index 33249d591..a178e8e34 100644 --- a/src/components/messages/UserToolResultMessage/UserToolErrorMessage.tsx +++ b/src/components/messages/UserToolResultMessage/UserToolErrorMessage.tsx @@ -2,7 +2,7 @@ import { feature } from 'bun:bundle' import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs' import * as React from 'react' import { BULLET_OPERATOR } from '../../../constants/figures.js' -import { Text } from '../../../ink.js' +import { Text } from '@anthropic/ink' import { filterToolProgressMessages, type Tool, diff --git a/src/components/messages/UserToolResultMessage/UserToolRejectMessage.tsx b/src/components/messages/UserToolResultMessage/UserToolRejectMessage.tsx index c1a37ef4e..c53d81e6d 100644 --- a/src/components/messages/UserToolResultMessage/UserToolRejectMessage.tsx +++ b/src/components/messages/UserToolResultMessage/UserToolRejectMessage.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useTerminalSize } from '../../../hooks/useTerminalSize.js' -import { useTheme } from '../../../ink.js' +import { useTheme } from '@anthropic/ink' import { filterToolProgressMessages, type Tool, diff --git a/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx b/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx index 931e0df3e..ffb8e23d5 100644 --- a/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx +++ b/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx @@ -2,7 +2,7 @@ import { feature } from 'bun:bundle' import figures from 'figures' import * as React from 'react' import { SentryErrorBoundary } from 'src/components/SentryErrorBoundary.js' -import { Box, Text, useTheme } from '../../../ink.js' +import { Box, Text, useTheme } from '@anthropic/ink' import { useAppState } from '../../../state/AppState.js' import { filterToolProgressMessages, diff --git a/src/components/messages/teamMemCollapsed.tsx b/src/components/messages/teamMemCollapsed.tsx index 63fcdaf0e..eaa9219ab 100644 --- a/src/components/messages/teamMemCollapsed.tsx +++ b/src/components/messages/teamMemCollapsed.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Text } from '../../ink.js' +import { Text } from '@anthropic/ink' import type { CollapsedReadSearchGroup } from '../../types/message.js' /** diff --git a/src/components/permissions/AskUserQuestionPermissionRequest/AskUserQuestionPermissionRequest.tsx b/src/components/permissions/AskUserQuestionPermissionRequest/AskUserQuestionPermissionRequest.tsx index 3768dbd73..562c6653c 100644 --- a/src/components/permissions/AskUserQuestionPermissionRequest/AskUserQuestionPermissionRequest.tsx +++ b/src/components/permissions/AskUserQuestionPermissionRequest/AskUserQuestionPermissionRequest.tsx @@ -12,8 +12,7 @@ import React, { } from 'react' import { useSettings } from '../../../hooks/useSettings.js' import { useTerminalSize } from '../../../hooks/useTerminalSize.js' -import { stringWidth } from '../../../ink/stringWidth.js' -import { useTheme } from '../../../ink.js' +import { stringWidth, useTheme } from '@anthropic/ink' import { useKeybindings } from '../../../keybindings/useKeybinding.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, diff --git a/src/components/permissions/AskUserQuestionPermissionRequest/PreviewBox.tsx b/src/components/permissions/AskUserQuestionPermissionRequest/PreviewBox.tsx index 7b4fd6149..686f2f4c9 100644 --- a/src/components/permissions/AskUserQuestionPermissionRequest/PreviewBox.tsx +++ b/src/components/permissions/AskUserQuestionPermissionRequest/PreviewBox.tsx @@ -1,8 +1,7 @@ import React, { Suspense, use, useMemo } from 'react' import { useSettings } from '../../../hooks/useSettings.js' import { useTerminalSize } from '../../../hooks/useTerminalSize.js' -import { stringWidth } from '../../../ink/stringWidth.js' -import { Ansi, Box, Text, useTheme } from '../../../ink.js' +import { Ansi, Box, Text, stringWidth, useTheme } from '@anthropic/ink' import { type CliHighlight, getCliHighlightPromise, diff --git a/src/components/permissions/AskUserQuestionPermissionRequest/PreviewQuestionView.tsx b/src/components/permissions/AskUserQuestionPermissionRequest/PreviewQuestionView.tsx index 78289da5f..6e80ce25f 100644 --- a/src/components/permissions/AskUserQuestionPermissionRequest/PreviewQuestionView.tsx +++ b/src/components/permissions/AskUserQuestionPermissionRequest/PreviewQuestionView.tsx @@ -1,8 +1,7 @@ import figures from 'figures' import React, { useCallback, useMemo, useRef, useState } from 'react' import { useTerminalSize } from '../../../hooks/useTerminalSize.js' -import type { KeyboardEvent } from '../../../ink/events/keyboard-event.js' -import { Box, Text } from '../../../ink.js' +import { type KeyboardEvent, Box, Text } from '@anthropic/ink' import { useKeybinding, useKeybindings, @@ -12,7 +11,7 @@ import type { Question } from '../../../tools/AskUserQuestionTool/AskUserQuestio import { getExternalEditor } from '../../../utils/editor.js' import { toIDEDisplayName } from '../../../utils/ide.js' import { editPromptInEditor } from '../../../utils/promptEditor.js' -import { Divider } from '../../design-system/Divider.js' +import { Divider } from '@anthropic/ink' import TextInput from '../../TextInput.js' import { PermissionRequestTitle } from '../PermissionRequestTitle.js' import { PreviewBox } from './PreviewBox.js' diff --git a/src/components/permissions/AskUserQuestionPermissionRequest/QuestionNavigationBar.tsx b/src/components/permissions/AskUserQuestionPermissionRequest/QuestionNavigationBar.tsx index 3440e9daf..082ba086c 100644 --- a/src/components/permissions/AskUserQuestionPermissionRequest/QuestionNavigationBar.tsx +++ b/src/components/permissions/AskUserQuestionPermissionRequest/QuestionNavigationBar.tsx @@ -1,8 +1,7 @@ import figures from 'figures' import React, { useMemo } from 'react' import { useTerminalSize } from '../../../hooks/useTerminalSize.js' -import { stringWidth } from '../../../ink/stringWidth.js' -import { Box, Text } from '../../../ink.js' +import { Box, Text, stringWidth } from '@anthropic/ink' import type { Question } from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js' import { truncateToWidth } from '../../../utils/format.js' diff --git a/src/components/permissions/AskUserQuestionPermissionRequest/QuestionView.tsx b/src/components/permissions/AskUserQuestionPermissionRequest/QuestionView.tsx index ef45238ab..ec4aa3ad7 100644 --- a/src/components/permissions/AskUserQuestionPermissionRequest/QuestionView.tsx +++ b/src/components/permissions/AskUserQuestionPermissionRequest/QuestionView.tsx @@ -1,7 +1,6 @@ import figures from 'figures' import React, { useCallback, useState } from 'react' -import type { KeyboardEvent } from '../../../ink/events/keyboard-event.js' -import { Box, Text } from '../../../ink.js' +import { type KeyboardEvent, Box, Text } from '@anthropic/ink' import { useAppState } from '../../../state/AppState.js' import type { Question, @@ -17,8 +16,9 @@ import { Select, SelectMulti, } from '../../CustomSelect/index.js' -import { Divider } from '../../design-system/Divider.js' +import { Divider } from '@anthropic/ink' import { FilePathLink } from '../../FilePathLink.js' + import { PermissionRequestTitle } from '../PermissionRequestTitle.js' import { PreviewQuestionView } from './PreviewQuestionView.js' import { QuestionNavigationBar } from './QuestionNavigationBar.js' diff --git a/src/components/permissions/AskUserQuestionPermissionRequest/SubmitQuestionsView.tsx b/src/components/permissions/AskUserQuestionPermissionRequest/SubmitQuestionsView.tsx index b17a26c2a..37e7e832c 100644 --- a/src/components/permissions/AskUserQuestionPermissionRequest/SubmitQuestionsView.tsx +++ b/src/components/permissions/AskUserQuestionPermissionRequest/SubmitQuestionsView.tsx @@ -1,10 +1,10 @@ import figures from 'figures' import React from 'react' -import { Box, Text } from '../../../ink.js' +import { Box, Text } from '@anthropic/ink' import type { Question } from '../../../tools/AskUserQuestionTool/AskUserQuestionTool.js' import type { PermissionDecision } from '../../../utils/permissions/PermissionResult.js' import { Select } from '../../CustomSelect/index.js' -import { Divider } from '../../design-system/Divider.js' +import { Divider } from '@anthropic/ink' import { PermissionRequestTitle } from '../PermissionRequestTitle.js' import { PermissionRuleExplanation } from '../PermissionRuleExplanation.js' import { QuestionNavigationBar } from './QuestionNavigationBar.js' diff --git a/src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx b/src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx index 1eb1ffffe..fb2c06da9 100644 --- a/src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx +++ b/src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx @@ -1,7 +1,7 @@ import { feature } from 'bun:bundle' import figures from 'figures' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { Box, Text, useTheme } from '../../../ink.js' +import { Box, Text, useTheme } from '@anthropic/ink' import { useKeybinding } from '../../../keybindings/useKeybinding.js' import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../../services/analytics/growthbook.js' import { diff --git a/src/components/permissions/ComputerUseApproval/ComputerUseApproval.tsx b/src/components/permissions/ComputerUseApproval/ComputerUseApproval.tsx index c591082e4..6eecf1be3 100644 --- a/src/components/permissions/ComputerUseApproval/ComputerUseApproval.tsx +++ b/src/components/permissions/ComputerUseApproval/ComputerUseApproval.tsx @@ -7,12 +7,12 @@ import { DEFAULT_GRANT_FLAGS } from '@ant/computer-use-mcp/types' import figures from 'figures' import * as React from 'react' import { useMemo, useState } from 'react' -import { Box, Text } from '../../../ink.js' +import { Box, Text } from '@anthropic/ink' import { execFileNoThrow } from '../../../utils/execFileNoThrow.js' import { plural } from '../../../utils/stringUtils.js' import type { OptionWithDescription } from '../../CustomSelect/select.js' import { Select } from '../../CustomSelect/select.js' -import { Dialog } from '../../design-system/Dialog.js' +import { Dialog } from '@anthropic/ink' type ComputerUseApprovalProps = { request: CuPermissionRequest diff --git a/src/components/permissions/EnterPlanModePermissionRequest/EnterPlanModePermissionRequest.tsx b/src/components/permissions/EnterPlanModePermissionRequest/EnterPlanModePermissionRequest.tsx index 4251891e0..9922f1680 100644 --- a/src/components/permissions/EnterPlanModePermissionRequest/EnterPlanModePermissionRequest.tsx +++ b/src/components/permissions/EnterPlanModePermissionRequest/EnterPlanModePermissionRequest.tsx @@ -1,6 +1,6 @@ import React from 'react' import { handlePlanModeTransition } from '../../../bootstrap/state.js' -import { Box, Text } from '../../../ink.js' +import { Box, Text } from '@anthropic/ink' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, diff --git a/src/components/permissions/ExitPlanModePermissionRequest/ExitPlanModePermissionRequest.tsx b/src/components/permissions/ExitPlanModePermissionRequest/ExitPlanModePermissionRequest.tsx index fddadaa7e..5a3ee5f1f 100644 --- a/src/components/permissions/ExitPlanModePermissionRequest/ExitPlanModePermissionRequest.tsx +++ b/src/components/permissions/ExitPlanModePermissionRequest/ExitPlanModePermissionRequest.tsx @@ -29,8 +29,7 @@ import { } from '../../../bootstrap/state.js' import { generateSessionName } from '../../../commands/rename/generateSessionName.js' import { launchUltraplan } from '../../../commands/ultraplan.js' -import type { KeyboardEvent } from '../../../ink/events/keyboard-event.js' -import { Box, Text } from '../../../ink.js' +import { type KeyboardEvent, Box, Text } from '@anthropic/ink' import type { AppState } from '../../../state/AppStateStore.js' import { AGENT_TOOL_NAME } from '../../../tools/AgentTool/constants.js' import { EXIT_PLAN_MODE_V2_TOOL_NAME } from '../../../tools/ExitPlanModeTool/constants.js' diff --git a/src/components/permissions/FallbackPermissionRequest.tsx b/src/components/permissions/FallbackPermissionRequest.tsx index 9b7fee994..23075aa58 100644 --- a/src/components/permissions/FallbackPermissionRequest.tsx +++ b/src/components/permissions/FallbackPermissionRequest.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useMemo } from 'react' import { getOriginalCwd } from '../../bootstrap/state.js' -import { Box, Text, useTheme } from '../../ink.js' +import { Box, Text, useTheme } from '@anthropic/ink' import { sanitizeToolNameForAnalytics } from '../../services/analytics/metadata.js' import { env } from '../../utils/env.js' import { shouldShowAlwaysAllowOptions } from '../../utils/permissions/permissionsLoader.js' diff --git a/src/components/permissions/FileEditPermissionRequest/FileEditPermissionRequest.tsx b/src/components/permissions/FileEditPermissionRequest/FileEditPermissionRequest.tsx index d3bae2c17..9f8683017 100644 --- a/src/components/permissions/FileEditPermissionRequest/FileEditPermissionRequest.tsx +++ b/src/components/permissions/FileEditPermissionRequest/FileEditPermissionRequest.tsx @@ -3,7 +3,7 @@ import React from 'react' import { FileEditToolDiff } from 'src/components/FileEditToolDiff.js' import { getCwd } from 'src/utils/cwd.js' import type { z } from 'zod/v4' -import { Text } from '../../../ink.js' +import { Text } from '@anthropic/ink' import { FileEditTool } from '../../../tools/FileEditTool/FileEditTool.js' import { FilePermissionDialog } from '../FilePermissionDialog/FilePermissionDialog.js' import { diff --git a/src/components/permissions/FilePermissionDialog/FilePermissionDialog.tsx b/src/components/permissions/FilePermissionDialog/FilePermissionDialog.tsx index b645949dc..58ac9b118 100644 --- a/src/components/permissions/FilePermissionDialog/FilePermissionDialog.tsx +++ b/src/components/permissions/FilePermissionDialog/FilePermissionDialog.tsx @@ -1,7 +1,7 @@ import { relative } from 'path' import React, { useMemo } from 'react' import { useDiffInIDE } from '../../../hooks/useDiffInIDE.js' -import { Box, Text } from '../../../ink.js' +import { Box, Text } from '@anthropic/ink' import type { ToolUseContext } from '../../../Tool.js' import { getLanguageName } from '../../../utils/cliHighlight.js' import { getCwd } from '../../../utils/cwd.js' diff --git a/src/components/permissions/FilePermissionDialog/permissionOptions.tsx b/src/components/permissions/FilePermissionDialog/permissionOptions.tsx index 3a3507234..3709a6502 100644 --- a/src/components/permissions/FilePermissionDialog/permissionOptions.tsx +++ b/src/components/permissions/FilePermissionDialog/permissionOptions.tsx @@ -2,7 +2,7 @@ import { homedir } from 'os' import { basename, join, sep } from 'path' import React, { type ReactNode } from 'react' import { getOriginalCwd } from '../../../bootstrap/state.js' -import { Text } from '../../../ink.js' +import { Text } from '@anthropic/ink' import { getShortcutDisplay } from '../../../keybindings/shortcutFormat.js' import type { ToolPermissionContext } from '../../../Tool.js' import { expandPath, getDirectoryForPath } from '../../../utils/path.js' diff --git a/src/components/permissions/FileWritePermissionRequest/FileWritePermissionRequest.tsx b/src/components/permissions/FileWritePermissionRequest/FileWritePermissionRequest.tsx index ce352858d..744673193 100644 --- a/src/components/permissions/FileWritePermissionRequest/FileWritePermissionRequest.tsx +++ b/src/components/permissions/FileWritePermissionRequest/FileWritePermissionRequest.tsx @@ -1,7 +1,7 @@ import { basename, relative } from 'path' import React, { useMemo } from 'react' import type { z } from 'zod/v4' -import { Text } from '../../../ink.js' +import { Text } from '@anthropic/ink' import { FileWriteTool } from '../../../tools/FileWriteTool/FileWriteTool.js' import { getCwd } from '../../../utils/cwd.js' import { isENOENT } from '../../../utils/errors.js' diff --git a/src/components/permissions/FileWritePermissionRequest/FileWriteToolDiff.tsx b/src/components/permissions/FileWritePermissionRequest/FileWriteToolDiff.tsx index 36147ef03..38084661f 100644 --- a/src/components/permissions/FileWritePermissionRequest/FileWriteToolDiff.tsx +++ b/src/components/permissions/FileWritePermissionRequest/FileWriteToolDiff.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { useMemo } from 'react' import { useTerminalSize } from '../../../hooks/useTerminalSize.js' -import { Box, NoSelect, Text } from '../../../ink.js' +import { Box, NoSelect, Text } from '@anthropic/ink' import { intersperse } from '../../../utils/array.js' import { getPatchForDisplay } from '../../../utils/diff.js' import { HighlightedCode } from '../../HighlightedCode.js' diff --git a/src/components/permissions/FilesystemPermissionRequest/FilesystemPermissionRequest.tsx b/src/components/permissions/FilesystemPermissionRequest/FilesystemPermissionRequest.tsx index ebfdc8817..67828f563 100644 --- a/src/components/permissions/FilesystemPermissionRequest/FilesystemPermissionRequest.tsx +++ b/src/components/permissions/FilesystemPermissionRequest/FilesystemPermissionRequest.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Box, Text, useTheme } from '../../../ink.js' +import { Box, Text, useTheme } from '@anthropic/ink' import { FallbackPermissionRequest } from '../FallbackPermissionRequest.js' import { FilePermissionDialog } from '../FilePermissionDialog/FilePermissionDialog.js' import type { ToolInput } from '../FilePermissionDialog/useFilePermissionDialog.js' diff --git a/src/components/permissions/NotebookEditPermissionRequest/NotebookEditPermissionRequest.tsx b/src/components/permissions/NotebookEditPermissionRequest/NotebookEditPermissionRequest.tsx index 6c03b94d3..9ebc09642 100644 --- a/src/components/permissions/NotebookEditPermissionRequest/NotebookEditPermissionRequest.tsx +++ b/src/components/permissions/NotebookEditPermissionRequest/NotebookEditPermissionRequest.tsx @@ -1,7 +1,7 @@ import { basename } from 'path' import React from 'react' import type { z } from 'zod/v4' -import { Text } from '../../../ink.js' +import { Text } from '@anthropic/ink' import { NotebookEditTool } from '../../../tools/NotebookEditTool/NotebookEditTool.js' import { logError } from '../../../utils/log.js' import { FilePermissionDialog } from '../FilePermissionDialog/FilePermissionDialog.js' diff --git a/src/components/permissions/NotebookEditPermissionRequest/NotebookEditToolDiff.tsx b/src/components/permissions/NotebookEditPermissionRequest/NotebookEditToolDiff.tsx index 9b5373142..4822dac9a 100644 --- a/src/components/permissions/NotebookEditPermissionRequest/NotebookEditToolDiff.tsx +++ b/src/components/permissions/NotebookEditPermissionRequest/NotebookEditToolDiff.tsx @@ -1,7 +1,7 @@ import { relative } from 'path' import * as React from 'react' import { Suspense, use, useMemo } from 'react' -import { Box, NoSelect, Text } from '../../../ink.js' +import { Box, NoSelect, Text } from '@anthropic/ink' import type { NotebookCellType, NotebookContent, diff --git a/src/components/permissions/PermissionDecisionDebugInfo.tsx b/src/components/permissions/PermissionDecisionDebugInfo.tsx index afd855343..fbbaf6b18 100644 --- a/src/components/permissions/PermissionDecisionDebugInfo.tsx +++ b/src/components/permissions/PermissionDecisionDebugInfo.tsx @@ -2,7 +2,7 @@ import { feature } from 'bun:bundle' import chalk from 'chalk' import figures from 'figures' import React, { useMemo } from 'react' -import { Ansi, Box, color, Text, useTheme } from '../../ink.js' +import { Ansi, Box, color, Text, useTheme } from '@anthropic/ink' import { useAppState } from '../../state/AppState.js' import type { PermissionMode } from '../../utils/permissions/PermissionMode.js' import { permissionModeTitle } from '../../utils/permissions/PermissionMode.js' diff --git a/src/components/permissions/PermissionDialog.tsx b/src/components/permissions/PermissionDialog.tsx index 210bbb16e..40e99f50f 100644 --- a/src/components/permissions/PermissionDialog.tsx +++ b/src/components/permissions/PermissionDialog.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Box } from '../../ink.js' +import { Box } from '@anthropic/ink' import type { Theme } from '../../utils/theme.js' import { PermissionRequestTitle } from './PermissionRequestTitle.js' import type { WorkerBadgeProps } from './WorkerBadge.js' diff --git a/src/components/permissions/PermissionExplanation.tsx b/src/components/permissions/PermissionExplanation.tsx index 2fe08a858..367fc7ccb 100644 --- a/src/components/permissions/PermissionExplanation.tsx +++ b/src/components/permissions/PermissionExplanation.tsx @@ -1,5 +1,5 @@ import React, { Suspense, use, useState } from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { useKeybinding } from '../../keybindings/useKeybinding.js' import { logEvent } from '../../services/analytics/index.js' import type { Message } from '../../types/message.js' diff --git a/src/components/permissions/PermissionPrompt.tsx b/src/components/permissions/PermissionPrompt.tsx index ae9ba0730..0e16fad25 100644 --- a/src/components/permissions/PermissionPrompt.tsx +++ b/src/components/permissions/PermissionPrompt.tsx @@ -1,5 +1,5 @@ import React, { type ReactNode, useCallback, useMemo, useState } from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import type { KeybindingAction } from '../../keybindings/types.js' import { useKeybindings } from '../../keybindings/useKeybinding.js' import { diff --git a/src/components/permissions/PermissionRequestTitle.tsx b/src/components/permissions/PermissionRequestTitle.tsx index 953cca22b..eafa1f71f 100644 --- a/src/components/permissions/PermissionRequestTitle.tsx +++ b/src/components/permissions/PermissionRequestTitle.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import type { Theme } from '../../utils/theme.js' import type { WorkerBadgeProps } from './WorkerBadge.js' diff --git a/src/components/permissions/PermissionRuleExplanation.tsx b/src/components/permissions/PermissionRuleExplanation.tsx index 406f7e3b8..3ed14a120 100644 --- a/src/components/permissions/PermissionRuleExplanation.tsx +++ b/src/components/permissions/PermissionRuleExplanation.tsx @@ -1,7 +1,8 @@ import { feature } from 'bun:bundle' import chalk from 'chalk' import React from 'react' -import { Ansi, Box, Text } from '../../ink.js' +import { Ansi, Box, Text } from '@anthropic/ink' +import ThemedText from '../design-system/ThemedText.js' import { useAppState } from '../../state/AppState.js' import type { PermissionDecision, @@ -9,7 +10,6 @@ import type { } from '../../utils/permissions/PermissionResult.js' import { permissionRuleValueToString } from '../../utils/permissions/permissionRuleParser.js' import type { Theme } from '../../utils/theme.js' -import ThemedText from '../design-system/ThemedText.js' export type PermissionRuleExplanationProps = { permissionResult: PermissionDecision diff --git a/src/components/permissions/PowerShellPermissionRequest/PowerShellPermissionRequest.tsx b/src/components/permissions/PowerShellPermissionRequest/PowerShellPermissionRequest.tsx index 5dcd0e488..16183ec79 100644 --- a/src/components/permissions/PowerShellPermissionRequest/PowerShellPermissionRequest.tsx +++ b/src/components/permissions/PowerShellPermissionRequest/PowerShellPermissionRequest.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { Box, Text, useTheme } from '../../../ink.js' +import { Box, Text, useTheme } from '@anthropic/ink' import { useKeybinding } from '../../../keybindings/useKeybinding.js' import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../../services/analytics/growthbook.js' import { diff --git a/src/components/permissions/SandboxPermissionRequest.tsx b/src/components/permissions/SandboxPermissionRequest.tsx index 9dc4d6629..cf7ef1195 100644 --- a/src/components/permissions/SandboxPermissionRequest.tsx +++ b/src/components/permissions/SandboxPermissionRequest.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Box, Text } from 'src/ink.js' +import { Box, Text } from '@anthropic/ink' import { type NetworkHostPattern, shouldAllowManagedSandboxDomainsOnly, diff --git a/src/components/permissions/SedEditPermissionRequest/SedEditPermissionRequest.tsx b/src/components/permissions/SedEditPermissionRequest/SedEditPermissionRequest.tsx index 209cd08f4..2135b5e22 100644 --- a/src/components/permissions/SedEditPermissionRequest/SedEditPermissionRequest.tsx +++ b/src/components/permissions/SedEditPermissionRequest/SedEditPermissionRequest.tsx @@ -5,7 +5,7 @@ import { getCwd } from 'src/utils/cwd.js' import { isENOENT } from 'src/utils/errors.js' import { detectEncodingForResolvedPath } from 'src/utils/fileRead.js' import { getFsImplementation } from 'src/utils/fsOperations.js' -import { Text } from '../../../ink.js' +import { Text } from '@anthropic/ink' import { BashTool } from '../../../tools/BashTool/BashTool.js' import { applySedSubstitution, diff --git a/src/components/permissions/SkillPermissionRequest/SkillPermissionRequest.tsx b/src/components/permissions/SkillPermissionRequest/SkillPermissionRequest.tsx index 799c88705..d9e4050fc 100644 --- a/src/components/permissions/SkillPermissionRequest/SkillPermissionRequest.tsx +++ b/src/components/permissions/SkillPermissionRequest/SkillPermissionRequest.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useMemo } from 'react' import { logError } from 'src/utils/log.js' import { getOriginalCwd } from '../../../bootstrap/state.js' -import { Box, Text } from '../../../ink.js' +import { Box, Text } from '@anthropic/ink' import { sanitizeToolNameForAnalytics } from '../../../services/analytics/metadata.js' import { SKILL_TOOL_NAME } from '../../../tools/SkillTool/constants.js' import { SkillTool } from '../../../tools/SkillTool/SkillTool.js' diff --git a/src/components/permissions/WebFetchPermissionRequest/WebFetchPermissionRequest.tsx b/src/components/permissions/WebFetchPermissionRequest/WebFetchPermissionRequest.tsx index da2498885..5e0625498 100644 --- a/src/components/permissions/WebFetchPermissionRequest/WebFetchPermissionRequest.tsx +++ b/src/components/permissions/WebFetchPermissionRequest/WebFetchPermissionRequest.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from 'react' -import { Box, Text, useTheme } from '../../../ink.js' +import { Box, Text, useTheme } from '@anthropic/ink' import { WebFetchTool } from '../../../tools/WebFetchTool/WebFetchTool.js' import { shouldShowAlwaysAllowOptions } from '../../../utils/permissions/permissionsLoader.js' import { diff --git a/src/components/permissions/WorkerBadge.tsx b/src/components/permissions/WorkerBadge.tsx index 61d5873ab..959346e6e 100644 --- a/src/components/permissions/WorkerBadge.tsx +++ b/src/components/permissions/WorkerBadge.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { BLACK_CIRCLE } from '../../constants/figures.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { toInkColor } from '../../utils/ink.js' export type WorkerBadgeProps = { diff --git a/src/components/permissions/WorkerPendingPermission.tsx b/src/components/permissions/WorkerPendingPermission.tsx index 06aab0334..2d7ef596e 100644 --- a/src/components/permissions/WorkerPendingPermission.tsx +++ b/src/components/permissions/WorkerPendingPermission.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { getAgentName, getTeammateColor, diff --git a/src/components/permissions/rules/AddPermissionRules.tsx b/src/components/permissions/rules/AddPermissionRules.tsx index 6e48e1dcb..e62442c3c 100644 --- a/src/components/permissions/rules/AddPermissionRules.tsx +++ b/src/components/permissions/rules/AddPermissionRules.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { useCallback } from 'react' import { Select } from '../../../components/CustomSelect/select.js' -import { Box, Text } from '../../../ink.js' +import { Box, Dialog, Text } from '@anthropic/ink' import type { ToolPermissionContext } from '../../../Tool.js' import type { PermissionBehavior, @@ -25,7 +25,6 @@ import { import { getRelativeSettingsFilePathForSource } from '../../../utils/settings/settings.js' import { plural } from '../../../utils/stringUtils.js' import type { OptionWithDescription } from '../../CustomSelect/select.js' -import { Dialog } from '../../design-system/Dialog.js' import { PermissionRuleDescription } from './PermissionRuleDescription.js' export function optionForPermissionSaveDestination( diff --git a/src/components/permissions/rules/AddWorkspaceDirectory.tsx b/src/components/permissions/rules/AddWorkspaceDirectory.tsx index 07d0a00ef..589928782 100644 --- a/src/components/permissions/rules/AddWorkspaceDirectory.tsx +++ b/src/components/permissions/rules/AddWorkspaceDirectory.tsx @@ -7,16 +7,13 @@ import { validateDirectoryForWorkspace, } from '../../../commands/add-dir/validation.js' import TextInput from '../../../components/TextInput.js' -import type { KeyboardEvent } from '../../../ink/events/keyboard-event.js' -import { Box, Text } from '../../../ink.js' +import { type KeyboardEvent, Box, Text } from '@anthropic/ink' import { useKeybinding } from '../../../keybindings/useKeybinding.js' import type { ToolPermissionContext } from '../../../Tool.js' import { getDirectoryCompletions } from '../../../utils/suggestions/directoryCompletion.js' import { ConfigurableShortcutHint } from '../../ConfigurableShortcutHint.js' import { Select } from '../../CustomSelect/select.js' -import { Byline } from '../../design-system/Byline.js' -import { Dialog } from '../../design-system/Dialog.js' -import { KeyboardShortcutHint } from '../../design-system/KeyboardShortcutHint.js' +import { Byline, Dialog, KeyboardShortcutHint } from '@anthropic/ink' import { PromptInputFooterSuggestions, type SuggestionItem, diff --git a/src/components/permissions/rules/PermissionRuleDescription.tsx b/src/components/permissions/rules/PermissionRuleDescription.tsx index ac8f0cd23..d4591b8d9 100644 --- a/src/components/permissions/rules/PermissionRuleDescription.tsx +++ b/src/components/permissions/rules/PermissionRuleDescription.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Text } from '../../../ink.js' +import { Text } from '@anthropic/ink' import { BashTool } from '../../../tools/BashTool/BashTool.js' import type { PermissionRuleValue } from '../../../utils/permissions/PermissionRule.js' diff --git a/src/components/permissions/rules/PermissionRuleInput.tsx b/src/components/permissions/rules/PermissionRuleInput.tsx index 36dfb6b63..fae8553d9 100644 --- a/src/components/permissions/rules/PermissionRuleInput.tsx +++ b/src/components/permissions/rules/PermissionRuleInput.tsx @@ -4,7 +4,7 @@ import { useState } from 'react' import TextInput from '../../../components/TextInput.js' import { useExitOnCtrlCDWithKeybindings } from '../../../hooks/useExitOnCtrlCDWithKeybindings.js' import { useTerminalSize } from '../../../hooks/useTerminalSize.js' -import { Box, Newline, Text } from '../../../ink.js' +import { Box, Newline, Text } from '@anthropic/ink' import { useKeybinding } from '../../../keybindings/useKeybinding.js' import { BashTool } from '../../../tools/BashTool/BashTool.js' import { WebFetchTool } from '../../../tools/WebFetchTool/WebFetchTool.js' diff --git a/src/components/permissions/rules/PermissionRuleList.tsx b/src/components/permissions/rules/PermissionRuleList.tsx index 129b58083..a35f31115 100644 --- a/src/components/permissions/rules/PermissionRuleList.tsx +++ b/src/components/permissions/rules/PermissionRuleList.tsx @@ -12,8 +12,7 @@ import type { CommandResultDisplay } from '../../../commands.js' import { Select } from '../../../components/CustomSelect/select.js' import { useExitOnCtrlCDWithKeybindings } from '../../../hooks/useExitOnCtrlCDWithKeybindings.js' import { useSearchInput } from '../../../hooks/useSearchInput.js' -import type { KeyboardEvent } from '../../../ink/events/keyboard-event.js' -import { Box, Text, useTerminalFocus } from '../../../ink.js' +import { type KeyboardEvent, Box, Text, useTerminalFocus } from '@anthropic/ink' import { useKeybinding } from '../../../keybindings/useKeybinding.js' import { type AutoModeDenial, @@ -34,13 +33,7 @@ import { } from '../../../utils/permissions/permissions.js' import type { UnreachableRule } from '../../../utils/permissions/shadowedRuleDetection.js' import { jsonStringify } from '../../../utils/slowOperations.js' -import { Pane } from '../../design-system/Pane.js' -import { - Tab, - Tabs, - useTabHeaderFocus, - useTabsWidth, -} from '../../design-system/Tabs.js' +import { Pane, Tab, Tabs, useTabHeaderFocus, useTabsWidth } from '@anthropic/ink' import { SearchBox } from '../../SearchBox.js' import type { Option } from '../../ui/option.js' import { AddPermissionRules } from './AddPermissionRules.js' diff --git a/src/components/permissions/rules/RecentDenialsTab.tsx b/src/components/permissions/rules/RecentDenialsTab.tsx index 17c13844d..4b4a281d2 100644 --- a/src/components/permissions/rules/RecentDenialsTab.tsx +++ b/src/components/permissions/rules/RecentDenialsTab.tsx @@ -1,14 +1,13 @@ import * as React from 'react' import { useCallback, useEffect, useState } from 'react' // eslint-disable-next-line custom-rules/prefer-use-keybindings -- 'r' is a view-specific key, not a global keybinding -import { Box, Text, useInput } from '../../../ink.js' +import { Box, Text, useInput } from '@anthropic/ink' import { type AutoModeDenial, getAutoModeDenials, } from '../../../utils/autoModeDenials.js' import { Select } from '../../CustomSelect/select.js' -import { StatusIcon } from '../../design-system/StatusIcon.js' -import { useTabHeaderFocus } from '../../design-system/Tabs.js' +import { StatusIcon } from '@anthropic/ink' type Props = { onHeaderFocusChange?: (focused: boolean) => void diff --git a/src/components/permissions/rules/RemoveWorkspaceDirectory.tsx b/src/components/permissions/rules/RemoveWorkspaceDirectory.tsx index e6eefade2..8d7e6fa99 100644 --- a/src/components/permissions/rules/RemoveWorkspaceDirectory.tsx +++ b/src/components/permissions/rules/RemoveWorkspaceDirectory.tsx @@ -1,10 +1,10 @@ import * as React from 'react' import { useCallback } from 'react' import { Select } from '../../../components/CustomSelect/select.js' -import { Box, Text } from '../../../ink.js' +import { Box, Text } from '@anthropic/ink' import type { ToolPermissionContext } from '../../../Tool.js' import { applyPermissionUpdate } from '../../../utils/permissions/PermissionUpdate.js' -import { Dialog } from '../../design-system/Dialog.js' +import { Dialog } from '@anthropic/ink' type Props = { directoryPath: string diff --git a/src/components/permissions/rules/WorkspaceTab.tsx b/src/components/permissions/rules/WorkspaceTab.tsx index 0dab0c7d0..e32899ea7 100644 --- a/src/components/permissions/rules/WorkspaceTab.tsx +++ b/src/components/permissions/rules/WorkspaceTab.tsx @@ -4,9 +4,8 @@ import { useCallback, useEffect } from 'react' import { getOriginalCwd } from '../../../bootstrap/state.js' import type { CommandResultDisplay } from '../../../commands.js' import { Select } from '../../../components/CustomSelect/select.js' -import { Box, Text } from '../../../ink.js' +import { Box, Text } from '@anthropic/ink' import type { ToolPermissionContext } from '../../../Tool.js' -import { useTabHeaderFocus } from '../../design-system/Tabs.js' type Props = { onExit: ( diff --git a/src/components/permissions/shellPermissionHelpers.tsx b/src/components/permissions/shellPermissionHelpers.tsx index 2c7a2db95..9b7c945f0 100644 --- a/src/components/permissions/shellPermissionHelpers.tsx +++ b/src/components/permissions/shellPermissionHelpers.tsx @@ -1,7 +1,7 @@ import { basename, sep } from 'path' import React, { type ReactNode } from 'react' import { getOriginalCwd } from '../../bootstrap/state.js' -import { Text } from '../../ink.js' +import { Text } from '@anthropic/ink' import type { PermissionUpdate } from '../../utils/permissions/PermissionUpdateSchema.js' import { permissionRuleExtractPrefix } from '../../utils/permissions/shellRuleMatching.js' diff --git a/src/components/sandbox/SandboxConfigTab.tsx b/src/components/sandbox/SandboxConfigTab.tsx index 58bfba688..37e00ce56 100644 --- a/src/components/sandbox/SandboxConfigTab.tsx +++ b/src/components/sandbox/SandboxConfigTab.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { SandboxManager, shouldAllowManagedSandboxDomainsOnly, diff --git a/src/components/sandbox/SandboxDependenciesTab.tsx b/src/components/sandbox/SandboxDependenciesTab.tsx index 75091910d..24efdf1ef 100644 --- a/src/components/sandbox/SandboxDependenciesTab.tsx +++ b/src/components/sandbox/SandboxDependenciesTab.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { getPlatform } from '../../utils/platform.js' import type { SandboxDependencyCheck } from '../../utils/sandbox/sandbox-adapter.js' diff --git a/src/components/sandbox/SandboxDoctorSection.tsx b/src/components/sandbox/SandboxDoctorSection.tsx index 5e7198c38..effa9500e 100644 --- a/src/components/sandbox/SandboxDoctorSection.tsx +++ b/src/components/sandbox/SandboxDoctorSection.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js' export function SandboxDoctorSection(): React.ReactNode { diff --git a/src/components/sandbox/SandboxOverridesTab.tsx b/src/components/sandbox/SandboxOverridesTab.tsx index 74c6d224b..257dcc670 100644 --- a/src/components/sandbox/SandboxOverridesTab.tsx +++ b/src/components/sandbox/SandboxOverridesTab.tsx @@ -1,9 +1,8 @@ import React from 'react' -import { Box, color, Link, Text, useTheme } from '../../ink.js' +import { Box, color, Link, Text, useTheme, useTabHeaderFocus } from '@anthropic/ink' import type { CommandResultDisplay } from '../../types/command.js' import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js' import { Select } from '../CustomSelect/select.js' -import { useTabHeaderFocus } from '../design-system/Tabs.js' type Props = { onComplete: ( diff --git a/src/components/sandbox/SandboxSettings.tsx b/src/components/sandbox/SandboxSettings.tsx index 05998577b..005d071e1 100644 --- a/src/components/sandbox/SandboxSettings.tsx +++ b/src/components/sandbox/SandboxSettings.tsx @@ -1,13 +1,11 @@ import React from 'react' -import { Box, color, Link, Text, useTheme } from '../../ink.js' +import { Box, color, Link, Text, useTheme, Pane, Tab, Tabs, useTabHeaderFocus } from '@anthropic/ink' import { useKeybindings } from '../../keybindings/useKeybinding.js' import type { CommandResultDisplay } from '../../types/command.js' import type { SandboxDependencyCheck } from '../../utils/sandbox/sandbox-adapter.js' import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js' import { getSettings_DEPRECATED } from '../../utils/settings/settings.js' import { Select } from '../CustomSelect/select.js' -import { Pane } from '../design-system/Pane.js' -import { Tab, Tabs, useTabHeaderFocus } from '../design-system/Tabs.js' import { SandboxConfigTab } from './SandboxConfigTab.js' import { SandboxDependenciesTab } from './SandboxDependenciesTab.js' import { SandboxOverridesTab } from './SandboxOverridesTab.js' diff --git a/src/components/shell/OutputLine.tsx b/src/components/shell/OutputLine.tsx index cf72760db..0b2c280af 100644 --- a/src/components/shell/OutputLine.tsx +++ b/src/components/shell/OutputLine.tsx @@ -1,8 +1,9 @@ import * as React from 'react' import { useMemo } from 'react' import { useTerminalSize } from '../../hooks/useTerminalSize.js' -import { Ansi, Text } from '../../ink.js' +import { Ansi, Text } from '@anthropic/ink' import { createHyperlink } from '../../utils/hyperlink.js' + import { jsonParse, jsonStringify } from '../../utils/slowOperations.js' import { renderTruncatedContent } from '../../utils/terminal.js' import { MessageResponse } from '../MessageResponse.js' diff --git a/src/components/shell/ShellProgressMessage.tsx b/src/components/shell/ShellProgressMessage.tsx index 99da5ac3b..a99bdbd0d 100644 --- a/src/components/shell/ShellProgressMessage.tsx +++ b/src/components/shell/ShellProgressMessage.tsx @@ -1,6 +1,6 @@ import React from 'react' import stripAnsi from 'strip-ansi' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { formatFileSize } from '../../utils/format.js' import { MessageResponse } from '../MessageResponse.js' import { OffscreenFreeze } from '../OffscreenFreeze.js' diff --git a/src/components/shell/ShellTimeDisplay.tsx b/src/components/shell/ShellTimeDisplay.tsx index 7e619dfba..67b5c373a 100644 --- a/src/components/shell/ShellTimeDisplay.tsx +++ b/src/components/shell/ShellTimeDisplay.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Text } from '../../ink.js' +import { Text } from '@anthropic/ink' import { formatDuration } from '../../utils/format.js' type Props = { diff --git a/src/components/skills/SkillsMenu.tsx b/src/components/skills/SkillsMenu.tsx index 5733688b9..4b33eee57 100644 --- a/src/components/skills/SkillsMenu.tsx +++ b/src/components/skills/SkillsMenu.tsx @@ -8,7 +8,7 @@ import { getCommandName, type PromptCommand, } from '../../commands.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { estimateSkillFrontmatterTokens, getSkillsPath, @@ -21,7 +21,7 @@ import { } from '../../utils/settings/constants.js' import { plural } from '../../utils/stringUtils.js' import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js' -import { Dialog } from '../design-system/Dialog.js' +import { Dialog } from '@anthropic/ink' // Skills are always PromptCommands with CommandBase properties type SkillCommand = CommandBase & PromptCommand diff --git a/src/components/tasks/AsyncAgentDetailDialog.tsx b/src/components/tasks/AsyncAgentDetailDialog.tsx index 4174d4fa5..2070b9a68 100644 --- a/src/components/tasks/AsyncAgentDetailDialog.tsx +++ b/src/components/tasks/AsyncAgentDetailDialog.tsx @@ -1,17 +1,14 @@ import React, { useMemo } from 'react' import type { DeepImmutable } from 'src/types/utils.js' import { useElapsedTime } from '../../hooks/useElapsedTime.js' -import type { KeyboardEvent } from '../../ink/events/keyboard-event.js' -import { Box, Text, useTheme } from '../../ink.js' +import { type KeyboardEvent, Box, Text, useTheme } from '@anthropic/ink' import { useKeybindings } from '../../keybindings/useKeybinding.js' import { getEmptyToolPermissionContext } from '../../Tool.js' import type { LocalAgentTaskState } from '../../tasks/LocalAgentTask/LocalAgentTask.js' import { getTools } from '../../tools.js' import { formatNumber } from '../../utils/format.js' import { extractTag } from '../../utils/messages.js' -import { Byline } from '../design-system/Byline.js' -import { Dialog } from '../design-system/Dialog.js' -import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js' +import { Byline, Dialog, KeyboardShortcutHint } from '@anthropic/ink' import { UserPlanMessage } from '../messages/UserPlanMessage.js' import { renderToolActivity } from './renderToolActivity.js' import { getTaskStatusColor, getTaskStatusIcon } from './taskStatusUtils.js' diff --git a/src/components/tasks/BackgroundTask.tsx b/src/components/tasks/BackgroundTask.tsx index fd48d09e7..7fdf1fa4f 100644 --- a/src/components/tasks/BackgroundTask.tsx +++ b/src/components/tasks/BackgroundTask.tsx @@ -1,9 +1,10 @@ import * as React from 'react' -import { Text } from 'src/ink.js' +import { Text } from '@anthropic/ink' +import { toInkColor } from '../../utils/ink.js' import type { BackgroundTaskState } from 'src/tasks/types.js' import type { DeepImmutable } from 'src/types/utils.js' import { truncate } from 'src/utils/format.js' -import { toInkColor } from 'src/utils/ink.js' + import { plural } from 'src/utils/stringUtils.js' import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js' import { RemoteSessionProgress } from './RemoteSessionProgress.js' diff --git a/src/components/tasks/BackgroundTaskStatus.tsx b/src/components/tasks/BackgroundTaskStatus.tsx index 26d46cf98..c315bfa90 100644 --- a/src/components/tasks/BackgroundTaskStatus.tsx +++ b/src/components/tasks/BackgroundTaskStatus.tsx @@ -2,7 +2,7 @@ import figures from 'figures' import * as React from 'react' import { useMemo, useState } from 'react' import { useTerminalSize } from 'src/hooks/useTerminalSize.js' -import { stringWidth } from 'src/ink/stringWidth.js' +import { stringWidth } from '@anthropic/ink' import { useAppState, useSetAppState } from 'src/state/AppState.js' import { enterTeammateView, @@ -16,14 +16,14 @@ import { type TaskState, } from 'src/tasks/types.js' import { calculateHorizontalScrollWindow } from 'src/utils/horizontalScroll.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { AGENT_COLOR_TO_THEME_COLOR, AGENT_COLORS, type AgentColorName, } from '../../tools/AgentTool/agentColorManager.js' import type { Theme } from '../../utils/theme.js' -import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js' +import { KeyboardShortcutHint } from '@anthropic/ink' import { shouldHideTasksFooter } from './taskStatusUtils.js' type Props = { diff --git a/src/components/tasks/BackgroundTasksDialog.tsx b/src/components/tasks/BackgroundTasksDialog.tsx index d9f119cf1..30eca7444 100644 --- a/src/components/tasks/BackgroundTasksDialog.tsx +++ b/src/components/tasks/BackgroundTasksDialog.tsx @@ -45,14 +45,11 @@ import { stopUltraplan } from '../../commands/ultraplan.js' import type { CommandResultDisplay } from '../../commands.js' import { useRegisterOverlay } from '../../context/overlayContext.js' import type { ExitState } from '../../hooks/useExitOnCtrlCDWithKeybindings.js' -import type { KeyboardEvent } from '../../ink/events/keyboard-event.js' -import { Box, Text } from '../../ink.js' +import { type KeyboardEvent, Box, Text } from '@anthropic/ink' import { useKeybindings } from '../../keybindings/useKeybinding.js' import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js' import { count } from '../../utils/array.js' -import { Byline } from '../design-system/Byline.js' -import { Dialog } from '../design-system/Dialog.js' -import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js' +import { Byline, Dialog, KeyboardShortcutHint } from '@anthropic/ink' import { AsyncAgentDetailDialog } from './AsyncAgentDetailDialog.js' import { BackgroundTask as BackgroundTaskComponent } from './BackgroundTask.js' import { DreamDetailDialog } from './DreamDetailDialog.js' diff --git a/src/components/tasks/DreamDetailDialog.tsx b/src/components/tasks/DreamDetailDialog.tsx index bea310946..67baab993 100644 --- a/src/components/tasks/DreamDetailDialog.tsx +++ b/src/components/tasks/DreamDetailDialog.tsx @@ -1,14 +1,11 @@ import React from 'react' import type { DeepImmutable } from 'src/types/utils.js' import { useElapsedTime } from '../../hooks/useElapsedTime.js' -import type { KeyboardEvent } from '../../ink/events/keyboard-event.js' -import { Box, Text } from '../../ink.js' +import { type KeyboardEvent, Box, Text } from '@anthropic/ink' import { useKeybindings } from '../../keybindings/useKeybinding.js' import type { DreamTaskState } from '../../tasks/DreamTask/DreamTask.js' import { plural } from '../../utils/stringUtils.js' -import { Byline } from '../design-system/Byline.js' -import { Dialog } from '../design-system/Dialog.js' -import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js' +import { Byline, Dialog, KeyboardShortcutHint } from '@anthropic/ink' type Props = { task: DeepImmutable diff --git a/src/components/tasks/InProcessTeammateDetailDialog.tsx b/src/components/tasks/InProcessTeammateDetailDialog.tsx index b59bbbd5e..c0a755a60 100644 --- a/src/components/tasks/InProcessTeammateDetailDialog.tsx +++ b/src/components/tasks/InProcessTeammateDetailDialog.tsx @@ -1,17 +1,15 @@ import React, { useMemo } from 'react' import type { DeepImmutable } from 'src/types/utils.js' import { useElapsedTime } from '../../hooks/useElapsedTime.js' -import type { KeyboardEvent } from '../../ink/events/keyboard-event.js' -import { Box, Text, useTheme } from '../../ink.js' +import { type KeyboardEvent, Box, Text, useTheme } from '@anthropic/ink' import { useKeybindings } from '../../keybindings/useKeybinding.js' import { getEmptyToolPermissionContext } from '../../Tool.js' import type { InProcessTeammateTaskState } from '../../tasks/InProcessTeammateTask/types.js' import { getTools } from '../../tools.js' import { formatNumber, truncateToWidth } from '../../utils/format.js' + +import { Byline, Dialog, KeyboardShortcutHint } from '@anthropic/ink' import { toInkColor } from '../../utils/ink.js' -import { Byline } from '../design-system/Byline.js' -import { Dialog } from '../design-system/Dialog.js' -import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js' import { renderToolActivity } from './renderToolActivity.js' import { describeTeammateActivity } from './taskStatusUtils.js' diff --git a/src/components/tasks/RemoteSessionDetailDialog.tsx b/src/components/tasks/RemoteSessionDetailDialog.tsx index 55c897fd9..ec2c43b42 100644 --- a/src/components/tasks/RemoteSessionDetailDialog.tsx +++ b/src/components/tasks/RemoteSessionDetailDialog.tsx @@ -6,8 +6,7 @@ import type { DeepImmutable } from 'src/types/utils.js' import type { CommandResultDisplay } from '../../commands.js' import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js' import { useElapsedTime } from '../../hooks/useElapsedTime.js' -import type { KeyboardEvent } from '../../ink/events/keyboard-event.js' -import { Box, Link, Text } from '../../ink.js' +import { type KeyboardEvent, Box, Link, Text } from '@anthropic/ink' import type { RemoteAgentTaskState } from '../../tasks/RemoteAgentTask/RemoteAgentTask.js' import { getRemoteTaskSessionUrl } from '../../tasks/RemoteAgentTask/RemoteAgentTask.js' import { @@ -24,9 +23,7 @@ import { EMPTY_LOOKUPS, normalizeMessages } from '../../utils/messages.js' import { plural } from '../../utils/stringUtils.js' import { teleportResumeCodeSession } from '../../utils/teleport.js' import { Select } from '../CustomSelect/select.js' -import { Byline } from '../design-system/Byline.js' -import { Dialog } from '../design-system/Dialog.js' -import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js' +import { Byline, Dialog, KeyboardShortcutHint } from '@anthropic/ink' import { Message } from '../Message.js' import { formatReviewStageCounts, diff --git a/src/components/tasks/RemoteSessionProgress.tsx b/src/components/tasks/RemoteSessionProgress.tsx index c1711cd8a..d7e0f8e68 100644 --- a/src/components/tasks/RemoteSessionProgress.tsx +++ b/src/components/tasks/RemoteSessionProgress.tsx @@ -3,7 +3,7 @@ import type { RemoteAgentTaskState } from 'src/tasks/RemoteAgentTask/RemoteAgent import type { DeepImmutable } from 'src/types/utils.js' import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js' import { useSettings } from '../../hooks/useSettings.js' -import { Text, useAnimationFrame } from '../../ink.js' +import { Text, useAnimationFrame } from '@anthropic/ink' import { count } from '../../utils/array.js' import { getRainbowColor } from '../../utils/thinking.js' diff --git a/src/components/tasks/ShellDetailDialog.tsx b/src/components/tasks/ShellDetailDialog.tsx index a81bafc8b..7627ec561 100644 --- a/src/components/tasks/ShellDetailDialog.tsx +++ b/src/components/tasks/ShellDetailDialog.tsx @@ -8,8 +8,7 @@ import React, { import type { DeepImmutable } from 'src/types/utils.js' import type { CommandResultDisplay } from '../../commands.js' import { useTerminalSize } from '../../hooks/useTerminalSize.js' -import type { KeyboardEvent } from '../../ink/events/keyboard-event.js' -import { Box, Text } from '../../ink.js' +import { type KeyboardEvent, Box, Text } from '@anthropic/ink' import { useKeybindings } from '../../keybindings/useKeybinding.js' import type { LocalShellTaskState } from '../../tasks/LocalShellTask/guards.js' import { @@ -19,9 +18,7 @@ import { } from '../../utils/format.js' import { tailFile } from '../../utils/fsOperations.js' import { getTaskOutputPath } from '../../utils/task/diskOutput.js' -import { Byline } from '../design-system/Byline.js' -import { Dialog } from '../design-system/Dialog.js' -import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js' +import { Byline, Dialog, KeyboardShortcutHint } from '@anthropic/ink' type Props = { shell: DeepImmutable diff --git a/src/components/tasks/ShellProgress.tsx b/src/components/tasks/ShellProgress.tsx index b70494c16..d5da46f63 100644 --- a/src/components/tasks/ShellProgress.tsx +++ b/src/components/tasks/ShellProgress.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from 'react' import React from 'react' -import { Text } from 'src/ink.js' +import { Text } from '@anthropic/ink' import type { TaskStatus } from 'src/Task.js' import type { LocalShellTaskState } from 'src/tasks/LocalShellTask/guards.js' import type { DeepImmutable } from 'src/types/utils.js' diff --git a/src/components/tasks/renderToolActivity.tsx b/src/components/tasks/renderToolActivity.tsx index a6e1c60a2..70abaa1bb 100644 --- a/src/components/tasks/renderToolActivity.tsx +++ b/src/components/tasks/renderToolActivity.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Text } from '../../ink.js' +import { Text } from '@anthropic/ink' import type { Tools } from '../../Tool.js' import { findToolByName } from '../../Tool.js' import type { ToolActivity } from '../../tasks/LocalAgentTask/LocalAgentTask.js' diff --git a/src/components/teams/TeamStatus.tsx b/src/components/teams/TeamStatus.tsx index ee066875b..79bf89667 100644 --- a/src/components/teams/TeamStatus.tsx +++ b/src/components/teams/TeamStatus.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Text } from '../../ink.js' +import { Text } from '@anthropic/ink' import { useAppState } from '../../state/AppState.js' type Props = { diff --git a/src/components/teams/TeamsDialog.tsx b/src/components/teams/TeamsDialog.tsx index 872212115..a21caaebe 100644 --- a/src/components/teams/TeamsDialog.tsx +++ b/src/components/teams/TeamsDialog.tsx @@ -4,9 +4,8 @@ import * as React from 'react' import { useCallback, useEffect, useMemo, useState } from 'react' import { useInterval } from 'usehooks-ts' import { useRegisterOverlay } from '../../context/overlayContext.js' -import { stringWidth } from '../../ink/stringWidth.js' // eslint-disable-next-line custom-rules/prefer-use-keybindings -- raw j/k/arrow dialog navigation -import { Box, Text, useInput } from '../../ink.js' +import { Box, Text, useInput, stringWidth } from '@anthropic/ink' import { useKeybindings } from '../../keybindings/useKeybinding.js' import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js' import { @@ -63,7 +62,7 @@ import { sendShutdownRequestToMailbox, writeToMailbox, } from '../../utils/teammateMailbox.js' -import { Dialog } from '../design-system/Dialog.js' +import { Dialog } from '@anthropic/ink' import ThemedText from '../design-system/ThemedText.js' type Props = { diff --git a/src/components/ui/OrderedList.tsx b/src/components/ui/OrderedList.tsx index ef468156f..dde5e5998 100644 --- a/src/components/ui/OrderedList.tsx +++ b/src/components/ui/OrderedList.tsx @@ -4,7 +4,7 @@ import React, { type ReactNode, useContext, } from 'react' -import { Box } from '../../ink.js' +import { Box } from '@anthropic/ink' import { OrderedListItem, OrderedListItemContext } from './OrderedListItem.js' const OrderedListContext = createContext({ marker: '' }) diff --git a/src/components/ui/OrderedListItem.tsx b/src/components/ui/OrderedListItem.tsx index f217f0eaf..7b60152c1 100644 --- a/src/components/ui/OrderedListItem.tsx +++ b/src/components/ui/OrderedListItem.tsx @@ -1,5 +1,5 @@ import React, { createContext, type ReactNode, useContext } from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' export const OrderedListItemContext = createContext({ marker: '' }) diff --git a/src/components/ui/TreeSelect.tsx b/src/components/ui/TreeSelect.tsx index 55ca33c88..9e6ec7f49 100644 --- a/src/components/ui/TreeSelect.tsx +++ b/src/components/ui/TreeSelect.tsx @@ -1,6 +1,5 @@ import React from 'react' -import type { KeyboardEvent } from '../../ink/events/keyboard-event.js' -import { Box } from '../../ink.js' +import { type KeyboardEvent, Box } from '@anthropic/ink' import { type OptionWithDescription, Select } from '../CustomSelect/select.js' export type TreeNode = { diff --git a/src/components/ultraplan/UltraplanChoiceDialog.tsx b/src/components/ultraplan/UltraplanChoiceDialog.tsx index f1fd40186..28a225088 100644 --- a/src/components/ultraplan/UltraplanChoiceDialog.tsx +++ b/src/components/ultraplan/UltraplanChoiceDialog.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { join } from 'path'; import { writeFile } from 'fs/promises'; import figures from 'figures'; -import { Box, Text, useInput, wrapText } from '../../ink.js'; +import { Box, Text, useInput, wrapText } from '@anthropic/ink'; import { useTerminalSize } from '../../hooks/useTerminalSize.js'; import { Select } from '../CustomSelect/select.js'; import { PermissionDialog } from '../permissions/PermissionDialog.js'; diff --git a/src/components/ultraplan/UltraplanLaunchDialog.tsx b/src/components/ultraplan/UltraplanLaunchDialog.tsx index bdba02de3..612384b58 100644 --- a/src/components/ultraplan/UltraplanLaunchDialog.tsx +++ b/src/components/ultraplan/UltraplanLaunchDialog.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Box, Text, Link } from '../../ink.js'; +import { Box, Text, Link } from '@anthropic/ink'; import { Select } from '../CustomSelect/select.js'; import { PermissionDialog } from '../permissions/PermissionDialog.js'; import { useAppState, useSetAppState } from '../../state/AppState.js'; diff --git a/src/components/wizard/WizardDialogLayout.tsx b/src/components/wizard/WizardDialogLayout.tsx index 34f20a261..0a03b255e 100644 --- a/src/components/wizard/WizardDialogLayout.tsx +++ b/src/components/wizard/WizardDialogLayout.tsx @@ -1,6 +1,6 @@ import React, { type ReactNode } from 'react' import type { Theme } from '../../utils/theme.js' -import { Dialog } from '../design-system/Dialog.js' +import { Dialog } from '@anthropic/ink' import { useWizard } from './useWizard.js' import { WizardNavigationFooter } from './WizardNavigationFooter.js' diff --git a/src/components/wizard/WizardNavigationFooter.tsx b/src/components/wizard/WizardNavigationFooter.tsx index 35a03ee81..96c547c65 100644 --- a/src/components/wizard/WizardNavigationFooter.tsx +++ b/src/components/wizard/WizardNavigationFooter.tsx @@ -1,9 +1,8 @@ import React, { type ReactNode } from 'react' import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js' -import { Byline } from '../design-system/Byline.js' -import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js' +import { Byline, KeyboardShortcutHint } from '@anthropic/ink' type Props = { instructions?: ReactNode diff --git a/src/context/QueuedMessageContext.tsx b/src/context/QueuedMessageContext.tsx index 575fc8619..d78c9487b 100644 --- a/src/context/QueuedMessageContext.tsx +++ b/src/context/QueuedMessageContext.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Box } from '../ink.js' +import { Box } from '@anthropic/ink' type QueuedMessageContextValue = { isQueued: boolean diff --git a/src/context/modalContext.tsx b/src/context/modalContext.tsx index b2263a071..7d8498784 100644 --- a/src/context/modalContext.tsx +++ b/src/context/modalContext.tsx @@ -1,5 +1,5 @@ import { createContext, type RefObject, useContext } from 'react' -import type { ScrollBoxHandle } from '../ink/components/ScrollBox.js' +import type { ScrollBoxHandle } from '@anthropic/ink' /** * Set by FullscreenLayout when rendering content in its `modal` slot — diff --git a/src/context/overlayContext.tsx b/src/context/overlayContext.tsx index 602c1268d..406e03f98 100644 --- a/src/context/overlayContext.tsx +++ b/src/context/overlayContext.tsx @@ -13,7 +13,7 @@ * so no manual cleanup or state management is needed. */ import { useContext, useEffect, useLayoutEffect } from 'react' -import instances from '../ink/instances.js' +import { instances } from '@anthropic/ink' import { AppStoreContext, useAppState } from '../state/AppState.js' // Non-modal overlays that shouldn't disable TextInput focus diff --git a/src/dialogLaunchers.tsx b/src/dialogLaunchers.tsx index 3d2e01e14..ab903ca1b 100644 --- a/src/dialogLaunchers.tsx +++ b/src/dialogLaunchers.tsx @@ -9,7 +9,7 @@ import React from 'react' import type { AssistantSession } from './assistant/sessionDiscovery.js' import type { StatsStore } from './context/stats.js' -import type { Root } from './ink.js' +import type { Root } from '@anthropic/ink' import { renderAndRun, showSetupDialog } from './interactiveHelpers.js' import { KeybindingSetup } from './keybindings/KeybindingProviderSetup.js' import type { AppState } from './state/AppStateStore.js' @@ -168,7 +168,7 @@ export async function launchTeleportRepoMismatchDialog( /** * Site ~4903: ResumeConversation mount (interactive session picker). - * Uses renderAndRun, NOT showSetupDialog. Wraps in . + * Wraps in and uses renderAndRun. * Preserves original Promise.all parallelism between getWorktreePaths and imports. */ export async function launchResumeChooser( diff --git a/src/hooks/notifs/useCanSwitchToExistingSubscription.tsx b/src/hooks/notifs/useCanSwitchToExistingSubscription.tsx index 0d70d5a2b..37d8542b0 100644 --- a/src/hooks/notifs/useCanSwitchToExistingSubscription.tsx +++ b/src/hooks/notifs/useCanSwitchToExistingSubscription.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { getOauthProfileFromApiKey } from 'src/services/oauth/getOauthProfile.js' import { isClaudeAISubscriber } from 'src/utils/auth.js' -import { Text } from '../../ink.js' +import { Text } from '@anthropic/ink' import { logEvent } from '../../services/analytics/index.js' import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js' import { useStartupNotification } from './useStartupNotification.js' diff --git a/src/hooks/notifs/useIDEStatusIndicator.tsx b/src/hooks/notifs/useIDEStatusIndicator.tsx index 4be07f551..70175d10f 100644 --- a/src/hooks/notifs/useIDEStatusIndicator.tsx +++ b/src/hooks/notifs/useIDEStatusIndicator.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef } from 'react' import { useNotifications } from 'src/context/notifications.js' -import { Text } from 'src/ink.js' +import { Text } from '@anthropic/ink' import type { MCPServerConnection } from 'src/services/mcp/types.js' import { getGlobalConfig, saveGlobalConfig } from 'src/utils/config.js' import { diff --git a/src/hooks/notifs/useLspInitializationNotification.tsx b/src/hooks/notifs/useLspInitializationNotification.tsx index f86243a51..7b096f2b0 100644 --- a/src/hooks/notifs/useLspInitializationNotification.tsx +++ b/src/hooks/notifs/useLspInitializationNotification.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { useInterval } from 'usehooks-ts' import { getIsRemoteMode, getIsScrollDraining } from '../../bootstrap/state.js' import { useNotifications } from '../../context/notifications.js' -import { Text } from '../../ink.js' +import { Text } from '@anthropic/ink' import { getInitializationStatus, getLspServerManager, diff --git a/src/hooks/notifs/useMcpConnectivityStatus.tsx b/src/hooks/notifs/useMcpConnectivityStatus.tsx index 83072ba0f..89adb8382 100644 --- a/src/hooks/notifs/useMcpConnectivityStatus.tsx +++ b/src/hooks/notifs/useMcpConnectivityStatus.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { useEffect } from 'react' import { useNotifications } from 'src/context/notifications.js' import { getIsRemoteMode } from '../../bootstrap/state.js' -import { Text } from '../../ink.js' +import { Text } from '@anthropic/ink' import { hasClaudeAiMcpEverConnected } from '../../services/mcp/claudeai.js' import type { MCPServerConnection } from '../../services/mcp/types.js' diff --git a/src/hooks/notifs/usePluginAutoupdateNotification.tsx b/src/hooks/notifs/usePluginAutoupdateNotification.tsx index bec229328..7468e8ba0 100644 --- a/src/hooks/notifs/usePluginAutoupdateNotification.tsx +++ b/src/hooks/notifs/usePluginAutoupdateNotification.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { useEffect, useState } from 'react' import { getIsRemoteMode } from '../../bootstrap/state.js' import { useNotifications } from '../../context/notifications.js' -import { Text } from '../../ink.js' +import { Text } from '@anthropic/ink' import { logForDebugging } from '../../utils/debug.js' import { onPluginsAutoUpdated } from '../../utils/plugins/pluginAutoupdate.js' diff --git a/src/hooks/notifs/usePluginInstallationStatus.tsx b/src/hooks/notifs/usePluginInstallationStatus.tsx index 20055403d..9ab3de8fe 100644 --- a/src/hooks/notifs/usePluginInstallationStatus.tsx +++ b/src/hooks/notifs/usePluginInstallationStatus.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { useEffect, useMemo } from 'react' import { getIsRemoteMode } from '../../bootstrap/state.js' import { useNotifications } from '../../context/notifications.js' -import { Text } from '../../ink.js' +import { Text } from '@anthropic/ink' import { useAppState } from '../../state/AppState.js' import { logForDebugging } from '../../utils/debug.js' import { plural } from '../../utils/stringUtils.js' diff --git a/src/hooks/notifs/useRateLimitWarningNotification.tsx b/src/hooks/notifs/useRateLimitWarningNotification.tsx index bfd3f193f..0e8520e96 100644 --- a/src/hooks/notifs/useRateLimitWarningNotification.tsx +++ b/src/hooks/notifs/useRateLimitWarningNotification.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { useEffect, useMemo, useRef, useState } from 'react' import { useNotifications } from 'src/context/notifications.js' -import { Text } from 'src/ink.js' +import { Text } from '@anthropic/ink' import { getRateLimitWarning, getUsingOverageText, diff --git a/src/hooks/toolPermission/handlers/interactiveHandler.ts b/src/hooks/toolPermission/handlers/interactiveHandler.ts index 6b3e4e80d..3439b9fcd 100644 --- a/src/hooks/toolPermission/handlers/interactiveHandler.ts +++ b/src/hooks/toolPermission/handlers/interactiveHandler.ts @@ -4,7 +4,7 @@ import { randomUUID } from 'crypto' import { logForDebugging } from 'src/utils/debug.js' import { getAllowedChannels } from '../../../bootstrap/state.js' import type { BridgePermissionCallbacks } from '../../../bridge/bridgePermissionCallbacks.js' -import { getTerminalFocused } from '../../../ink/terminal-focus-state.js' +import { getTerminalFocused } from '@anthropic/ink' import { CHANNEL_PERMISSION_REQUEST_METHOD, type ChannelPermissionRequestParams, diff --git a/src/hooks/useArrowKeyHistory.tsx b/src/hooks/useArrowKeyHistory.tsx index 69e7d6460..d874e32b0 100644 --- a/src/hooks/useArrowKeyHistory.tsx +++ b/src/hooks/useArrowKeyHistory.tsx @@ -4,7 +4,7 @@ import { useNotifications } from 'src/context/notifications.js' import { ConfigurableShortcutHint } from '../components/ConfigurableShortcutHint.js' import { FOOTER_TEMPORARY_STATUS_TIMEOUT } from '../components/PromptInput/Notifications.js' import { getHistory } from '../history.js' -import { Text } from '../ink.js' +import { Text } from '@anthropic/ink' import type { PromptInputMode } from '../types/textInputTypes.js' import type { HistoryEntry, PastedContent } from '../utils/config.js' diff --git a/src/hooks/useAssistantHistory.ts b/src/hooks/useAssistantHistory.ts index c5348d20e..36da73b92 100644 --- a/src/hooks/useAssistantHistory.ts +++ b/src/hooks/useAssistantHistory.ts @@ -13,7 +13,7 @@ import { type HistoryAuthCtx, type HistoryPage, } from '../assistant/sessionHistory.js' -import type { ScrollBoxHandle } from '../ink/components/ScrollBox.js' +import type { ScrollBoxHandle } from '@anthropic/ink' import type { RemoteSessionConfig } from '../remote/RemoteSessionManager.js' import { convertSDKMessage } from '../remote/sdkMessageAdapter.js' import type { Message, SystemInformationalMessage } from '../types/message.js' diff --git a/src/hooks/useAwaySummary.ts b/src/hooks/useAwaySummary.ts index 115209ae6..9caf55f24 100644 --- a/src/hooks/useAwaySummary.ts +++ b/src/hooks/useAwaySummary.ts @@ -1,9 +1,6 @@ import { feature } from 'bun:bundle' import { useEffect, useRef } from 'react' -import { - getTerminalFocusState, - subscribeTerminalFocus, -} from '../ink/terminal-focus-state.js' +import { getTerminalFocusState, subscribeTerminalFocus } from '@anthropic/ink' import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' import { generateAwaySummary } from '../services/awaySummary.js' import type { Message } from '../types/message.js' diff --git a/src/hooks/useBackgroundTaskNavigation.ts b/src/hooks/useBackgroundTaskNavigation.ts index c1a9b14fd..ad94c641d 100644 --- a/src/hooks/useBackgroundTaskNavigation.ts +++ b/src/hooks/useBackgroundTaskNavigation.ts @@ -1,7 +1,6 @@ import { useEffect, useRef } from 'react' -import { KeyboardEvent } from '../ink/events/keyboard-event.js' -// eslint-disable-next-line custom-rules/prefer-use-keybindings -- backward-compat bridge until REPL wires handleKeyDown to -import { useInput } from '../ink.js' +import { KeyboardEvent, useInput } from '@anthropic/ink' +// backward-compat bridge until REPL wires handleKeyDown to import { type AppState, useAppState, diff --git a/src/hooks/useBlink.ts b/src/hooks/useBlink.ts index 33cac6974..cecf54c53 100644 --- a/src/hooks/useBlink.ts +++ b/src/hooks/useBlink.ts @@ -1,4 +1,4 @@ -import { type DOMElement, useAnimationFrame, useTerminalFocus } from '../ink.js' +import { type DOMElement, useAnimationFrame, useTerminalFocus } from '@anthropic/ink' const BLINK_INTERVAL_MS = 600 diff --git a/src/hooks/useCanUseTool.tsx b/src/hooks/useCanUseTool.tsx index 78b9397c0..cf8af94f8 100644 --- a/src/hooks/useCanUseTool.tsx +++ b/src/hooks/useCanUseTool.tsx @@ -8,7 +8,7 @@ import { } from 'src/services/analytics/index.js' import { sanitizeToolNameForAnalytics } from 'src/services/analytics/metadata.js' import type { ToolUseConfirm } from '../components/permissions/PermissionRequest.js' -import { Text } from '../ink.js' +import { Text } from '@anthropic/ink' import type { ToolPermissionContext, Tool as ToolType, diff --git a/src/hooks/useChromeExtensionNotification.tsx b/src/hooks/useChromeExtensionNotification.tsx index dc058df0e..4f043de2e 100644 --- a/src/hooks/useChromeExtensionNotification.tsx +++ b/src/hooks/useChromeExtensionNotification.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Text } from '../ink.js' +import { Text } from '@anthropic/ink' import { isClaudeAISubscriber } from '../utils/auth.js' import { isChromeExtensionInstalled, diff --git a/src/hooks/useCopyOnSelect.ts b/src/hooks/useCopyOnSelect.ts index 778ef5a1d..e1a239009 100644 --- a/src/hooks/useCopyOnSelect.ts +++ b/src/hooks/useCopyOnSelect.ts @@ -1,6 +1,6 @@ import { useEffect, useRef } from 'react' -import { useTheme } from '../components/design-system/ThemeProvider.js' -import type { useSelection } from '../ink/hooks/use-selection.js' +import { useTheme } from '@anthropic/ink' +import type { useSelection } from '@anthropic/ink' import { getGlobalConfig } from '../utils/config.js' import { getTheme } from '../utils/theme.js' diff --git a/src/hooks/useDoublePress.ts b/src/hooks/useDoublePress.ts index 7844fbd66..65fb00440 100644 --- a/src/hooks/useDoublePress.ts +++ b/src/hooks/useDoublePress.ts @@ -1,62 +1,3 @@ -// Creates a function that calls one function on the first call and another -// function on the second call within a certain timeout - -import { useCallback, useEffect, useRef } from 'react' - -export const DOUBLE_PRESS_TIMEOUT_MS = 800 - -export function useDoublePress( - setPending: (pending: boolean) => void, - onDoublePress: () => void, - onFirstPress?: () => void, -): () => void { - const lastPressRef = useRef(0) - const timeoutRef = useRef(undefined) - - const clearTimeoutSafe = useCallback(() => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current) - timeoutRef.current = undefined - } - }, []) - - // Cleanup timeout on unmount - useEffect(() => { - return () => { - clearTimeoutSafe() - } - }, [clearTimeoutSafe]) - - return useCallback(() => { - const now = Date.now() - const timeSinceLastPress = now - lastPressRef.current - const isDoublePress = - timeSinceLastPress <= DOUBLE_PRESS_TIMEOUT_MS && - timeoutRef.current !== undefined - - if (isDoublePress) { - // Double press detected - clearTimeoutSafe() - setPending(false) - onDoublePress() - } else { - // First press - onFirstPress?.() - setPending(true) - - // Clear any existing timeout and set new one - clearTimeoutSafe() - timeoutRef.current = setTimeout( - (setPending, timeoutRef) => { - setPending(false) - timeoutRef.current = undefined - }, - DOUBLE_PRESS_TIMEOUT_MS, - setPending, - timeoutRef, - ) - } - - lastPressRef.current = now - }, [setPending, onDoublePress, onFirstPress, clearTimeoutSafe]) -} +// Re-export from @anthropic/ink hooks module +export { useDoublePress } from '@anthropic/ink' +export { DOUBLE_PRESS_TIMEOUT_MS } from '@anthropic/ink' diff --git a/src/hooks/useExitOnCtrlCD.ts b/src/hooks/useExitOnCtrlCD.ts index 23ba7ad58..e9af0157c 100644 --- a/src/hooks/useExitOnCtrlCD.ts +++ b/src/hooks/useExitOnCtrlCD.ts @@ -1,5 +1,5 @@ import { useCallback, useMemo, useState } from 'react' -import useApp from '../ink/hooks/use-app.js' +import { useApp } from '@anthropic/ink' import type { KeybindingContextName } from '../keybindings/types.js' import { useDoublePress } from './useDoublePress.js' diff --git a/src/hooks/useGlobalKeybindings.tsx b/src/hooks/useGlobalKeybindings.tsx index a41b1b6a5..1dd171cb8 100644 --- a/src/hooks/useGlobalKeybindings.tsx +++ b/src/hooks/useGlobalKeybindings.tsx @@ -6,7 +6,7 @@ */ import { feature } from 'bun:bundle' import { useCallback } from 'react' -import instances from '../ink/instances.js' +import { instances } from '@anthropic/ink' import { useKeybinding } from '../keybindings/useKeybinding.js' import type { Screen } from '../screens/REPL.js' import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' diff --git a/src/hooks/useHistorySearch.ts b/src/hooks/useHistorySearch.ts index b48c880b7..04bf31b67 100644 --- a/src/hooks/useHistorySearch.ts +++ b/src/hooks/useHistorySearch.ts @@ -5,9 +5,8 @@ import { getValueFromInput, } from '../components/PromptInput/inputModes.js' import { makeHistoryReader } from '../history.js' -import { KeyboardEvent } from '../ink/events/keyboard-event.js' -// eslint-disable-next-line custom-rules/prefer-use-keybindings -- backward-compat bridge until consumers wire handleKeyDown to -import { useInput } from '../ink.js' +import { KeyboardEvent, useInput } from '@anthropic/ink' +// backward-compat bridge until consumers wire handleKeyDown to import { useKeybinding, useKeybindings } from '../keybindings/useKeybinding.js' import type { PromptInputMode } from '../types/textInputTypes.js' import type { HistoryEntry } from '../utils/config.js' diff --git a/src/hooks/useInboxPoller.ts b/src/hooks/useInboxPoller.ts index 361ba636d..ac2c98555 100644 --- a/src/hooks/useInboxPoller.ts +++ b/src/hooks/useInboxPoller.ts @@ -3,7 +3,7 @@ import { useCallback, useEffect, useRef } from 'react' import { useInterval } from 'usehooks-ts' import type { ToolUseConfirm } from '../components/permissions/PermissionRequest.js' import { TEAMMATE_MESSAGE_TAG } from '../constants/xml.js' -import { useTerminalNotification } from '../ink/useTerminalNotification.js' +import { useTerminalNotification } from '@anthropic/ink' import { sendNotification } from '../services/notifier.js' import { type AppState, diff --git a/src/hooks/useMinDisplayTime.ts b/src/hooks/useMinDisplayTime.ts index 587b96938..92fc51005 100644 --- a/src/hooks/useMinDisplayTime.ts +++ b/src/hooks/useMinDisplayTime.ts @@ -1,35 +1,2 @@ -import { useEffect, useRef, useState } from 'react' - -/** - * Throttles a value so each distinct value stays visible for at least `minMs`. - * Prevents fast-cycling progress text from flickering past before it's readable. - * - * Unlike debounce (wait for quiet) or throttle (limit rate), this guarantees - * each value gets its minimum screen time before being replaced. - */ -export function useMinDisplayTime(value: T, minMs: number): T { - const [displayed, setDisplayed] = useState(value) - const lastShownAtRef = useRef(0) - - useEffect(() => { - const elapsed = Date.now() - lastShownAtRef.current - if (elapsed >= minMs) { - lastShownAtRef.current = Date.now() - setDisplayed(value) - return - } - const timer = setTimeout( - (shownAtRef, setFn, v) => { - shownAtRef.current = Date.now() - setFn(v) - }, - minMs - elapsed, - lastShownAtRef, - setDisplayed, - value, - ) - return () => clearTimeout(timer) - }, [value, minMs]) - - return displayed -} +// Re-export from @anthropic/ink hooks module +export { useMinDisplayTime } from '@anthropic/ink' diff --git a/src/hooks/useNotifyAfterTimeout.ts b/src/hooks/useNotifyAfterTimeout.ts index 8b0ce315c..c9c19f191 100644 --- a/src/hooks/useNotifyAfterTimeout.ts +++ b/src/hooks/useNotifyAfterTimeout.ts @@ -3,7 +3,7 @@ import { getLastInteractionTime, updateLastInteractionTime, } from '../bootstrap/state.js' -import { useTerminalNotification } from '../ink/useTerminalNotification.js' +import { useTerminalNotification } from '@anthropic/ink' import { sendNotification } from '../services/notifier.js' // The time threshold in milliseconds for considering an interaction "recent" (6 seconds) export const DEFAULT_INTERACTION_THRESHOLD_MS = 6000 diff --git a/src/hooks/useOfficialMarketplaceNotification.tsx b/src/hooks/useOfficialMarketplaceNotification.tsx index 25cf62254..6ba179e9b 100644 --- a/src/hooks/useOfficialMarketplaceNotification.tsx +++ b/src/hooks/useOfficialMarketplaceNotification.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import type { Notification } from '../context/notifications.js' -import { Text } from '../ink.js' +import { Text } from '@anthropic/ink' import { logForDebugging } from '../utils/debug.js' import { checkAndInstallOfficialMarketplace } from '../utils/plugins/officialMarketplaceStartupCheck.js' import { useStartupNotification } from './notifs/useStartupNotification.js' diff --git a/src/hooks/usePasteHandler.ts b/src/hooks/usePasteHandler.ts index d6257b9a2..a2ed7f306 100644 --- a/src/hooks/usePasteHandler.ts +++ b/src/hooks/usePasteHandler.ts @@ -2,7 +2,7 @@ import { basename } from 'path' import React from 'react' import { logError } from 'src/utils/log.js' import { useDebounceCallback } from 'usehooks-ts' -import type { InputEvent, Key } from '../ink.js' +import type { InputEvent, Key } from '@anthropic/ink' import { getImageFromClipboard, isImageFilePath, diff --git a/src/hooks/usePluginRecommendationBase.tsx b/src/hooks/usePluginRecommendationBase.tsx index 23930fba4..1e2fd7b5d 100644 --- a/src/hooks/usePluginRecommendationBase.tsx +++ b/src/hooks/usePluginRecommendationBase.tsx @@ -8,7 +8,7 @@ import figures from 'figures' import * as React from 'react' import { getIsRemoteMode } from '../bootstrap/state.js' import type { useNotifications } from '../context/notifications.js' -import { Text } from '../ink.js' +import { Text } from '@anthropic/ink' import { logError } from '../utils/log.js' import { getPluginById } from '../utils/plugins/marketplaceManager.js' diff --git a/src/hooks/usePromptSuggestion.ts b/src/hooks/usePromptSuggestion.ts index 0a0a35f9c..e080314a8 100644 --- a/src/hooks/usePromptSuggestion.ts +++ b/src/hooks/usePromptSuggestion.ts @@ -1,5 +1,5 @@ import { useCallback, useRef } from 'react' -import { useTerminalFocus } from '../ink/hooks/use-terminal-focus.js' +import { useTerminalFocus } from '@anthropic/ink' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, diff --git a/src/hooks/useReplBridge.tsx b/src/hooks/useReplBridge.tsx index 522202891..7be394869 100644 --- a/src/hooks/useReplBridge.tsx +++ b/src/hooks/useReplBridge.tsx @@ -19,7 +19,7 @@ import type { SDKMessage, } from '../entrypoints/agentSdkTypes.js' import type { SDKControlResponse } from '../entrypoints/sdk/controlTypes.js' -import { Text } from '../ink.js' +import { Text } from '@anthropic/ink' import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' import { useAppState, diff --git a/src/hooks/useSearchInput.ts b/src/hooks/useSearchInput.ts index a72fbf4b4..41860b0eb 100644 --- a/src/hooks/useSearchInput.ts +++ b/src/hooks/useSearchInput.ts @@ -1,7 +1,6 @@ import { useCallback, useState } from 'react' -import { KeyboardEvent } from '../ink/events/keyboard-event.js' -// eslint-disable-next-line custom-rules/prefer-use-keybindings -- backward-compat bridge until consumers wire handleKeyDown to -import { useInput } from '../ink.js' +import { KeyboardEvent, useInput } from '@anthropic/ink' +// backward-compat bridge until consumers wire handleKeyDown to import { Cursor, getLastKill, diff --git a/src/hooks/useTerminalSize.ts b/src/hooks/useTerminalSize.ts index 68e24df87..ca3630ba6 100644 --- a/src/hooks/useTerminalSize.ts +++ b/src/hooks/useTerminalSize.ts @@ -1,15 +1,2 @@ -import { useContext } from 'react' -import { - type TerminalSize, - TerminalSizeContext, -} from 'src/ink/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 -} +// Re-export from @anthropic/ink hooks module +export { useTerminalSize } from '@anthropic/ink' diff --git a/src/hooks/useTextInput.ts b/src/hooks/useTextInput.ts index 90c4c4f82..21e0dbf19 100644 --- a/src/hooks/useTextInput.ts +++ b/src/hooks/useTextInput.ts @@ -3,7 +3,7 @@ import { useNotifications } from 'src/context/notifications.js' import stripAnsi from 'strip-ansi' import { markBackslashReturnUsed } from '../commands/terminalSetup/terminalSetup.js' import { addToHistory } from '../history.js' -import type { Key } from '../ink.js' +import type { Key } from '@anthropic/ink' import type { InlineGhostText, TextInputState, diff --git a/src/hooks/useTimeout.ts b/src/hooks/useTimeout.ts index faed236af..6d5cb83a8 100644 --- a/src/hooks/useTimeout.ts +++ b/src/hooks/useTimeout.ts @@ -1,14 +1,2 @@ -import { useEffect, useState } from 'react' - -export function useTimeout(delay: number, resetTrigger?: number): boolean { - const [isElapsed, setIsElapsed] = useState(false) - - useEffect(() => { - setIsElapsed(false) - const timer = setTimeout(setIsElapsed, delay, true) - - return () => clearTimeout(timer) - }, [delay, resetTrigger]) - - return isElapsed -} +// Re-export from @anthropic/ink hooks module +export { useTimeout } from '@anthropic/ink' diff --git a/src/hooks/useTypeahead.tsx b/src/hooks/useTypeahead.tsx index 0623c2fb4..d9bf1a06a 100644 --- a/src/hooks/useTypeahead.tsx +++ b/src/hooks/useTypeahead.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useNotifications } from 'src/context/notifications.js' -import { Text } from 'src/ink.js' +import { Text } from '@anthropic/ink' import { logEvent } from 'src/services/analytics/index.js' import { useDebounceCallback } from 'usehooks-ts' import { type Command, getCommandName } from '../commands.js' @@ -17,9 +17,8 @@ import { useIsModalOverlayActive, useRegisterOverlay, } from '../context/overlayContext.js' -import { KeyboardEvent } from '../ink/events/keyboard-event.js' -// eslint-disable-next-line custom-rules/prefer-use-keybindings -- backward-compat bridge until consumers wire handleKeyDown to -import { useInput } from '../ink.js' +import { KeyboardEvent, useInput } from '@anthropic/ink' +// backward-compat bridge until consumers wire handleKeyDown to import { useOptionalKeybindingContext, useRegisterKeybindingContext, diff --git a/src/hooks/useVimInput.ts b/src/hooks/useVimInput.ts index 0aabc9117..f49bb5003 100644 --- a/src/hooks/useVimInput.ts +++ b/src/hooks/useVimInput.ts @@ -1,5 +1,5 @@ import React, { useCallback, useState } from 'react' -import type { Key } from '../ink.js' +import type { Key } from '@anthropic/ink' import type { VimInputState, VimMode } from '../types/textInputTypes.js' import { Cursor } from '../utils/Cursor.js' import { lastGrapheme } from '../utils/intl.js' diff --git a/src/hooks/useVirtualScroll.ts b/src/hooks/useVirtualScroll.ts index 388b0bad9..bdfe337dd 100644 --- a/src/hooks/useVirtualScroll.ts +++ b/src/hooks/useVirtualScroll.ts @@ -7,8 +7,7 @@ import { useRef, useSyncExternalStore, } from 'react' -import type { ScrollBoxHandle } from '../ink/components/ScrollBox.js' -import type { DOMElement } from '../ink/dom.js' +import type { ScrollBoxHandle, DOMElement } from '@anthropic/ink' /** * Estimated height (rows) for items not yet measured. Intentionally LOW: diff --git a/src/hooks/useVoice.ts b/src/hooks/useVoice.ts index 30c099170..0ac154e37 100644 --- a/src/hooks/useVoice.ts +++ b/src/hooks/useVoice.ts @@ -8,7 +8,7 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { useSetVoiceState } from '../context/voice.js' -import { useTerminalFocus } from '../ink/hooks/use-terminal-focus.js' +import { useTerminalFocus } from '@anthropic/ink' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, diff --git a/src/hooks/useVoiceIntegration.tsx b/src/hooks/useVoiceIntegration.tsx index 7cedb1c0f..b798a61ae 100644 --- a/src/hooks/useVoiceIntegration.tsx +++ b/src/hooks/useVoiceIntegration.tsx @@ -8,9 +8,8 @@ import { useSetVoiceState, useVoiceState, } from '../context/voice.js' -import { KeyboardEvent } from '../ink/events/keyboard-event.js' -// eslint-disable-next-line custom-rules/prefer-use-keybindings -- backward-compat bridge until REPL wires handleKeyDown to -import { useInput } from '../ink.js' +import { KeyboardEvent, useInput } from '@anthropic/ink' +// backward-compat bridge until REPL wires handleKeyDown to import { useOptionalKeybindingContext } from '../keybindings/KeybindingContext.js' import { keystrokesEqual } from '../keybindings/resolver.js' import type { ParsedKeystroke } from '../keybindings/types.js' diff --git a/src/ink.ts b/src/ink.ts deleted file mode 100644 index a06b343cc..000000000 --- a/src/ink.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { createElement, type ReactNode } from 'react' -import { ThemeProvider } from './components/design-system/ThemeProvider.js' -import inkRender, { - type Instance, - createRoot as inkCreateRoot, - type RenderOptions, - type Root, -} from './ink/root.js' - -export type { RenderOptions, Instance, Root } - -// Wrap all CC render calls with ThemeProvider so ThemedBox/ThemedText work -// without every call site having to mount it. Ink itself is theme-agnostic. -function withTheme(node: ReactNode): ReactNode { - return createElement(ThemeProvider, null, node) -} - -export async function render( - node: ReactNode, - options?: NodeJS.WriteStream | RenderOptions, -): Promise { - return inkRender(withTheme(node), options) -} - -export async function createRoot(options?: RenderOptions): Promise { - const root = await inkCreateRoot(options) - return { - ...root, - render: node => root.render(withTheme(node)), - } -} - -export { color } from './components/design-system/color.js' -export type { Props as BoxProps } from './components/design-system/ThemedBox.js' -export { default as Box } from './components/design-system/ThemedBox.js' -export type { Props as TextProps } from './components/design-system/ThemedText.js' -export { default as Text } from './components/design-system/ThemedText.js' -export { - ThemeProvider, - usePreviewTheme, - useTheme, - useThemeSetting, -} from './components/design-system/ThemeProvider.js' -export { Ansi } from './ink/Ansi.js' -export type { Props as AppProps } from './ink/components/AppContext.js' -export type { Props as BaseBoxProps } from './ink/components/Box.js' -export { default as BaseBox } from './ink/components/Box.js' -export type { - ButtonState, - Props as ButtonProps, -} from './ink/components/Button.js' -export { default as Button } from './ink/components/Button.js' -export type { Props as LinkProps } from './ink/components/Link.js' -export { default as Link } from './ink/components/Link.js' -export type { Props as NewlineProps } from './ink/components/Newline.js' -export { default as Newline } from './ink/components/Newline.js' -export { NoSelect } from './ink/components/NoSelect.js' -export { RawAnsi } from './ink/components/RawAnsi.js' -export { default as Spacer } from './ink/components/Spacer.js' -export type { Props as StdinProps } from './ink/components/StdinContext.js' -export type { Props as BaseTextProps } from './ink/components/Text.js' -export { default as BaseText } from './ink/components/Text.js' -export type { DOMElement } from './ink/dom.js' -export { ClickEvent } from './ink/events/click-event.js' -export { EventEmitter } from './ink/events/emitter.js' -export { Event } from './ink/events/event.js' -export type { Key } from './ink/events/input-event.js' -export { InputEvent } from './ink/events/input-event.js' -export type { TerminalFocusEventType } from './ink/events/terminal-focus-event.js' -export { TerminalFocusEvent } from './ink/events/terminal-focus-event.js' -export { FocusManager } from './ink/focus.js' -export type { FlickerReason } from './ink/frame.js' -export { useAnimationFrame } from './ink/hooks/use-animation-frame.js' -export { default as useApp } from './ink/hooks/use-app.js' -export { default as useInput } from './ink/hooks/use-input.js' -export { useAnimationTimer, useInterval } from './ink/hooks/use-interval.js' -export { useSelection } from './ink/hooks/use-selection.js' -export { default as useStdin } from './ink/hooks/use-stdin.js' -export { useTabStatus } from './ink/hooks/use-tab-status.js' -export { useTerminalFocus } from './ink/hooks/use-terminal-focus.js' -export { useTerminalTitle } from './ink/hooks/use-terminal-title.js' -export { useTerminalViewport } from './ink/hooks/use-terminal-viewport.js' -export { default as measureElement } from './ink/measure-element.js' -export { supportsTabStatus } from './ink/termio/osc.js' -export { default as wrapText } from './ink/wrap-text.js' diff --git a/src/ink/src/bootstrap/state.ts b/src/ink/src/bootstrap/state.ts deleted file mode 100644 index 875ce2bdc..000000000 --- a/src/ink/src/bootstrap/state.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Auto-generated type stub — replace with real implementation -export type flushInteractionTime = any; diff --git a/src/ink/src/native-ts/yoga-layout/index.ts b/src/ink/src/native-ts/yoga-layout/index.ts deleted file mode 100644 index c75a2b090..000000000 --- a/src/ink/src/native-ts/yoga-layout/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Auto-generated type stub — replace with real implementation -export type getYogaCounters = any; diff --git a/src/ink/src/utils/debug.ts b/src/ink/src/utils/debug.ts deleted file mode 100644 index c64d5960c..000000000 --- a/src/ink/src/utils/debug.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Auto-generated type stub — replace with real implementation -export type logForDebugging = any; diff --git a/src/ink/src/utils/log.ts b/src/ink/src/utils/log.ts deleted file mode 100644 index cf30e90da..000000000 --- a/src/ink/src/utils/log.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Auto-generated type stub — replace with real implementation -export type logError = any; diff --git a/src/interactiveHelpers.tsx b/src/interactiveHelpers.tsx index 4efd14124..7c66edd1a 100644 --- a/src/interactiveHelpers.tsx +++ b/src/interactiveHelpers.tsx @@ -18,8 +18,8 @@ import type { Command } from './commands.js' import { createStatsStore, type StatsStore } from './context/stats.js' import { getSystemContext } from './context.js' import { initializeTelemetryAfterTrust } from './entrypoints/init.js' -import { isSynchronizedOutputSupported } from './ink/terminal.js' -import type { RenderOptions, Root, TextProps } from './ink.js' +import { isSynchronizedOutputSupported } from '@anthropic/ink' +import type { RenderOptions, Root, TextProps } from '@anthropic/ink' import { KeybindingSetup } from './keybindings/KeybindingProviderSetup.js' import { startDeferredPrefetches } from './main.js' import { @@ -102,7 +102,7 @@ export async function exitWithMessage( beforeExit?: () => Promise }, ): Promise { - const { Text } = await import('./ink.js') + const { Text } = await import('@anthropic/ink') const color = options?.color const exitCode = options?.exitCode ?? 1 root.render( diff --git a/src/keybindings/KeybindingContext.tsx b/src/keybindings/KeybindingContext.tsx index bc85e81c0..10cbb8507 100644 --- a/src/keybindings/KeybindingContext.tsx +++ b/src/keybindings/KeybindingContext.tsx @@ -1,225 +1,7 @@ -import React, { - createContext, - type RefObject, - useContext, - useLayoutEffect, - useMemo, -} from 'react' -import type { Key } from '../ink.js' -import { - type ChordResolveResult, - getBindingDisplayText, - resolveKeyWithChordState, -} from './resolver.js' -import type { - KeybindingContextName, - ParsedBinding, - ParsedKeystroke, -} from './types.js' - -/** Handler registration for action callbacks */ -type HandlerRegistration = { - action: string - context: KeybindingContextName - handler: () => void -} - -type KeybindingContextValue = { - /** Resolve a key input to an action name (with chord support) */ - resolve: ( - input: string, - key: Key, - activeContexts: KeybindingContextName[], - ) => ChordResolveResult - - /** Update the pending chord state */ - setPendingChord: (pending: ParsedKeystroke[] | null) => void - - /** Get display text for an action (e.g., "ctrl+t") */ - getDisplayText: ( - action: string, - context: KeybindingContextName, - ) => string | undefined - - /** All parsed bindings (for help display) */ - bindings: ParsedBinding[] - - /** Current pending chord keystrokes (null if not in a chord) */ - pendingChord: ParsedKeystroke[] | null - - /** Currently active keybinding contexts (for priority resolution) */ - activeContexts: Set - - /** Register a context as active (call on mount) */ - registerActiveContext: (context: KeybindingContextName) => void - - /** Unregister a context (call on unmount) */ - unregisterActiveContext: (context: KeybindingContextName) => void - - /** Register a handler for an action (used by useKeybinding) */ - registerHandler: (registration: HandlerRegistration) => () => void - - /** Invoke all handlers for an action (used by ChordInterceptor) */ - invokeAction: (action: string) => boolean -} - -const KeybindingContext = createContext(null) - -type ProviderProps = { - bindings: ParsedBinding[] - /** Ref for immediate access to pending chord (avoids React state delay) */ - pendingChordRef: RefObject - /** State value for re-renders (UI updates) */ - pendingChord: ParsedKeystroke[] | null - setPendingChord: (pending: ParsedKeystroke[] | null) => void - activeContexts: Set - registerActiveContext: (context: KeybindingContextName) => void - unregisterActiveContext: (context: KeybindingContextName) => void - /** Ref to handler registry (used by ChordInterceptor) */ - handlerRegistryRef: RefObject>> - children: React.ReactNode -} - -export function KeybindingProvider({ - bindings, - pendingChordRef, - pendingChord, - setPendingChord, - activeContexts, - registerActiveContext, - unregisterActiveContext, - handlerRegistryRef, - children, -}: ProviderProps): React.ReactNode { - const value = useMemo(() => { - const getDisplay = (action: string, context: KeybindingContextName) => - getBindingDisplayText(action, context, bindings) - - // Register a handler for an action - const registerHandler = (registration: HandlerRegistration) => { - const registry = handlerRegistryRef.current - if (!registry) return () => {} - - if (!registry.has(registration.action)) { - registry.set(registration.action, new Set()) - } - registry.get(registration.action)!.add(registration) - - // Return unregister function - return () => { - const handlers = registry.get(registration.action) - if (handlers) { - handlers.delete(registration) - if (handlers.size === 0) { - registry.delete(registration.action) - } - } - } - } - - // Invoke all handlers for an action - const invokeAction = (action: string): boolean => { - const registry = handlerRegistryRef.current - if (!registry) return false - - const handlers = registry.get(action) - if (!handlers || handlers.size === 0) return false - - // Find handlers whose context is active - for (const registration of handlers) { - if (activeContexts.has(registration.context)) { - registration.handler() - return true - } - } - return false - } - - return { - // Use ref for immediate access to pending chord, avoiding React state delay - // This is critical for chord sequences where the second key might be pressed - // before React re-renders with the updated pendingChord state - resolve: (input, key, contexts) => - resolveKeyWithChordState( - input, - key, - contexts, - bindings, - pendingChordRef.current, - ), - setPendingChord, - getDisplayText: getDisplay, - bindings, - pendingChord, - activeContexts, - registerActiveContext, - unregisterActiveContext, - registerHandler, - invokeAction, - } - }, [ - bindings, - pendingChordRef, - pendingChord, - setPendingChord, - activeContexts, - registerActiveContext, - unregisterActiveContext, - handlerRegistryRef, - ]) - - return ( - - {children} - - ) -} - -export function useKeybindingContext(): KeybindingContextValue { - const ctx = useContext(KeybindingContext) - if (!ctx) { - throw new Error( - 'useKeybindingContext must be used within KeybindingProvider', - ) - } - return ctx -} - -/** - * Optional hook that returns undefined outside of KeybindingProvider. - * Useful for components that may render before provider is available. - */ -export function useOptionalKeybindingContext(): KeybindingContextValue | null { - return useContext(KeybindingContext) -} - -/** - * Hook to register a keybinding context as active while the component is mounted. - * - * When a context is registered, its keybindings take precedence over Global bindings. - * This allows context-specific bindings (like ThemePicker's ctrl+t) to override - * global bindings (like the todo toggle) when the context is active. - * - * @example - * ```tsx - * function ThemePicker() { - * useRegisterKeybindingContext('ThemePicker') - * // Now ThemePicker's ctrl+t binding takes precedence over Global - * } - * ``` - */ -export function useRegisterKeybindingContext( - context: KeybindingContextName, - isActive: boolean = true, -): void { - const keybindingContext = useOptionalKeybindingContext() - - useLayoutEffect(() => { - if (!keybindingContext || !isActive) return - - keybindingContext.registerActiveContext(context) - return () => { - keybindingContext.unregisterActiveContext(context) - } - }, [context, keybindingContext, isActive]) -} +// Re-export from @anthropic/ink keybindings module +export { + KeybindingProvider, + useKeybindingContext, + useOptionalKeybindingContext, + useRegisterKeybindingContext, +} from '@anthropic/ink' diff --git a/src/keybindings/KeybindingProviderSetup.tsx b/src/keybindings/KeybindingProviderSetup.tsx index 2397468c8..f288a7f65 100644 --- a/src/keybindings/KeybindingProviderSetup.tsx +++ b/src/keybindings/KeybindingProviderSetup.tsx @@ -1,41 +1,21 @@ /** - * Setup utilities for integrating KeybindingProvider into the app. + * App-specific wrapper around ink's KeybindingSetup. * - * This file provides the bindings and a composed provider that can be - * added to the app's component tree. It loads both default bindings and - * user-defined bindings from ~/.claude/keybindings.json, with hot-reload - * support when the file changes. + * Wires up app-specific dependencies (notification system, binding loading, + * file watching, debug logging) and re-exports as KeybindingSetup. */ -import React, { useCallback, useEffect, useRef, useState } from 'react' +import { useCallback } from 'react' import { useNotifications } from '../context/notifications.js' -import type { InputEvent } from '../ink/events/input-event.js' -// ChordInterceptor intentionally uses useInput to intercept all keystrokes before -// other handlers process them - this is required for chord sequence support -// eslint-disable-next-line custom-rules/prefer-use-keybindings -import { type Key, useInput } from '../ink.js' import { count } from '../utils/array.js' import { logForDebugging } from '../utils/debug.js' import { plural } from '../utils/stringUtils.js' -import { KeybindingProvider } from './KeybindingContext.js' +import { KeybindingSetup as InkKeybindingSetup } from '@anthropic/ink' +import type { KeybindingWarning } from '@anthropic/ink' import { initializeKeybindingWatcher, - type KeybindingsLoadResult, loadKeybindingsSyncWithWarnings, subscribeToKeybindingChanges, } from './loadUserBindings.js' -import { resolveKeyWithChordState } from './resolver.js' -import type { - KeybindingContextName, - ParsedBinding, - ParsedKeystroke, -} from './types.js' -import type { KeybindingWarning } from './validate.js' - -/** - * Timeout for chord sequences in milliseconds. - * If the user doesn't complete the chord within this time, it's cancelled. - */ -const CHORD_TIMEOUT_MS = 1000 type Props = { children: React.ReactNode @@ -61,321 +41,51 @@ type Props = { * - User bindings override defaults (later entries win) * - Chord support with automatic timeout */ -/** - * Display keybinding warnings to the user via notifications. - * Shows a brief message pointing to /doctor for details. - */ -function useKeybindingWarnings( - warnings: KeybindingWarning[], - isReload: boolean, -): void { +export function KeybindingSetup({ children }: Props): React.ReactNode { const { addNotification, removeNotification } = useNotifications() - useEffect(() => { - const notificationKey = 'keybinding-config-warning' + const handleWarnings = useCallback( + (warnings: KeybindingWarning[], _isReload: boolean) => { + const notificationKey = 'keybinding-config-warning' - if (warnings.length === 0) { - removeNotification(notificationKey) - return - } - - const errorCount = count(warnings, w => w.severity === 'error') - const warnCount = count(warnings, w => w.severity === 'warning') - - let message: string - if (errorCount > 0 && warnCount > 0) { - message = `Found ${errorCount} keybinding ${plural(errorCount, 'error')} and ${warnCount} ${plural(warnCount, 'warning')}` - } else if (errorCount > 0) { - message = `Found ${errorCount} keybinding ${plural(errorCount, 'error')}` - } else { - message = `Found ${warnCount} keybinding ${plural(warnCount, 'warning')}` - } - message += ' · /doctor for details' - - addNotification({ - key: notificationKey, - text: message, - color: errorCount > 0 ? 'error' : 'warning', - priority: errorCount > 0 ? 'immediate' : 'high', - // Keep visible for 60 seconds like settings errors - timeoutMs: 60000, - }) - }, [warnings, isReload, addNotification, removeNotification]) -} - -export function KeybindingSetup({ children }: Props): React.ReactNode { - // Load bindings synchronously for initial render - const [{ bindings, warnings }, setLoadResult] = - useState(() => { - const result = loadKeybindingsSyncWithWarnings() - logForDebugging( - `[keybindings] KeybindingSetup initialized with ${result.bindings.length} bindings, ${result.warnings.length} warnings`, - ) - return result - }) - - // Track if this is a reload (not initial load) - const [isReload, setIsReload] = useState(false) - - // Display warnings via notifications - useKeybindingWarnings(warnings, isReload) - - // Chord state management - use ref for immediate access, state for re-renders - // The ref is used by resolve() to get the current value without waiting for re-render - // The state is used to trigger re-renders when needed (e.g., for UI updates) - const pendingChordRef = useRef(null) - const [pendingChord, setPendingChordState] = useState< - ParsedKeystroke[] | null - >(null) - const chordTimeoutRef = useRef(null) - - // Handler registry for action callbacks (used by ChordInterceptor to invoke handlers) - const handlerRegistryRef = useRef( - new Map< - string, - Set<{ - action: string - context: KeybindingContextName - handler: () => void - }> - >(), - ) - - // Active context tracking for keybinding priority resolution - // Using a ref instead of state for synchronous updates - input handlers need - // to see the current value immediately, not after a React render cycle. - const activeContextsRef = useRef>(new Set()) - - const registerActiveContext = useCallback( - (context: KeybindingContextName) => { - activeContextsRef.current.add(context) - }, - [], - ) - - const unregisterActiveContext = useCallback( - (context: KeybindingContextName) => { - activeContextsRef.current.delete(context) - }, - [], - ) - - // Clear chord timeout when component unmounts or chord changes - const clearChordTimeout = useCallback(() => { - if (chordTimeoutRef.current) { - clearTimeout(chordTimeoutRef.current) - chordTimeoutRef.current = null - } - }, []) - - // Wrapper for setPendingChord that manages timeout and syncs ref+state - const setPendingChord = useCallback( - (pending: ParsedKeystroke[] | null) => { - clearChordTimeout() - - if (pending !== null) { - // Set timeout to cancel chord if not completed - chordTimeoutRef.current = setTimeout( - (pendingChordRef, setPendingChordState) => { - logForDebugging('[keybindings] Chord timeout - cancelling') - pendingChordRef.current = null - setPendingChordState(null) - }, - CHORD_TIMEOUT_MS, - pendingChordRef, - setPendingChordState, - ) - } - - // Update ref immediately for synchronous access in resolve() - pendingChordRef.current = pending - // Update state to trigger re-renders for UI updates - setPendingChordState(pending) - }, - [clearChordTimeout], - ) - - useEffect(() => { - // Initialize file watcher (idempotent - only runs once) - void initializeKeybindingWatcher() - - // Subscribe to changes - const unsubscribe = subscribeToKeybindingChanges(result => { - // Any callback invocation is a reload since initial load happens - // synchronously in useState, not via this subscription - setIsReload(true) - - setLoadResult(result) - logForDebugging( - `[keybindings] Reloaded: ${result.bindings.length} bindings, ${result.warnings.length} warnings`, - ) - }) - - return () => { - unsubscribe() - clearChordTimeout() - } - }, [clearChordTimeout]) - - return ( - - - {children} - - ) -} - -/** - * Global chord interceptor that registers useInput FIRST (before children). - * - * This component intercepts keystrokes that are part of chord sequences and - * stops propagation before other handlers (like PromptInput) can see them. - * - * Without this, the second key of a chord (e.g., 'r' in "ctrl+c r") would be - * captured by PromptInput and added to the input field before the keybinding - * system could recognize it as completing a chord. - */ -type HandlerRegistration = { - action: string - context: KeybindingContextName - handler: () => void -} - -function ChordInterceptor({ - bindings, - pendingChordRef, - setPendingChord, - activeContexts, - handlerRegistryRef, -}: { - bindings: ParsedBinding[] - pendingChordRef: React.RefObject - setPendingChord: (pending: ParsedKeystroke[] | null) => void - activeContexts: Set - handlerRegistryRef: React.RefObject>> -}): null { - const handleInput = useCallback( - (input: string, key: Key, event: InputEvent) => { - // Wheel events can never start chord sequences — scroll:lineUp/Down are - // single-key bindings handled by per-component useKeybindings hooks, not - // here. Skip the registry scan. Mid-chord wheel still falls through so - // scrolling cancels the pending chord like any other non-matching key. - if ((key.wheelUp || key.wheelDown) && pendingChordRef.current === null) { + if (warnings.length === 0) { + removeNotification(notificationKey) return } - // Build context list from registered handlers + activeContexts + Global - // This ensures we can resolve chords for all contexts that have handlers - const registry = handlerRegistryRef.current - const handlerContexts = new Set() - if (registry) { - for (const handlers of registry.values()) { - for (const registration of handlers) { - handlerContexts.add(registration.context) - } - } + const errorCount = count(warnings, w => w.severity === 'error') + const warnCount = count(warnings, w => w.severity === 'warning') + + let message: string + if (errorCount > 0 && warnCount > 0) { + message = `Found ${errorCount} keybinding ${plural(errorCount, 'error')} and ${warnCount} ${plural(warnCount, 'warning')}` + } else if (errorCount > 0) { + message = `Found ${errorCount} keybinding ${plural(errorCount, 'error')}` + } else { + message = `Found ${warnCount} keybinding ${plural(warnCount, 'warning')}` } - const contexts: KeybindingContextName[] = [ - ...handlerContexts, - ...activeContexts, - 'Global', - ] + message += ' · /doctor for details' - // Track whether we're completing a chord (pending was non-null) - const wasInChord = pendingChordRef.current !== null - - // Check if this keystroke is part of a chord sequence - const result = resolveKeyWithChordState( - input, - key, - contexts, - bindings, - pendingChordRef.current, - ) - - switch (result.type) { - case 'chord_started': - // This key starts a chord - store pending state and stop propagation - setPendingChord(result.pending) - event.stopImmediatePropagation() - break - - case 'match': { - // Clear pending state - setPendingChord(null) - - // Only invoke handlers and stop propagation for chord completions - // (multi-keystroke sequences). Single-keystroke matches should propagate - // to per-hook handlers to avoid interfering with other input handling - // (e.g., Enter needs to reach useTypeahead for autocomplete acceptance - // before the submit handler fires). - if (wasInChord) { - // Find and invoke the handler for this action - // We need to check that the handler's context is in our resolved contexts - // (which includes handlerContexts + activeContexts + Global) - const contextsSet = new Set(contexts) - if (registry) { - const handlers = registry.get(result.action) - if (handlers && handlers.size > 0) { - // Find handlers whose context is in our resolved contexts - for (const registration of handlers) { - if (contextsSet.has(registration.context)) { - registration.handler() - event.stopImmediatePropagation() - break // Only invoke the first matching handler - } - } - } - } - } - break - } - - case 'chord_cancelled': - // Invalid key during chord - clear pending state and swallow the - // keystroke so it doesn't propagate as a standalone action - // (e.g., ctrl+x ctrl+c should not fire app:interrupt). - setPendingChord(null) - event.stopImmediatePropagation() - break - - case 'unbound': - // Key is explicitly unbound - clear pending state and swallow - // the keystroke (it was part of a chord sequence). - setPendingChord(null) - event.stopImmediatePropagation() - break - - case 'none': - // No chord involvement - let other handlers process - break - } + addNotification({ + key: notificationKey, + text: message, + color: errorCount > 0 ? 'error' : 'warning', + priority: errorCount > 0 ? 'immediate' : 'high', + timeoutMs: 60000, + }) }, - [ - bindings, - pendingChordRef, - setPendingChord, - activeContexts, - handlerRegistryRef, - ], + [addNotification, removeNotification], ) - useInput(handleInput) - - return null + return ( + + {children} + + ) } diff --git a/src/keybindings/match.ts b/src/keybindings/match.ts index 2b407173f..ab4b976ef 100644 --- a/src/keybindings/match.ts +++ b/src/keybindings/match.ts @@ -1,120 +1,2 @@ -import type { Key } from '../ink.js' -import type { ParsedBinding, ParsedKeystroke } from './types.js' - -/** - * Modifier keys from Ink's Key type that we care about for matching. - * Note: `fn` from Key is intentionally excluded as it's rarely used and - * not commonly configurable in terminal applications. - */ -type InkModifiers = Pick - -/** - * Extract modifiers from an Ink Key object. - * This function ensures we're explicitly extracting the modifiers we care about. - */ -function getInkModifiers(key: Key): InkModifiers { - return { - ctrl: key.ctrl, - shift: key.shift, - meta: key.meta, - super: key.super, - } -} - -/** - * Extract the normalized key name from Ink's Key + input. - * Maps Ink's boolean flags (key.escape, key.return, etc.) to string names - * that match our ParsedKeystroke.key format. - */ -export function getKeyName(input: string, key: Key): string | null { - if (key.escape) return 'escape' - if (key.return) return 'enter' - if (key.tab) return 'tab' - if (key.backspace) return 'backspace' - if (key.delete) return 'delete' - if (key.upArrow) return 'up' - if (key.downArrow) return 'down' - if (key.leftArrow) return 'left' - if (key.rightArrow) return 'right' - if (key.pageUp) return 'pageup' - if (key.pageDown) return 'pagedown' - if (key.wheelUp) return 'wheelup' - if (key.wheelDown) return 'wheeldown' - if (key.home) return 'home' - if (key.end) return 'end' - if (input.length === 1) return input.toLowerCase() - return null -} - -/** - * Check if all modifiers match between Ink Key and ParsedKeystroke. - * - * Alt and Meta: Ink historically set `key.meta` for Alt/Option. A `meta` - * modifier in config is treated as an alias for `alt` — both match when - * `key.meta` is true. - * - * Super (Cmd/Win): distinct from alt/meta. Only arrives via the kitty - * keyboard protocol on supporting terminals. A `cmd`/`super` binding will - * simply never fire on terminals that don't send it. - */ -function modifiersMatch( - inkMods: InkModifiers, - target: ParsedKeystroke, -): boolean { - // Check ctrl modifier - if (inkMods.ctrl !== target.ctrl) return false - - // Check shift modifier - if (inkMods.shift !== target.shift) return false - - // Alt and meta both map to key.meta in Ink (terminal limitation) - // So we check if EITHER alt OR meta is required in target - const targetNeedsMeta = target.alt || target.meta - if (inkMods.meta !== targetNeedsMeta) return false - - // Super (cmd/win) is a distinct modifier from alt/meta - if (inkMods.super !== target.super) return false - - return true -} - -/** - * Check if a ParsedKeystroke matches the given Ink input + Key. - * - * The display text will show platform-appropriate names (opt on macOS, alt elsewhere). - */ -export function matchesKeystroke( - input: string, - key: Key, - target: ParsedKeystroke, -): boolean { - const keyName = getKeyName(input, key) - if (keyName !== target.key) return false - - const inkMods = getInkModifiers(key) - - // QUIRK: Ink sets key.meta=true when escape is pressed (see input-event.ts). - // This is a legacy behavior from how escape sequences work in terminals. - // We need to ignore the meta modifier when matching the escape key itself, - // otherwise bindings like "escape" (without modifiers) would never match. - if (key.escape) { - return modifiersMatch({ ...inkMods, meta: false }, target) - } - - return modifiersMatch(inkMods, target) -} - -/** - * Check if Ink's Key + input matches a parsed binding's first keystroke. - * For single-keystroke bindings only (Phase 1). - */ -export function matchesBinding( - input: string, - key: Key, - binding: ParsedBinding, -): boolean { - if (binding.chord.length !== 1) return false - const keystroke = binding.chord[0] - if (!keystroke) return false - return matchesKeystroke(input, key, keystroke) -} +// Re-export from @anthropic/ink keybindings module +export { getKeyName, matchesKeystroke, matchesBinding } from '@anthropic/ink' diff --git a/src/keybindings/parser.ts b/src/keybindings/parser.ts index ead1a1a8f..e5aa67996 100644 --- a/src/keybindings/parser.ts +++ b/src/keybindings/parser.ts @@ -1,203 +1,10 @@ -import type { - Chord, - KeybindingBlock, - ParsedBinding, - ParsedKeystroke, -} from './types.js' - -/** - * Parse a keystroke string like "ctrl+shift+k" into a ParsedKeystroke. - * Supports various modifier aliases (ctrl/control, alt/opt/option/meta, - * cmd/command/super/win). - */ -export function parseKeystroke(input: string): ParsedKeystroke { - const parts = input.split('+') - const keystroke: ParsedKeystroke = { - key: '', - ctrl: false, - alt: false, - shift: false, - meta: false, - super: false, - } - for (const part of parts) { - const lower = part.toLowerCase() - switch (lower) { - case 'ctrl': - case 'control': - keystroke.ctrl = true - break - case 'alt': - case 'opt': - case 'option': - keystroke.alt = true - break - case 'shift': - keystroke.shift = true - break - case 'meta': - keystroke.meta = true - break - case 'cmd': - case 'command': - case 'super': - case 'win': - keystroke.super = true - break - case 'esc': - keystroke.key = 'escape' - break - case 'return': - keystroke.key = 'enter' - break - case 'space': - keystroke.key = ' ' - break - case '↑': - keystroke.key = 'up' - break - case '↓': - keystroke.key = 'down' - break - case '←': - keystroke.key = 'left' - break - case '→': - keystroke.key = 'right' - break - default: - keystroke.key = lower - break - } - } - - return keystroke -} - -/** - * Parse a chord string like "ctrl+k ctrl+s" into an array of ParsedKeystrokes. - */ -export function parseChord(input: string): Chord { - // A lone space character IS the space key binding, not a separator - if (input === ' ') return [parseKeystroke('space')] - return input.trim().split(/\s+/).map(parseKeystroke) -} - -/** - * Convert a ParsedKeystroke to its canonical string representation for display. - */ -export function keystrokeToString(ks: ParsedKeystroke): string { - const parts: string[] = [] - if (ks.ctrl) parts.push('ctrl') - if (ks.alt) parts.push('alt') - if (ks.shift) parts.push('shift') - if (ks.meta) parts.push('meta') - if (ks.super) parts.push('cmd') - // Use readable names for display - const displayKey = keyToDisplayName(ks.key) - parts.push(displayKey) - return parts.join('+') -} - -/** - * Map internal key names to human-readable display names. - */ -function keyToDisplayName(key: string): string { - switch (key) { - case 'escape': - return 'Esc' - case ' ': - return 'Space' - case 'tab': - return 'tab' - case 'enter': - return 'Enter' - case 'backspace': - return 'Backspace' - case 'delete': - return 'Delete' - case 'up': - return '↑' - case 'down': - return '↓' - case 'left': - return '←' - case 'right': - return '→' - case 'pageup': - return 'PageUp' - case 'pagedown': - return 'PageDown' - case 'home': - return 'Home' - case 'end': - return 'End' - default: - return key - } -} - -/** - * Convert a Chord to its canonical string representation for display. - */ -export function chordToString(chord: Chord): string { - return chord.map(keystrokeToString).join(' ') -} - -/** - * Display platform type - a subset of Platform that we care about for display. - * WSL and unknown are treated as linux for display purposes. - */ -type DisplayPlatform = 'macos' | 'windows' | 'linux' | 'wsl' | 'unknown' - -/** - * Convert a ParsedKeystroke to a platform-appropriate display string. - * Uses "opt" for alt on macOS, "alt" elsewhere. - */ -export function keystrokeToDisplayString( - ks: ParsedKeystroke, - platform: DisplayPlatform = 'linux', -): string { - const parts: string[] = [] - if (ks.ctrl) parts.push('ctrl') - // Alt/meta are equivalent in terminals, show platform-appropriate name - if (ks.alt || ks.meta) { - // Only macOS uses "opt", all other platforms use "alt" - parts.push(platform === 'macos' ? 'opt' : 'alt') - } - if (ks.shift) parts.push('shift') - if (ks.super) { - parts.push(platform === 'macos' ? 'cmd' : 'super') - } - // Use readable names for display - const displayKey = keyToDisplayName(ks.key) - parts.push(displayKey) - return parts.join('+') -} - -/** - * Convert a Chord to a platform-appropriate display string. - */ -export function chordToDisplayString( - chord: Chord, - platform: DisplayPlatform = 'linux', -): string { - return chord.map(ks => keystrokeToDisplayString(ks, platform)).join(' ') -} - -/** - * Parse keybinding blocks (from JSON config) into a flat list of ParsedBindings. - */ -export function parseBindings(blocks: KeybindingBlock[]): ParsedBinding[] { - const bindings: ParsedBinding[] = [] - for (const block of blocks) { - for (const [key, action] of Object.entries(block.bindings)) { - bindings.push({ - chord: parseChord(key), - action, - context: block.context, - }) - } - } - return bindings -} +// Re-export from @anthropic/ink keybindings module +export { + parseKeystroke, + parseChord, + keystrokeToString, + chordToString, + keystrokeToDisplayString, + chordToDisplayString, + parseBindings, +} from '@anthropic/ink' diff --git a/src/keybindings/resolver.ts b/src/keybindings/resolver.ts index 746404957..0e168e6c7 100644 --- a/src/keybindings/resolver.ts +++ b/src/keybindings/resolver.ts @@ -1,244 +1,9 @@ -import type { Key } from '../ink.js' -import { getKeyName, matchesBinding } from './match.js' -import { chordToString } from './parser.js' -import type { - KeybindingContextName, - ParsedBinding, - ParsedKeystroke, -} from './types.js' - -export type ResolveResult = - | { type: 'match'; action: string } - | { type: 'none' } - | { type: 'unbound' } - -export type ChordResolveResult = - | { type: 'match'; action: string } - | { type: 'none' } - | { type: 'unbound' } - | { type: 'chord_started'; pending: ParsedKeystroke[] } - | { type: 'chord_cancelled' } - -/** - * Resolve a key input to an action. - * Pure function - no state, no side effects, just matching logic. - * - * @param input - The character input from Ink - * @param key - The Key object from Ink with modifier flags - * @param activeContexts - Array of currently active contexts (e.g., ['Chat', 'Global']) - * @param bindings - All parsed bindings to search through - * @returns The resolution result - */ -export function resolveKey( - input: string, - key: Key, - activeContexts: KeybindingContextName[], - bindings: ParsedBinding[], -): ResolveResult { - // Find matching bindings (last one wins for user overrides) - let match: ParsedBinding | undefined - const ctxSet = new Set(activeContexts) - - for (const binding of bindings) { - // Phase 1: Only single-keystroke bindings - if (binding.chord.length !== 1) continue - if (!ctxSet.has(binding.context)) continue - - if (matchesBinding(input, key, binding)) { - match = binding - } - } - - if (!match) { - return { type: 'none' } - } - - if (match.action === null) { - return { type: 'unbound' } - } - - return { type: 'match', action: match.action } -} - -/** - * Get display text for an action from bindings (e.g., "ctrl+t" for "app:toggleTodos"). - * Searches in reverse order so user overrides take precedence. - */ -export function getBindingDisplayText( - action: string, - context: KeybindingContextName, - bindings: ParsedBinding[], -): string | undefined { - // Find the last binding for this action in this context - const binding = bindings.findLast( - b => b.action === action && b.context === context, - ) - return binding ? chordToString(binding.chord) : undefined -} - -/** - * Build a ParsedKeystroke from Ink's input/key. - */ -function buildKeystroke(input: string, key: Key): ParsedKeystroke | null { - const keyName = getKeyName(input, key) - if (!keyName) return null - - // QUIRK: Ink sets key.meta=true when escape is pressed (see input-event.ts). - // This is legacy terminal behavior - we should NOT record this as a modifier - // for the escape key itself, otherwise chord matching will fail. - const effectiveMeta = key.escape ? false : key.meta - - return { - key: keyName, - ctrl: key.ctrl, - alt: effectiveMeta, - shift: key.shift, - meta: effectiveMeta, - super: key.super, - } -} - -/** - * Compare two ParsedKeystrokes for equality. Collapses alt/meta into - * one logical modifier — legacy terminals can't distinguish them (see - * match.ts modifiersMatch), so "alt+k" and "meta+k" are the same key. - * Super (cmd/win) is distinct — only arrives via kitty keyboard protocol. - */ -export function keystrokesEqual( - a: ParsedKeystroke, - b: ParsedKeystroke, -): boolean { - return ( - a.key === b.key && - a.ctrl === b.ctrl && - a.shift === b.shift && - (a.alt || a.meta) === (b.alt || b.meta) && - a.super === b.super - ) -} - -/** - * Check if a chord prefix matches the beginning of a binding's chord. - */ -function chordPrefixMatches( - prefix: ParsedKeystroke[], - binding: ParsedBinding, -): boolean { - if (prefix.length >= binding.chord.length) return false - for (let i = 0; i < prefix.length; i++) { - const prefixKey = prefix[i] - const bindingKey = binding.chord[i] - if (!prefixKey || !bindingKey) return false - if (!keystrokesEqual(prefixKey, bindingKey)) return false - } - return true -} - -/** - * Check if a full chord matches a binding's chord. - */ -function chordExactlyMatches( - chord: ParsedKeystroke[], - binding: ParsedBinding, -): boolean { - if (chord.length !== binding.chord.length) return false - for (let i = 0; i < chord.length; i++) { - const chordKey = chord[i] - const bindingKey = binding.chord[i] - if (!chordKey || !bindingKey) return false - if (!keystrokesEqual(chordKey, bindingKey)) return false - } - return true -} - -/** - * Resolve a key with chord state support. - * - * This function handles multi-keystroke chord bindings like "ctrl+k ctrl+s". - * - * @param input - The character input from Ink - * @param key - The Key object from Ink with modifier flags - * @param activeContexts - Array of currently active contexts - * @param bindings - All parsed bindings - * @param pending - Current chord state (null if not in a chord) - * @returns Resolution result with chord state - */ -export function resolveKeyWithChordState( - input: string, - key: Key, - activeContexts: KeybindingContextName[], - bindings: ParsedBinding[], - pending: ParsedKeystroke[] | null, -): ChordResolveResult { - // Cancel chord on escape - if (key.escape && pending !== null) { - return { type: 'chord_cancelled' } - } - - // Build current keystroke - const currentKeystroke = buildKeystroke(input, key) - if (!currentKeystroke) { - if (pending !== null) { - return { type: 'chord_cancelled' } - } - return { type: 'none' } - } - - // Build the full chord sequence to test - const testChord = pending - ? [...pending, currentKeystroke] - : [currentKeystroke] - - // Filter bindings by active contexts (Set lookup: O(n) instead of O(n·m)) - const ctxSet = new Set(activeContexts) - const contextBindings = bindings.filter(b => ctxSet.has(b.context)) - - // Check if this could be a prefix for longer chords. Group by chord - // string so a later null-override shadows the default it unbinds — - // otherwise null-unbinding `ctrl+x ctrl+k` still makes `ctrl+x` enter - // chord-wait and the single-key binding on the prefix never fires. - const chordWinners = new Map() - for (const binding of contextBindings) { - if ( - binding.chord.length > testChord.length && - chordPrefixMatches(testChord, binding) - ) { - chordWinners.set(chordToString(binding.chord), binding.action) - } - } - let hasLongerChords = false - for (const action of chordWinners.values()) { - if (action !== null) { - hasLongerChords = true - break - } - } - - // If this keystroke could start a longer chord, prefer that - // (even if there's an exact single-key match) - if (hasLongerChords) { - return { type: 'chord_started', pending: testChord } - } - - // Check for exact matches (last one wins) - let exactMatch: ParsedBinding | undefined - for (const binding of contextBindings) { - if (chordExactlyMatches(testChord, binding)) { - exactMatch = binding - } - } - - if (exactMatch) { - if (exactMatch.action === null) { - return { type: 'unbound' } - } - return { type: 'match', action: exactMatch.action } - } - - // No match and no potential longer chords - if (pending !== null) { - return { type: 'chord_cancelled' } - } - - return { type: 'none' } -} +// Re-export from @anthropic/ink keybindings module +export { + resolveKey, + resolveKeyWithChordState, + getBindingDisplayText, + keystrokesEqual, + type ResolveResult, + type ChordResolveResult, +} from '@anthropic/ink' diff --git a/src/keybindings/types.ts b/src/keybindings/types.ts index e3284422f..0420c8588 100644 --- a/src/keybindings/types.ts +++ b/src/keybindings/types.ts @@ -1,7 +1,9 @@ -// Auto-generated stub — replace with real implementation -export type ParsedBinding = any; -export type ParsedKeystroke = any; -export type KeybindingContextName = any; -export type KeybindingBlock = any; -export type Chord = any; -export type KeybindingAction = any; +// Re-export types from @anthropic/ink keybindings module +export type { + ParsedBinding, + ParsedKeystroke, + KeybindingContextName, + KeybindingBlock, + Chord, + KeybindingAction, +} from '@anthropic/ink' diff --git a/src/keybindings/useKeybinding.ts b/src/keybindings/useKeybinding.ts index 02b07ce19..3a53a8f8d 100644 --- a/src/keybindings/useKeybinding.ts +++ b/src/keybindings/useKeybinding.ts @@ -1,196 +1,2 @@ -import { useCallback, useEffect } from 'react' -import type { InputEvent } from '../ink/events/input-event.js' -import { type Key, useInput } from '../ink.js' -import { useOptionalKeybindingContext } from './KeybindingContext.js' -import type { KeybindingContextName } from './types.js' - -type Options = { - /** Which context this binding belongs to (default: 'Global') */ - context?: KeybindingContextName - /** Only handle when active (like useInput's isActive) */ - isActive?: boolean -} - -/** - * Ink-native hook for handling a keybinding. - * - * The handler stays in the component (React way). - * The binding (keystroke → action) comes from config. - * - * Supports chord sequences (e.g., "ctrl+k ctrl+s"). When a chord is started, - * the hook will manage the pending state automatically. - * - * Uses stopImmediatePropagation() to prevent other handlers from firing - * once this binding is handled. - * - * @example - * ```tsx - * useKeybinding('app:toggleTodos', () => { - * setShowTodos(prev => !prev) - * }, { context: 'Global' }) - * ``` - */ -export function useKeybinding( - action: string, - handler: () => void | false | Promise, - options: Options = {}, -): void { - const { context = 'Global', isActive = true } = options - const keybindingContext = useOptionalKeybindingContext() - - // Register handler with the context for ChordInterceptor to invoke - useEffect(() => { - if (!keybindingContext || !isActive) return - return keybindingContext.registerHandler({ action, context, handler }) - }, [action, context, handler, keybindingContext, isActive]) - - const handleInput = useCallback( - (input: string, key: Key, event: InputEvent) => { - // If no keybinding context available, skip resolution - if (!keybindingContext) return - - // Build context list: registered active contexts + this context + Global - // More specific contexts (registered ones) take precedence over Global - const contextsToCheck: KeybindingContextName[] = [ - ...keybindingContext.activeContexts, - context, - 'Global', - ] - // Deduplicate while preserving order (first occurrence wins for priority) - const uniqueContexts = [...new Set(contextsToCheck)] - - const result = keybindingContext.resolve(input, key, uniqueContexts) - - switch (result.type) { - case 'match': - // Chord completed (if any) - clear pending state - keybindingContext.setPendingChord(null) - if (result.action === action) { - if (handler() !== false) { - event.stopImmediatePropagation() - } - } - break - case 'chord_started': - // User started a chord sequence - update pending state - keybindingContext.setPendingChord(result.pending) - event.stopImmediatePropagation() - break - case 'chord_cancelled': - // Chord was cancelled (escape or invalid key) - keybindingContext.setPendingChord(null) - break - case 'unbound': - // Explicitly unbound - clear any pending chord - keybindingContext.setPendingChord(null) - event.stopImmediatePropagation() - break - case 'none': - // No match - let other handlers try - break - } - }, - [action, context, handler, keybindingContext], - ) - - useInput(handleInput, { isActive }) -} - -/** - * Handle multiple keybindings in one hook (reduces useInput calls). - * - * Supports chord sequences. When a chord is started, the hook will - * manage the pending state automatically. - * - * @example - * ```tsx - * useKeybindings({ - * 'chat:submit': () => handleSubmit(), - * 'chat:cancel': () => handleCancel(), - * }, { context: 'Chat' }) - * ``` - */ -export function useKeybindings( - // Handler returning `false` means "not consumed" — the event propagates - // to later useInput/useKeybindings handlers. Useful for fall-through: - // e.g. ScrollKeybindingHandler's scroll:line* returns false when the - // ScrollBox content fits (scroll is a no-op), letting a child component's - // handler take the wheel event for list navigation instead. Promise - // is allowed for fire-and-forget async handlers (the `!== false` check - // only skips propagation for a sync `false`, not a pending Promise). - handlers: Record void | false | Promise>, - options: Options = {}, -): void { - const { context = 'Global', isActive = true } = options - const keybindingContext = useOptionalKeybindingContext() - - // Register all handlers with the context for ChordInterceptor to invoke - useEffect(() => { - if (!keybindingContext || !isActive) return - - const unregisterFns: Array<() => void> = [] - for (const [action, handler] of Object.entries(handlers)) { - unregisterFns.push( - keybindingContext.registerHandler({ action, context, handler }), - ) - } - - return () => { - for (const unregister of unregisterFns) { - unregister() - } - } - }, [context, handlers, keybindingContext, isActive]) - - const handleInput = useCallback( - (input: string, key: Key, event: InputEvent) => { - // If no keybinding context available, skip resolution - if (!keybindingContext) return - - // Build context list: registered active contexts + this context + Global - // More specific contexts (registered ones) take precedence over Global - const contextsToCheck: KeybindingContextName[] = [ - ...keybindingContext.activeContexts, - context, - 'Global', - ] - // Deduplicate while preserving order (first occurrence wins for priority) - const uniqueContexts = [...new Set(contextsToCheck)] - - const result = keybindingContext.resolve(input, key, uniqueContexts) - - switch (result.type) { - case 'match': - // Chord completed (if any) - clear pending state - keybindingContext.setPendingChord(null) - if (result.action in handlers) { - const handler = handlers[result.action] - if (handler && handler() !== false) { - event.stopImmediatePropagation() - } - } - break - case 'chord_started': - // User started a chord sequence - update pending state - keybindingContext.setPendingChord(result.pending) - event.stopImmediatePropagation() - break - case 'chord_cancelled': - // Chord was cancelled (escape or invalid key) - keybindingContext.setPendingChord(null) - break - case 'unbound': - // Explicitly unbound - clear any pending chord - keybindingContext.setPendingChord(null) - event.stopImmediatePropagation() - break - case 'none': - // No match - let other handlers try - break - } - }, - [context, handlers, keybindingContext], - ) - - useInput(handleInput, { isActive }) -} +// Re-export from @anthropic/ink keybindings module +export { useKeybinding, useKeybindings } from '@anthropic/ink' diff --git a/src/main.tsx b/src/main.tsx index 24bead113..16d31925a 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -26,23 +26,23 @@ startKeychainPrefetch(); import { feature } from "bun:bundle"; import { - Command as CommanderCommand, - InvalidArgumentError, - Option, -} from "@commander-js/extra-typings"; -import chalk from "chalk"; -import { readFileSync } from "fs"; -import mapValues from "lodash-es/mapValues.js"; -import pickBy from "lodash-es/pickBy.js"; -import uniqBy from "lodash-es/uniqBy.js"; -import React from "react"; -import { getOauthConfig } from "./constants/oauth.js"; -import { getRemoteSessionUrl } from "./constants/product.js"; -import { getSystemContext, getUserContext } from "./context.js"; -import { init, initializeTelemetryAfterTrust } from "./entrypoints/init.js"; -import { addToHistory } from "./history.js"; -import type { Root } from "./ink.js"; -import { launchRepl } from "./replLauncher.js"; + Command as CommanderCommand, + InvalidArgumentError, + Option, +} from '@commander-js/extra-typings' +import chalk from 'chalk' +import { readFileSync } from 'fs' +import mapValues from 'lodash-es/mapValues.js' +import pickBy from 'lodash-es/pickBy.js' +import uniqBy from 'lodash-es/uniqBy.js' +import React from 'react' +import { getOauthConfig } from './constants/oauth.js' +import { getRemoteSessionUrl } from './constants/product.js' +import { getSystemContext, getUserContext } from './context.js' +import { init, initializeTelemetryAfterTrust } from './entrypoints/init.js' +import { addToHistory } from './history.js' +import type { Root } from '@anthropic/ink' +import { launchRepl } from './replLauncher.js' import { hasGrowthBookEnvOverride, initializeGrowthBook, @@ -150,10 +150,10 @@ import { relative, resolve } from "path"; import { isAnalyticsDisabled } from "src/services/analytics/config.js"; import { getFeatureValue_CACHED_MAY_BE_STALE } from "src/services/analytics/growthbook.js"; import { - type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, - logEvent, -} from "src/services/analytics/index.js"; -import { initializeAnalyticsGates } from "src/services/analytics/sink.js"; + type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, + logEvent, +} from 'src/services/analytics/index.js' +import { initializeAnalyticsGates } from 'src/services/analytics/sink.js' import { getOriginalCwd, setAdditionalDirectoriesForClaudeMd, @@ -165,15 +165,15 @@ import { import { filterCommandsForRemoteMode, getCommands } from "./commands.js"; import type { StatsStore } from "./context/stats.js"; import { - launchAssistantInstallWizard, - launchAssistantSessionChooser, - launchInvalidSettingsDialog, - launchResumeChooser, - launchSnapshotUpdateDialog, - launchTeleportRepoMismatchDialog, - launchTeleportResumeWrapper, -} from "./dialogLaunchers.js"; -import { SHOW_CURSOR } from "./ink/termio/dec.js"; + launchAssistantInstallWizard, + launchAssistantSessionChooser, + launchInvalidSettingsDialog, + launchResumeChooser, + launchSnapshotUpdateDialog, + launchTeleportRepoMismatchDialog, + launchTeleportResumeWrapper, +} from './dialogLaunchers.js' +import { SHOW_CURSOR } from '@anthropic/ink' import { exitWithError, exitWithMessage, @@ -203,7 +203,6 @@ import { } from "./tools/AgentTool/loadAgentsDir.js"; import type { LogOption } from "./types/logs.js"; import type { Message as MessageType } from "./types/message.js"; -import { assertMinVersion } from "./utils/autoUpdater.js"; import { CLAUDE_IN_CHROME_SKILL_HINT, CLAUDE_IN_CHROME_SKILL_HINT_WITH_WEBBROWSER, @@ -390,7 +389,6 @@ const autoModeStateModule = feature("TRANSCRIPT_CLASSIFIER") : null; // TeleportRepoMismatchDialog, TeleportResumeWrapper dynamically imported at call sites -import { migrateAutoUpdatesToSettings } from "./migrations/migrateAutoUpdatesToSettings.js"; import { migrateBypassPermissionsAcceptedToSettings } from "./migrations/migrateBypassPermissionsAcceptedToSettings.js"; import { migrateEnableAllProjectMcpServersToSettings } from "./migrations/migrateEnableAllProjectMcpServersToSettings.js"; import { migrateFennecToOpus } from "./migrations/migrateFennecToOpus.js"; @@ -440,10 +438,10 @@ import { validateSessionRepository, } from "./utils/teleport.js"; import { - shouldEnableThinkingByDefault, - type ThinkingConfig, -} from "./utils/thinking.js"; -import { initUser, resetUserCache } from "./utils/user.js"; + shouldEnableThinkingByDefault, + type ThinkingConfig, +} from './utils/thinking.js' +import { initUser, resetUserCache } from './utils/user.js' import { getTmuxInstallInstructions, isTmuxAvailable, @@ -587,7 +585,6 @@ async function logStartupTelemetry(): Promise { const CURRENT_MIGRATION_VERSION = 11; function runMigrations(): void { if (getGlobalConfig().migrationVersion !== CURRENT_MIGRATION_VERSION) { - migrateAutoUpdatesToSettings(); migrateBypassPermissionsAcceptedToSettings(); migrateEnableAllProjectMcpServersToSettings(); resetProToOpusDefault(); @@ -2731,7 +2728,6 @@ async function run(): Promise { console.error(warning); }); - void assertMinVersion(); // claude.ai config fetch: -p mode only (interactive uses useManageMCPConnections // two-phase loading). Kicked off here to overlap with setup(); awaited @@ -3370,8 +3366,8 @@ async function run(): Promise { installAsciicastRecorder(); } - const { createRoot } = await import("./ink.js"); - root = await createRoot(ctx.renderOptions); + const { createRoot } = await import('@anthropic/ink') + root = await createRoot(ctx.renderOptions) // Log startup time now, before any blocking dialog renders. Logging // from REPL's first render (the old location) included however long @@ -6365,20 +6361,20 @@ async function run(): Promise { ); // END ANT-ONLY - // Setup token command - program - .command("setup-token") - .description( - "Set up a long-lived authentication token (requires Claude subscription)", - ) - .action(async () => { - const [{ setupTokenHandler }, { createRoot }] = await Promise.all([ - import("./cli/handlers/util.js"), - import("./ink.js"), - ]); - const root = await createRoot(getBaseRenderOptions(false)); - await setupTokenHandler(root); - }); + // Setup token command + program + .command('setup-token') + .description( + 'Set up a long-lived authentication token (requires Claude subscription)', + ) + .action(async () => { + const [{ setupTokenHandler }, { createRoot }] = await Promise.all([ + import('./cli/handlers/util.js'), + import('@anthropic/ink'), + ]) + const root = await createRoot(getBaseRenderOptions(false)) + await setupTokenHandler(root) + }) // Agents command - list configured agents program @@ -6482,35 +6478,21 @@ async function run(): Promise { }); } - // Doctor command - check installation health - program - .command("doctor") - .description( - "Check the health of your Claude Code auto-updater. Note: The workspace trust dialog is skipped and stdio servers from .mcp.json are spawned for health checks. Only use this command in directories you trust.", - ) - .action(async () => { - const [{ doctorHandler }, { createRoot }] = await Promise.all([ - import("./cli/handlers/util.js"), - import("./ink.js"), - ]); - const root = await createRoot(getBaseRenderOptions(false)); - await doctorHandler(root); - }); + // Doctor command - check installation health + program + .command('doctor') + .description( + 'Check the health of your Claude Code auto-updater. Note: The workspace trust dialog is skipped and stdio servers from .mcp.json are spawned for health checks. Only use this command in directories you trust.', + ) + .action(async () => { + const [{ doctorHandler }, { createRoot }] = await Promise.all([ + import('./cli/handlers/util.js'), + import('@anthropic/ink'), + ]) + const root = await createRoot(getBaseRenderOptions(false)) + await doctorHandler(root) + }) - // claude update - // - // For SemVer-compliant versioning with build metadata (X.X.X+SHA): - // - We perform exact string comparison (including SHA) to detect any change - // - This ensures users always get the latest build, even when only the SHA changes - // - UI shows both versions including build metadata for clarity - program - .command("update") - .alias("upgrade") - .description("Check for updates and install if available") - .action(async () => { - const { update } = await import("src/cli/update.js"); - await update(); - }); // claude up — run the project's CLAUDE.md "# claude up" setup instructions. if (process.env.USER_TYPE === "ant") { diff --git a/src/replLauncher.tsx b/src/replLauncher.tsx index 664e95839..91550a3bd 100644 --- a/src/replLauncher.tsx +++ b/src/replLauncher.tsx @@ -1,6 +1,6 @@ import React from 'react' import type { StatsStore } from './context/stats.js' -import type { Root } from './ink.js' +import type { Root } from '@anthropic/ink' import type { Props as REPLProps } from './screens/REPL.js' import type { AppState } from './state/AppStateStore.js' import type { FpsMetrics } from './utils/fpsTracker.js' diff --git a/src/screens/Doctor.tsx b/src/screens/Doctor.tsx index d8de3714a..6ba73f2a4 100644 --- a/src/screens/Doctor.tsx +++ b/src/screens/Doctor.tsx @@ -15,13 +15,13 @@ import { getClaudeConfigHomeDir } from 'src/utils/envUtils.js' import type { SettingSource } from 'src/utils/settings/constants.js' import { getOriginalCwd } from '../bootstrap/state.js' import type { CommandResultDisplay } from '../commands.js' -import { Pane } from '../components/design-system/Pane.js' +import { Pane } from '@anthropic/ink' import { PressEnterToContinue } from '../components/PressEnterToContinue.js' import { SandboxDoctorSection } from '../components/sandbox/SandboxDoctorSection.js' import { ValidationErrorsList } from '../components/ValidationErrorsList.js' import { useSettingsErrors } from '../hooks/notifs/useSettingsErrors.js' import { useExitOnCtrlCDWithKeybindings } from '../hooks/useExitOnCtrlCDWithKeybindings.js' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import { useKeybindings } from '../keybindings/useKeybinding.js' import { useAppState } from '../state/AppState.js' import { getPluginErrorMessage } from '../types/plugin.js' diff --git a/src/screens/REPL.tsx b/src/screens/REPL.tsx index 83669907c..be3fe3d68 100644 --- a/src/screens/REPL.tsx +++ b/src/screens/REPL.tsx @@ -14,19 +14,18 @@ import { dirname, join } from 'path'; import { tmpdir } from 'os'; import figures from 'figures'; // eslint-disable-next-line custom-rules/prefer-use-keybindings -- / n N Esc [ v are bare letters in transcript modal context, same class as g/G/j/k in ScrollKeybindingHandler -import { useInput } from '../ink.js'; -import { useSearchInput } from '../hooks/useSearchInput.js'; -import { useTerminalSize } from '../hooks/useTerminalSize.js'; -import { useSearchHighlight } from '../ink/hooks/use-search-highlight.js'; -import type { JumpHandle } from '../components/VirtualMessageList.js'; -import { renderMessagesToPlainText } from '../utils/exportRenderer.js'; -import { openFileInExternalEditor } from '../utils/editor.js'; -import { writeFile } from 'fs/promises'; -import { Box, Text, useStdin, useTheme, useTerminalFocus, useTerminalTitle, useTabStatus } from '../ink.js'; -import type { TabStatusKind } from '../ink/hooks/use-tab-status.js'; -import { CostThresholdDialog } from '../components/CostThresholdDialog.js'; -import { IdleReturnDialog } from '../components/IdleReturnDialog.js'; -import * as React from 'react'; +import { useInput } from '@anthropic/ink' +import { useSearchInput } from '../hooks/useSearchInput.js' +import { useTerminalSize } from '../hooks/useTerminalSize.js' +import { useSearchHighlight } from '@anthropic/ink' +import type { JumpHandle } from '../components/VirtualMessageList.js' +import { renderMessagesToPlainText } from '../utils/exportRenderer.js' +import { openFileInExternalEditor } from '../utils/editor.js' +import { writeFile } from 'fs/promises' +import { type TabStatusKind, Box, Text, useStdin, useTheme, useTerminalFocus, useTerminalTitle, useTabStatus } from '@anthropic/ink' +import { CostThresholdDialog } from '../components/CostThresholdDialog.js' +import { IdleReturnDialog } from '../components/IdleReturnDialog.js' +import * as React from 'react' import { useEffect, useMemo, @@ -36,12 +35,14 @@ import { useDeferredValue, useLayoutEffect, type RefObject, -} from 'react'; -import { useNotifications } from '../context/notifications.js'; -import { sendNotification } from '../services/notifier.js'; -import { startPreventSleep, stopPreventSleep } from '../services/preventSleep.js'; -import { useTerminalNotification } from '../ink/useTerminalNotification.js'; -import { hasCursorUpViewportYankBug } from '../ink/terminal.js'; +} from 'react' +import { useNotifications } from '../context/notifications.js' +import { sendNotification } from '../services/notifier.js' +import { + startPreventSleep, + stopPreventSleep, +} from '../services/preventSleep.js' +import { useTerminalNotification, hasCursorUpViewportYankBug } from '@anthropic/ink' import { createFileStateCacheWithSizeLimit, mergeFileStateCaches, @@ -446,13 +447,21 @@ import { UltraplanChoiceDialog } from '../components/ultraplan/UltraplanChoiceDi import { UltraplanLaunchDialog } from '../components/ultraplan/UltraplanLaunchDialog.js'; import { launchUltraplan } from '../commands/ultraplan.js'; // Session manager removed - using AppState now -import type { RemoteSessionConfig } from '../remote/RemoteSessionManager.js'; -import { REMOTE_SAFE_COMMANDS } from '../commands.js'; -import type { RemoteMessageContent } from '../utils/teleport/api.js'; -import { FullscreenLayout, useUnseenDivider, computeUnseenDivider } from '../components/FullscreenLayout.js'; -import { isFullscreenEnvEnabled, maybeGetTmuxMouseHint, isMouseTrackingEnabled } from '../utils/fullscreen.js'; -import { AlternateScreen } from '../ink/components/AlternateScreen.js'; -import { ScrollKeybindingHandler } from '../components/ScrollKeybindingHandler.js'; +import type { RemoteSessionConfig } from '../remote/RemoteSessionManager.js' +import { REMOTE_SAFE_COMMANDS } from '../commands.js' +import type { RemoteMessageContent } from '../utils/teleport/api.js' +import { + FullscreenLayout, + useUnseenDivider, + computeUnseenDivider, +} from '../components/FullscreenLayout.js' +import { + isFullscreenEnvEnabled, + maybeGetTmuxMouseHint, + isMouseTrackingEnabled, +} from '../utils/fullscreen.js' +import { AlternateScreen } from '@anthropic/ink' +import { ScrollKeybindingHandler } from '../components/ScrollKeybindingHandler.js' import { useMessageActions, MessageActionsKeybindings, @@ -460,10 +469,13 @@ import { type MessageActionsState, type MessageActionsNav, type MessageActionCaps, -} from '../components/messageActions.js'; -import { setClipboard } from '../ink/termio/osc.js'; -import type { ScrollBoxHandle } from '../ink/components/ScrollBox.js'; -import { createAttachmentMessage, getQueuedCommandAttachments } from '../utils/attachments.js'; +} from '../components/messageActions.js' +import { setClipboard } from '@anthropic/ink' +import type { ScrollBoxHandle } from '@anthropic/ink' +import { + createAttachmentMessage, + getQueuedCommandAttachments, +} from '../utils/attachments.js' // Stable empty array for hooks that accept MCPServerConnection[] — avoids // creating a new [] literal on every render in remote mode, which would diff --git a/src/screens/ResumeConversation.tsx b/src/screens/ResumeConversation.tsx index 019327ff3..a3a8229a2 100644 --- a/src/screens/ResumeConversation.tsx +++ b/src/screens/ResumeConversation.tsx @@ -7,8 +7,8 @@ import type { Command } from '../commands.js' import { LogSelector } from '../components/LogSelector.js' import { Spinner } from '../components/Spinner.js' import { restoreCostStateForSession } from '../cost-tracker.js' -import { setClipboard } from '../ink/termio/osc.js' -import { Box, Text } from '../ink.js' +import { setClipboard } from '@anthropic/ink' +import { Box, Text } from '@anthropic/ink' import { useKeybinding } from '../keybindings/useKeybinding.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, diff --git a/src/services/mcpServerApproval.tsx b/src/services/mcpServerApproval.tsx index 4b92d1280..78be862f3 100644 --- a/src/services/mcpServerApproval.tsx +++ b/src/services/mcpServerApproval.tsx @@ -1,7 +1,7 @@ import React from 'react' import { MCPServerApprovalDialog } from '../components/MCPServerApprovalDialog.js' import { MCPServerMultiselectDialog } from '../components/MCPServerMultiselectDialog.js' -import type { Root } from '../ink.js' +import type { Root } from '@anthropic/ink' import { KeybindingSetup } from '../keybindings/KeybindingProviderSetup.js' import { AppStateProvider } from '../state/AppState.js' import { getMcpConfigsByScope } from './mcp/config.js' diff --git a/src/services/notifier.ts b/src/services/notifier.ts index a1a865cf1..1eb8f99db 100644 --- a/src/services/notifier.ts +++ b/src/services/notifier.ts @@ -1,4 +1,4 @@ -import type { TerminalNotification } from '../ink/useTerminalNotification.js' +import type { TerminalNotification } from '@anthropic/ink' import { getGlobalConfig } from '../utils/config.js' import { env } from '../utils/env.js' import { execFileNoThrow } from '../utils/execFileNoThrow.js' diff --git a/src/services/remoteManagedSettings/securityCheck.tsx b/src/services/remoteManagedSettings/securityCheck.tsx index 857103408..f0d9b1ca7 100644 --- a/src/services/remoteManagedSettings/securityCheck.tsx +++ b/src/services/remoteManagedSettings/securityCheck.tsx @@ -6,7 +6,7 @@ import { hasDangerousSettings, hasDangerousSettingsChanged, } from '../../components/ManagedSettingsSecurityDialog/utils.js' -import { render } from '../../ink.js' +import { wrappedRender as render } from '@anthropic/ink' import { KeybindingSetup } from '../../keybindings/KeybindingProviderSetup.js' import { AppStateProvider } from '../../state/AppState.js' import { gracefulShutdownSync } from '../../utils/gracefulShutdown.js' diff --git a/src/services/tips/tipRegistry.ts b/src/services/tips/tipRegistry.ts index c99edfdc9..05b727dcc 100644 --- a/src/services/tips/tipRegistry.ts +++ b/src/services/tips/tipRegistry.ts @@ -8,7 +8,7 @@ import { } from 'src/utils/settings/settings.js' import { shouldOfferTerminalSetup } from '../../commands/terminalSetup/terminalSetup.js' import { getDesktopUpsellConfig } from '../../components/DesktopUpsell/DesktopUpsellStartup.js' -import { color } from '../../components/design-system/color.js' +import { color } from '@anthropic/ink' import { shouldShowOverageCreditUpsell } from '../../components/LogoV2/OverageCreditUpsell.js' import { getShortcutDisplay } from '../../keybindings/shortcutFormat.js' import { isKairosCronEnabled } from '../../tools/ScheduleCronTool/prompt.js' diff --git a/src/tools/AgentTool/UI.tsx b/src/tools/AgentTool/UI.tsx index aaa312e20..b6bc20da4 100644 --- a/src/tools/AgentTool/UI.tsx +++ b/src/tools/AgentTool/UI.tsx @@ -8,8 +8,7 @@ import { CtrlOToExpand, SubAgentProvider, } from 'src/components/CtrlOToExpand.js' -import { Byline } from 'src/components/design-system/Byline.js' -import { KeyboardShortcutHint } from 'src/components/design-system/KeyboardShortcutHint.js' +import { Byline, KeyboardShortcutHint } from '@anthropic/ink' import type { z } from 'zod/v4' import { AgentProgressLine } from '../../components/AgentProgressLine.js' import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js' @@ -18,7 +17,7 @@ import { Markdown } from '../../components/Markdown.js' import { Message as MessageComponent } from '../../components/Message.js' import { MessageResponse } from '../../components/MessageResponse.js' import { ToolUseLoader } from '../../components/ToolUseLoader.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { getDumpPromptsPath } from '../../services/api/dumpPrompts.js' import { findToolByName, type Tools } from '../../Tool.js' import type { Message, ProgressMessage } from '../../types/message.js' diff --git a/src/tools/AskUserQuestionTool/AskUserQuestionTool.tsx b/src/tools/AskUserQuestionTool/AskUserQuestionTool.tsx index e71a5c665..4c77e7da6 100644 --- a/src/tools/AskUserQuestionTool/AskUserQuestionTool.tsx +++ b/src/tools/AskUserQuestionTool/AskUserQuestionTool.tsx @@ -8,7 +8,7 @@ import { MessageResponse } from 'src/components/MessageResponse.js' import { BLACK_CIRCLE } from 'src/constants/figures.js' import { getModeColor } from 'src/utils/permissions/PermissionMode.js' import { z } from 'zod/v4' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import type { Tool } from '../../Tool.js' import { buildTool, type ToolDef } from '../../Tool.js' import { lazySchema } from '../../utils/lazySchema.js' diff --git a/src/tools/BashTool/BashToolResultMessage.tsx b/src/tools/BashTool/BashToolResultMessage.tsx index 4b5c0b613..6b1534bfb 100644 --- a/src/tools/BashTool/BashToolResultMessage.tsx +++ b/src/tools/BashTool/BashToolResultMessage.tsx @@ -1,10 +1,10 @@ import React from 'react' import { removeSandboxViolationTags } from 'src/utils/sandbox/sandbox-ui-utils.js' -import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js' +import { KeyboardShortcutHint } from '@anthropic/ink' import { MessageResponse } from '../../components/MessageResponse.js' import { OutputLine } from '../../components/shell/OutputLine.js' import { ShellTimeDisplay } from '../../components/shell/ShellTimeDisplay.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import type { Out as BashOut } from './BashTool.js' type Props = { diff --git a/src/tools/BashTool/UI.tsx b/src/tools/BashTool/UI.tsx index 1d5c3f5aa..2b0b3bfd3 100644 --- a/src/tools/BashTool/UI.tsx +++ b/src/tools/BashTool/UI.tsx @@ -1,10 +1,10 @@ import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs' import * as React from 'react' -import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js' +import { KeyboardShortcutHint } from '@anthropic/ink' import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js' import { MessageResponse } from '../../components/MessageResponse.js' import { ShellProgressMessage } from '../../components/shell/ShellProgressMessage.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { useKeybinding } from '../../keybindings/useKeybinding.js' import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js' import { useAppStateStore, useSetAppState } from '../../state/AppState.js' diff --git a/src/tools/BriefTool/UI.tsx b/src/tools/BriefTool/UI.tsx index e68adccf6..ea3a28f9f 100644 --- a/src/tools/BriefTool/UI.tsx +++ b/src/tools/BriefTool/UI.tsx @@ -2,7 +2,7 @@ import figures from 'figures' import React from 'react' import { Markdown } from '../../components/Markdown.js' import { BLACK_CIRCLE } from '../../constants/figures.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import type { ProgressMessage } from '../../types/message.js' import { getDisplayPath } from '../../utils/file.js' import { formatFileSize } from '../../utils/format.js' diff --git a/src/tools/ConfigTool/UI.tsx b/src/tools/ConfigTool/UI.tsx index 36ce77e62..da8ced8a1 100644 --- a/src/tools/ConfigTool/UI.tsx +++ b/src/tools/ConfigTool/UI.tsx @@ -1,6 +1,6 @@ import React from 'react' import { MessageResponse } from '../../components/MessageResponse.js' -import { Text } from '../../ink.js' +import { Text } from '@anthropic/ink' import { jsonStringify } from '../../utils/slowOperations.js' import type { Input, Output } from './ConfigTool.js' diff --git a/src/tools/EnterPlanModeTool/UI.tsx b/src/tools/EnterPlanModeTool/UI.tsx index 830708309..a1bc5d6c8 100644 --- a/src/tools/EnterPlanModeTool/UI.tsx +++ b/src/tools/EnterPlanModeTool/UI.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { BLACK_CIRCLE } from 'src/constants/figures.js' import { getModeColor } from 'src/utils/permissions/PermissionMode.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import type { ToolProgressData } from '../../Tool.js' import type { ProgressMessage } from '../../types/message.js' import type { ThemeName } from '../../utils/theme.js' diff --git a/src/tools/EnterWorktreeTool/UI.tsx b/src/tools/EnterWorktreeTool/UI.tsx index 5a859fd72..985186d34 100644 --- a/src/tools/EnterWorktreeTool/UI.tsx +++ b/src/tools/EnterWorktreeTool/UI.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import type { ToolProgressData } from '../../Tool.js' import type { ProgressMessage } from '../../types/message.js' import type { ThemeName } from '../../utils/theme.js' diff --git a/src/tools/ExitPlanModeTool/UI.tsx b/src/tools/ExitPlanModeTool/UI.tsx index b73c4e53f..789ea4ccf 100644 --- a/src/tools/ExitPlanModeTool/UI.tsx +++ b/src/tools/ExitPlanModeTool/UI.tsx @@ -4,7 +4,7 @@ import { MessageResponse } from 'src/components/MessageResponse.js' import { RejectedPlanMessage } from 'src/components/messages/UserToolResultMessage/RejectedPlanMessage.js' import { BLACK_CIRCLE } from 'src/constants/figures.js' import { getModeColor } from 'src/utils/permissions/PermissionMode.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import type { ToolProgressData } from '../../Tool.js' import type { ProgressMessage } from '../../types/message.js' import { getDisplayPath } from '../../utils/file.js' diff --git a/src/tools/ExitWorktreeTool/UI.tsx b/src/tools/ExitWorktreeTool/UI.tsx index d624f9720..a3eccf4ba 100644 --- a/src/tools/ExitWorktreeTool/UI.tsx +++ b/src/tools/ExitWorktreeTool/UI.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import type { ToolProgressData } from '../../Tool.js' import type { ProgressMessage } from '../../types/message.js' import type { ThemeName } from '../../utils/theme.js' diff --git a/src/tools/FileEditTool/UI.tsx b/src/tools/FileEditTool/UI.tsx index b43f520ee..57223d000 100644 --- a/src/tools/FileEditTool/UI.tsx +++ b/src/tools/FileEditTool/UI.tsx @@ -7,8 +7,9 @@ import { MessageResponse } from 'src/components/MessageResponse.js' import { extractTag } from 'src/utils/messages.js' import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js' import { FileEditToolUpdatedMessage } from '../../components/FileEditToolUpdatedMessage.js' + +import { Text } from '@anthropic/ink' import { FilePathLink } from '../../components/FilePathLink.js' -import { Text } from '../../ink.js' import type { Tools } from '../../Tool.js' import type { Message, ProgressMessage } from '../../types/message.js' import { adjustHunkLineNumbers, CONTEXT_LINES } from '../../utils/diff.js' diff --git a/src/tools/FileReadTool/UI.tsx b/src/tools/FileReadTool/UI.tsx index f7849957e..017d55e86 100644 --- a/src/tools/FileReadTool/UI.tsx +++ b/src/tools/FileReadTool/UI.tsx @@ -2,9 +2,10 @@ import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs import * as React from 'react' import { extractTag } from 'src/utils/messages.js' import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js' -import { FilePathLink } from '../../components/FilePathLink.js' + import { MessageResponse } from '../../components/MessageResponse.js' -import { Text } from '../../ink.js' +import { Text } from '@anthropic/ink' +import { FilePathLink } from '../../components/FilePathLink.js' import { FILE_NOT_FOUND_CWD_NOTE, getDisplayPath } from '../../utils/file.js' import { formatFileSize } from '../../utils/format.js' import { getPlansDirectory } from '../../utils/plans.js' diff --git a/src/tools/FileWriteTool/UI.tsx b/src/tools/FileWriteTool/UI.tsx index 183edc811..8ea218fb7 100644 --- a/src/tools/FileWriteTool/UI.tsx +++ b/src/tools/FileWriteTool/UI.tsx @@ -9,10 +9,11 @@ import { CtrlOToExpand } from '../../components/CtrlOToExpand.js' import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js' import { FileEditToolUpdatedMessage } from '../../components/FileEditToolUpdatedMessage.js' import { FileEditToolUseRejectedMessage } from '../../components/FileEditToolUseRejectedMessage.js' -import { FilePathLink } from '../../components/FilePathLink.js' + import { HighlightedCode } from '../../components/HighlightedCode.js' import { useTerminalSize } from '../../hooks/useTerminalSize.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' +import { FilePathLink } from '../../components/FilePathLink.js' import type { ToolProgressData } from '../../Tool.js' import type { ProgressMessage } from '../../types/message.js' import { getCwd } from '../../utils/cwd.js' diff --git a/src/tools/GlobTool/UI.tsx b/src/tools/GlobTool/UI.tsx index 03a76237b..b70934fa4 100644 --- a/src/tools/GlobTool/UI.tsx +++ b/src/tools/GlobTool/UI.tsx @@ -4,7 +4,7 @@ import { MessageResponse } from 'src/components/MessageResponse.js' import { extractTag } from 'src/utils/messages.js' import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js' import { TOOL_SUMMARY_MAX_LENGTH } from '../../constants/toolLimits.js' -import { Text } from '../../ink.js' +import { Text } from '@anthropic/ink' import { FILE_NOT_FOUND_CWD_NOTE, getDisplayPath } from '../../utils/file.js' import { truncate } from '../../utils/format.js' import { GrepTool } from '../GrepTool/GrepTool.js' diff --git a/src/tools/GrepTool/UI.tsx b/src/tools/GrepTool/UI.tsx index dd26d0cb7..4bc319175 100644 --- a/src/tools/GrepTool/UI.tsx +++ b/src/tools/GrepTool/UI.tsx @@ -4,7 +4,7 @@ import { CtrlOToExpand } from '../../components/CtrlOToExpand.js' import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js' import { MessageResponse } from '../../components/MessageResponse.js' import { TOOL_SUMMARY_MAX_LENGTH } from '../../constants/toolLimits.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import type { ToolProgressData } from '../../Tool.js' import type { ProgressMessage } from '../../types/message.js' import { FILE_NOT_FOUND_CWD_NOTE, getDisplayPath } from '../../utils/file.js' diff --git a/src/tools/LSPTool/UI.tsx b/src/tools/LSPTool/UI.tsx index 2ca53e9a3..2a5403b12 100644 --- a/src/tools/LSPTool/UI.tsx +++ b/src/tools/LSPTool/UI.tsx @@ -3,7 +3,7 @@ import React from 'react' import { CtrlOToExpand } from '../../components/CtrlOToExpand.js' import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js' import { MessageResponse } from '../../components/MessageResponse.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { getDisplayPath } from '../../utils/file.js' import { extractTag } from '../../utils/messages.js' import type { Input, Output } from './LSPTool.js' diff --git a/src/tools/ListMcpResourcesTool/UI.tsx b/src/tools/ListMcpResourcesTool/UI.tsx index 4343123c0..6f9b18559 100644 --- a/src/tools/ListMcpResourcesTool/UI.tsx +++ b/src/tools/ListMcpResourcesTool/UI.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { MessageResponse } from '../../components/MessageResponse.js' import { OutputLine } from '../../components/shell/OutputLine.js' -import { Text } from '../../ink.js' +import { Text } from '@anthropic/ink' import type { ToolProgressData } from '../../Tool.js' import type { ProgressMessage } from '../../types/message.js' import { jsonStringify } from '../../utils/slowOperations.js' diff --git a/src/tools/MCPTool/UI.tsx b/src/tools/MCPTool/UI.tsx index bdfb3e4ab..5fa55c7a2 100644 --- a/src/tools/MCPTool/UI.tsx +++ b/src/tools/MCPTool/UI.tsx @@ -2,19 +2,19 @@ import { feature } from 'bun:bundle' import figures from 'figures' import * as React from 'react' import type { z } from 'zod/v4' -import { ProgressBar } from '../../components/design-system/ProgressBar.js' +import { ProgressBar } from '@anthropic/ink' import { MessageResponse } from '../../components/MessageResponse.js' import { linkifyUrlsInText, OutputLine, } from '../../components/shell/OutputLine.js' -import { stringWidth } from '../../ink/stringWidth.js' -import { Ansi, Box, Text } from '../../ink.js' +import { Ansi, Box, Text, stringWidth } from '@anthropic/ink' +import { createHyperlink } from '../../utils/hyperlink.js' import type { ToolProgressData } from '../../Tool.js' import type { ProgressMessage } from '../../types/message.js' import type { MCPProgress } from '../../types/tools.js' import { formatNumber } from '../../utils/format.js' -import { createHyperlink } from '../../utils/hyperlink.js' + import { getContentSizeEstimate, type MCPToolResult, diff --git a/src/tools/NotebookEditTool/UI.tsx b/src/tools/NotebookEditTool/UI.tsx index 3d777308a..e209aaba7 100644 --- a/src/tools/NotebookEditTool/UI.tsx +++ b/src/tools/NotebookEditTool/UI.tsx @@ -5,11 +5,12 @@ import { extractTag } from 'src/utils/messages.js' import type { ThemeName } from 'src/utils/theme.js' import type { z } from 'zod/v4' import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js' -import { FilePathLink } from '../../components/FilePathLink.js' + import { HighlightedCode } from '../../components/HighlightedCode.js' import { MessageResponse } from '../../components/MessageResponse.js' import { NotebookEditToolUseRejectedMessage } from '../../components/NotebookEditToolUseRejectedMessage.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' +import { FilePathLink } from '../../components/FilePathLink.js' import type { Tools } from '../../Tool.js' import { getDisplayPath } from '../../utils/file.js' import type { inputSchema, Output } from './NotebookEditTool.js' diff --git a/src/tools/PowerShellTool/UI.tsx b/src/tools/PowerShellTool/UI.tsx index 0f9438510..8b27bfa12 100644 --- a/src/tools/PowerShellTool/UI.tsx +++ b/src/tools/PowerShellTool/UI.tsx @@ -1,12 +1,12 @@ import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs' import * as React from 'react' -import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js' +import { KeyboardShortcutHint } from '@anthropic/ink' import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js' import { MessageResponse } from '../../components/MessageResponse.js' import { OutputLine } from '../../components/shell/OutputLine.js' import { ShellProgressMessage } from '../../components/shell/ShellProgressMessage.js' import { ShellTimeDisplay } from '../../components/shell/ShellTimeDisplay.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import type { Tool } from '../../Tool.js' import type { ProgressMessage } from '../../types/message.js' import type { PowerShellProgress } from '../../types/tools.js' diff --git a/src/tools/ReadMcpResourceTool/UI.tsx b/src/tools/ReadMcpResourceTool/UI.tsx index 7459608a3..d2641adc2 100644 --- a/src/tools/ReadMcpResourceTool/UI.tsx +++ b/src/tools/ReadMcpResourceTool/UI.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import type { z } from 'zod/v4' import { MessageResponse } from '../../components/MessageResponse.js' import { OutputLine } from '../../components/shell/OutputLine.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import type { ToolProgressData } from '../../Tool.js' import type { ProgressMessage } from '../../types/message.js' import { jsonStringify } from '../../utils/slowOperations.js' diff --git a/src/tools/RemoteTriggerTool/UI.tsx b/src/tools/RemoteTriggerTool/UI.tsx index 3b44a786e..5aae25157 100644 --- a/src/tools/RemoteTriggerTool/UI.tsx +++ b/src/tools/RemoteTriggerTool/UI.tsx @@ -1,6 +1,6 @@ import React from 'react' import { MessageResponse } from '../../components/MessageResponse.js' -import { Text } from '../../ink.js' +import { Text } from '@anthropic/ink' import { countCharInString } from '../../utils/stringUtils.js' import type { Input, Output } from './RemoteTriggerTool.js' diff --git a/src/tools/ScheduleCronTool/UI.tsx b/src/tools/ScheduleCronTool/UI.tsx index 439f7048f..7e4928a81 100644 --- a/src/tools/ScheduleCronTool/UI.tsx +++ b/src/tools/ScheduleCronTool/UI.tsx @@ -1,6 +1,6 @@ import React from 'react' import { MessageResponse } from '../../components/MessageResponse.js' -import { Text } from '../../ink.js' +import { Text } from '@anthropic/ink' import { truncate } from '../../utils/format.js' import type { CreateOutput } from './CronCreateTool.js' import type { DeleteOutput } from './CronDeleteTool.js' diff --git a/src/tools/SendMessageTool/UI.tsx b/src/tools/SendMessageTool/UI.tsx index 2cd05a935..38d17a8d4 100644 --- a/src/tools/SendMessageTool/UI.tsx +++ b/src/tools/SendMessageTool/UI.tsx @@ -1,6 +1,6 @@ import React from 'react' import { MessageResponse } from '../../components/MessageResponse.js' -import { Text } from '../../ink.js' +import { Text } from '@anthropic/ink' import { jsonParse } from '../../utils/slowOperations.js' import type { Input, SendMessageToolOutput } from './SendMessageTool.js' diff --git a/src/tools/SkillTool/UI.tsx b/src/tools/SkillTool/UI.tsx index 558ef9c57..7898ee348 100644 --- a/src/tools/SkillTool/UI.tsx +++ b/src/tools/SkillTool/UI.tsx @@ -5,10 +5,10 @@ import { FallbackToolUseErrorMessage } from 'src/components/FallbackToolUseError import { FallbackToolUseRejectedMessage } from 'src/components/FallbackToolUseRejectedMessage.js' import type { z } from 'zod/v4' import type { Command } from '../../commands.js' -import { Byline } from '../../components/design-system/Byline.js' +import { Byline } from '@anthropic/ink' import { Message as MessageComponent } from '../../components/Message.js' import { MessageResponse } from '../../components/MessageResponse.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import type { Tools } from '../../Tool.js' import type { ProgressMessage } from '../../types/message.js' import { buildSubagentLookups, EMPTY_LOOKUPS } from '../../utils/messages.js' diff --git a/src/tools/SkillTool/prompt.ts b/src/tools/SkillTool/prompt.ts index b05a58f3d..ef8267a78 100644 --- a/src/tools/SkillTool/prompt.ts +++ b/src/tools/SkillTool/prompt.ts @@ -6,7 +6,7 @@ import { getSlashCommandToolSkills, } from 'src/commands.js' import { COMMAND_NAME_TAG } from '../../constants/xml.js' -import { stringWidth } from '../../ink/stringWidth.js' +import { stringWidth } from '@anthropic/ink' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, diff --git a/src/tools/TaskOutputTool/TaskOutputTool.tsx b/src/tools/TaskOutputTool/TaskOutputTool.tsx index 9d8897411..4da47ffbc 100644 --- a/src/tools/TaskOutputTool/TaskOutputTool.tsx +++ b/src/tools/TaskOutputTool/TaskOutputTool.tsx @@ -3,7 +3,7 @@ import { z } from 'zod/v4' import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js' import { FallbackToolUseRejectedMessage } from '../../components/FallbackToolUseRejectedMessage.js' import { MessageResponse } from '../../components/MessageResponse.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js' import type { TaskType } from '../../Task.js' import type { Tool } from '../../Tool.js' diff --git a/src/tools/TaskStopTool/UI.tsx b/src/tools/TaskStopTool/UI.tsx index 6d25894bf..e4b2e8501 100644 --- a/src/tools/TaskStopTool/UI.tsx +++ b/src/tools/TaskStopTool/UI.tsx @@ -1,7 +1,6 @@ import React from 'react' import { MessageResponse } from '../../components/MessageResponse.js' -import { stringWidth } from '../../ink/stringWidth.js' -import { Text } from '../../ink.js' +import { Text, stringWidth } from '@anthropic/ink' import { truncateToWidthNoEllipsis } from '../../utils/format.js' import type { Output } from './TaskStopTool.js' diff --git a/src/tools/TungstenTool/TungstenTool.js b/src/tools/TungstenTool/TungstenTool.js index dbd53ee4a..3d6635d37 100644 --- a/src/tools/TungstenTool/TungstenTool.js +++ b/src/tools/TungstenTool/TungstenTool.js @@ -2,3 +2,6 @@ export const TungstenTool = { name: 'TungstenTool', isEnabled: () => false, } + +export const clearSessionsWithTungstenUsage = () => {} +export const resetInitializationState = () => {} diff --git a/src/tools/WebFetchTool/UI.tsx b/src/tools/WebFetchTool/UI.tsx index a32c95463..546bf727e 100644 --- a/src/tools/WebFetchTool/UI.tsx +++ b/src/tools/WebFetchTool/UI.tsx @@ -1,7 +1,7 @@ import React from 'react' import { MessageResponse } from '../../components/MessageResponse.js' import { TOOL_SUMMARY_MAX_LENGTH } from '../../constants/toolLimits.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import type { ToolProgressData } from '../../Tool.js' import type { ProgressMessage } from '../../types/message.js' import { formatFileSize, truncate } from '../../utils/format.js' diff --git a/src/tools/WebSearchTool/UI.tsx b/src/tools/WebSearchTool/UI.tsx index af8584b29..005f95db9 100644 --- a/src/tools/WebSearchTool/UI.tsx +++ b/src/tools/WebSearchTool/UI.tsx @@ -1,7 +1,7 @@ import React from 'react' import { MessageResponse } from '../../components/MessageResponse.js' import { TOOL_SUMMARY_MAX_LENGTH } from '../../constants/toolLimits.js' -import { Box, Text } from '../../ink.js' +import { Box, Text } from '@anthropic/ink' import type { ProgressMessage } from '../../types/message.js' import { truncate } from '../../utils/format.js' import type { diff --git a/src/types/ink-elements.d.ts b/src/types/ink-elements.d.ts index d5053283e..f98f08e53 100644 --- a/src/types/ink-elements.d.ts +++ b/src/types/ink-elements.d.ts @@ -2,11 +2,7 @@ // 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 '../ink/events/click-event.js'; -import type { FocusEvent } from '../ink/events/focus-event.js'; -import type { KeyboardEvent } from '../ink/events/keyboard-event.js'; -import type { Styles, TextStyles } from '../ink/styles.js'; -import type { DOMElement } from '../ink/dom.js'; +import type { ClickEvent, FocusEvent, KeyboardEvent, Styles, TextStyles, DOMElement } from '@anthropic/ink'; declare global { namespace JSX { diff --git a/src/types/ink-jsx.d.ts b/src/types/ink-jsx.d.ts index 8c3704959..2c8911c71 100644 --- a/src/types/ink-jsx.d.ts +++ b/src/types/ink-jsx.d.ts @@ -9,11 +9,7 @@ * augmentation to work correctly. */ import type { ReactNode, Ref } from 'react'; -import type { ClickEvent } from '../ink/events/click-event.js'; -import type { FocusEvent } from '../ink/events/focus-event.js'; -import type { KeyboardEvent } from '../ink/events/keyboard-event.js'; -import type { Styles, TextStyles } from '../ink/styles.js'; -import type { DOMElement } from '../ink/dom.js'; +import type { ClickEvent, FocusEvent, KeyboardEvent, Styles, TextStyles, DOMElement } from '@anthropic/ink'; declare module 'react' { namespace JSX { diff --git a/src/types/textInputTypes.ts b/src/types/textInputTypes.ts index 1e77b56fe..7e5cb047e 100644 --- a/src/types/textInputTypes.ts +++ b/src/types/textInputTypes.ts @@ -2,7 +2,7 @@ import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs import type { UUID } from 'crypto' import type React from 'react' import type { PermissionResult } from '../entrypoints/agentSdkTypes.js' -import type { Key } from '../ink.js' +import type { Key } from '@anthropic/ink' import type { PastedContent } from '../utils/config.js' import type { ImageDimensions } from '../utils/imageResizer.js' import type { TextHighlight } from '../utils/textHighlighting.js' diff --git a/src/utils/Cursor.ts b/src/utils/Cursor.ts index 0317ece77..6968ff473 100644 --- a/src/utils/Cursor.ts +++ b/src/utils/Cursor.ts @@ -1,5 +1,4 @@ -import { stringWidth } from '../ink/stringWidth.js' -import { wrapAnsi } from '../ink/wrapAnsi.js' +import { stringWidth, wrapAnsi } from '@anthropic/ink' import { firstGrapheme, getGraphemeSegmenter, diff --git a/src/utils/__tests__/treeify.test.ts b/src/utils/__tests__/treeify.test.ts index e9f553755..6e60c66bc 100644 --- a/src/utils/__tests__/treeify.test.ts +++ b/src/utils/__tests__/treeify.test.ts @@ -8,7 +8,7 @@ mock.module("figures", () => ({ }, })); -mock.module("src/components/design-system/color.js", () => ({ +mock.module("src/ink.js", () => ({ color: (colorKey: string, themeName: string) => (text: string) => text, })); diff --git a/src/utils/ansiToPng.ts b/src/utils/ansiToPng.ts index f823fbbf8..499ecedc6 100644 --- a/src/utils/ansiToPng.ts +++ b/src/utils/ansiToPng.ts @@ -19,7 +19,7 @@ */ import { deflateSync } from 'zlib' -import { stringWidth } from '../ink/stringWidth.js' +import { stringWidth } from '@anthropic/ink' import { type AnsiColor, DEFAULT_BG, diff --git a/src/utils/autoRunIssue.tsx b/src/utils/autoRunIssue.tsx index 971f25635..3a6275bb5 100644 --- a/src/utils/autoRunIssue.tsx +++ b/src/utils/autoRunIssue.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { useEffect, useRef } from 'react' -import { KeyboardShortcutHint } from '../components/design-system/KeyboardShortcutHint.js' -import { Box, Text } from '../ink.js' +import { KeyboardShortcutHint } from '@anthropic/ink' +import { Box, Text } from '@anthropic/ink' import { useKeybinding } from '../keybindings/useKeybinding.js' type Props = { diff --git a/src/utils/claudeInChrome/mcpServer.ts b/src/utils/claudeInChrome/mcpServer.ts index 4195d2c4f..02f0d629c 100644 --- a/src/utils/claudeInChrome/mcpServer.ts +++ b/src/utils/claudeInChrome/mcpServer.ts @@ -4,6 +4,7 @@ import { type Logger, type PermissionMode, } from '@ant/claude-for-chrome-mcp' +import { initializeAnalyticsSink } from '../../services/analytics/sink.js' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { format } from 'util' import { shutdownDatadog } from '../../services/analytics/datadog.js' @@ -13,7 +14,7 @@ import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, } from '../../services/analytics/index.js' -import { initializeAnalyticsSink } from '../../services/analytics/sink.js' + import { getClaudeAIOAuthTokens } from '../auth.js' import { enableConfigs, getGlobalConfig, saveGlobalConfig } from '../config.js' import { logForDebugging } from '../debug.js' diff --git a/src/utils/claudeInChrome/toolRendering.tsx b/src/utils/claudeInChrome/toolRendering.tsx index d760085fb..6f77bcf0b 100644 --- a/src/utils/claudeInChrome/toolRendering.tsx +++ b/src/utils/claudeInChrome/toolRendering.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { MessageResponse } from '../../components/MessageResponse.js' -import { supportsHyperlinks } from '../../ink/supports-hyperlinks.js' -import { Link, Text } from '../../ink.js' +import { supportsHyperlinks } from '@anthropic/ink' +import { Link, Text } from '@anthropic/ink' import { renderToolResultMessage as renderDefaultMCPToolResultMessage } from '../../tools/MCPTool/UI.js' import type { MCPToolResult } from '../../utils/mcpValidation.js' import { truncateToWidth } from '../format.js' diff --git a/src/utils/completionCache.ts b/src/utils/completionCache.ts index 3f0c9d2f7..ec288c3e6 100644 --- a/src/utils/completionCache.ts +++ b/src/utils/completionCache.ts @@ -3,8 +3,8 @@ import { mkdir, readFile, writeFile } from 'fs/promises' import { homedir } from 'os' import { dirname, join } from 'path' import { pathToFileURL } from 'url' -import { color } from '../components/design-system/color.js' -import { supportsHyperlinks } from '../ink/supports-hyperlinks.js' +import { color } from '@anthropic/ink' +import { supportsHyperlinks } from '@anthropic/ink' import { logForDebugging } from './debug.js' import { isENOENT } from './errors.js' import { execFileNoThrow } from './execFileNoThrow.js' diff --git a/src/utils/computerUse/drainRunLoop.ts b/src/utils/computerUse/drainRunLoop.ts index 28dac7c57..9788766e9 100644 --- a/src/utils/computerUse/drainRunLoop.ts +++ b/src/utils/computerUse/drainRunLoop.ts @@ -18,7 +18,7 @@ let pump: ReturnType | undefined let pending = 0 function drainTick(cu: ReturnType): void { - ;(cu as any)._drainMainRunLoop() + ;(cu as any)?._drainMainRunLoop?.() } function retain(): void { diff --git a/src/utils/computerUse/mcpServer.ts b/src/utils/computerUse/mcpServer.ts index d51d80ab2..f759335c5 100644 --- a/src/utils/computerUse/mcpServer.ts +++ b/src/utils/computerUse/mcpServer.ts @@ -2,13 +2,14 @@ import { buildComputerUseTools, createComputerUseMcpServer, } from '@ant/computer-use-mcp' +import { initializeAnalyticsSink } from '../../services/analytics/sink.js' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js' import { homedir } from 'os' import { shutdownDatadog } from '../../services/analytics/datadog.js' import { shutdown1PEventLogging } from '../../services/analytics/firstPartyEventLogger.js' -import { initializeAnalyticsSink } from '../../services/analytics/sink.js' + import { enableConfigs } from '../config.js' import { logForDebugging } from '../debug.js' import { filterAppsForDescription } from './appNames.js' diff --git a/src/utils/computerUse/toolRendering.tsx b/src/utils/computerUse/toolRendering.tsx index 4b5da230e..0295e19e3 100644 --- a/src/utils/computerUse/toolRendering.tsx +++ b/src/utils/computerUse/toolRendering.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { MessageResponse } from '../../components/MessageResponse.js' -import { Text } from '../../ink.js' +import { Text } from '@anthropic/ink' import { truncateToWidth } from '../format.js' import type { MCPToolResult } from '../mcpValidation.js' diff --git a/src/utils/deepLink/protocolHandler.ts b/src/utils/deepLink/protocolHandler.ts index 0134aef51..511754dc7 100644 --- a/src/utils/deepLink/protocolHandler.ts +++ b/src/utils/deepLink/protocolHandler.ts @@ -11,6 +11,7 @@ * directly — there is no terminal attached. */ +import { parseDeepLink } from './parseDeepLink.js' import { homedir } from 'os' import { logForDebugging } from '../debug.js' import { @@ -19,7 +20,7 @@ import { } from '../githubRepoPathMapping.js' import { jsonStringify } from '../slowOperations.js' import { readLastFetchTime } from './banner.js' -import { parseDeepLink } from './parseDeepLink.js' + import { MACOS_BUNDLE_ID } from './registerProtocol.js' import { launchInTerminal } from './terminalLauncher.js' diff --git a/src/utils/deepLink/registerProtocol.ts b/src/utils/deepLink/registerProtocol.ts index 0e630ee61..a877cbaaa 100644 --- a/src/utils/deepLink/registerProtocol.ts +++ b/src/utils/deepLink/registerProtocol.ts @@ -13,6 +13,7 @@ * Windows — Writes registry keys under HKEY_CURRENT_USER\Software\Classes */ +import { DEEP_LINK_PROTOCOL } from './parseDeepLink.js' import { promises as fs } from 'fs' import * as os from 'os' import * as path from 'path' @@ -28,7 +29,6 @@ import { execFileNoThrow } from '../execFileNoThrow.js' import { getInitialSettings } from '../settings/settings.js' import { which } from '../which.js' import { getUserBinDir, getXDGDataHome } from '../xdg.js' -import { DEEP_LINK_PROTOCOL } from './parseDeepLink.js' export const MACOS_BUNDLE_ID = 'com.anthropic.claude-code-url-handler' const APP_NAME = 'Claude Code URL Handler' diff --git a/src/utils/editor.ts b/src/utils/editor.ts index 2924b77b9..2cd6aa86e 100644 --- a/src/utils/editor.ts +++ b/src/utils/editor.ts @@ -6,7 +6,7 @@ import { } from 'child_process' import memoize from 'lodash-es/memoize.js' import { basename } from 'path' -import instances from '../ink/instances.js' +import { instances } from '@anthropic/ink' import { logForDebugging } from './debug.js' import { whichSync } from './which.js' diff --git a/src/utils/gracefulShutdown.ts b/src/utils/gracefulShutdown.ts index 391dce288..8b8a5b9c7 100644 --- a/src/utils/gracefulShutdown.ts +++ b/src/utils/gracefulShutdown.ts @@ -10,25 +10,7 @@ import { getSessionId, isSessionPersistenceDisabled, } from '../bootstrap/state.js' -import instances from '../ink/instances.js' -import { - DISABLE_KITTY_KEYBOARD, - DISABLE_MODIFY_OTHER_KEYS, -} from '../ink/termio/csi.js' -import { - DBP, - DFE, - DISABLE_MOUSE_TRACKING, - EXIT_ALT_SCREEN, - SHOW_CURSOR, -} from '../ink/termio/dec.js' -import { - CLEAR_ITERM2_PROGRESS, - CLEAR_TAB_STATUS, - CLEAR_TERMINAL_TITLE, - supportsTabStatus, - wrapForMultiplexer, -} from '../ink/termio/osc.js' +import { DISABLE_KITTY_KEYBOARD, DISABLE_MODIFY_OTHER_KEYS, DBP, DFE, DISABLE_MOUSE_TRACKING, EXIT_ALT_SCREEN, SHOW_CURSOR, CLEAR_ITERM2_PROGRESS, CLEAR_TAB_STATUS, CLEAR_TERMINAL_TITLE, instances, supportsTabStatus, wrapForMultiplexer } from '@anthropic/ink' import { shutdownDatadog } from '../services/analytics/datadog.js' import { shutdown1PEventLogging } from '../services/analytics/firstPartyEventLogger.js' import { diff --git a/src/utils/highlightMatch.tsx b/src/utils/highlightMatch.tsx index 31ed36045..23945f976 100644 --- a/src/utils/highlightMatch.tsx +++ b/src/utils/highlightMatch.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Text } from '../ink.js' +import { Text } from '@anthropic/ink' /** * Inverse-highlight every occurrence of `query` in `text` (case-insensitive). diff --git a/src/utils/hyperlink.ts b/src/utils/hyperlink.ts index 306dd93ad..732527380 100644 --- a/src/utils/hyperlink.ts +++ b/src/utils/hyperlink.ts @@ -1,5 +1,5 @@ import chalk from 'chalk' -import { supportsHyperlinks } from '../ink/supports-hyperlinks.js' +import { supportsHyperlinks } from '@anthropic/ink' // OSC 8 hyperlink escape sequences // Format: \e]8;;URL\e\\TEXT\e]8;;\e\\ diff --git a/src/utils/ink.ts b/src/utils/ink.ts index ea2669aca..915837bc9 100644 --- a/src/utils/ink.ts +++ b/src/utils/ink.ts @@ -1,4 +1,4 @@ -import type { TextProps } from '../ink.js' +import type { TextProps } from '@anthropic/ink' import { AGENT_COLOR_TO_THEME_COLOR, type AgentColorName, diff --git a/src/utils/logoV2Utils.ts b/src/utils/logoV2Utils.ts index f7f9720b4..4357119d7 100644 --- a/src/utils/logoV2Utils.ts +++ b/src/utils/logoV2Utils.ts @@ -1,5 +1,5 @@ import { getDirectConnectServerUrl, getSessionId } from '../bootstrap/state.js' -import { stringWidth } from '../ink/stringWidth.js' +import { stringWidth } from '@anthropic/ink' import type { LogOption } from '../types/logs.js' import { getSubscriptionName, isClaudeAISubscriber } from './auth.js' import { getCwd } from './cwd.js' diff --git a/src/utils/markdown.ts b/src/utils/markdown.ts index b2c673e6d..ae1e3902e 100644 --- a/src/utils/markdown.ts +++ b/src/utils/markdown.ts @@ -1,13 +1,13 @@ import chalk from 'chalk' import { marked, type Token, type Tokens } from 'marked' import stripAnsi from 'strip-ansi' -import { color } from '../components/design-system/color.js' +import { color } from '@anthropic/ink' import { BLOCKQUOTE_BAR } from '../constants/figures.js' -import { stringWidth } from '../ink/stringWidth.js' -import { supportsHyperlinks } from '../ink/supports-hyperlinks.js' +import { stringWidth, supportsHyperlinks } from '@anthropic/ink' +import { createHyperlink } from '../utils/hyperlink.js' import type { CliHighlight } from './cliHighlight.js' import { logForDebugging } from './debug.js' -import { createHyperlink } from './hyperlink.js' + import { stripPromptXMLTags } from './messages.js' import type { ThemeName } from './theme.js' diff --git a/src/utils/preflightChecks.tsx b/src/utils/preflightChecks.tsx index 445cd12a8..fed5b1236 100644 --- a/src/utils/preflightChecks.tsx +++ b/src/utils/preflightChecks.tsx @@ -4,7 +4,7 @@ import { logEvent } from 'src/services/analytics/index.js' import { Spinner } from '../components/Spinner.js' import { getOauthConfig } from '../constants/oauth.js' import { useTimeout } from '../hooks/useTimeout.js' -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import { getSSLErrorHint } from '../services/api/errorUtils.js' import { getUserAgent } from './http.js' import { logError } from './log.js' diff --git a/src/utils/promptEditor.ts b/src/utils/promptEditor.ts index 36a2b7bcd..0b0a65241 100644 --- a/src/utils/promptEditor.ts +++ b/src/utils/promptEditor.ts @@ -3,7 +3,7 @@ import { formatPastedTextRef, getPastedTextRefNumLines, } from '../history.js' -import instances from '../ink/instances.js' +import { instances } from '@anthropic/ink' import type { PastedContent } from './config.js' import { classifyGuiEditor, getExternalEditor } from './editor.js' import { execSync_DEPRECATED } from './execSyncWrapper.js' diff --git a/src/utils/renderOptions.ts b/src/utils/renderOptions.ts index 37bb65050..c601b36a3 100644 --- a/src/utils/renderOptions.ts +++ b/src/utils/renderOptions.ts @@ -1,6 +1,6 @@ import { openSync } from 'fs' import { ReadStream } from 'tty' -import type { RenderOptions } from '../ink.js' +import type { RenderOptions } from '@anthropic/ink' import { isEnvTruthy } from './envUtils.js' import { logError } from './log.js' diff --git a/src/utils/sinks.ts b/src/utils/sinks.ts index 386ce6ae4..ba3999587 100644 --- a/src/utils/sinks.ts +++ b/src/utils/sinks.ts @@ -1,5 +1,6 @@ -import { initializeAnalyticsSink } from '../services/analytics/sink.js' + import { initializeErrorLogSink } from './errorLogSink.js' +import { initializeAnalyticsSink } from '../services/analytics/sink.js' /** * Attach error log and analytics sinks, draining any events queued before diff --git a/src/utils/sliceAnsi.ts b/src/utils/sliceAnsi.ts index 8e1a0a07f..565a5f8cd 100644 --- a/src/utils/sliceAnsi.ts +++ b/src/utils/sliceAnsi.ts @@ -5,7 +5,7 @@ import { tokenize, undoAnsiCodes, } from '@alcalzone/ansi-tokenize' -import { stringWidth } from '../ink/stringWidth.js' +import { stringWidth } from '@anthropic/ink' // A code is an "end code" if its code equals its endCode (e.g., hyperlink close) function isEndCode(code: AnsiCode): boolean { diff --git a/src/utils/staticRender.tsx b/src/utils/staticRender.tsx index 66a809c19..2f066fe96 100644 --- a/src/utils/staticRender.tsx +++ b/src/utils/staticRender.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { useLayoutEffect } from 'react' import { PassThrough } from 'stream' import stripAnsi from 'strip-ansi' -import { render, useApp } from '../ink.js' +import { wrappedRender as render, useApp } from '@anthropic/ink' // This is a workaround for the fact that Ink doesn't support multiple // components in the same render tree. Instead of using a we just render diff --git a/src/utils/status.tsx b/src/utils/status.tsx index 6d66bc83f..5b4012fff 100644 --- a/src/utils/status.tsx +++ b/src/utils/status.tsx @@ -1,7 +1,7 @@ import chalk from 'chalk' import figures from 'figures' import * as React from 'react' -import { color, Text } from '../ink.js' +import { color, Text } from '@anthropic/ink' import type { MCPServerConnection } from '../services/mcp/types.js' import { getAccountInformation, isClaudeAISubscriber } from './auth.js' import { diff --git a/src/utils/statusNoticeDefinitions.tsx b/src/utils/statusNoticeDefinitions.tsx index e04f9e3d3..9ad173fd0 100644 --- a/src/utils/statusNoticeDefinitions.tsx +++ b/src/utils/statusNoticeDefinitions.tsx @@ -1,5 +1,5 @@ // biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered -import { Box, Text } from '../ink.js' +import { Box, Text } from '@anthropic/ink' import * as React from 'react' import { getLargeMemoryFiles, diff --git a/src/utils/swarm/It2SetupPrompt.tsx b/src/utils/swarm/It2SetupPrompt.tsx index 688e2c8f4..166c984c7 100644 --- a/src/utils/swarm/It2SetupPrompt.tsx +++ b/src/utils/swarm/It2SetupPrompt.tsx @@ -3,11 +3,11 @@ import { type OptionWithDescription, Select, } from '../../components/CustomSelect/index.js' -import { Pane } from '../../components/design-system/Pane.js' +import { Pane } from '@anthropic/ink' import { Spinner } from '../../components/Spinner.js' import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js' // eslint-disable-next-line custom-rules/prefer-use-keybindings -- enter to proceed through setup steps -import { Box, Text, useInput } from '../../ink.js' +import { Box, Text, useInput } from '@anthropic/ink' import { useKeybinding } from '../../keybindings/useKeybinding.js' import { detectPythonPackageManager, diff --git a/src/utils/teleport.tsx b/src/utils/teleport.tsx index 04202dbc7..513196fe4 100644 --- a/src/utils/teleport.tsx +++ b/src/utils/teleport.tsx @@ -17,7 +17,7 @@ import { } from '../components/TeleportError.js' import { getOauthConfig } from '../constants/oauth.js' import type { SDKMessage } from '../entrypoints/agentSdkTypes.js' -import type { Root } from '../ink.js' +import type { Root } from '@anthropic/ink' import { KeybindingSetup } from '../keybindings/KeybindingProviderSetup.js' import { queryHaiku } from '../services/api/claude.js' import { diff --git a/src/utils/terminal.ts b/src/utils/terminal.ts index a87f020c8..a935f700c 100644 --- a/src/utils/terminal.ts +++ b/src/utils/terminal.ts @@ -1,6 +1,6 @@ import chalk from 'chalk' import { ctrlOToExpand } from '../components/CtrlOToExpand.js' -import { stringWidth } from '../ink/stringWidth.js' +import { stringWidth } from '@anthropic/ink' import sliceAnsi from './sliceAnsi.js' // Text rendering utilities for terminal display diff --git a/src/utils/terminalPanel.ts b/src/utils/terminalPanel.ts index 034d22bed..9cf56c554 100644 --- a/src/utils/terminalPanel.ts +++ b/src/utils/terminalPanel.ts @@ -17,7 +17,7 @@ import { spawn, spawnSync } from 'child_process' import { getSessionId } from '../bootstrap/state.js' -import instances from '../ink/instances.js' +import { instances } from '@anthropic/ink' import { registerCleanup } from './cleanupRegistry.js' import { pwd } from './cwd.js' import { logForDebugging } from './debug.js' diff --git a/src/utils/treeify.ts b/src/utils/treeify.ts index 28dc9d83b..3b270ad4d 100644 --- a/src/utils/treeify.ts +++ b/src/utils/treeify.ts @@ -1,5 +1,5 @@ import figures from 'figures' -import { color } from '../components/design-system/color.js' +import { color } from '@anthropic/ink' import type { Theme, ThemeName } from './theme.js' export type TreeNode = { diff --git a/src/utils/truncate.ts b/src/utils/truncate.ts index fa35c10e4..aff5c72ef 100644 --- a/src/utils/truncate.ts +++ b/src/utils/truncate.ts @@ -1,6 +1,6 @@ // Width-aware truncation/wrapping — needs ink/stringWidth (not leaf-safe). -import { stringWidth } from '../ink/stringWidth.js' +import { stringWidth } from '@anthropic/ink' import { getGraphemeSegmenter } from './intl.js' /**