style(B1-5): 格式化 components其余 + hooks + tools (232 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
claude-code-best
2026-04-04 22:50:19 +08:00
parent 9ba95d209e
commit a574ea205b
232 changed files with 40275 additions and 40143 deletions

View File

@@ -1,135 +1,105 @@
import { c as _c } from "react/compiler-runtime"; import * as React from 'react'
import * as React from 'react'; import { Box, Text } from '../ink.js'
import { Box, Text } from '../ink.js'; import { formatNumber } from '../utils/format.js'
import { formatNumber } from '../utils/format.js'; import type { Theme } from '../utils/theme.js'
import type { Theme } from '../utils/theme.js';
type Props = { type Props = {
agentType: string; agentType: string
description?: string; description?: string
name?: string; name?: string
descriptionColor?: keyof Theme; descriptionColor?: keyof Theme
taskDescription?: string; taskDescription?: string
toolUseCount: number; toolUseCount: number
tokens: number | null; tokens: number | null
color?: keyof Theme; color?: keyof Theme
isLast: boolean; isLast: boolean
isResolved: boolean; isResolved: boolean
isError: boolean; isError: boolean
isAsync?: boolean; isAsync?: boolean
shouldAnimate: boolean; shouldAnimate: boolean
lastToolInfo?: string | null; lastToolInfo?: string | null
hideType?: boolean; hideType?: boolean
}; }
export function AgentProgressLine(t0) {
const $ = _c(32); export function AgentProgressLine({
const { agentType,
agentType, description,
description, name,
name, descriptionColor,
descriptionColor, taskDescription,
taskDescription, toolUseCount,
toolUseCount, tokens,
tokens, color,
color, isLast,
isLast, isResolved,
isResolved, isError: _isError,
isAsync: t1, isAsync = false,
lastToolInfo, shouldAnimate: _shouldAnimate,
hideType: t2 lastToolInfo,
} = t0; hideType = false,
const isAsync = t1 === undefined ? false : t1; }: Props): React.ReactNode {
const hideType = t2 === undefined ? false : t2; const treeChar = isLast ? '└─' : '├─'
const treeChar = isLast ? "\u2514\u2500" : "\u251C\u2500"; const isBackgrounded = isAsync && isResolved
const isBackgrounded = isAsync && isResolved;
let t3; // Determine the status text
if ($[0] !== isBackgrounded || $[1] !== isResolved || $[2] !== lastToolInfo || $[3] !== taskDescription) { const getStatusText = (): string => {
t3 = () => { if (!isResolved) {
if (!isResolved) { return lastToolInfo || 'Initializing…'
return lastToolInfo || "Initializing\u2026"; }
} if (isBackgrounded) {
if (isBackgrounded) { return taskDescription ?? 'Running in the background'
return taskDescription ?? "Running in the background"; }
} return 'Done'
return "Done"; }
};
$[0] = isBackgrounded; return (
$[1] = isResolved; <Box flexDirection="column">
$[2] = lastToolInfo; <Box paddingLeft={3}>
$[3] = taskDescription; <Text dimColor>{treeChar} </Text>
$[4] = t3; <Text dimColor={!isResolved}>
} else { {hideType ? (
t3 = $[4]; <>
} <Text bold>{name ?? description ?? agentType}</Text>
const getStatusText = t3; {name && description && <Text dimColor>: {description}</Text>}
let t4; </>
if ($[5] !== treeChar) { ) : (
t4 = <Text dimColor={true}>{treeChar} </Text>; <>
$[5] = treeChar; <Text
$[6] = t4; bold
} else { backgroundColor={color}
t4 = $[6]; color={color ? 'inverseText' : undefined}
} >
const t5 = !isResolved; {agentType}
let t6; </Text>
if ($[7] !== agentType || $[8] !== color || $[9] !== description || $[10] !== descriptionColor || $[11] !== hideType || $[12] !== name) { {description && (
t6 = hideType ? <><Text bold={true}>{name ?? description ?? agentType}</Text>{name && description && <Text dimColor={true}>: {description}</Text>}</> : <><Text bold={true} backgroundColor={color} color={color ? "inverseText" : undefined}>{agentType}</Text>{description && <>{" ("}<Text backgroundColor={descriptionColor} color={descriptionColor ? "inverseText" : undefined}>{description}</Text>{")"}</>}</>; <>
$[7] = agentType; {' ('}
$[8] = color; <Text
$[9] = description; backgroundColor={descriptionColor}
$[10] = descriptionColor; color={descriptionColor ? 'inverseText' : undefined}
$[11] = hideType; >
$[12] = name; {description}
$[13] = t6; </Text>
} else { {')'}
t6 = $[13]; </>
} )}
let t7; </>
if ($[14] !== isBackgrounded || $[15] !== tokens || $[16] !== toolUseCount) { )}
t7 = !isBackgrounded && <>{" \xB7 "}{toolUseCount} tool {toolUseCount === 1 ? "use" : "uses"}{tokens !== null && <> · {formatNumber(tokens)} tokens</>}</>; {!isBackgrounded && (
$[14] = isBackgrounded; <>
$[15] = tokens; {' · '}
$[16] = toolUseCount; {toolUseCount} tool {toolUseCount === 1 ? 'use' : 'uses'}
$[17] = t7; {tokens !== null && <> · {formatNumber(tokens)} tokens</>}
} else { </>
t7 = $[17]; )}
} </Text>
let t8; </Box>
if ($[18] !== t5 || $[19] !== t6 || $[20] !== t7) { {!isBackgrounded && (
t8 = <Text dimColor={t5}>{t6}{t7}</Text>; <Box paddingLeft={3} flexDirection="row">
$[18] = t5; <Text dimColor>{isLast ? ' ⎿ ' : '│ ⎿ '}</Text>
$[19] = t6; <Text dimColor>{getStatusText()}</Text>
$[20] = t7; </Box>
$[21] = t8; )}
} else { </Box>
t8 = $[21]; )
}
let t9;
if ($[22] !== t4 || $[23] !== t8) {
t9 = <Box paddingLeft={3}>{t4}{t8}</Box>;
$[22] = t4;
$[23] = t8;
$[24] = t9;
} else {
t9 = $[24];
}
let t10;
if ($[25] !== getStatusText || $[26] !== isBackgrounded || $[27] !== isLast) {
t10 = !isBackgrounded && <Box paddingLeft={3} flexDirection="row"><Text dimColor={true}>{isLast ? " \u23BF " : "\u2502 \u23BF "}</Text><Text dimColor={true}>{getStatusText()}</Text></Box>;
$[25] = getStatusText;
$[26] = isBackgrounded;
$[27] = isLast;
$[28] = t10;
} else {
t10 = $[28];
}
let t11;
if ($[29] !== t10 || $[30] !== t9) {
t11 = <Box flexDirection="column">{t9}{t10}</Box>;
$[29] = t10;
$[30] = t9;
$[31] = t11;
} else {
t11 = $[31];
}
return t11;
} }

View File

@@ -1,55 +1,37 @@
import { c as _c } from "react/compiler-runtime"; import React from 'react'
import React from 'react'; import { FpsMetricsProvider } from '../context/fpsMetrics.js'
import { FpsMetricsProvider } from '../context/fpsMetrics.js'; import { StatsProvider, type StatsStore } from '../context/stats.js'
import { StatsProvider, type StatsStore } from '../context/stats.js'; import { type AppState, AppStateProvider } from '../state/AppState.js'
import { type AppState, AppStateProvider } from '../state/AppState.js'; import { onChangeAppState } from '../state/onChangeAppState.js'
import { onChangeAppState } from '../state/onChangeAppState.js'; import type { FpsMetrics } from '../utils/fpsTracker.js'
import type { FpsMetrics } from '../utils/fpsTracker.js';
type Props = { type Props = {
getFpsMetrics: () => FpsMetrics | undefined; getFpsMetrics: () => FpsMetrics | undefined
stats?: StatsStore; stats?: StatsStore
initialState: AppState; initialState: AppState
children: React.ReactNode; children: React.ReactNode
}; }
/** /**
* Top-level wrapper for interactive sessions. * Top-level wrapper for interactive sessions.
* Provides FPS metrics, stats context, and app state to the component tree. * Provides FPS metrics, stats context, and app state to the component tree.
*/ */
export function App(t0) { export function App({
const $ = _c(9); getFpsMetrics,
const { stats,
getFpsMetrics, initialState,
stats, children,
initialState, }: Props): React.ReactNode {
children return (
} = t0; <FpsMetricsProvider getFpsMetrics={getFpsMetrics}>
let t1; <StatsProvider store={stats}>
if ($[0] !== children || $[1] !== initialState) { <AppStateProvider
t1 = <AppStateProvider initialState={initialState} onChangeAppState={onChangeAppState}>{children}</AppStateProvider>; initialState={initialState}
$[0] = children; onChangeAppState={onChangeAppState}
$[1] = initialState; >
$[2] = t1; {children}
} else { </AppStateProvider>
t1 = $[2]; </StatsProvider>
} </FpsMetricsProvider>
let t2; )
if ($[3] !== stats || $[4] !== t1) {
t2 = <StatsProvider store={stats}>{t1}</StatsProvider>;
$[3] = stats;
$[4] = t1;
$[5] = t2;
} else {
t2 = $[5];
}
let t3;
if ($[6] !== getFpsMetrics || $[7] !== t2) {
t3 = <FpsMetricsProvider getFpsMetrics={getFpsMetrics}>{t2}</FpsMetricsProvider>;
$[6] = getFpsMetrics;
$[7] = t2;
$[8] = t3;
} else {
t3 = $[8];
}
return t3;
} }

View File

@@ -1,122 +1,79 @@
import { c as _c } from "react/compiler-runtime"; import React from 'react'
import React from 'react'; import { Text } from '../ink.js'
import { Text } from '../ink.js'; import { saveGlobalConfig } from '../utils/config.js'
import { saveGlobalConfig } from '../utils/config.js'; import { Select } from './CustomSelect/index.js'
import { Select } from './CustomSelect/index.js'; import { Dialog } from './design-system/Dialog.js'
import { Dialog } from './design-system/Dialog.js';
type Props = { type Props = {
customApiKeyTruncated: string; customApiKeyTruncated: string
onDone(approved: boolean): void; onDone(approved: boolean): void
}; }
export function ApproveApiKey(t0) {
const $ = _c(17); export function ApproveApiKey({
const { customApiKeyTruncated,
customApiKeyTruncated, onDone,
onDone }: Props): React.ReactNode {
} = t0; function onChange(value: 'yes' | 'no') {
let t1; switch (value) {
if ($[0] !== customApiKeyTruncated || $[1] !== onDone) { case 'yes': {
t1 = function onChange(value) { saveGlobalConfig(current => ({
bb2: switch (value) { ...current,
case "yes": customApiKeyResponses: {
{ ...current.customApiKeyResponses,
saveGlobalConfig(current_0 => ({ approved: [
...current_0, ...(current.customApiKeyResponses?.approved ?? []),
customApiKeyResponses: { customApiKeyTruncated,
...current_0.customApiKeyResponses, ],
approved: [...(current_0.customApiKeyResponses?.approved ?? []), customApiKeyTruncated] },
} }))
})); onDone(true)
onDone(true); break
break bb2; }
} case 'no': {
case "no": saveGlobalConfig(current => ({
{ ...current,
saveGlobalConfig(current => ({ customApiKeyResponses: {
...current, ...current.customApiKeyResponses,
customApiKeyResponses: { rejected: [
...current.customApiKeyResponses, ...(current.customApiKeyResponses?.rejected ?? []),
rejected: [...(current.customApiKeyResponses?.rejected ?? []), customApiKeyTruncated] customApiKeyTruncated,
} ],
})); },
onDone(false); }))
} onDone(false)
} break
}; }
$[0] = customApiKeyTruncated; }
$[1] = onDone; }
$[2] = t1;
} else { return (
t1 = $[2]; <Dialog
} title="Detected a custom API key in your environment"
const onChange = t1; color="warning"
let t2; onCancel={() => onChange('no')}
if ($[3] !== onChange) { >
t2 = () => onChange("no"); <Text>
$[3] = onChange; <Text bold>ANTHROPIC_API_KEY</Text>
$[4] = t2; <Text>: sk-ant-...{customApiKeyTruncated}</Text>
} else { </Text>
t2 = $[4]; <Text>Do you want to use this API key?</Text>
} <Select
let t3; defaultValue="no"
if ($[5] === Symbol.for("react.memo_cache_sentinel")) { defaultFocusValue="no"
t3 = <Text bold={true}>ANTHROPIC_API_KEY</Text>; options={[
$[5] = t3; { label: 'Yes', value: 'yes' },
} else { {
t3 = $[5]; label: (
} <Text>
let t4; No (<Text bold>recommended</Text>)
if ($[6] !== customApiKeyTruncated) { </Text>
t4 = <Text>{t3}<Text>: sk-ant-...{customApiKeyTruncated}</Text></Text>; ),
$[6] = customApiKeyTruncated; value: 'no',
$[7] = t4; },
} else { ]}
t4 = $[7]; onChange={value => onChange(value as 'yes' | 'no')}
} onCancel={() => onChange('no')}
let t5; />
if ($[8] === Symbol.for("react.memo_cache_sentinel")) { </Dialog>
t5 = <Text>Do you want to use this API key?</Text>; )
$[8] = t5;
} else {
t5 = $[8];
}
let t6;
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
t6 = {
label: "Yes",
value: "yes"
};
$[9] = t6;
} else {
t6 = $[9];
}
let t7;
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
t7 = [t6, {
label: <Text>No (<Text bold={true}>recommended</Text>)</Text>,
value: "no"
}];
$[10] = t7;
} else {
t7 = $[10];
}
let t8;
if ($[11] !== onChange) {
t8 = <Select defaultValue="no" defaultFocusValue="no" options={t7} onChange={value_0 => onChange(value_0 as 'yes' | 'no')} onCancel={() => onChange("no")} />;
$[11] = onChange;
$[12] = t8;
} else {
t8 = $[12];
}
let t9;
if ($[13] !== t2 || $[14] !== t4 || $[15] !== t8) {
t9 = <Dialog title="Detected a custom API key in your environment" color="warning" onCancel={t2}>{t4}{t5}{t8}</Dialog>;
$[13] = t2;
$[14] = t4;
$[15] = t8;
$[16] = t9;
} else {
t9 = $[16];
}
return t9;
} }

View File

@@ -1,141 +1,86 @@
import { c as _c } from "react/compiler-runtime"; import React from 'react'
import React from 'react'; import { logEvent } from 'src/services/analytics/index.js'
import { logEvent } from 'src/services/analytics/index.js'; import { Box, Link, Text } from '../ink.js'
import { Box, Link, Text } from '../ink.js'; import { updateSettingsForSource } from '../utils/settings/settings.js'
import { updateSettingsForSource } from '../utils/settings/settings.js'; import { Select } from './CustomSelect/index.js'
import { Select } from './CustomSelect/index.js'; import { Dialog } from './design-system/Dialog.js'
import { Dialog } from './design-system/Dialog.js';
// NOTE: This copy is legally reviewed — do not modify without Legal team approval. // NOTE: This copy is legally reviewed — do not modify without Legal team approval.
export const AUTO_MODE_DESCRIPTION = "Auto mode lets Claude handle permission prompts automatically — Claude checks each tool call for risky actions and prompt injection before executing. Actions Claude identifies as safe are executed, while actions Claude identifies as risky are blocked and Claude may try a different approach. Ideal for long-running tasks. Sessions are slightly more expensive. Claude can make mistakes that allow harmful commands to run, it's recommended to only use in isolated environments. Shift+Tab to change mode."; export const AUTO_MODE_DESCRIPTION =
"Auto mode lets Claude handle permission prompts automatically — Claude checks each tool call for risky actions and prompt injection before executing. Actions Claude identifies as safe are executed, while actions Claude identifies as risky are blocked and Claude may try a different approach. Ideal for long-running tasks. Sessions are slightly more expensive. Claude can make mistakes that allow harmful commands to run, it's recommended to only use in isolated environments. Shift+Tab to change mode."
type Props = { type Props = {
onAccept(): void; onAccept(): void
onDecline(): void; onDecline(): void
// Startup gate: decline exits the process, so relabel accordingly. // Startup gate: decline exits the process, so relabel accordingly.
declineExits?: boolean; declineExits?: boolean
}; }
export function AutoModeOptInDialog(t0) {
const $ = _c(18); export function AutoModeOptInDialog({
const { onAccept,
onAccept, onDecline,
onDecline, declineExits,
declineExits }: Props): React.ReactNode {
} = t0; React.useEffect(() => {
let t1; logEvent('tengu_auto_mode_opt_in_dialog_shown', {})
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { }, [])
t1 = [];
$[0] = t1; function onChange(value: 'accept' | 'accept-default' | 'decline') {
} else { switch (value) {
t1 = $[0]; case 'accept': {
} logEvent('tengu_auto_mode_opt_in_dialog_accept', {})
React.useEffect(_temp, t1); updateSettingsForSource('userSettings', {
let t2; skipAutoPermissionPrompt: true,
if ($[1] !== onAccept || $[2] !== onDecline) { })
t2 = function onChange(value) { onAccept()
bb3: switch (value) { break
case "accept":
{
logEvent("tengu_auto_mode_opt_in_dialog_accept", {});
updateSettingsForSource("userSettings", {
skipAutoPermissionPrompt: true
});
onAccept();
break bb3;
}
case "accept-default":
{
logEvent("tengu_auto_mode_opt_in_dialog_accept_default", {});
updateSettingsForSource("userSettings", {
skipAutoPermissionPrompt: true,
permissions: {
defaultMode: "auto"
}
});
onAccept();
break bb3;
}
case "decline":
{
logEvent("tengu_auto_mode_opt_in_dialog_decline", {});
onDecline();
}
} }
}; case 'accept-default': {
$[1] = onAccept; logEvent('tengu_auto_mode_opt_in_dialog_accept_default', {})
$[2] = onDecline; updateSettingsForSource('userSettings', {
$[3] = t2; skipAutoPermissionPrompt: true,
} else { permissions: { defaultMode: 'auto' },
t2 = $[3]; })
onAccept()
break
}
case 'decline': {
logEvent('tengu_auto_mode_opt_in_dialog_decline', {})
onDecline()
break
}
}
} }
const onChange = t2;
let t3; return (
if ($[4] === Symbol.for("react.memo_cache_sentinel")) { <Dialog title="Enable auto mode?" color="warning" onCancel={onDecline}>
t3 = <Box flexDirection="column" gap={1}><Text>{AUTO_MODE_DESCRIPTION}</Text><Link url="https://code.claude.com/docs/en/security" /></Box>; <Box flexDirection="column" gap={1}>
$[4] = t3; <Text>{AUTO_MODE_DESCRIPTION}</Text>
} else {
t3 = $[4]; <Link url="https://code.claude.com/docs/en/security" />
} </Box>
let t4;
if ($[5] === Symbol.for("react.memo_cache_sentinel")) { <Select
t4 = true ? [{ options={[
label: "Yes, and make it my default mode", ...("external" !== 'ant'
value: "accept-default" as const ? [
}] : []; {
$[5] = t4; label: 'Yes, and make it my default mode',
} else { value: 'accept-default' as const,
t4 = $[5]; },
} ]
let t5; : []),
if ($[6] === Symbol.for("react.memo_cache_sentinel")) { { label: 'Yes, enable auto mode', value: 'accept' as const },
t5 = { {
label: "Yes, enable auto mode", label: declineExits ? 'No, exit' : 'No, go back',
value: "accept" as const value: 'decline' as const,
}; },
$[6] = t5; ]}
} else { onChange={value =>
t5 = $[6]; onChange(value as 'accept' | 'accept-default' | 'decline')
} }
const t6 = declineExits ? "No, exit" : "No, go back"; onCancel={onDecline}
let t7; />
if ($[7] !== t6) { </Dialog>
t7 = [...t4, t5, { )
label: t6,
value: "decline" as const
}];
$[7] = t6;
$[8] = t7;
} else {
t7 = $[8];
}
let t8;
if ($[9] !== onChange) {
t8 = value_0 => onChange(value_0 as 'accept' | 'accept-default' | 'decline');
$[9] = onChange;
$[10] = t8;
} else {
t8 = $[10];
}
let t9;
if ($[11] !== onDecline || $[12] !== t7 || $[13] !== t8) {
t9 = <Select options={t7} onChange={t8} onCancel={onDecline} />;
$[11] = onDecline;
$[12] = t7;
$[13] = t8;
$[14] = t9;
} else {
t9 = $[14];
}
let t10;
if ($[15] !== onDecline || $[16] !== t9) {
t10 = <Dialog title="Enable auto mode?" color="warning" onCancel={onDecline}>{t3}{t9}</Dialog>;
$[15] = onDecline;
$[16] = t9;
$[17] = t10;
} else {
t10 = $[17];
}
return t10;
}
function _temp() {
logEvent("tengu_auto_mode_opt_in_dialog_shown", {});
} }

View File

@@ -1,197 +1,264 @@
import * as React from 'react'; import * as React from 'react'
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react'
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; import {
import { useInterval } from 'usehooks-ts'; type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
import { useUpdateNotification } from '../hooks/useUpdateNotification.js'; logEvent,
import { Box, Text } from '../ink.js'; } from 'src/services/analytics/index.js'
import { type AutoUpdaterResult, getLatestVersion, getMaxVersion, type InstallStatus, installGlobalPackage, shouldSkipVersion } from '../utils/autoUpdater.js'; import { useInterval } from 'usehooks-ts'
import { getGlobalConfig, isAutoUpdaterDisabled } from '../utils/config.js'; import { useUpdateNotification } from '../hooks/useUpdateNotification.js'
import { logForDebugging } from '../utils/debug.js'; import { Box, Text } from '../ink.js'
import { getCurrentInstallationType } from '../utils/doctorDiagnostic.js'; import {
import { installOrUpdateClaudePackage, localInstallationExists } from '../utils/localInstaller.js'; type AutoUpdaterResult,
import { removeInstalledSymlink } from '../utils/nativeInstaller/index.js'; getLatestVersion,
import { gt, gte } from '../utils/semver.js'; getMaxVersion,
import { getInitialSettings } from '../utils/settings/settings.js'; type InstallStatus,
installGlobalPackage,
shouldSkipVersion,
} from '../utils/autoUpdater.js'
import { getGlobalConfig, isAutoUpdaterDisabled } from '../utils/config.js'
import { logForDebugging } from '../utils/debug.js'
import { getCurrentInstallationType } from '../utils/doctorDiagnostic.js'
import {
installOrUpdateClaudePackage,
localInstallationExists,
} from '../utils/localInstaller.js'
import { removeInstalledSymlink } from '../utils/nativeInstaller/index.js'
import { gt, gte } from '../utils/semver.js'
import { getInitialSettings } from '../utils/settings/settings.js'
type Props = { type Props = {
isUpdating: boolean; isUpdating: boolean
onChangeIsUpdating: (isUpdating: boolean) => void; onChangeIsUpdating: (isUpdating: boolean) => void
onAutoUpdaterResult: (autoUpdaterResult: AutoUpdaterResult) => void; onAutoUpdaterResult: (autoUpdaterResult: AutoUpdaterResult) => void
autoUpdaterResult: AutoUpdaterResult | null; autoUpdaterResult: AutoUpdaterResult | null
showSuccessMessage: boolean; showSuccessMessage: boolean
verbose: boolean; verbose: boolean
}; }
export function AutoUpdater({ export function AutoUpdater({
isUpdating, isUpdating,
onChangeIsUpdating, onChangeIsUpdating,
onAutoUpdaterResult, onAutoUpdaterResult,
autoUpdaterResult, autoUpdaterResult,
showSuccessMessage, showSuccessMessage,
verbose verbose,
}: Props): React.ReactNode { }: Props): React.ReactNode {
const [versions, setVersions] = useState<{ const [versions, setVersions] = useState<{
global?: string | null; global?: string | null
latest?: string | null; latest?: string | null
}>({}); }>({})
const [hasLocalInstall, setHasLocalInstall] = useState(false); const [hasLocalInstall, setHasLocalInstall] = useState(false)
const updateSemver = useUpdateNotification(autoUpdaterResult?.version); const updateSemver = useUpdateNotification(autoUpdaterResult?.version)
useEffect(() => { useEffect(() => {
void localInstallationExists().then(setHasLocalInstall); void localInstallationExists().then(setHasLocalInstall)
}, []); }, [])
// Track latest isUpdating value in a ref so the memoized checkForUpdates // Track latest isUpdating value in a ref so the memoized checkForUpdates
// callback always sees the current value. Without this, the 30-minute // callback always sees the current value. Without this, the 30-minute
// interval fires with a stale closure where isUpdating is false, allowing // interval fires with a stale closure where isUpdating is false, allowing
// a concurrent installGlobalPackage() to run while one is already in // a concurrent installGlobalPackage() to run while one is already in
// progress. // progress.
const isUpdatingRef = useRef(isUpdating); const isUpdatingRef = useRef(isUpdating)
isUpdatingRef.current = isUpdating; isUpdatingRef.current = isUpdating
const checkForUpdates = React.useCallback(async () => { const checkForUpdates = React.useCallback(async () => {
if (isUpdatingRef.current) { if (isUpdatingRef.current) {
return; return
} }
if (("production" as string) === 'test' || ("production" as string) === 'development') {
logForDebugging('AutoUpdater: Skipping update check in test/dev environment'); if (
return; "production" === 'test' ||
"production" === 'development'
) {
logForDebugging(
'AutoUpdater: Skipping update check in test/dev environment',
)
return
} }
const currentVersion = MACRO.VERSION;
const channel = getInitialSettings()?.autoUpdatesChannel ?? 'latest'; const currentVersion = MACRO.VERSION
let latestVersion = await getLatestVersion(channel); const channel = getInitialSettings()?.autoUpdatesChannel ?? 'latest'
const isDisabled = isAutoUpdaterDisabled(); let latestVersion = await getLatestVersion(channel)
const isDisabled = isAutoUpdaterDisabled()
// Check if max version is set (server-side kill switch for auto-updates) // Check if max version is set (server-side kill switch for auto-updates)
const maxVersion = await getMaxVersion(); const maxVersion = await getMaxVersion()
if (maxVersion && latestVersion && gt(latestVersion, maxVersion)) { if (maxVersion && latestVersion && gt(latestVersion, maxVersion)) {
logForDebugging(`AutoUpdater: maxVersion ${maxVersion} is set, capping update from ${latestVersion} to ${maxVersion}`); logForDebugging(
`AutoUpdater: maxVersion ${maxVersion} is set, capping update from ${latestVersion} to ${maxVersion}`,
)
if (gte(currentVersion, maxVersion)) { if (gte(currentVersion, maxVersion)) {
logForDebugging(`AutoUpdater: current version ${currentVersion} is already at or above maxVersion ${maxVersion}, skipping update`); logForDebugging(
setVersions({ `AutoUpdater: current version ${currentVersion} is already at or above maxVersion ${maxVersion}, skipping update`,
global: currentVersion, )
latest: latestVersion setVersions({ global: currentVersion, latest: latestVersion })
}); return
return;
} }
latestVersion = maxVersion; latestVersion = maxVersion
} }
setVersions({
global: currentVersion, setVersions({ global: currentVersion, latest: latestVersion })
latest: latestVersion
});
// Check if update needed and perform update // Check if update needed and perform update
if (!isDisabled && currentVersion && latestVersion && !gte(currentVersion, latestVersion) && !shouldSkipVersion(latestVersion)) { if (
const startTime = Date.now(); !isDisabled &&
onChangeIsUpdating(true); currentVersion &&
latestVersion &&
!gte(currentVersion, latestVersion) &&
!shouldSkipVersion(latestVersion)
) {
const startTime = Date.now()
onChangeIsUpdating(true)
// Remove native installer symlink since we're using JS-based updates // Remove native installer symlink since we're using JS-based updates
// But only if user hasn't migrated to native installation // But only if user hasn't migrated to native installation
const config = getGlobalConfig(); const config = getGlobalConfig()
if (config.installMethod !== 'native') { if (config.installMethod !== 'native') {
await removeInstalledSymlink(); await removeInstalledSymlink()
} }
// Detect actual running installation type // Detect actual running installation type
const installationType = await getCurrentInstallationType(); const installationType = await getCurrentInstallationType()
logForDebugging(`AutoUpdater: Detected installation type: ${installationType}`); logForDebugging(
`AutoUpdater: Detected installation type: ${installationType}`,
)
// Skip update for development builds // Skip update for development builds
if (installationType === 'development') { if (installationType === 'development') {
logForDebugging('AutoUpdater: Cannot auto-update development build'); logForDebugging('AutoUpdater: Cannot auto-update development build')
onChangeIsUpdating(false); onChangeIsUpdating(false)
return; return
} }
// Choose the appropriate update method based on what's actually running // Choose the appropriate update method based on what's actually running
let installStatus: InstallStatus; let installStatus: InstallStatus
let updateMethod: 'local' | 'global'; let updateMethod: 'local' | 'global'
if (installationType === 'npm-local') { if (installationType === 'npm-local') {
// Use local update for local installations // Use local update for local installations
logForDebugging('AutoUpdater: Using local update method'); logForDebugging('AutoUpdater: Using local update method')
updateMethod = 'local'; updateMethod = 'local'
installStatus = await installOrUpdateClaudePackage(channel); installStatus = await installOrUpdateClaudePackage(channel)
} else if (installationType === 'npm-global') { } else if (installationType === 'npm-global') {
// Use global update for global installations // Use global update for global installations
logForDebugging('AutoUpdater: Using global update method'); logForDebugging('AutoUpdater: Using global update method')
updateMethod = 'global'; updateMethod = 'global'
installStatus = await installGlobalPackage(); installStatus = await installGlobalPackage()
} else if (installationType === 'native') { } else if (installationType === 'native') {
// This shouldn't happen - native should use NativeAutoUpdater // This shouldn't happen - native should use NativeAutoUpdater
logForDebugging('AutoUpdater: Unexpected native installation in non-native updater'); logForDebugging(
onChangeIsUpdating(false); 'AutoUpdater: Unexpected native installation in non-native updater',
return; )
onChangeIsUpdating(false)
return
} else { } else {
// Fallback to config-based detection for unknown types // Fallback to config-based detection for unknown types
logForDebugging(`AutoUpdater: Unknown installation type, falling back to config`); logForDebugging(
const isMigrated = config.installMethod === 'local'; `AutoUpdater: Unknown installation type, falling back to config`,
updateMethod = isMigrated ? 'local' : 'global'; )
const isMigrated = config.installMethod === 'local'
updateMethod = isMigrated ? 'local' : 'global'
if (isMigrated) { if (isMigrated) {
installStatus = await installOrUpdateClaudePackage(channel); installStatus = await installOrUpdateClaudePackage(channel)
} else { } else {
installStatus = await installGlobalPackage(); installStatus = await installGlobalPackage()
} }
} }
onChangeIsUpdating(false);
onChangeIsUpdating(false)
if (installStatus === 'success') { if (installStatus === 'success') {
logEvent('tengu_auto_updater_success', { logEvent('tengu_auto_updater_success', {
fromVersion: currentVersion as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, fromVersion:
toVersion: latestVersion as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, currentVersion as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
toVersion:
latestVersion as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
durationMs: Date.now() - startTime, durationMs: Date.now() - startTime,
wasMigrated: updateMethod === 'local', wasMigrated: updateMethod === 'local',
installationType: installationType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS installationType:
}); installationType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
} else { } else {
logEvent('tengu_auto_updater_fail', { logEvent('tengu_auto_updater_fail', {
fromVersion: currentVersion as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, fromVersion:
attemptedVersion: latestVersion as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, currentVersion as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
status: installStatus as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, attemptedVersion:
latestVersion as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
status:
installStatus as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
durationMs: Date.now() - startTime, durationMs: Date.now() - startTime,
wasMigrated: updateMethod === 'local', wasMigrated: updateMethod === 'local',
installationType: installationType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS installationType:
}); installationType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
} }
onAutoUpdaterResult({ onAutoUpdaterResult({
version: latestVersion, version: latestVersion,
status: installStatus status: installStatus,
}); })
} }
// isUpdating intentionally omitted from deps; we read isUpdatingRef // isUpdating intentionally omitted from deps; we read isUpdatingRef
// instead so the guard is always current without changing callback // instead so the guard is always current without changing callback
// identity (which would re-trigger the initial-check useEffect below). // identity (which would re-trigger the initial-check useEffect below).
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
// biome-ignore lint/correctness/useExhaustiveDependencies: isUpdating read via ref // biome-ignore lint/correctness/useExhaustiveDependencies: isUpdating read via ref
}, [onAutoUpdaterResult]); }, [onAutoUpdaterResult])
// Initial check // Initial check
useEffect(() => { useEffect(() => {
void checkForUpdates(); void checkForUpdates()
}, [checkForUpdates]); }, [checkForUpdates])
// Check every 30 minutes // Check every 30 minutes
useInterval(checkForUpdates, 30 * 60 * 1000); useInterval(checkForUpdates, 30 * 60 * 1000)
if (!autoUpdaterResult?.version && (!versions.global || !versions.latest)) { if (!autoUpdaterResult?.version && (!versions.global || !versions.latest)) {
return null; return null
} }
if (!autoUpdaterResult?.version && !isUpdating) { if (!autoUpdaterResult?.version && !isUpdating) {
return null; return null
} }
return <Box flexDirection="row" gap={1}>
{verbose && <Text dimColor wrap="truncate"> return (
<Box flexDirection="row" gap={1}>
{verbose && (
<Text dimColor wrap="truncate">
globalVersion: {versions.global} &middot; latestVersion:{' '} globalVersion: {versions.global} &middot; latestVersion:{' '}
{versions.latest} {versions.latest}
</Text>} </Text>
{isUpdating ? <> )}
{isUpdating ? (
<>
<Box> <Box>
<Text color="text" dimColor wrap="truncate"> <Text color="text" dimColor wrap="truncate">
Auto-updating Auto-updating
</Text> </Text>
</Box> </Box>
</> : autoUpdaterResult?.status === 'success' && showSuccessMessage && updateSemver && <Text color="success" wrap="truncate"> </>
) : (
autoUpdaterResult?.status === 'success' &&
showSuccessMessage &&
updateSemver && (
<Text color="success" wrap="truncate">
Update installed · Restart to apply Update installed · Restart to apply
</Text>} </Text>
{(autoUpdaterResult?.status === 'install_failed' || autoUpdaterResult?.status === 'no_permissions') && <Text color="error" wrap="truncate"> )
)}
{(autoUpdaterResult?.status === 'install_failed' ||
autoUpdaterResult?.status === 'no_permissions') && (
<Text color="error" wrap="truncate">
Auto-update failed &middot; Try <Text bold>claude doctor</Text> or{' '} Auto-update failed &middot; Try <Text bold>claude doctor</Text> or{' '}
<Text bold> <Text bold>
{hasLocalInstall ? `cd ~/.claude/local && npm update ${MACRO.PACKAGE_URL}` : `npm i -g ${MACRO.PACKAGE_URL}`} {hasLocalInstall
? `cd ~/.claude/local && npm update ${MACRO.PACKAGE_URL}`
: `npm i -g ${MACRO.PACKAGE_URL}`}
</Text> </Text>
</Text>} </Text>
</Box>; )}
</Box>
)
} }

View File

@@ -1,90 +1,90 @@
import { c as _c } from "react/compiler-runtime"; import { feature } from 'bun:bundle'
import { feature } from 'bun:bundle'; import * as React from 'react'
import * as React from 'react'; import type { AutoUpdaterResult } from '../utils/autoUpdater.js'
import type { AutoUpdaterResult } from '../utils/autoUpdater.js'; import { isAutoUpdaterDisabled } from '../utils/config.js'
import { isAutoUpdaterDisabled } from '../utils/config.js'; import { logForDebugging } from '../utils/debug.js'
import { logForDebugging } from '../utils/debug.js'; import { getCurrentInstallationType } from '../utils/doctorDiagnostic.js'
import { getCurrentInstallationType } from '../utils/doctorDiagnostic.js'; import { AutoUpdater } from './AutoUpdater.js'
import { AutoUpdater } from './AutoUpdater.js'; import { NativeAutoUpdater } from './NativeAutoUpdater.js'
import { NativeAutoUpdater } from './NativeAutoUpdater.js'; import { PackageManagerAutoUpdater } from './PackageManagerAutoUpdater.js'
import { PackageManagerAutoUpdater } from './PackageManagerAutoUpdater.js';
type Props = { type Props = {
isUpdating: boolean; isUpdating: boolean
onChangeIsUpdating: (isUpdating: boolean) => void; onChangeIsUpdating: (isUpdating: boolean) => void
onAutoUpdaterResult: (autoUpdaterResult: AutoUpdaterResult) => void; onAutoUpdaterResult: (autoUpdaterResult: AutoUpdaterResult) => void
autoUpdaterResult: AutoUpdaterResult | null; autoUpdaterResult: AutoUpdaterResult | null
showSuccessMessage: boolean; showSuccessMessage: boolean
verbose: boolean; verbose: boolean
}; }
export function AutoUpdaterWrapper(t0) {
const $ = _c(17); export function AutoUpdaterWrapper({
const { isUpdating,
isUpdating, onChangeIsUpdating,
onChangeIsUpdating, onAutoUpdaterResult,
onAutoUpdaterResult, autoUpdaterResult,
autoUpdaterResult, showSuccessMessage,
showSuccessMessage, verbose,
verbose }: Props): React.ReactNode {
} = t0; const [useNativeInstaller, setUseNativeInstaller] = React.useState<
const [useNativeInstaller, setUseNativeInstaller] = React.useState(null); boolean | null
const [isPackageManager, setIsPackageManager] = React.useState(null); >(null)
let t1; const [isPackageManager, setIsPackageManager] = React.useState<
let t2; boolean | null
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { >(null)
t1 = () => {
const checkInstallation = async function checkInstallation() { React.useEffect(() => {
if (feature("SKIP_DETECTION_WHEN_AUTOUPDATES_DISABLED") && isAutoUpdaterDisabled()) { async function checkInstallation() {
logForDebugging("AutoUpdaterWrapper: Skipping detection, auto-updates disabled"); // Skip installation type detection if auto-updates are disabled (ant-only)
return; // This avoids potentially slow package manager detection (spawnSync calls)
} if (
const installationType = await getCurrentInstallationType(); feature('SKIP_DETECTION_WHEN_AUTOUPDATES_DISABLED') &&
logForDebugging(`AutoUpdaterWrapper: Installation type: ${installationType}`); isAutoUpdaterDisabled()
setUseNativeInstaller(installationType === "native"); ) {
setIsPackageManager(installationType === "package-manager"); logForDebugging(
}; 'AutoUpdaterWrapper: Skipping detection, auto-updates disabled',
checkInstallation(); )
}; return
t2 = []; }
$[0] = t1;
$[1] = t2; const installationType = await getCurrentInstallationType()
} else { logForDebugging(
t1 = $[0]; `AutoUpdaterWrapper: Installation type: ${installationType}`,
t2 = $[1]; )
} setUseNativeInstaller(installationType === 'native')
React.useEffect(t1, t2); setIsPackageManager(installationType === 'package-manager')
if (useNativeInstaller === null || isPackageManager === null) { }
return null;
} void checkInstallation()
if (isPackageManager) { }, [])
let t3;
if ($[2] !== autoUpdaterResult || $[3] !== isUpdating || $[4] !== onAutoUpdaterResult || $[5] !== onChangeIsUpdating || $[6] !== showSuccessMessage || $[7] !== verbose) { // Don't render until we know the installation type
t3 = <PackageManagerAutoUpdater verbose={verbose} onAutoUpdaterResult={onAutoUpdaterResult} autoUpdaterResult={autoUpdaterResult} isUpdating={isUpdating} onChangeIsUpdating={onChangeIsUpdating} showSuccessMessage={showSuccessMessage} />; if (useNativeInstaller === null || isPackageManager === null) {
$[2] = autoUpdaterResult; return null
$[3] = isUpdating; }
$[4] = onAutoUpdaterResult;
$[5] = onChangeIsUpdating; if (isPackageManager) {
$[6] = showSuccessMessage; return (
$[7] = verbose; <PackageManagerAutoUpdater
$[8] = t3; verbose={verbose}
} else { onAutoUpdaterResult={onAutoUpdaterResult}
t3 = $[8]; autoUpdaterResult={autoUpdaterResult}
} isUpdating={isUpdating}
return t3; onChangeIsUpdating={onChangeIsUpdating}
} showSuccessMessage={showSuccessMessage}
const Updater = useNativeInstaller ? NativeAutoUpdater : AutoUpdater; />
let t3; )
if ($[9] !== Updater || $[10] !== autoUpdaterResult || $[11] !== isUpdating || $[12] !== onAutoUpdaterResult || $[13] !== onChangeIsUpdating || $[14] !== showSuccessMessage || $[15] !== verbose) { }
t3 = <Updater verbose={verbose} onAutoUpdaterResult={onAutoUpdaterResult} autoUpdaterResult={autoUpdaterResult} isUpdating={isUpdating} onChangeIsUpdating={onChangeIsUpdating} showSuccessMessage={showSuccessMessage} />;
$[9] = Updater; const Updater = useNativeInstaller ? NativeAutoUpdater : AutoUpdater
$[10] = autoUpdaterResult;
$[11] = isUpdating; return (
$[12] = onAutoUpdaterResult; <Updater
$[13] = onChangeIsUpdating; verbose={verbose}
$[14] = showSuccessMessage; onAutoUpdaterResult={onAutoUpdaterResult}
$[15] = verbose; autoUpdaterResult={autoUpdaterResult}
$[16] = t3; isUpdating={isUpdating}
} else { onChangeIsUpdating={onChangeIsUpdating}
t3 = $[16]; showSuccessMessage={showSuccessMessage}
} />
return t3; )
} }

View File

@@ -1,81 +1,76 @@
import { c as _c } from "react/compiler-runtime"; import React, { useEffect, useState } from 'react'
import React, { useEffect, useState } from 'react'; import { Box, Link, Text } from '../ink.js'
import { Box, Link, Text } from '../ink.js'; import {
import { type AwsAuthStatus, AwsAuthStatusManager } from '../utils/awsAuthStatusManager.js'; type AwsAuthStatus,
const URL_RE = /https?:\/\/\S+/; AwsAuthStatusManager,
export function AwsAuthStatusBox() { } from '../utils/awsAuthStatusManager.js'
const $ = _c(11);
let t0; const URL_RE = /https?:\/\/\S+/
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = AwsAuthStatusManager.getInstance().getStatus(); export function AwsAuthStatusBox(): React.ReactNode {
$[0] = t0; const [status, setStatus] = useState<AwsAuthStatus>(
} else { AwsAuthStatusManager.getInstance().getStatus(),
t0 = $[0]; )
}
const [status, setStatus] = useState(t0); useEffect(() => {
let t1; // Subscribe to status updates
let t2; const unsubscribe = AwsAuthStatusManager.getInstance().subscribe(setStatus)
if ($[1] === Symbol.for("react.memo_cache_sentinel")) { return unsubscribe
t1 = () => { }, [])
const unsubscribe = AwsAuthStatusManager.getInstance().subscribe(setStatus);
return unsubscribe; // Don't show anything if not authenticating and no error
};
t2 = [];
$[1] = t1;
$[2] = t2;
} else {
t1 = $[1];
t2 = $[2];
}
useEffect(t1, t2);
if (!status.isAuthenticating && !status.error && status.output.length === 0) { if (!status.isAuthenticating && !status.error && status.output.length === 0) {
return null; return null
} }
// Don't show if authentication succeeded (no error and not authenticating)
if (!status.isAuthenticating && !status.error) { if (!status.isAuthenticating && !status.error) {
return null; return null
} }
let t3;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) { return (
t3 = <Text bold={true} color="permission">Cloud Authentication</Text>; <Box
$[3] = t3; flexDirection="column"
} else { borderStyle="round"
t3 = $[3]; borderColor="permission"
} paddingX={1}
let t4; marginY={1}
if ($[4] !== status.output) { >
t4 = status.output.length > 0 && <Box flexDirection="column" marginTop={1}>{status.output.slice(-5).map(_temp)}</Box>; <Text bold color="permission">
$[4] = status.output; Cloud Authentication
$[5] = t4; </Text>
} else {
t4 = $[5]; {status.output.length > 0 && (
} <Box flexDirection="column" marginTop={1}>
let t5; {status.output.slice(-5).map((line, index) => {
if ($[6] !== status.error) { const m = line.match(URL_RE)
t5 = status.error && <Box marginTop={1}><Text color="error">{status.error}</Text></Box>; if (!m) {
$[6] = status.error; return (
$[7] = t5; <Text key={index} dimColor>
} else { {line}
t5 = $[7]; </Text>
} )
let t6; }
if ($[8] !== t4 || $[9] !== t5) { const url = m[0]
t6 = <Box flexDirection="column" borderStyle="round" borderColor="permission" paddingX={1} marginY={1}>{t3}{t4}{t5}</Box>; const start = m.index ?? 0
$[8] = t4; const before = line.slice(0, start)
$[9] = t5; const after = line.slice(start + url.length)
$[10] = t6; return (
} else { <Text key={index} dimColor>
t6 = $[10]; {before}
} <Link url={url}>{url}</Link>
return t6; {after}
} </Text>
function _temp(line, index) { )
const m = line.match(URL_RE); })}
if (!m) { </Box>
return <Text key={index} dimColor={true}>{line}</Text>; )}
}
const url = m[0]; {status.error && (
const start = m.index ?? 0; <Box marginTop={1}>
const before = line.slice(0, start); <Text color="error">{status.error}</Text>
const after = line.slice(start + url.length); </Box>
return <Text key={index} dimColor={true}>{before}<Link url={url}>{url}</Link>{after}</Text>; )}
</Box>
)
} }

View File

@@ -1,135 +1,162 @@
import { c as _c } from "react/compiler-runtime"; import React from 'react'
import React from 'react'; import { renderPlaceholder } from '../hooks/renderPlaceholder.js'
import { renderPlaceholder } from '../hooks/renderPlaceholder.js'; import { usePasteHandler } from '../hooks/usePasteHandler.js'
import { usePasteHandler } from '../hooks/usePasteHandler.js'; import { useDeclaredCursor } from '../ink/hooks/use-declared-cursor.js'
import { useDeclaredCursor } from '../ink/hooks/use-declared-cursor.js'; import { Ansi, Box, Text, useInput } from '../ink.js'
import { Ansi, Box, Text, useInput } from '../ink.js'; import type {
import type { BaseInputState, BaseTextInputProps } from '../types/textInputTypes.js'; BaseInputState,
import type { TextHighlight } from '../utils/textHighlighting.js'; BaseTextInputProps,
import { HighlightedInput } from './PromptInput/ShimmeredInput.js'; } from '../types/textInputTypes.js'
import type { TextHighlight } from '../utils/textHighlighting.js'
import { HighlightedInput } from './PromptInput/ShimmeredInput.js'
type BaseTextInputComponentProps = BaseTextInputProps & { type BaseTextInputComponentProps = BaseTextInputProps & {
inputState: BaseInputState; inputState: BaseInputState
children?: React.ReactNode; children?: React.ReactNode
terminalFocus: boolean; terminalFocus: boolean
highlights?: TextHighlight[]; highlights?: TextHighlight[]
invert?: (text: string) => string; invert?: (text: string) => string
hidePlaceholderText?: boolean; hidePlaceholderText?: boolean
}; }
/** /**
* A base component for text inputs that handles rendering and basic input * A base component for text inputs that handles rendering and basic input
*/ */
export function BaseTextInput(t0) { export function BaseTextInput({
const $ = _c(14); inputState,
const { children,
inputState, terminalFocus,
children, invert,
terminalFocus, hidePlaceholderText,
invert, ...props
hidePlaceholderText, }: BaseTextInputComponentProps): React.ReactNode {
...props const { onInput, renderedValue, cursorLine, cursorColumn } = inputState
} = t0;
const { // Park the native terminal cursor at the input caret. Terminal emulators
onInput, // position IME preedit text at the physical cursor, and screen readers /
renderedValue, // screen magnifiers track it — so parking here makes CJK input appear
cursorLine, // inline and lets accessibility tools follow the input. The Box ref below
cursorColumn // is the yoga layout origin; (cursorLine, cursorColumn) is relative to it.
} = inputState; // Only active when the input is focused, showing its cursor, and the
const t1 = Boolean(props.focus && props.showCursor && terminalFocus); // terminal itself has focus.
let t2; const cursorRef = useDeclaredCursor({
if ($[0] !== cursorColumn || $[1] !== cursorLine || $[2] !== t1) { line: cursorLine,
t2 = { column: cursorColumn,
line: cursorLine, active: Boolean(props.focus && props.showCursor && terminalFocus),
column: cursorColumn, })
active: t1
}; const { wrappedOnInput, isPasting } = usePasteHandler({
$[0] = cursorColumn;
$[1] = cursorLine;
$[2] = t1;
$[3] = t2;
} else {
t2 = $[3];
}
const cursorRef = useDeclaredCursor(t2);
const {
wrappedOnInput,
isPasting: t3
} = usePasteHandler({
onPaste: props.onPaste, onPaste: props.onPaste,
onInput: (input, key) => { onInput: (input, key) => {
// Prevent Enter key from triggering submission during paste
if (isPasting && key.return) { if (isPasting && key.return) {
return; return
} }
onInput(input, key); onInput(input, key)
}, },
onImagePaste: props.onImagePaste onImagePaste: props.onImagePaste,
}); })
const isPasting = t3;
const { // Notify parent when paste state changes
onIsPastingChange const { onIsPastingChange } = props
} = props;
React.useEffect(() => { React.useEffect(() => {
if (onIsPastingChange) { if (onIsPastingChange) {
onIsPastingChange(isPasting); onIsPastingChange(isPasting)
} }
}, [isPasting, onIsPastingChange]); }, [isPasting, onIsPastingChange])
const {
showPlaceholder, const { showPlaceholder, renderedPlaceholder } = renderPlaceholder({
renderedPlaceholder
} = renderPlaceholder({
placeholder: props.placeholder, placeholder: props.placeholder,
value: props.value, value: props.value,
showCursor: props.showCursor, showCursor: props.showCursor,
focus: props.focus, focus: props.focus,
terminalFocus, terminalFocus,
invert, invert,
hidePlaceholderText hidePlaceholderText,
}); })
useInput(wrappedOnInput, {
isActive: props.focus useInput(wrappedOnInput, { isActive: props.focus })
});
const commandWithoutArgs = props.value && props.value.trim().indexOf(" ") === -1 || props.value && props.value.endsWith(" "); // Show argument hint only when we have a value and the hint is provided
const showArgumentHint = Boolean(props.argumentHint && props.value && commandWithoutArgs && props.value.startsWith("/")); // Only show the argument hint when:
const cursorFiltered = props.showCursor && props.highlights ? props.highlights.filter(h => h.dimColor || props.cursorOffset < h.start || props.cursorOffset >= h.end) : props.highlights; // 1. We have a hint to show
const { // 2. We have a command typed (value is not empty)
viewportCharOffset, // 3. The command doesn't have arguments yet (no text after the space)
viewportCharEnd // 4. We're actually typing a command (the value starts with /)
} = inputState; const commandWithoutArgs =
const filteredHighlights = cursorFiltered && viewportCharOffset > 0 ? cursorFiltered.filter(h_0 => h_0.end > viewportCharOffset && h_0.start < viewportCharEnd).map(h_1 => ({ (props.value && props.value.trim().indexOf(' ') === -1) ||
...h_1, (props.value && props.value.endsWith(' '))
start: Math.max(0, h_1.start - viewportCharOffset),
end: h_1.end - viewportCharOffset const showArgumentHint = Boolean(
})) : cursorFiltered; props.argumentHint &&
const hasHighlights = filteredHighlights && filteredHighlights.length > 0; props.value &&
commandWithoutArgs &&
props.value.startsWith('/'),
)
// Filter out highlights that contain the cursor position
const cursorFiltered =
props.showCursor && props.highlights
? props.highlights.filter(
h =>
h.dimColor ||
props.cursorOffset < h.start ||
props.cursorOffset >= h.end,
)
: props.highlights
// Adjust highlights for viewport windowing: highlight positions reference the
// full input text, but renderedValue only contains the windowed subset.
const { viewportCharOffset, viewportCharEnd } = inputState
const filteredHighlights =
cursorFiltered && viewportCharOffset > 0
? cursorFiltered
.filter(h => h.end > viewportCharOffset && h.start < viewportCharEnd)
.map(h => ({
...h,
start: Math.max(0, h.start - viewportCharOffset),
end: h.end - viewportCharOffset,
}))
: cursorFiltered
const hasHighlights = filteredHighlights && filteredHighlights.length > 0
if (hasHighlights) { if (hasHighlights) {
return <Box ref={cursorRef}><HighlightedInput text={renderedValue} highlights={filteredHighlights} />{showArgumentHint && <Text dimColor={true}>{props.value?.endsWith(" ") ? "" : " "}{props.argumentHint}</Text>}{children}</Box>; return (
<Box ref={cursorRef}>
<HighlightedInput
text={renderedValue}
highlights={filteredHighlights}
/>
{showArgumentHint && (
<Text dimColor>
{props.value?.endsWith(' ') ? '' : ' '}
{props.argumentHint}
</Text>
)}
{children}
</Box>
)
} }
const T0 = Box;
const T1 = Text; return (
const t4 = "truncate-end"; <Box ref={cursorRef}>
const t5 = showPlaceholder && props.placeholderElement ? props.placeholderElement : showPlaceholder && renderedPlaceholder ? <Ansi>{renderedPlaceholder}</Ansi> : <Ansi>{renderedValue}</Ansi>; <Text wrap="truncate-end" dimColor={props.dimColor}>
const t6 = showArgumentHint && <Text dimColor={true}>{props.value?.endsWith(" ") ? "" : " "}{props.argumentHint}</Text>; {showPlaceholder && props.placeholderElement ? (
let t7; props.placeholderElement
if ($[4] !== T1 || $[5] !== children || $[6] !== props || $[7] !== t5 || $[8] !== t6) { ) : showPlaceholder && renderedPlaceholder ? (
t7 = <T1 wrap={t4} dimColor={props.dimColor}>{t5}{t6}{children}</T1>; <Ansi>{renderedPlaceholder}</Ansi>
$[4] = T1; ) : (
$[5] = children; <Ansi>{renderedValue}</Ansi>
$[6] = props; )}
$[7] = t5; {showArgumentHint && (
$[8] = t6; <Text dimColor>
$[9] = t7; {props.value?.endsWith(' ') ? '' : ' '}
} else { {props.argumentHint}
t7 = $[9]; </Text>
} )}
let t8; {children}
if ($[10] !== T0 || $[11] !== cursorRef || $[12] !== t7) { </Text>
t8 = <T0 ref={cursorRef}>{t7}</T0>; </Box>
$[10] = T0; )
$[11] = cursorRef;
$[12] = t7;
$[13] = t8;
} else {
t8 = $[13];
}
return t8;
} }

View File

@@ -1,55 +1,42 @@
import { c as _c } from "react/compiler-runtime"; import React from 'react'
import React from 'react'; import { Box } from '../ink.js'
import { Box } from '../ink.js'; import { BashTool } from '../tools/BashTool/BashTool.js'
import { BashTool } from '../tools/BashTool/BashTool.js'; import type { ShellProgress } from '../types/tools.js'
import type { ShellProgress } from '../types/tools.js'; import { UserBashInputMessage } from './messages/UserBashInputMessage.js'
import { UserBashInputMessage } from './messages/UserBashInputMessage.js'; import { ShellProgressMessage } from './shell/ShellProgressMessage.js'
import { ShellProgressMessage } from './shell/ShellProgressMessage.js';
type Props = { type Props = {
input: string; input: string
progress: ShellProgress | null; progress: ShellProgress | null
verbose: boolean; verbose: boolean
}; }
export function BashModeProgress(t0) {
const $ = _c(8); export function BashModeProgress({
const { input,
input, progress,
progress, verbose,
verbose }: Props): React.ReactNode {
} = t0; return (
const t1 = `<bash-input>${input}</bash-input>`; <Box flexDirection="column" marginTop={1}>
let t2; <UserBashInputMessage
if ($[0] !== t1) { addMargin={false}
t2 = <UserBashInputMessage addMargin={false} param={{ param={{ text: `<bash-input>${input}</bash-input>`, type: 'text' }}
text: t1, />
type: "text" {progress ? (
}} />; <ShellProgressMessage
$[0] = t1; fullOutput={progress.fullOutput}
$[1] = t2; output={progress.output}
} else { elapsedTimeSeconds={progress.elapsedTimeSeconds}
t2 = $[1]; totalLines={progress.totalLines}
} verbose={verbose}
let t3; />
if ($[2] !== progress || $[3] !== verbose) { ) : (
t3 = progress ? <ShellProgressMessage fullOutput={progress.fullOutput} output={progress.output} elapsedTimeSeconds={progress.elapsedTimeSeconds} totalLines={progress.totalLines} verbose={verbose} /> : BashTool.renderToolUseProgressMessage?.([], { BashTool.renderToolUseProgressMessage?.([], {
verbose, verbose,
tools: [], tools: [],
terminalSize: undefined terminalSize: undefined,
}); })
$[2] = progress; )}
$[3] = verbose; </Box>
$[4] = t3; )
} else {
t3 = $[4];
}
let t4;
if ($[5] !== t2 || $[6] !== t3) {
t4 = <Box flexDirection="column" marginTop={1}>{t2}{t3}</Box>;
$[5] = t2;
$[6] = t3;
$[7] = t4;
} else {
t4 = $[7];
}
return t4;
} }

View File

@@ -1,400 +1,160 @@
import { c as _c } from "react/compiler-runtime"; import { basename } from 'path'
import { basename } from 'path'; import { toString as qrToString } from 'qrcode'
import { toString as qrToString } from 'qrcode'; import * as React from 'react'
import * as React from 'react'; import { useEffect, useState } from 'react'
import { useEffect, useState } from 'react'; import { getOriginalCwd } from '../bootstrap/state.js'
import { getOriginalCwd } from '../bootstrap/state.js'; import {
import { buildActiveFooterText, buildIdleFooterText, FAILED_FOOTER_TEXT, getBridgeStatus } from '../bridge/bridgeStatusUtil.js'; buildActiveFooterText,
import { BRIDGE_FAILED_INDICATOR, BRIDGE_READY_INDICATOR } from '../constants/figures.js'; buildIdleFooterText,
import { useRegisterOverlay } from '../context/overlayContext.js'; FAILED_FOOTER_TEXT,
getBridgeStatus,
} from '../bridge/bridgeStatusUtil.js'
import {
BRIDGE_FAILED_INDICATOR,
BRIDGE_READY_INDICATOR,
} 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 // 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 '../ink.js'
import { useKeybindings } from '../keybindings/useKeybinding.js'; import { useKeybindings } from '../keybindings/useKeybinding.js'
import { useAppState, useSetAppState } from '../state/AppState.js'; import { useAppState, useSetAppState } from '../state/AppState.js'
import { saveGlobalConfig } from '../utils/config.js'; import { saveGlobalConfig } from '../utils/config.js'
import { getBranch } from '../utils/git.js'; import { getBranch } from '../utils/git.js'
import { Dialog } from './design-system/Dialog.js'; import { Dialog } from './design-system/Dialog.js'
type Props = { type Props = {
onDone: () => void; onDone: () => void
}; }
export function BridgeDialog(t0) {
const $ = _c(87); export function BridgeDialog({ onDone }: Props): React.ReactNode {
const { useRegisterOverlay('bridge-dialog')
onDone
} = t0; const connected = useAppState(s => s.replBridgeConnected)
useRegisterOverlay("bridge-dialog", undefined); const sessionActive = useAppState(s => s.replBridgeSessionActive)
const connected = useAppState(_temp); const reconnecting = useAppState(s => s.replBridgeReconnecting)
const sessionActive = useAppState(_temp2); const connectUrl = useAppState(s => s.replBridgeConnectUrl)
const reconnecting = useAppState(_temp3); const sessionUrl = useAppState(s => s.replBridgeSessionUrl)
const connectUrl = useAppState(_temp4); const error = useAppState(s => s.replBridgeError)
const sessionUrl = useAppState(_temp5); const explicit = useAppState(s => s.replBridgeExplicit)
const error = useAppState(_temp6); const environmentId = useAppState(s => s.replBridgeEnvironmentId)
const explicit = useAppState(_temp7); const sessionId = useAppState(s => s.replBridgeSessionId)
const environmentId = useAppState(_temp8); const verbose = useAppState(s => s.verbose)
const sessionId = useAppState(_temp9); const setAppState = useSetAppState()
const verbose = useAppState(_temp0);
const setAppState = useSetAppState(); const [showQR, setShowQR] = useState(false)
const [showQR, setShowQR] = useState(false); const [qrText, setQrText] = useState('')
const [qrText, setQrText] = useState(""); const [branchName, setBranchName] = useState('')
const [branchName, setBranchName] = useState("");
let t1; const repoName = basename(getOriginalCwd())
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = basename(getOriginalCwd()); // Fetch branch name on mount
$[0] = t1; useEffect(() => {
} else { getBranch()
t1 = $[0]; .then(setBranchName)
} .catch(() => {})
const repoName = t1; }, [])
let t2;
let t3; // The URL to display/QR: session URL when connected, connect URL when ready
if ($[1] === Symbol.for("react.memo_cache_sentinel")) { const displayUrl = sessionActive ? sessionUrl : connectUrl
t2 = () => {
getBranch().then(setBranchName).catch(_temp1); // Generate QR code when URL changes or QR is toggled on
}; useEffect(() => {
t3 = []; if (!showQR || !displayUrl) {
$[1] = t2; setQrText('')
$[2] = t3; return
} else { }
t2 = $[1]; qrToString(displayUrl, {
t3 = $[2]; type: 'utf8',
} errorCorrectionLevel: 'L',
useEffect(t2, t3); small: true,
const displayUrl = sessionActive ? sessionUrl : connectUrl; })
let t4; .then(setQrText)
let t5; .catch(() => setQrText(''))
if ($[3] !== displayUrl || $[4] !== showQR) { }, [showQR, displayUrl])
t4 = () => {
if (!showQR || !displayUrl) { useKeybindings(
setQrText(""); {
return; 'confirm:yes': onDone,
'confirm:toggle': () => {
setShowQR(prev => !prev)
},
},
{ context: 'Confirmation' },
)
useInput(input => {
if (input === 'd') {
// Persist opt-out only for CLI-flag/command-activated bridge.
// Config-driven and GB-auto-connect users get session-only disconnect
// — writing false would silently undo a Settings choice or opt a
// GB-rollout user out permanently.
if (explicit) {
saveGlobalConfig(current => {
if (current.remoteControlAtStartup === false) return current
return { ...current, remoteControlAtStartup: false }
})
} }
qrToString(displayUrl, { setAppState(prev => {
type: "utf8", if (!prev.replBridgeEnabled) return prev
errorCorrectionLevel: "L", return { ...prev, replBridgeEnabled: false }
small: true })
}).then(setQrText).catch(() => setQrText("")); onDone()
};
t5 = [showQR, displayUrl];
$[3] = displayUrl;
$[4] = showQR;
$[5] = t4;
$[6] = t5;
} else {
t4 = $[5];
t5 = $[6];
}
useEffect(t4, t5);
let t6;
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
t6 = () => {
setShowQR(_temp10);
};
$[7] = t6;
} else {
t6 = $[7];
}
let t7;
if ($[8] !== onDone) {
t7 = {
"confirm:yes": onDone,
"confirm:toggle": t6
};
$[8] = onDone;
$[9] = t7;
} else {
t7 = $[9];
}
let t8;
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
t8 = {
context: "Confirmation"
};
$[10] = t8;
} else {
t8 = $[10];
}
useKeybindings(t7, t8);
let t9;
if ($[11] !== explicit || $[12] !== onDone || $[13] !== setAppState) {
t9 = input => {
if (input === "d") {
if (explicit) {
saveGlobalConfig(_temp11);
}
setAppState(_temp12);
onDone();
}
};
$[11] = explicit;
$[12] = onDone;
$[13] = setAppState;
$[14] = t9;
} else {
t9 = $[14];
}
useInput(t9);
let t10;
if ($[15] !== connected || $[16] !== error || $[17] !== reconnecting || $[18] !== sessionActive) {
t10 = getBridgeStatus({
error,
connected,
sessionActive,
reconnecting
});
$[15] = connected;
$[16] = error;
$[17] = reconnecting;
$[18] = sessionActive;
$[19] = t10;
} else {
t10 = $[19];
}
const {
label: statusLabel,
color: statusColor
} = t10;
const indicator = error ? BRIDGE_FAILED_INDICATOR : BRIDGE_READY_INDICATOR;
let T0;
let T1;
let footerText;
let t11;
let t12;
let t13;
let t14;
let t15;
let t16;
let t17;
if ($[20] !== branchName || $[21] !== displayUrl || $[22] !== environmentId || $[23] !== error || $[24] !== indicator || $[25] !== onDone || $[26] !== qrText || $[27] !== sessionActive || $[28] !== sessionId || $[29] !== showQR || $[30] !== statusColor || $[31] !== statusLabel || $[32] !== verbose) {
const qrLines = qrText ? qrText.split("\n").filter(_temp13) : [];
let contextParts;
if ($[43] !== branchName) {
contextParts = [];
if (repoName) {
contextParts.push(repoName);
}
if (branchName) {
contextParts.push(branchName);
}
$[43] = branchName;
$[44] = contextParts;
} else {
contextParts = $[44];
} }
const contextSuffix = contextParts.length > 0 ? " \xB7 " + contextParts.join(" \xB7 ") : ""; })
let t18;
if ($[45] !== displayUrl || $[46] !== error || $[47] !== sessionActive) { const { label: statusLabel, color: statusColor } = getBridgeStatus({
t18 = error ? FAILED_FOOTER_TEXT : displayUrl ? sessionActive ? buildActiveFooterText(displayUrl) : buildIdleFooterText(displayUrl) : undefined; error,
$[45] = displayUrl; connected,
$[46] = error; sessionActive,
$[47] = sessionActive; reconnecting,
$[48] = t18; })
} else { const indicator = error ? BRIDGE_FAILED_INDICATOR : BRIDGE_READY_INDICATOR
t18 = $[48]; const qrLines = qrText ? qrText.split('\n').filter(l => l.length > 0) : []
}
footerText = t18; // Build suffix with repo and branch (matches standalone bridge format)
T1 = Dialog; const contextParts: string[] = []
t15 = "Remote Control"; if (repoName) contextParts.push(repoName)
t16 = onDone; if (branchName) contextParts.push(branchName)
t17 = true; const contextSuffix =
T0 = Box; contextParts.length > 0 ? ' \u00b7 ' + contextParts.join(' \u00b7 ') : ''
t11 = "column";
t12 = 1; // Footer text matches standalone bridge
let t19; const footerText = error
if ($[49] !== indicator || $[50] !== statusColor || $[51] !== statusLabel) { ? FAILED_FOOTER_TEXT
t19 = <Text color={statusColor}>{indicator} {statusLabel}</Text>; : displayUrl
$[49] = indicator; ? sessionActive
$[50] = statusColor; ? buildActiveFooterText(displayUrl)
$[51] = statusLabel; : buildIdleFooterText(displayUrl)
$[52] = t19; : undefined
} else {
t19 = $[52]; return (
} <Dialog title="Remote Control" onCancel={onDone} hideInputGuide>
let t20; <Box flexDirection="column" gap={1}>
if ($[53] !== contextSuffix) { <Box flexDirection="column">
t20 = <Text dimColor={true}>{contextSuffix}</Text>; <Text>
$[53] = contextSuffix; <Text color={statusColor}>
$[54] = t20; {indicator} {statusLabel}
} else { </Text>
t20 = $[54]; <Text dimColor>{contextSuffix}</Text>
} </Text>
let t21; {error && <Text color="error">{error}</Text>}
if ($[55] !== t19 || $[56] !== t20) { {verbose && environmentId && (
t21 = <Text>{t19}{t20}</Text>; <Text dimColor>Environment: {environmentId}</Text>
$[55] = t19; )}
$[56] = t20; {verbose && sessionId && <Text dimColor>Session: {sessionId}</Text>}
$[57] = t21; </Box>
} else { {showQR && qrLines.length > 0 && (
t21 = $[57]; <Box flexDirection="column">
} {qrLines.map((line, i) => (
let t22; <Text key={i}>{line}</Text>
if ($[58] !== error) { ))}
t22 = error && <Text color="error">{error}</Text>; </Box>
$[58] = error; )}
$[59] = t22; {footerText && <Text dimColor>{footerText}</Text>}
} else { <Text dimColor>
t22 = $[59]; d to disconnect · space for QR code · Enter/Esc to close
} </Text>
let t23; </Box>
if ($[60] !== environmentId || $[61] !== verbose) { </Dialog>
t23 = verbose && environmentId && <Text dimColor={true}>Environment: {environmentId}</Text>; )
$[60] = environmentId;
$[61] = verbose;
$[62] = t23;
} else {
t23 = $[62];
}
let t24;
if ($[63] !== sessionId || $[64] !== verbose) {
t24 = verbose && sessionId && <Text dimColor={true}>Session: {sessionId}</Text>;
$[63] = sessionId;
$[64] = verbose;
$[65] = t24;
} else {
t24 = $[65];
}
if ($[66] !== t21 || $[67] !== t22 || $[68] !== t23 || $[69] !== t24) {
t13 = <Box flexDirection="column">{t21}{t22}{t23}{t24}</Box>;
$[66] = t21;
$[67] = t22;
$[68] = t23;
$[69] = t24;
$[70] = t13;
} else {
t13 = $[70];
}
t14 = showQR && qrLines.length > 0 && <Box flexDirection="column">{qrLines.map(_temp14)}</Box>;
$[20] = branchName;
$[21] = displayUrl;
$[22] = environmentId;
$[23] = error;
$[24] = indicator;
$[25] = onDone;
$[26] = qrText;
$[27] = sessionActive;
$[28] = sessionId;
$[29] = showQR;
$[30] = statusColor;
$[31] = statusLabel;
$[32] = verbose;
$[33] = T0;
$[34] = T1;
$[35] = footerText;
$[36] = t11;
$[37] = t12;
$[38] = t13;
$[39] = t14;
$[40] = t15;
$[41] = t16;
$[42] = t17;
} else {
T0 = $[33];
T1 = $[34];
footerText = $[35];
t11 = $[36];
t12 = $[37];
t13 = $[38];
t14 = $[39];
t15 = $[40];
t16 = $[41];
t17 = $[42];
}
let t18;
if ($[71] !== footerText) {
t18 = footerText && <Text dimColor={true}>{footerText}</Text>;
$[71] = footerText;
$[72] = t18;
} else {
t18 = $[72];
}
let t19;
if ($[73] === Symbol.for("react.memo_cache_sentinel")) {
t19 = <Text dimColor={true}>d to disconnect · space for QR code · Enter/Esc to close</Text>;
$[73] = t19;
} else {
t19 = $[73];
}
let t20;
if ($[74] !== T0 || $[75] !== t11 || $[76] !== t12 || $[77] !== t13 || $[78] !== t14 || $[79] !== t18) {
t20 = <T0 flexDirection={t11} gap={t12}>{t13}{t14}{t18}{t19}</T0>;
$[74] = T0;
$[75] = t11;
$[76] = t12;
$[77] = t13;
$[78] = t14;
$[79] = t18;
$[80] = t20;
} else {
t20 = $[80];
}
let t21;
if ($[81] !== T1 || $[82] !== t15 || $[83] !== t16 || $[84] !== t17 || $[85] !== t20) {
t21 = <T1 title={t15} onCancel={t16} hideInputGuide={t17}>{t20}</T1>;
$[81] = T1;
$[82] = t15;
$[83] = t16;
$[84] = t17;
$[85] = t20;
$[86] = t21;
} else {
t21 = $[86];
}
return t21;
}
function _temp14(line, i) {
return <Text key={i}>{line}</Text>;
}
function _temp13(l) {
return l.length > 0;
}
function _temp12(prev_0) {
if (!prev_0.replBridgeEnabled) {
return prev_0;
}
return {
...prev_0,
replBridgeEnabled: false
};
}
function _temp11(current) {
if (current.remoteControlAtStartup === false) {
return current;
}
return {
...current,
remoteControlAtStartup: false
};
}
function _temp10(prev) {
return !prev;
}
function _temp1() {}
function _temp0(s_8) {
return s_8.verbose;
}
function _temp9(s_7) {
return s_7.replBridgeSessionId;
}
function _temp8(s_6) {
return s_6.replBridgeEnvironmentId;
}
function _temp7(s_5) {
return s_5.replBridgeExplicit;
}
function _temp6(s_4) {
return s_4.replBridgeError;
}
function _temp5(s_3) {
return s_3.replBridgeSessionUrl;
}
function _temp4(s_2) {
return s_2.replBridgeConnectUrl;
}
function _temp3(s_1) {
return s_1.replBridgeReconnecting;
}
function _temp2(s_0) {
return s_0.replBridgeSessionActive;
}
function _temp(s) {
return s.replBridgeConnected;
} }

View File

@@ -1,86 +1,73 @@
import { c as _c } from "react/compiler-runtime"; import React, { useCallback } from 'react'
import React, { useCallback } from 'react'; import { logEvent } from 'src/services/analytics/index.js'
import { logEvent } from 'src/services/analytics/index.js'; import { Box, Link, Newline, Text } from '../ink.js'
import { Box, Link, Newline, Text } from '../ink.js'; import { gracefulShutdownSync } from '../utils/gracefulShutdown.js'
import { gracefulShutdownSync } from '../utils/gracefulShutdown.js'; import { updateSettingsForSource } from '../utils/settings/settings.js'
import { updateSettingsForSource } from '../utils/settings/settings.js'; import { Select } from './CustomSelect/index.js'
import { Select } from './CustomSelect/index.js'; import { Dialog } from './design-system/Dialog.js'
import { Dialog } from './design-system/Dialog.js';
type Props = { type Props = {
onAccept(): void; onAccept(): void
}; }
export function BypassPermissionsModeDialog(t0) {
const $ = _c(7); export function BypassPermissionsModeDialog({
const { onAccept,
onAccept }: Props): React.ReactNode {
} = t0; React.useEffect(() => {
let t1; logEvent('tengu_bypass_permissions_mode_dialog_shown', {})
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { }, [])
t1 = [];
$[0] = t1; function onChange(value: 'accept' | 'decline') {
} else { switch (value) {
t1 = $[0]; case 'accept': {
} logEvent('tengu_bypass_permissions_mode_dialog_accept', {})
React.useEffect(_temp, t1);
let t2; updateSettingsForSource('userSettings', {
if ($[1] !== onAccept) { skipDangerousModePermissionPrompt: true,
t2 = function onChange(value) { })
bb3: switch (value) { onAccept()
case "accept": break
{
logEvent("tengu_bypass_permissions_mode_dialog_accept", {});
updateSettingsForSource("userSettings", {
skipDangerousModePermissionPrompt: true
});
onAccept();
break bb3;
}
case "decline":
{
gracefulShutdownSync(1);
}
} }
}; case 'decline': {
$[1] = onAccept; gracefulShutdownSync(1)
$[2] = t2; break
} else { }
t2 = $[2]; }
} }
const onChange = t2;
const handleEscape = _temp2; const handleEscape = useCallback(() => {
let t3; gracefulShutdownSync(0)
if ($[3] === Symbol.for("react.memo_cache_sentinel")) { }, [])
t3 = <Box flexDirection="column" gap={1}><Text>In Bypass Permissions mode, Claude Code will not ask for your approval before running potentially dangerous commands.<Newline />This mode should only be used in a sandboxed container/VM that has restricted internet access and can easily be restored if damaged.</Text><Text>By proceeding, you accept all responsibility for actions taken while running in Bypass Permissions mode.</Text><Link url="https://code.claude.com/docs/en/security" /></Box>;
$[3] = t3; return (
} else { <Dialog
t3 = $[3]; title="WARNING: Claude Code running in Bypass Permissions mode"
} color="error"
let t4; onCancel={handleEscape}
if ($[4] === Symbol.for("react.memo_cache_sentinel")) { >
t4 = [{ <Box flexDirection="column" gap={1}>
label: "No, exit", <Text>
value: "decline" In Bypass Permissions mode, Claude Code will not ask for your approval
}, { before running potentially dangerous commands.
label: "Yes, I accept", <Newline />
value: "accept" This mode should only be used in a sandboxed container/VM that has
}]; restricted internet access and can easily be restored if damaged.
$[4] = t4; </Text>
} else { <Text>
t4 = $[4]; By proceeding, you accept all responsibility for actions taken while
} running in Bypass Permissions mode.
let t5; </Text>
if ($[5] !== onChange) {
t5 = <Dialog title="WARNING: Claude Code running in Bypass Permissions mode" color="error" onCancel={handleEscape}>{t3}<Select options={t4} onChange={value_0 => onChange(value_0 as 'accept' | 'decline')} /></Dialog>; <Link url="https://code.claude.com/docs/en/security" />
$[5] = onChange; </Box>
$[6] = t5;
} else { <Select
t5 = $[6]; options={[
} { label: 'No, exit', value: 'decline' },
return t5; { label: 'Yes, I accept', value: 'accept' },
} ]}
function _temp2() { onChange={value => onChange(value as 'accept' | 'decline')}
gracefulShutdownSync(0); />
} </Dialog>
function _temp() { )
logEvent("tengu_bypass_permissions_mode_dialog_shown", {});
} }

View File

@@ -1,101 +1,57 @@
import { c as _c } from "react/compiler-runtime"; import React from 'react'
import React from 'react'; import { Text } from '../ink.js'
import { Text } from '../ink.js'; import { Select } from './CustomSelect/index.js'
import { Select } from './CustomSelect/index.js'; import { Dialog } from './design-system/Dialog.js'
import { Dialog } from './design-system/Dialog.js';
export type ChannelDowngradeChoice = 'downgrade' | 'stay' | 'cancel'; export type ChannelDowngradeChoice = 'downgrade' | 'stay' | 'cancel'
type Props = { type Props = {
currentVersion: string; currentVersion: string
onChoice: (choice: ChannelDowngradeChoice) => void; onChoice: (choice: ChannelDowngradeChoice) => void
}; }
/** /**
* Dialog shown when switching from latest to stable channel. * Dialog shown when switching from latest to stable channel.
* Allows user to choose whether to downgrade or stay on current version. * Allows user to choose whether to downgrade or stay on current version.
*/ */
export function ChannelDowngradeDialog(t0) { export function ChannelDowngradeDialog({
const $ = _c(17); currentVersion,
const { onChoice,
currentVersion, }: Props): React.ReactNode {
onChoice function handleSelect(value: ChannelDowngradeChoice): void {
} = t0; onChoice(value)
let t1;
if ($[0] !== onChoice) {
t1 = function handleSelect(value) {
onChoice(value);
};
$[0] = onChoice;
$[1] = t1;
} else {
t1 = $[1];
} }
const handleSelect = t1;
let t2; function handleCancel(): void {
if ($[2] !== onChoice) { onChoice('cancel')
t2 = function handleCancel() {
onChoice("cancel");
};
$[2] = onChoice;
$[3] = t2;
} else {
t2 = $[3];
} }
const handleCancel = t2;
let t3; return (
if ($[4] !== currentVersion) { <Dialog
t3 = <Text>The stable channel may have an older version than what you're currently running ({currentVersion}).</Text>; title="Switch to Stable Channel"
$[4] = currentVersion; onCancel={handleCancel}
$[5] = t3; color="permission"
} else { hideBorder
t3 = $[5]; hideInputGuide
} >
let t4; <Text>
if ($[6] === Symbol.for("react.memo_cache_sentinel")) { The stable channel may have an older version than what you&apos;re
t4 = <Text dimColor={true}>How would you like to handle this?</Text>; currently running ({currentVersion}).
$[6] = t4; </Text>
} else { <Text dimColor>How would you like to handle this?</Text>
t4 = $[6]; <Select
} options={[
let t5; {
if ($[7] === Symbol.for("react.memo_cache_sentinel")) { label: 'Allow possible downgrade to stable version',
t5 = { value: 'downgrade' as ChannelDowngradeChoice,
label: "Allow possible downgrade to stable version", },
value: "downgrade" as ChannelDowngradeChoice {
}; label: `Stay on current version (${currentVersion}) until stable catches up`,
$[7] = t5; value: 'stay' as ChannelDowngradeChoice,
} else { },
t5 = $[7]; ]}
} onChange={handleSelect}
const t6 = `Stay on current version (${currentVersion}) until stable catches up`; />
let t7; </Dialog>
if ($[8] !== t6) { )
t7 = [t5, {
label: t6,
value: "stay" as ChannelDowngradeChoice
}];
$[8] = t6;
$[9] = t7;
} else {
t7 = $[9];
}
let t8;
if ($[10] !== handleSelect || $[11] !== t7) {
t8 = <Select options={t7} onChange={handleSelect} />;
$[10] = handleSelect;
$[11] = t7;
$[12] = t8;
} else {
t8 = $[12];
}
let t9;
if ($[13] !== handleCancel || $[14] !== t3 || $[15] !== t8) {
t9 = <Dialog title="Switch to Stable Channel" onCancel={handleCancel} color="permission" hideBorder={true} hideInputGuide={true}>{t3}{t4}{t8}</Dialog>;
$[13] = handleCancel;
$[14] = t3;
$[15] = t8;
$[16] = t9;
} else {
t9 = $[16];
}
return t9;
} }

View File

@@ -1,53 +1,71 @@
import * as React from 'react'; import * as React from 'react'
import { Box, Text } from '../../ink.js'; import { Box, Text } from '../../ink.js'
import { Select } from '../CustomSelect/select.js'; import { Select } from '../CustomSelect/select.js'
import { PermissionDialog } from '../permissions/PermissionDialog.js'; import { PermissionDialog } from '../permissions/PermissionDialog.js'
type Props = { type Props = {
pluginName: string; pluginName: string
pluginDescription?: string; pluginDescription?: string
marketplaceName: string; marketplaceName: string
sourceCommand: string; sourceCommand: string
onResponse: (response: 'yes' | 'no' | 'disable') => void; onResponse: (response: 'yes' | 'no' | 'disable') => void
}; }
const AUTO_DISMISS_MS = 30_000;
const AUTO_DISMISS_MS = 30_000
export function PluginHintMenu({ export function PluginHintMenu({
pluginName, pluginName,
pluginDescription, pluginDescription,
marketplaceName, marketplaceName,
sourceCommand, sourceCommand,
onResponse onResponse,
}: Props): React.ReactNode { }: Props): React.ReactNode {
const onResponseRef = React.useRef(onResponse); const onResponseRef = React.useRef(onResponse)
onResponseRef.current = onResponse; onResponseRef.current = onResponse
React.useEffect(() => { React.useEffect(() => {
const timeoutId = setTimeout(ref => ref.current('no'), AUTO_DISMISS_MS, onResponseRef); const timeoutId = setTimeout(
return () => clearTimeout(timeoutId); ref => ref.current('no'),
}, []); AUTO_DISMISS_MS,
onResponseRef,
)
return () => clearTimeout(timeoutId)
}, [])
function onSelect(value: string): void { function onSelect(value: string): void {
switch (value) { switch (value) {
case 'yes': case 'yes':
onResponse('yes'); onResponse('yes')
break; break
case 'disable': case 'disable':
onResponse('disable'); onResponse('disable')
break; break
default: default:
onResponse('no'); onResponse('no')
} }
} }
const options = [{
label: <Text> const options = [
{
label: (
<Text>
Yes, install <Text bold>{pluginName}</Text> Yes, install <Text bold>{pluginName}</Text>
</Text>, </Text>
value: 'yes' ),
}, { value: 'yes',
label: 'No', },
value: 'no' {
}, { label: 'No',
label: "No, and don't show plugin installation hints again", value: 'no',
value: 'disable' },
}]; {
return <PermissionDialog title="Plugin Recommendation"> label: "No, and don't show plugin installation hints again",
value: 'disable',
},
]
return (
<PermissionDialog title="Plugin Recommendation">
<Box flexDirection="column" paddingX={2} paddingY={1}> <Box flexDirection="column" paddingX={2} paddingY={1}>
<Box marginBottom={1}> <Box marginBottom={1}>
<Text dimColor> <Text dimColor>
@@ -63,15 +81,22 @@ export function PluginHintMenu({
<Text dimColor>Marketplace:</Text> <Text dimColor>Marketplace:</Text>
<Text> {marketplaceName}</Text> <Text> {marketplaceName}</Text>
</Box> </Box>
{pluginDescription && <Box> {pluginDescription && (
<Box>
<Text dimColor>{pluginDescription}</Text> <Text dimColor>{pluginDescription}</Text>
</Box>} </Box>
)}
<Box marginTop={1}> <Box marginTop={1}>
<Text>Would you like to install it?</Text> <Text>Would you like to install it?</Text>
</Box> </Box>
<Box> <Box>
<Select options={options} onChange={onSelect} onCancel={() => onResponse('no')} /> <Select
options={options}
onChange={onSelect}
onCancel={() => onResponse('no')}
/>
</Box> </Box>
</Box> </Box>
</PermissionDialog>; </PermissionDialog>
)
} }

View File

@@ -1,120 +1,78 @@
import { c as _c } from "react/compiler-runtime"; import React from 'react'
import React from 'react'; import { logEvent } from 'src/services/analytics/index.js'
import { logEvent } from 'src/services/analytics/index.js';
// eslint-disable-next-line custom-rules/prefer-use-keybindings -- enter to continue // eslint-disable-next-line custom-rules/prefer-use-keybindings -- enter to continue
import { Box, Link, Newline, Text, useInput } from '../ink.js'; import { Box, Link, Newline, Text, useInput } from '../ink.js'
import { isChromeExtensionInstalled } from '../utils/claudeInChrome/setup.js'; import { isChromeExtensionInstalled } from '../utils/claudeInChrome/setup.js'
import { saveGlobalConfig } from '../utils/config.js'; import { saveGlobalConfig } from '../utils/config.js'
import { Dialog } from './design-system/Dialog.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'; const CHROME_EXTENSION_URL = 'https://claude.ai/chrome'
const CHROME_PERMISSIONS_URL = 'https://clau.de/chrome/permissions'
type Props = { type Props = {
onDone(): void; onDone(): void
};
export function ClaudeInChromeOnboarding(t0) {
const $ = _c(20);
const {
onDone
} = t0;
const [isExtensionInstalled, setIsExtensionInstalled] = React.useState(false);
let t1;
let t2;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = () => {
logEvent("tengu_claude_in_chrome_onboarding_shown", {});
isChromeExtensionInstalled().then(setIsExtensionInstalled);
saveGlobalConfig(_temp);
};
t2 = [];
$[0] = t1;
$[1] = t2;
} else {
t1 = $[0];
t2 = $[1];
}
React.useEffect(t1, t2);
let t3;
if ($[2] !== onDone) {
t3 = (_input, key) => {
if (key.return) {
onDone();
}
};
$[2] = onDone;
$[3] = t3;
} else {
t3 = $[3];
}
useInput(t3);
let t4;
if ($[4] !== isExtensionInstalled) {
t4 = !isExtensionInstalled && <><Newline /><Newline />Requires the Chrome extension. Get started at{" "}<Link url={CHROME_EXTENSION_URL} /></>;
$[4] = isExtensionInstalled;
$[5] = t4;
} else {
t4 = $[5];
}
let t5;
if ($[6] !== t4) {
t5 = <Text>Claude in Chrome works with the Chrome extension to let you control your browser directly from Claude Code. You can navigate websites, fill forms, capture screenshots, record GIFs, and debug with console logs and network requests.{t4}</Text>;
$[6] = t4;
$[7] = t5;
} else {
t5 = $[7];
}
let t6;
if ($[8] !== isExtensionInstalled) {
t6 = isExtensionInstalled && <>{" "}(<Link url={CHROME_PERMISSIONS_URL} />)</>;
$[8] = isExtensionInstalled;
$[9] = t6;
} else {
t6 = $[9];
}
let t7;
if ($[10] !== t6) {
t7 = <Text dimColor={true}>Site-level permissions are inherited from the Chrome extension. Manage permissions in the Chrome extension settings to control which sites Claude can browse, click, and type on{t6}.</Text>;
$[10] = t6;
$[11] = t7;
} else {
t7 = $[11];
}
let t8;
if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
t8 = <Text bold={true} color="chromeYellow">/chrome</Text>;
$[12] = t8;
} else {
t8 = $[12];
}
let t9;
if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
t9 = <Text dimColor={true}>For more info, use{" "}{t8}{" "}or visit <Link url="https://code.claude.com/docs/en/chrome" /></Text>;
$[13] = t9;
} else {
t9 = $[13];
}
let t10;
if ($[14] !== t5 || $[15] !== t7) {
t10 = <Box flexDirection="column" gap={1}>{t5}{t7}{t9}</Box>;
$[14] = t5;
$[15] = t7;
$[16] = t10;
} else {
t10 = $[16];
}
let t11;
if ($[17] !== onDone || $[18] !== t10) {
t11 = <Dialog title="Claude in Chrome (Beta)" onCancel={onDone} color="chromeYellow">{t10}</Dialog>;
$[17] = onDone;
$[18] = t10;
$[19] = t11;
} else {
t11 = $[19];
}
return t11;
} }
function _temp(current) {
return { export function ClaudeInChromeOnboarding({ onDone }: Props): React.ReactNode {
...current, const [isExtensionInstalled, setIsExtensionInstalled] = React.useState(false)
hasCompletedClaudeInChromeOnboarding: true
}; React.useEffect(() => {
logEvent('tengu_claude_in_chrome_onboarding_shown', {})
void isChromeExtensionInstalled().then(setIsExtensionInstalled)
saveGlobalConfig(current => {
return { ...current, hasCompletedClaudeInChromeOnboarding: true }
})
}, [])
// Handle Enter to continue
useInput((_input, key) => {
if (key.return) {
onDone()
}
})
return (
<Dialog
title="Claude in Chrome (Beta)"
onCancel={onDone}
color="chromeYellow"
>
<Box flexDirection="column" gap={1}>
<Text>
Claude in Chrome works with the Chrome extension to let you control
your browser directly from Claude Code. You can navigate websites,
fill forms, capture screenshots, record GIFs, and debug with console
logs and network requests.
{!isExtensionInstalled && (
<>
<Newline />
<Newline />
Requires the Chrome extension. Get started at{' '}
<Link url={CHROME_EXTENSION_URL} />
</>
)}
</Text>
<Text dimColor>
Site-level permissions are inherited from the Chrome extension. Manage
permissions in the Chrome extension settings to control which sites
Claude can browse, click, and type on
{isExtensionInstalled && (
<>
{' '}
(<Link url={CHROME_PERMISSIONS_URL} />)
</>
)}
.
</Text>
<Text dimColor>
For more info, use{' '}
<Text bold color="chromeYellow">
/chrome
</Text>{' '}
or visit <Link url="https://code.claude.com/docs/en/chrome" />
</Text>
</Box>
</Dialog>
)
} }

View File

@@ -1,136 +1,93 @@
import { c as _c } from "react/compiler-runtime"; import React, { useCallback } from 'react'
import React, { useCallback } from 'react'; import { logEvent } from 'src/services/analytics/index.js'
import { logEvent } from 'src/services/analytics/index.js'; import { Box, Link, Text } from '../ink.js'
import { Box, Link, Text } from '../ink.js'; import type { ExternalClaudeMdInclude } from '../utils/claudemd.js'
import type { ExternalClaudeMdInclude } from '../utils/claudemd.js'; import { saveCurrentProjectConfig } from '../utils/config.js'
import { saveCurrentProjectConfig } from '../utils/config.js'; import { Select } from './CustomSelect/index.js'
import { Select } from './CustomSelect/index.js'; import { Dialog } from './design-system/Dialog.js'
import { Dialog } from './design-system/Dialog.js';
type Props = { type Props = {
onDone(): void; onDone(): void
isStandaloneDialog?: boolean; isStandaloneDialog?: boolean
externalIncludes?: ExternalClaudeMdInclude[]; externalIncludes?: ExternalClaudeMdInclude[]
}; }
export function ClaudeMdExternalIncludesDialog(t0) {
const $ = _c(18); export function ClaudeMdExternalIncludesDialog({
const { onDone,
onDone, isStandaloneDialog,
isStandaloneDialog, externalIncludes,
externalIncludes }: Props): React.ReactNode {
} = t0; React.useEffect(() => {
let t1; // Log when dialog is shown
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { logEvent('tengu_claude_md_includes_dialog_shown', {})
t1 = []; }, [])
$[0] = t1;
} else { const handleSelection = useCallback(
t1 = $[0]; (value: 'yes' | 'no') => {
} if (value === 'no') {
React.useEffect(_temp, t1); logEvent('tengu_claude_md_external_includes_dialog_declined', {})
let t2; // Mark that we've shown the dialog but it was declined
if ($[1] !== onDone) { saveCurrentProjectConfig(current => ({
t2 = value => { ...current,
if (value === "no") { hasClaudeMdExternalIncludesApproved: false,
logEvent("tengu_claude_md_external_includes_dialog_declined", {}); hasClaudeMdExternalIncludesWarningShown: true,
saveCurrentProjectConfig(_temp2); }))
} else { } else {
logEvent("tengu_claude_md_external_includes_dialog_accepted", {}); logEvent('tengu_claude_md_external_includes_dialog_accepted', {})
saveCurrentProjectConfig(_temp3); saveCurrentProjectConfig(current => ({
...current,
hasClaudeMdExternalIncludesApproved: true,
hasClaudeMdExternalIncludesWarningShown: true,
}))
} }
onDone();
}; onDone()
$[1] = onDone; },
$[2] = t2; [onDone],
} else { )
t2 = $[2];
} const handleEscape = useCallback(() => {
const handleSelection = t2; handleSelection('no')
let t3; }, [handleSelection])
if ($[3] !== handleSelection) {
t3 = () => { return (
handleSelection("no"); <Dialog
}; title="Allow external CLAUDE.md file imports?"
$[3] = handleSelection; color="warning"
$[4] = t3; onCancel={handleEscape}
} else { hideBorder={!isStandaloneDialog}
t3 = $[4]; hideInputGuide={!isStandaloneDialog}
} >
const handleEscape = t3; <Text>
const t4 = !isStandaloneDialog; This project&apos;s CLAUDE.md imports files outside the current working
const t5 = !isStandaloneDialog; directory. Never allow this for third-party repositories.
let t6; </Text>
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
t6 = <Text>This project's CLAUDE.md imports files outside the current working directory. Never allow this for third-party repositories.</Text>; {externalIncludes && externalIncludes.length > 0 && (
$[5] = t6; <Box flexDirection="column">
} else { <Text dimColor>External imports:</Text>
t6 = $[5]; {externalIncludes.map((include, i) => (
} <Text key={i} dimColor>
let t7; {' '}
if ($[6] !== externalIncludes) { {include.path}
t7 = externalIncludes && externalIncludes.length > 0 && <Box flexDirection="column"><Text dimColor={true}>External imports:</Text>{externalIncludes.map(_temp4)}</Box>; </Text>
$[6] = externalIncludes; ))}
$[7] = t7; </Box>
} else { )}
t7 = $[7];
} <Text dimColor>
let t8; Important: Only use Claude Code with files you trust. Accessing
if ($[8] === Symbol.for("react.memo_cache_sentinel")) { untrusted files may pose security risks{' '}
t8 = <Text dimColor={true}>Important: Only use Claude Code with files you trust. Accessing untrusted files may pose security risks{" "}<Link url="https://code.claude.com/docs/en/security" />{" "}</Text>; <Link url="https://code.claude.com/docs/en/security" />{' '}
$[8] = t8; </Text>
} else {
t8 = $[8]; <Select
} options={[
let t9; { label: 'Yes, allow external imports', value: 'yes' },
if ($[9] === Symbol.for("react.memo_cache_sentinel")) { { label: 'No, disable external imports', value: 'no' },
t9 = [{ ]}
label: "Yes, allow external imports", onChange={value => handleSelection(value as 'yes' | 'no')}
value: "yes" />
}, { </Dialog>
label: "No, disable external imports", )
value: "no"
}];
$[9] = t9;
} else {
t9 = $[9];
}
let t10;
if ($[10] !== handleSelection) {
t10 = <Select options={t9} onChange={value_0 => handleSelection(value_0 as 'yes' | 'no')} />;
$[10] = handleSelection;
$[11] = t10;
} else {
t10 = $[11];
}
let t11;
if ($[12] !== handleEscape || $[13] !== t10 || $[14] !== t4 || $[15] !== t5 || $[16] !== t7) {
t11 = <Dialog title="Allow external CLAUDE.md file imports?" color="warning" onCancel={handleEscape} hideBorder={t4} hideInputGuide={t5}>{t6}{t7}{t8}{t10}</Dialog>;
$[12] = handleEscape;
$[13] = t10;
$[14] = t4;
$[15] = t5;
$[16] = t7;
$[17] = t11;
} else {
t11 = $[17];
}
return t11;
}
function _temp4(include, i) {
return <Text key={i} dimColor={true}>{" "}{include.path}</Text>;
}
function _temp3(current_0) {
return {
...current_0,
hasClaudeMdExternalIncludesApproved: true,
hasClaudeMdExternalIncludesWarningShown: true
};
}
function _temp2(current) {
return {
...current,
hasClaudeMdExternalIncludesApproved: false,
hasClaudeMdExternalIncludesWarningShown: true
};
}
function _temp() {
logEvent("tengu_claude_md_includes_dialog_shown", {});
} }

View File

@@ -1,16 +1,16 @@
import { c as _c } from "react/compiler-runtime"; import * as React from 'react'
import * as React from 'react'; import { pathToFileURL } from 'url'
import { pathToFileURL } from 'url'; import Link from '../ink/components/Link.js'
import Link from '../ink/components/Link.js'; import { supportsHyperlinks } from '../ink/supports-hyperlinks.js'
import { supportsHyperlinks } from '../ink/supports-hyperlinks.js'; import { Text } from '../ink.js'
import { Text } from '../ink.js'; import { getStoredImagePath } from '../utils/imageStore.js'
import { getStoredImagePath } from '../utils/imageStore.js'; import type { Theme } from '../utils/theme.js'
import type { Theme } from '../utils/theme.js';
type Props = { type Props = {
imageId: number; imageId: number
backgroundColor?: keyof Theme; backgroundColor?: keyof Theme
isSelected?: boolean; isSelected?: boolean
}; }
/** /**
* Renders an image reference like [Image #1] as a clickable link. * Renders an image reference like [Image #1] as a clickable link.
@@ -20,53 +20,42 @@ type Props = {
* - Terminal doesn't support hyperlinks * - Terminal doesn't support hyperlinks
* - Image file is not found in the store * - Image file is not found in the store
*/ */
export function ClickableImageRef(t0) { export function ClickableImageRef({
const $ = _c(13); imageId,
const { backgroundColor,
imageId, isSelected = false,
backgroundColor, }: Props): React.ReactNode {
isSelected: t1 const imagePath = getStoredImagePath(imageId)
} = t0; const displayText = `[Image #${imageId}]`
const isSelected = t1 === undefined ? false : t1;
const imagePath = getStoredImagePath(imageId); // If we have a stored image and terminal supports hyperlinks, make it clickable
const displayText = `[Image #${imageId}]`;
if (imagePath && supportsHyperlinks()) { if (imagePath && supportsHyperlinks()) {
const fileUrl = pathToFileURL(imagePath).href; const fileUrl = pathToFileURL(imagePath).href
let t2;
let t3; return (
if ($[0] !== backgroundColor || $[1] !== displayText || $[2] !== isSelected) { <Link
t2 = <Text backgroundColor={backgroundColor} inverse={isSelected}>{displayText}</Text>; url={fileUrl}
t3 = <Text backgroundColor={backgroundColor} inverse={isSelected} bold={isSelected}>{displayText}</Text>; fallback={
$[0] = backgroundColor; <Text backgroundColor={backgroundColor} inverse={isSelected}>
$[1] = displayText; {displayText}
$[2] = isSelected; </Text>
$[3] = t2; }
$[4] = t3; >
} else { <Text
t2 = $[3]; backgroundColor={backgroundColor}
t3 = $[4]; inverse={isSelected}
} bold={isSelected}
let t4; >
if ($[5] !== fileUrl || $[6] !== t2 || $[7] !== t3) { {displayText}
t4 = <Link url={fileUrl} fallback={t2}>{t3}</Link>; </Text>
$[5] = fileUrl; </Link>
$[6] = t2; )
$[7] = t3;
$[8] = t4;
} else {
t4 = $[8];
}
return t4;
} }
let t2;
if ($[9] !== backgroundColor || $[10] !== displayText || $[11] !== isSelected) { // Fallback: styled but not clickable
t2 = <Text backgroundColor={backgroundColor} inverse={isSelected}>{displayText}</Text>; return (
$[9] = backgroundColor; <Text backgroundColor={backgroundColor} inverse={isSelected}>
$[10] = displayText; {displayText}
$[11] = isSelected; </Text>
$[12] = t2; )
} else {
t2 = $[12];
}
return t2;
} }

View File

@@ -1,117 +1,101 @@
import { c as _c } from "react/compiler-runtime"; import * as React from 'react'
import * as React from 'react'; import { BLACK_CIRCLE } from '../constants/figures.js'
import { BLACK_CIRCLE } from '../constants/figures.js'; import { Box, Text } from '../ink.js'
import { Box, Text } from '../ink.js'; import type { Screen } from '../screens/REPL.js'
import type { Screen } from '../screens/REPL.js'; import type { NormalizedUserMessage } from '../types/message.js'
import type { NormalizedUserMessage } from '../types/message.js'; import { getUserMessageText } from '../utils/messages.js'
import { getUserMessageText } from '../utils/messages.js'; import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js'
import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js'; import { MessageResponse } from './MessageResponse.js'
import { MessageResponse } from './MessageResponse.js';
type Props = { type Props = {
message: NormalizedUserMessage; message: NormalizedUserMessage
screen: Screen; screen: Screen
}; }
export function CompactSummary(t0) {
const $ = _c(24); export function CompactSummary({ message, screen }: Props): React.ReactNode {
const { const isTranscriptMode = screen === 'transcript'
message, const textContent = getUserMessageText(message) || ''
screen const metadata = message.summarizeMetadata
} = t0;
const isTranscriptMode = screen === "transcript"; // "Summarize from here" with metadata
let t1; if (metadata) {
if ($[0] !== message) { return (
t1 = getUserMessageText(message) || ""; <Box flexDirection="column" marginTop={1}>
$[0] = message; <Box flexDirection="row">
$[1] = t1; <Box minWidth={2}>
} else { <Text color="text">{BLACK_CIRCLE}</Text>
t1 = $[1]; </Box>
} <Box flexDirection="column">
const textContent = t1; <Text bold>Summarized conversation</Text>
const metadata = message.summarizeMetadata; {!isTranscriptMode && (
if (metadata) { <MessageResponse>
let t2; <Box flexDirection="column">
if ($[2] === Symbol.for("react.memo_cache_sentinel")) { <Text dimColor>
t2 = <Box minWidth={2}><Text color="text">{BLACK_CIRCLE}</Text></Box>; Summarized {metadata.messagesSummarized} messages{' '}
$[2] = t2; {metadata.direction === 'up_to'
} else { ? 'up to this point'
t2 = $[2]; : 'from this point'}
} </Text>
let t3; {metadata.userContext && (
if ($[3] === Symbol.for("react.memo_cache_sentinel")) { <Text dimColor>
t3 = <Text bold={true}>Summarized conversation</Text>; Context: {'\u201c'}
$[3] = t3; {metadata.userContext}
} else { {'\u201d'}
t3 = $[3]; </Text>
} )}
let t4; <Text dimColor>
if ($[4] !== isTranscriptMode || $[5] !== metadata) { <ConfigurableShortcutHint
t4 = !isTranscriptMode && <MessageResponse><Box flexDirection="column"><Text dimColor={true}>Summarized {metadata.messagesSummarized} messages{" "}{metadata.direction === "up_to" ? "up to this point" : "from this point"}</Text>{metadata.userContext && <Text dimColor={true}>Context: {"\u201C"}{metadata.userContext}{"\u201D"}</Text>}<Text dimColor={true}><ConfigurableShortcutHint action="app:toggleTranscript" context="Global" fallback="ctrl+o" description="expand history" parens={true} /></Text></Box></MessageResponse>; action="app:toggleTranscript"
$[4] = isTranscriptMode; context="Global"
$[5] = metadata; fallback="ctrl+o"
$[6] = t4; description="expand history"
} else { parens
t4 = $[6]; />
} </Text>
let t5; </Box>
if ($[7] !== isTranscriptMode || $[8] !== textContent) { </MessageResponse>
t5 = isTranscriptMode && <MessageResponse><Text>{textContent}</Text></MessageResponse>; )}
$[7] = isTranscriptMode; {isTranscriptMode && (
$[8] = textContent; <MessageResponse>
$[9] = t5; <Text>{textContent}</Text>
} else { </MessageResponse>
t5 = $[9]; )}
} </Box>
let t6; </Box>
if ($[10] !== t4 || $[11] !== t5) { </Box>
t6 = <Box flexDirection="column" marginTop={1}><Box flexDirection="row">{t2}<Box flexDirection="column">{t3}{t4}{t5}</Box></Box></Box>; )
$[10] = t4; }
$[11] = t5;
$[12] = t6; // Default compact summary (auto-compact)
} else { return (
t6 = $[12]; <Box flexDirection="column" marginTop={1}>
} <Box flexDirection="row">
return t6; <Box minWidth={2}>
} <Text color="text">{BLACK_CIRCLE}</Text>
let t2; </Box>
if ($[13] === Symbol.for("react.memo_cache_sentinel")) { <Box flexDirection="column">
t2 = <Box minWidth={2}><Text color="text">{BLACK_CIRCLE}</Text></Box>; <Text bold>
$[13] = t2; Compact summary
} else { {!isTranscriptMode && (
t2 = $[13]; <Text dimColor>
} {' '}
let t3; <ConfigurableShortcutHint
if ($[14] !== isTranscriptMode) { action="app:toggleTranscript"
t3 = !isTranscriptMode && <Text dimColor={true}>{" "}<ConfigurableShortcutHint action="app:toggleTranscript" context="Global" fallback="ctrl+o" description="expand" parens={true} /></Text>; context="Global"
$[14] = isTranscriptMode; fallback="ctrl+o"
$[15] = t3; description="expand"
} else { parens
t3 = $[15]; />
} </Text>
let t4; )}
if ($[16] !== t3) { </Text>
t4 = <Box flexDirection="row">{t2}<Box flexDirection="column"><Text bold={true}>Compact summary{t3}</Text></Box></Box>; </Box>
$[16] = t3; </Box>
$[17] = t4; {isTranscriptMode && (
} else { <MessageResponse>
t4 = $[17]; <Text>{textContent}</Text>
} </MessageResponse>
let t5; )}
if ($[18] !== isTranscriptMode || $[19] !== textContent) { </Box>
t5 = isTranscriptMode && <MessageResponse><Text>{textContent}</Text></MessageResponse>; )
$[18] = isTranscriptMode;
$[19] = textContent;
$[20] = t5;
} else {
t5 = $[20];
}
let t6;
if ($[21] !== t4 || $[22] !== t5) {
t6 = <Box flexDirection="column" marginTop={1}>{t4}{t5}</Box>;
$[21] = t4;
$[22] = t5;
$[23] = t6;
} else {
t6 = $[23];
}
return t6;
} }

View File

@@ -1,22 +1,25 @@
import { c as _c } from "react/compiler-runtime"; import * as React from 'react'
import * as React from 'react'; import type {
import type { KeybindingAction, KeybindingContextName } from '../keybindings/types.js'; KeybindingAction,
import { useShortcutDisplay } from '../keybindings/useShortcutDisplay.js'; KeybindingContextName,
import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js'; } from '../keybindings/types.js'
import { useShortcutDisplay } from '../keybindings/useShortcutDisplay.js'
import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js'
type Props = { type Props = {
/** The keybinding action (e.g., 'app:toggleTranscript') */ /** The keybinding action (e.g., 'app:toggleTranscript') */
action: KeybindingAction; action: KeybindingAction
/** The keybinding context (e.g., 'Global') */ /** The keybinding context (e.g., 'Global') */
context: KeybindingContextName; context: KeybindingContextName
/** Default shortcut if keybinding not configured */ /** Default shortcut if keybinding not configured */
fallback: string; fallback: string
/** The action description text (e.g., 'expand') */ /** The action description text (e.g., 'expand') */
description: string; description: string
/** Whether to wrap in parentheses */ /** Whether to wrap in parentheses */
parens?: boolean; parens?: boolean
/** Whether to show in bold */ /** Whether to show in bold */
bold?: boolean; bold?: boolean
}; }
/** /**
* KeyboardShortcutHint that displays the user-configured shortcut. * KeyboardShortcutHint that displays the user-configured shortcut.
@@ -30,27 +33,21 @@ type Props = {
* description="expand" * description="expand"
* /> * />
*/ */
export function ConfigurableShortcutHint(t0) { export function ConfigurableShortcutHint({
const $ = _c(5); action,
const { context,
action, fallback,
context, description,
fallback, parens,
description, bold,
parens, }: Props): React.ReactNode {
bold const shortcut = useShortcutDisplay(action, context, fallback)
} = t0; return (
const shortcut = useShortcutDisplay(action, context, fallback); <KeyboardShortcutHint
let t1; shortcut={shortcut}
if ($[0] !== bold || $[1] !== description || $[2] !== parens || $[3] !== shortcut) { action={description}
t1 = <KeyboardShortcutHint shortcut={shortcut} action={description} parens={parens} bold={bold} />; parens={parens}
$[0] = bold; bold={bold}
$[1] = description; />
$[2] = parens; )
$[3] = shortcut;
$[4] = t1;
} else {
t1 = $[4];
}
return t1;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,46 +1,38 @@
import { c as _c } from "react/compiler-runtime"; import figures from 'figures'
import figures from 'figures'; import * as React from 'react'
import * as React from 'react'; import { Box, Text } from '../ink.js'
import { Box, Text } from '../ink.js'; import type { ContextSuggestion } from '../utils/contextSuggestions.js'
import type { ContextSuggestion } from '../utils/contextSuggestions.js'; import { formatTokens } from '../utils/format.js'
import { formatTokens } from '../utils/format.js'; import { StatusIcon } from './design-system/StatusIcon.js'
import { StatusIcon } from './design-system/StatusIcon.js';
type Props = { type Props = {
suggestions: ContextSuggestion[]; suggestions: ContextSuggestion[]
};
export function ContextSuggestions(t0) {
const $ = _c(5);
const {
suggestions
} = t0;
if (suggestions.length === 0) {
return null;
}
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <Text bold={true}>Suggestions</Text>;
$[0] = t1;
} else {
t1 = $[0];
}
let t2;
if ($[1] !== suggestions) {
t2 = suggestions.map(_temp);
$[1] = suggestions;
$[2] = t2;
} else {
t2 = $[2];
}
let t3;
if ($[3] !== t2) {
t3 = <Box flexDirection="column" marginTop={1}>{t1}{t2}</Box>;
$[3] = t2;
$[4] = t3;
} else {
t3 = $[4];
}
return t3;
} }
function _temp(suggestion, i) {
return <Box key={i} flexDirection="column" marginTop={i === 0 ? 0 : 1}><Box><StatusIcon status={suggestion.severity} withSpace={true} /><Text bold={true}>{suggestion.title}</Text>{suggestion.savingsTokens ? <Text dimColor={true}>{" "}{figures.arrowRight} save ~{formatTokens(suggestion.savingsTokens)}</Text> : null}</Box><Box marginLeft={2}><Text dimColor={true}>{suggestion.detail}</Text></Box></Box>; export function ContextSuggestions({ suggestions }: Props): React.ReactNode {
if (suggestions.length === 0) return null
return (
<Box flexDirection="column" marginTop={1}>
<Text bold>Suggestions</Text>
{suggestions.map((suggestion, i) => (
<Box key={i} flexDirection="column" marginTop={i === 0 ? 0 : 1}>
<Box>
<StatusIcon status={suggestion.severity} withSpace />
<Text bold>{suggestion.title}</Text>
{suggestion.savingsTokens ? (
<Text dimColor>
{' '}
{figures.arrowRight} save ~
{formatTokens(suggestion.savingsTokens)}
</Text>
) : null}
</Box>
<Box marginLeft={2}>
<Text dimColor>{suggestion.detail}</Text>
</Box>
</Box>
))}
</Box>
)
} }

View File

@@ -1,15 +1,18 @@
import { c as _c } from "react/compiler-runtime"; import { feature } from 'bun:bundle'
import { feature } from 'bun:bundle'; import * as React from 'react'
import * as React from 'react'; import { Box, Text } from '../ink.js'
import { Box, Text } from '../ink.js'; import type { ContextData } from '../utils/analyzeContext.js'
import type { ContextData } from '../utils/analyzeContext.js'; import { generateContextSuggestions } from '../utils/contextSuggestions.js'
import { generateContextSuggestions } from '../utils/contextSuggestions.js'; import { getDisplayPath } from '../utils/file.js'
import { getDisplayPath } from '../utils/file.js'; import { formatTokens } from '../utils/format.js'
import { formatTokens } from '../utils/format.js'; import {
import { getSourceDisplayName, type SettingSource } from '../utils/settings/constants.js'; getSourceDisplayName,
import { plural } from '../utils/stringUtils.js'; type SettingSource,
import { ContextSuggestions } from './ContextSuggestions.js'; } from '../utils/settings/constants.js'
const RESERVED_CATEGORY_NAME = 'Autocompact buffer'; import { plural } from '../utils/stringUtils.js'
import { ContextSuggestions } from './ContextSuggestions.js'
const RESERVED_CATEGORY_NAME = 'Autocompact buffer'
/** /**
* One-liner for the legend header showing what context-collapse has done. * One-liner for the legend header showing what context-collapse has done.
@@ -18,95 +21,100 @@ const RESERVED_CATEGORY_NAME = 'Autocompact buffer';
* their context was rewritten — the <collapsed> placeholders are isMeta * their context was rewritten — the <collapsed> placeholders are isMeta
* and don't appear in the conversation view. * and don't appear in the conversation view.
*/ */
function CollapseStatus() { function CollapseStatus(): React.ReactNode {
const $ = _c(2); if (feature('CONTEXT_COLLAPSE')) {
if (feature("CONTEXT_COLLAPSE")) { /* eslint-disable @typescript-eslint/no-require-imports */
let t0; const { getStats, isContextCollapseEnabled } =
let t1; require('../services/contextCollapse/index.js') as typeof import('../services/contextCollapse/index.js')
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { /* eslint-enable @typescript-eslint/no-require-imports */
t1 = Symbol.for("react.early_return_sentinel"); if (!isContextCollapseEnabled()) return null
bb0: {
const { const s = getStats()
getStats, const { health: h } = s
isContextCollapseEnabled
} = require("../services/contextCollapse/index.js") as typeof import('../services/contextCollapse/index.js'); const parts: string[] = []
if (!isContextCollapseEnabled()) { if (s.collapsedSpans > 0) {
t1 = null; parts.push(
break bb0; `${s.collapsedSpans} ${plural(s.collapsedSpans, 'span')} summarized (${s.collapsedMessages} msgs)`,
} )
const s = getStats();
const {
health: h
} = s;
const parts = [];
if (s.collapsedSpans > 0) {
parts.push(`${s.collapsedSpans} ${plural(s.collapsedSpans, "span")} summarized (${s.collapsedMessages} msgs)`);
}
if (s.stagedSpans > 0) {
parts.push(`${s.stagedSpans} staged`);
}
const summary = parts.length > 0 ? parts.join(", ") : h.totalSpawns > 0 ? `${h.totalSpawns} ${plural(h.totalSpawns, "spawn")}, nothing staged yet` : "waiting for first trigger";
let line2 = null;
if (h.totalErrors > 0) {
line2 = <Text color="warning">Collapse errors: {h.totalErrors}/{h.totalSpawns} spawns failed{h.lastError ? ` (last: ${h.lastError.slice(0, 60)})` : ""}</Text>;
} else {
if (h.emptySpawnWarningEmitted) {
line2 = <Text color="warning">Collapse idle: {h.totalEmptySpawns} consecutive empty runs</Text>;
}
}
t0 = <><Text dimColor={true}>Context strategy: collapse ({summary})</Text>{line2}</>;
}
$[0] = t0;
$[1] = t1;
} else {
t0 = $[0];
t1 = $[1];
} }
if (t1 !== Symbol.for("react.early_return_sentinel")) { if (s.stagedSpans > 0) parts.push(`${s.stagedSpans} staged`)
return t1; const summary =
parts.length > 0
? parts.join(', ')
: h.totalSpawns > 0
? `${h.totalSpawns} ${plural(h.totalSpawns, 'spawn')}, nothing staged yet`
: 'waiting for first trigger'
let line2: React.ReactNode = null
if (h.totalErrors > 0) {
line2 = (
<Text color="warning">
Collapse errors: {h.totalErrors}/{h.totalSpawns} spawns failed
{h.lastError ? ` (last: ${h.lastError.slice(0, 60)})` : ''}
</Text>
)
} else if (h.emptySpawnWarningEmitted) {
line2 = (
<Text color="warning">
Collapse idle: {h.totalEmptySpawns} consecutive empty runs
</Text>
)
} }
return t0;
return (
<>
<Text dimColor>Context strategy: collapse ({summary})</Text>
{line2}
</>
)
} }
return null; return null
} }
// Order for displaying source groups: Project > User > Managed > Plugin > Built-in // Order for displaying source groups: Project > User > Managed > Plugin > Built-in
const SOURCE_DISPLAY_ORDER = ['Project', 'User', 'Managed', 'Plugin', 'Built-in']; const SOURCE_DISPLAY_ORDER = [
'Project',
'User',
'Managed',
'Plugin',
'Built-in',
]
/** Group items by source type for display, sorted by tokens descending within each group */ /** Group items by source type for display, sorted by tokens descending within each group */
function groupBySource<T extends { function groupBySource<
source: SettingSource | 'plugin' | 'built-in'; T extends { source: SettingSource | 'plugin' | 'built-in'; tokens: number },
tokens: number; >(items: T[]): Map<string, T[]> {
}>(items: T[]): Map<string, T[]> { const groups = new Map<string, T[]>()
const groups = new Map<string, T[]>();
for (const item of items) { for (const item of items) {
const key = getSourceDisplayName(item.source); const key = getSourceDisplayName(item.source)
const existing = groups.get(key) || []; const existing = groups.get(key) || []
existing.push(item); existing.push(item)
groups.set(key, existing); groups.set(key, existing)
} }
// Sort each group by tokens descending // Sort each group by tokens descending
for (const [key, group] of groups.entries()) { for (const [key, group] of groups.entries()) {
groups.set(key, group.sort((a, b) => b.tokens - a.tokens)); groups.set(
key,
group.sort((a, b) => b.tokens - a.tokens),
)
} }
// Return groups in consistent order // Return groups in consistent order
const orderedGroups = new Map<string, T[]>(); const orderedGroups = new Map<string, T[]>()
for (const source of SOURCE_DISPLAY_ORDER) { for (const source of SOURCE_DISPLAY_ORDER) {
const group = groups.get(source); const group = groups.get(source)
if (group) { if (group) {
orderedGroups.set(source, group); orderedGroups.set(source, group)
} }
} }
return orderedGroups; return orderedGroups
} }
interface Props { interface Props {
data: ContextData; data: ContextData
} }
export function ContextVisualization(t0) {
const $ = _c(87); export function ContextVisualization({ data }: Props): React.ReactNode {
const {
data
} = t0;
const { const {
categories, categories,
totalTokens, totalTokens,
@@ -116,373 +124,371 @@ export function ContextVisualization(t0) {
model, model,
memoryFiles, memoryFiles,
mcpTools, mcpTools,
deferredBuiltinTools: t1, deferredBuiltinTools = [],
systemTools, systemTools,
systemPromptSections, systemPromptSections,
agents, agents,
skills, skills,
messageBreakdown messageBreakdown,
} = data; } = data
let T0;
let T1; // Filter out categories with 0 tokens for the legend, and exclude Free space, Autocompact buffer, and deferred
let t2; const visibleCategories = categories.filter(
let t3; cat =>
let t4; cat.tokens > 0 &&
let t5; cat.name !== 'Free space' &&
let t6; cat.name !== RESERVED_CATEGORY_NAME &&
let t7; !cat.isDeferred,
let t8; )
let t9; // Check if MCP tools are deferred (loaded on-demand via tool search)
if ($[0] !== categories || $[1] !== gridRows || $[2] !== mcpTools || $[3] !== model || $[4] !== percentage || $[5] !== rawMaxTokens || $[6] !== systemTools || $[7] !== t1 || $[8] !== totalTokens) { const hasDeferredMcpTools = categories.some(
const deferredBuiltinTools = t1 === undefined ? [] : t1; cat => cat.isDeferred && cat.name.includes('MCP'),
const visibleCategories = categories.filter(_temp); )
let t10; // Check if builtin tools are deferred
if ($[19] !== categories) { const hasDeferredBuiltinTools = deferredBuiltinTools.length > 0
t10 = categories.some(_temp2); const autocompactCategory = categories.find(
$[19] = categories; cat => cat.name === RESERVED_CATEGORY_NAME,
$[20] = t10; )
} else {
t10 = $[20]; return (
} <Box flexDirection="column" paddingLeft={1}>
const hasDeferredMcpTools = t10; <Text bold>Context Usage</Text>
const hasDeferredBuiltinTools = deferredBuiltinTools.length > 0; <Box flexDirection="row" gap={2}>
const autocompactCategory = categories.find(_temp3); {/* Fixed size grid */}
T1 = Box; <Box flexDirection="column" flexShrink={0}>
t6 = "column"; {gridRows.map((row, rowIndex) => (
t7 = 1; <Box key={rowIndex} flexDirection="row" marginLeft={-1}>
if ($[21] === Symbol.for("react.memo_cache_sentinel")) { {row.map((square, colIndex) => {
t8 = <Text bold={true}>Context Usage</Text>; if (square.categoryName === 'Free space') {
$[21] = t8; return (
} else { <Text key={colIndex} dimColor>
t8 = $[21]; {'⛶ '}
} </Text>
let t11; )
if ($[22] !== gridRows) { }
t11 = gridRows.map(_temp5); if (square.categoryName === RESERVED_CATEGORY_NAME) {
$[22] = gridRows; return (
$[23] = t11; <Text key={colIndex} color={square.color}>
} else { {'⛝ '}
t11 = $[23]; </Text>
} )
let t12; }
if ($[24] !== t11) { return (
t12 = <Box flexDirection="column" flexShrink={0}>{t11}</Box>; <Text key={colIndex} color={square.color}>
$[24] = t11; {square.squareFullness >= 0.7 ? '⛁ ' : '⛀ '}
$[25] = t12; </Text>
} else { )
t12 = $[25]; })}
} </Box>
let t13; ))}
if ($[26] !== totalTokens) { </Box>
t13 = formatTokens(totalTokens);
$[26] = totalTokens; {/* Legend to the right */}
$[27] = t13; <Box flexDirection="column" gap={0} flexShrink={0}>
} else { <Text dimColor>
t13 = $[27]; {model} · {formatTokens(totalTokens)}/{formatTokens(rawMaxTokens)}{' '}
} tokens ({percentage}%)
let t14; </Text>
if ($[28] !== rawMaxTokens) { <CollapseStatus />
t14 = formatTokens(rawMaxTokens); <Text> </Text>
$[28] = rawMaxTokens; <Text dimColor italic>
$[29] = t14; Estimated usage by category
} else { </Text>
t14 = $[29]; {visibleCategories.map((cat, index) => {
} const tokenDisplay = formatTokens(cat.tokens)
let t15; // Show "N/A" for deferred categories since they don't count toward context
if ($[30] !== model || $[31] !== percentage || $[32] !== t13 || $[33] !== t14) { const percentDisplay = cat.isDeferred
t15 = <Text dimColor={true}>{model} · {t13}/{t14}{" "}tokens ({percentage}%)</Text>; ? 'N/A'
$[30] = model; : `${((cat.tokens / rawMaxTokens) * 100).toFixed(1)}%`
$[31] = percentage; const isReserved = cat.name === RESERVED_CATEGORY_NAME
$[32] = t13; const displayName = cat.name
$[33] = t14; // Deferred categories don't appear in grid, so show blank instead of symbol
$[34] = t15; const symbol = cat.isDeferred ? ' ' : isReserved ? '⛝' : '⛁'
} else {
t15 = $[34]; return (
} <Box key={index}>
let t16; <Text color={cat.color}>{symbol}</Text>
let t17; <Text> {displayName}: </Text>
let t18; <Text dimColor>
if ($[35] === Symbol.for("react.memo_cache_sentinel")) { {tokenDisplay} tokens ({percentDisplay})
t16 = <CollapseStatus />; </Text>
t17 = <Text> </Text>; </Box>
t18 = <Text dimColor={true} italic={true}>Estimated usage by category</Text>; )
$[35] = t16; })}
$[36] = t17; {(categories.find(c => c.name === 'Free space')?.tokens ?? 0) > 0 && (
$[37] = t18; <Box>
} else { <Text dimColor></Text>
t16 = $[35]; <Text> Free space: </Text>
t17 = $[36]; <Text dimColor>
t18 = $[37]; {formatTokens(
} categories.find(c => c.name === 'Free space')?.tokens || 0,
let t19; )}{' '}
if ($[38] !== rawMaxTokens) { (
t19 = (cat_2, index) => { {(
const tokenDisplay = formatTokens(cat_2.tokens); ((categories.find(c => c.name === 'Free space')?.tokens ||
const percentDisplay = cat_2.isDeferred ? "N/A" : `${(cat_2.tokens / rawMaxTokens * 100).toFixed(1)}%`; 0) /
const isReserved = cat_2.name === RESERVED_CATEGORY_NAME; rawMaxTokens) *
const displayName = cat_2.name; 100
const symbol = cat_2.isDeferred ? " " : isReserved ? "\u26DD" : "\u26C1"; ).toFixed(1)}
return <Box key={index}><Text color={cat_2.color}>{symbol}</Text><Text> {displayName}: </Text><Text dimColor={true}>{tokenDisplay} tokens ({percentDisplay})</Text></Box>; %)
}; </Text>
$[38] = rawMaxTokens; </Box>
$[39] = t19; )}
} else { {autocompactCategory && autocompactCategory.tokens > 0 && (
t19 = $[39]; <Box>
} <Text color={autocompactCategory.color}></Text>
const t20 = visibleCategories.map(t19); <Text dimColor> {autocompactCategory.name}: </Text>
let t21; <Text dimColor>
if ($[40] !== categories || $[41] !== rawMaxTokens) { {formatTokens(autocompactCategory.tokens)} tokens (
t21 = (categories.find(_temp6)?.tokens ?? 0) > 0 && <Box><Text dimColor={true}></Text><Text> Free space: </Text><Text dimColor={true}>{formatTokens(categories.find(_temp7)?.tokens || 0)}{" "}({((categories.find(_temp8)?.tokens || 0) / rawMaxTokens * 100).toFixed(1)}%)</Text></Box>; {((autocompactCategory.tokens / rawMaxTokens) * 100).toFixed(1)}
$[40] = categories; %)
$[41] = rawMaxTokens; </Text>
$[42] = t21; </Box>
} else { )}
t21 = $[42]; </Box>
} </Box>
const t22 = autocompactCategory && autocompactCategory.tokens > 0 && <Box><Text color={autocompactCategory.color}></Text><Text dimColor={true}> {autocompactCategory.name}: </Text><Text dimColor={true}>{formatTokens(autocompactCategory.tokens)} tokens ({(autocompactCategory.tokens / rawMaxTokens * 100).toFixed(1)}%)</Text></Box>;
let t23; <Box flexDirection="column" marginLeft={-1}>
if ($[43] !== t15 || $[44] !== t20 || $[45] !== t21 || $[46] !== t22) { {mcpTools.length > 0 && (
t23 = <Box flexDirection="column" gap={0} flexShrink={0}>{t15}{t16}{t17}{t18}{t20}{t21}{t22}</Box>; <Box flexDirection="column" marginTop={1}>
$[43] = t15; <Box>
$[44] = t20; <Text bold>MCP tools</Text>
$[45] = t21; <Text dimColor>
$[46] = t22; {' '}
$[47] = t23; · /mcp{hasDeferredMcpTools ? ' (loaded on-demand)' : ''}
} else { </Text>
t23 = $[47]; </Box>
} {/* Show loaded tools first */}
if ($[48] !== t12 || $[49] !== t23) { {mcpTools.some(t => t.isLoaded) && (
t9 = <Box flexDirection="row" gap={2}>{t12}{t23}</Box>; <Box flexDirection="column" marginTop={1}>
$[48] = t12; <Text dimColor>Loaded</Text>
$[49] = t23; {mcpTools
$[50] = t9; .filter(t => t.isLoaded)
} else { .map((tool, i) => (
t9 = $[50]; <Box key={i}>
} <Text> {tool.name}: </Text>
T0 = Box; <Text dimColor>{formatTokens(tool.tokens)} tokens</Text>
t2 = "column"; </Box>
t3 = -1; ))}
if ($[51] !== hasDeferredMcpTools || $[52] !== mcpTools) { </Box>
t4 = mcpTools.length > 0 && <Box flexDirection="column" marginTop={1}><Box><Text bold={true}>MCP tools</Text><Text dimColor={true}>{" "}· /mcp{hasDeferredMcpTools ? " (loaded on-demand)" : ""}</Text></Box>{mcpTools.some(_temp9) && <Box flexDirection="column" marginTop={1}><Text dimColor={true}>Loaded</Text>{mcpTools.filter(_temp0).map(_temp1)}</Box>}{hasDeferredMcpTools && mcpTools.some(_temp10) && <Box flexDirection="column" marginTop={1}><Text dimColor={true}>Available</Text>{mcpTools.filter(_temp11).map(_temp12)}</Box>}{!hasDeferredMcpTools && mcpTools.map(_temp13)}</Box>; )}
$[51] = hasDeferredMcpTools; {/* Show available (deferred) tools */}
$[52] = mcpTools; {hasDeferredMcpTools && mcpTools.some(t => !t.isLoaded) && (
$[53] = t4; <Box flexDirection="column" marginTop={1}>
} else { <Text dimColor>Available</Text>
t4 = $[53]; {mcpTools
} .filter(t => !t.isLoaded)
t5 = (systemTools && systemTools.length > 0 || hasDeferredBuiltinTools) && false && <Box flexDirection="column" marginTop={1}><Box><Text bold={true}>[ANT-ONLY] System tools</Text>{hasDeferredBuiltinTools && <Text dimColor={true}> (some loaded on-demand)</Text>}</Box><Box flexDirection="column" marginTop={1}><Text dimColor={true}>Loaded</Text>{systemTools?.map(_temp14)}{deferredBuiltinTools.filter(_temp15).map(_temp16)}</Box>{hasDeferredBuiltinTools && deferredBuiltinTools.some(_temp17) && <Box flexDirection="column" marginTop={1}><Text dimColor={true}>Available</Text>{deferredBuiltinTools.filter(_temp18).map(_temp19)}</Box>}</Box>; .map((tool, i) => (
$[0] = categories; <Box key={i}>
$[1] = gridRows; <Text dimColor> {tool.name}</Text>
$[2] = mcpTools; </Box>
$[3] = model; ))}
$[4] = percentage; </Box>
$[5] = rawMaxTokens; )}
$[6] = systemTools; {/* Show all tools normally when not deferred */}
$[7] = t1; {!hasDeferredMcpTools &&
$[8] = totalTokens; mcpTools.map((tool, i) => (
$[9] = T0; <Box key={i}>
$[10] = T1; <Text> {tool.name}: </Text>
$[11] = t2; <Text dimColor>{formatTokens(tool.tokens)} tokens</Text>
$[12] = t3; </Box>
$[13] = t4; ))}
$[14] = t5; </Box>
$[15] = t6; )}
$[16] = t7;
$[17] = t8; {/* Show builtin tools: always-loaded + deferred (ant-only) */}
$[18] = t9; {((systemTools && systemTools.length > 0) || hasDeferredBuiltinTools) &&
} else { process.env.USER_TYPE === 'ant' && (
T0 = $[9]; <Box flexDirection="column" marginTop={1}>
T1 = $[10]; <Box>
t2 = $[11]; <Text bold>[ANT-ONLY] System tools</Text>
t3 = $[12]; {hasDeferredBuiltinTools && (
t4 = $[13]; <Text dimColor> (some loaded on-demand)</Text>
t5 = $[14]; )}
t6 = $[15]; </Box>
t7 = $[16]; {/* Always-loaded + deferred-but-loaded tools */}
t8 = $[17]; <Box flexDirection="column" marginTop={1}>
t9 = $[18]; <Text dimColor>Loaded</Text>
} {systemTools?.map((tool, i) => (
let t10; <Box key={`sys-${i}`}>
if ($[54] !== systemPromptSections) { <Text> {tool.name}: </Text>
t10 = systemPromptSections && systemPromptSections.length > 0 && false && <Box flexDirection="column" marginTop={1}><Text bold={true}>[ANT-ONLY] System prompt sections</Text>{systemPromptSections.map(_temp20)}</Box>; <Text dimColor>{formatTokens(tool.tokens)} tokens</Text>
$[54] = systemPromptSections; </Box>
$[55] = t10; ))}
} else { {deferredBuiltinTools
t10 = $[55]; .filter(t => t.isLoaded)
} .map((tool, i) => (
let t11; <Box key={`def-${i}`}>
if ($[56] !== agents) { <Text> {tool.name}: </Text>
t11 = agents.length > 0 && <Box flexDirection="column" marginTop={1}><Box><Text bold={true}>Custom agents</Text><Text dimColor={true}> · /agents</Text></Box>{Array.from(groupBySource(agents).entries()).map(_temp22)}</Box>; <Text dimColor>{formatTokens(tool.tokens)} tokens</Text>
$[56] = agents; </Box>
$[57] = t11; ))}
} else { </Box>
t11 = $[57]; {/* Deferred (not yet loaded) tools */}
} {hasDeferredBuiltinTools &&
let t12; deferredBuiltinTools.some(t => !t.isLoaded) && (
if ($[58] !== memoryFiles) { <Box flexDirection="column" marginTop={1}>
t12 = memoryFiles.length > 0 && <Box flexDirection="column" marginTop={1}><Box><Text bold={true}>Memory files</Text><Text dimColor={true}> · /memory</Text></Box>{memoryFiles.map(_temp23)}</Box>; <Text dimColor>Available</Text>
$[58] = memoryFiles; {deferredBuiltinTools
$[59] = t12; .filter(t => !t.isLoaded)
} else { .map((tool, i) => (
t12 = $[59]; <Box key={i}>
} <Text dimColor> {tool.name}</Text>
let t13; </Box>
if ($[60] !== skills) { ))}
t13 = skills && skills.tokens > 0 && <Box flexDirection="column" marginTop={1}><Box><Text bold={true}>Skills</Text><Text dimColor={true}> · /skills</Text></Box>{Array.from(groupBySource(skills.skillFrontmatter).entries()).map(_temp25)}</Box>; </Box>
$[60] = skills; )}
$[61] = t13; </Box>
} else { )}
t13 = $[61];
} {systemPromptSections &&
let t14; systemPromptSections.length > 0 &&
if ($[62] !== messageBreakdown) { process.env.USER_TYPE === 'ant' && (
t14 = messageBreakdown && false && <Box flexDirection="column" marginTop={1}><Text bold={true}>[ANT-ONLY] Message breakdown</Text><Box flexDirection="column" marginLeft={1}><Box><Text>Tool calls: </Text><Text dimColor={true}>{formatTokens(messageBreakdown.toolCallTokens)} tokens</Text></Box><Box><Text>Tool results: </Text><Text dimColor={true}>{formatTokens(messageBreakdown.toolResultTokens)} tokens</Text></Box><Box><Text>Attachments: </Text><Text dimColor={true}>{formatTokens(messageBreakdown.attachmentTokens)} tokens</Text></Box><Box><Text>Assistant messages (non-tool): </Text><Text dimColor={true}>{formatTokens(messageBreakdown.assistantMessageTokens)} tokens</Text></Box><Box><Text>User messages (non-tool-result): </Text><Text dimColor={true}>{formatTokens(messageBreakdown.userMessageTokens)} tokens</Text></Box></Box>{messageBreakdown.toolCallsByType.length > 0 && <Box flexDirection="column" marginTop={1}><Text bold={true}>[ANT-ONLY] Top tools</Text>{messageBreakdown.toolCallsByType.slice(0, 5).map(_temp26)}</Box>}{messageBreakdown.attachmentsByType.length > 0 && <Box flexDirection="column" marginTop={1}><Text bold={true}>[ANT-ONLY] Top attachments</Text>{messageBreakdown.attachmentsByType.slice(0, 5).map(_temp27)}</Box>}</Box>; <Box flexDirection="column" marginTop={1}>
$[62] = messageBreakdown; <Text bold>[ANT-ONLY] System prompt sections</Text>
$[63] = t14; {systemPromptSections.map((section, i) => (
} else { <Box key={i}>
t14 = $[63]; <Text> {section.name}: </Text>
} <Text dimColor>{formatTokens(section.tokens)} tokens</Text>
let t15; </Box>
if ($[64] !== T0 || $[65] !== t10 || $[66] !== t11 || $[67] !== t12 || $[68] !== t13 || $[69] !== t14 || $[70] !== t2 || $[71] !== t3 || $[72] !== t4 || $[73] !== t5) { ))}
t15 = <T0 flexDirection={t2} marginLeft={t3}>{t4}{t5}{t10}{t11}{t12}{t13}{t14}</T0>; </Box>
$[64] = T0; )}
$[65] = t10;
$[66] = t11; {agents.length > 0 && (
$[67] = t12; <Box flexDirection="column" marginTop={1}>
$[68] = t13; <Box>
$[69] = t14; <Text bold>Custom agents</Text>
$[70] = t2; <Text dimColor> · /agents</Text>
$[71] = t3; </Box>
$[72] = t4; {Array.from(groupBySource(agents).entries()).map(
$[73] = t5; ([sourceDisplay, sourceAgents]) => (
$[74] = t15; <Box key={sourceDisplay} flexDirection="column" marginTop={1}>
} else { <Text dimColor>{sourceDisplay}</Text>
t15 = $[74]; {sourceAgents.map((agent, i) => (
} <Box key={i}>
let t16; <Text> {agent.agentType}: </Text>
if ($[75] !== data) { <Text dimColor>{formatTokens(agent.tokens)} tokens</Text>
t16 = generateContextSuggestions(data); </Box>
$[75] = data; ))}
$[76] = t16; </Box>
} else { ),
t16 = $[76]; )}
} </Box>
let t17; )}
if ($[77] !== t16) {
t17 = <ContextSuggestions suggestions={t16} />; {memoryFiles.length > 0 && (
$[77] = t16; <Box flexDirection="column" marginTop={1}>
$[78] = t17; <Box>
} else { <Text bold>Memory files</Text>
t17 = $[78]; <Text dimColor> · /memory</Text>
} </Box>
let t18; {memoryFiles.map((file, i) => (
if ($[79] !== T1 || $[80] !== t15 || $[81] !== t17 || $[82] !== t6 || $[83] !== t7 || $[84] !== t8 || $[85] !== t9) { <Box key={i}>
t18 = <T1 flexDirection={t6} paddingLeft={t7}>{t8}{t9}{t15}{t17}</T1>; <Text> {getDisplayPath(file.path)}: </Text>
$[79] = T1; <Text dimColor>{formatTokens(file.tokens)} tokens</Text>
$[80] = t15; </Box>
$[81] = t17; ))}
$[82] = t6; </Box>
$[83] = t7; )}
$[84] = t8;
$[85] = t9; {skills && skills.tokens > 0 && (
$[86] = t18; <Box flexDirection="column" marginTop={1}>
} else { <Box>
t18 = $[86]; <Text bold>Skills</Text>
} <Text dimColor> · /skills</Text>
return t18; </Box>
} {Array.from(groupBySource(skills.skillFrontmatter).entries()).map(
function _temp27(attachment, i_10) { ([sourceDisplay, sourceSkills]) => (
return <Box key={i_10} marginLeft={1}><Text> {attachment.name}: </Text><Text dimColor={true}>{formatTokens(attachment.tokens)} tokens</Text></Box>; <Box key={sourceDisplay} flexDirection="column" marginTop={1}>
} <Text dimColor>{sourceDisplay}</Text>
function _temp26(tool_5, i_9) { {sourceSkills.map((skill, i) => (
return <Box key={i_9} marginLeft={1}><Text> {tool_5.name}: </Text><Text dimColor={true}>calls {formatTokens(tool_5.callTokens)}, results{" "}{formatTokens(tool_5.resultTokens)}</Text></Box>; <Box key={i}>
} <Text> {skill.name}: </Text>
function _temp25(t0) { <Text dimColor>{formatTokens(skill.tokens)} tokens</Text>
const [sourceDisplay_0, sourceSkills] = t0; </Box>
return <Box key={sourceDisplay_0} flexDirection="column" marginTop={1}><Text dimColor={true}>{sourceDisplay_0}</Text>{sourceSkills.map(_temp24)}</Box>; ))}
} </Box>
function _temp24(skill, i_8) { ),
return <Box key={i_8}><Text> {skill.name}: </Text><Text dimColor={true}>{formatTokens(skill.tokens)} tokens</Text></Box>; )}
} </Box>
function _temp23(file, i_7) { )}
return <Box key={i_7}><Text> {getDisplayPath(file.path)}: </Text><Text dimColor={true}>{formatTokens(file.tokens)} tokens</Text></Box>;
} {messageBreakdown && process.env.USER_TYPE === 'ant' && (
function _temp22(t0) { <Box flexDirection="column" marginTop={1}>
const [sourceDisplay, sourceAgents] = t0; <Text bold>[ANT-ONLY] Message breakdown</Text>
return <Box key={sourceDisplay} flexDirection="column" marginTop={1}><Text dimColor={true}>{sourceDisplay}</Text>{sourceAgents.map(_temp21)}</Box>;
} <Box flexDirection="column" marginLeft={1}>
function _temp21(agent, i_6) { <Box>
return <Box key={i_6}><Text> {agent.agentType}: </Text><Text dimColor={true}>{formatTokens(agent.tokens)} tokens</Text></Box>; <Text>Tool calls: </Text>
} <Text dimColor>
function _temp20(section, i_5) { {formatTokens(messageBreakdown.toolCallTokens)} tokens
return <Box key={i_5}><Text> {section.name}: </Text><Text dimColor={true}>{formatTokens(section.tokens)} tokens</Text></Box>; </Text>
} </Box>
function _temp19(tool_4, i_4) {
return <Box key={i_4}><Text dimColor={true}> {tool_4.name}</Text></Box>; <Box>
} <Text>Tool results: </Text>
function _temp18(t_4) { <Text dimColor>
return !t_4.isLoaded; {formatTokens(messageBreakdown.toolResultTokens)} tokens
} </Text>
function _temp17(t_5) { </Box>
return !t_5.isLoaded;
} <Box>
function _temp16(tool_3, i_3) { <Text>Attachments: </Text>
return <Box key={`def-${i_3}`}><Text> {tool_3.name}: </Text><Text dimColor={true}>{formatTokens(tool_3.tokens)} tokens</Text></Box>; <Text dimColor>
} {formatTokens(messageBreakdown.attachmentTokens)} tokens
function _temp15(t_3) { </Text>
return t_3.isLoaded; </Box>
}
function _temp14(tool_2, i_2) { <Box>
return <Box key={`sys-${i_2}`}><Text> {tool_2.name}: </Text><Text dimColor={true}>{formatTokens(tool_2.tokens)} tokens</Text></Box>; <Text>Assistant messages (non-tool): </Text>
} <Text dimColor>
function _temp13(tool_1, i_1) { {formatTokens(messageBreakdown.assistantMessageTokens)} tokens
return <Box key={i_1}><Text> {tool_1.name}: </Text><Text dimColor={true}>{formatTokens(tool_1.tokens)} tokens</Text></Box>; </Text>
} </Box>
function _temp12(tool_0, i_0) {
return <Box key={i_0}><Text dimColor={true}> {tool_0.name}</Text></Box>; <Box>
} <Text>User messages (non-tool-result): </Text>
function _temp11(t_1) { <Text dimColor>
return !t_1.isLoaded; {formatTokens(messageBreakdown.userMessageTokens)} tokens
} </Text>
function _temp10(t_2) { </Box>
return !t_2.isLoaded; </Box>
}
function _temp1(tool, i) { {messageBreakdown.toolCallsByType.length > 0 && (
return <Box key={i}><Text> {tool.name}: </Text><Text dimColor={true}>{formatTokens(tool.tokens)} tokens</Text></Box>; <Box flexDirection="column" marginTop={1}>
} <Text bold>[ANT-ONLY] Top tools</Text>
function _temp0(t) { {messageBreakdown.toolCallsByType.slice(0, 5).map((tool, i) => (
return t.isLoaded; <Box key={i} marginLeft={1}>
} <Text> {tool.name}: </Text>
function _temp9(t_0) { <Text dimColor>
return t_0.isLoaded; calls {formatTokens(tool.callTokens)}, results{' '}
} {formatTokens(tool.resultTokens)}
function _temp8(c_0) { </Text>
return c_0.name === "Free space"; </Box>
} ))}
function _temp7(c) { </Box>
return c.name === "Free space"; )}
}
function _temp6(c_1) { {messageBreakdown.attachmentsByType.length > 0 && (
return c_1.name === "Free space"; <Box flexDirection="column" marginTop={1}>
} <Text bold>[ANT-ONLY] Top attachments</Text>
function _temp5(row, rowIndex) { {messageBreakdown.attachmentsByType
return <Box key={rowIndex} flexDirection="row" marginLeft={-1}>{row.map(_temp4)}</Box>; .slice(0, 5)
} .map((attachment, i) => (
function _temp4(square, colIndex) { <Box key={i} marginLeft={1}>
if (square.categoryName === "Free space") { <Text> {attachment.name}: </Text>
return <Text key={colIndex} dimColor={true}>{"\u26F6 "}</Text>; <Text dimColor>
} {formatTokens(attachment.tokens)} tokens
if (square.categoryName === RESERVED_CATEGORY_NAME) { </Text>
return <Text key={colIndex} color={square.color}>{"\u26DD "}</Text>; </Box>
} ))}
return <Text key={colIndex} color={square.color}>{square.squareFullness >= 0.7 ? "\u26C1 " : "\u26C0 "}</Text>; </Box>
} )}
function _temp3(cat_1) { </Box>
return cat_1.name === RESERVED_CATEGORY_NAME; )}
} </Box>
function _temp2(cat_0) { <ContextSuggestions suggestions={generateContextSuggestions(data)} />
return cat_0.isDeferred && cat_0.name.includes("MCP"); </Box>
} )
function _temp(cat) {
return cat.tokens > 0 && cat.name !== "Free space" && cat.name !== RESERVED_CATEGORY_NAME && !cat.isDeferred;
} }

View File

@@ -1,4 +1,3 @@
import { c as _c } from "react/compiler-runtime";
/** /**
* CoordinatorTaskPanel — Steerable list of background agents. * CoordinatorTaskPanel — Steerable list of background agents.
* *
@@ -7,18 +6,28 @@ import { c as _c } from "react/compiler-runtime";
* always; a timestamp shows until passed. Enter to view/steer, x to dismiss. * always; a timestamp shows until passed. Enter to view/steer, x to dismiss.
*/ */
import figures from 'figures'; import figures from 'figures'
import * as React from 'react'; import * as React from 'react'
import { BLACK_CIRCLE, PAUSE_ICON, PLAY_ICON } from '../constants/figures.js'; import { BLACK_CIRCLE, PAUSE_ICON, PLAY_ICON } from '../constants/figures.js'
import { useTerminalSize } from '../hooks/useTerminalSize.js'; import { useTerminalSize } from '../hooks/useTerminalSize.js'
import { stringWidth } from '../ink/stringWidth.js'; import { stringWidth } from '../ink/stringWidth.js'
import { Box, Text, wrapText } from '../ink.js'; import { Box, Text, wrapText } from '../ink.js'
import { type AppState, useAppState, useSetAppState } from '../state/AppState.js'; import {
import { enterTeammateView, exitTeammateView } from '../state/teammateViewHelpers.js'; type AppState,
import { isPanelAgentTask, type LocalAgentTaskState } from '../tasks/LocalAgentTask/LocalAgentTask.js'; useAppState,
import { formatDuration, formatNumber } from '../utils/format.js'; useSetAppState,
import { evictTerminalTask } from '../utils/task/framework.js'; } from '../state/AppState.js'
import { isTerminalStatus } from './tasks/taskStatusUtils.js'; import {
enterTeammateView,
exitTeammateView,
} from '../state/teammateViewHelpers.js'
import {
isPanelAgentTask,
type LocalAgentTaskState,
} from '../tasks/LocalAgentTask/LocalAgentTask.js'
import { formatDuration, formatNumber } from '../utils/format.js'
import { evictTerminalTask } from '../utils/task/framework.js'
import { isTerminalStatus } from './tasks/taskStatusUtils.js'
/** /**
* Which panel-managed tasks currently have a visible row. * Which panel-managed tasks currently have a visible row.
@@ -28,51 +37,83 @@ import { isTerminalStatus } from './tasks/taskStatusUtils.js';
* the filter time-dependent. Shared by panel render, useCoordinatorTaskCount, * the filter time-dependent. Shared by panel render, useCoordinatorTaskCount,
* and index resolvers so the math can't drift. * and index resolvers so the math can't drift.
*/ */
export function getVisibleAgentTasks(tasks: AppState['tasks']): LocalAgentTaskState[] { export function getVisibleAgentTasks(
return Object.values(tasks).filter((t): t is LocalAgentTaskState => isPanelAgentTask(t) && t.evictAfter !== 0).sort((a, b) => a.startTime - b.startTime); tasks: AppState['tasks'],
): LocalAgentTaskState[] {
return Object.values(tasks)
.filter(
(t): t is LocalAgentTaskState =>
isPanelAgentTask(t) && t.evictAfter !== 0,
)
.sort((a, b) => a.startTime - b.startTime)
} }
export function CoordinatorTaskPanel(): React.ReactNode { export function CoordinatorTaskPanel(): React.ReactNode {
const tasks = useAppState(s => s.tasks); const tasks = useAppState(s => s.tasks)
const viewingAgentTaskId = useAppState(s_0 => s_0.viewingAgentTaskId); const viewingAgentTaskId = useAppState(s => s.viewingAgentTaskId)
const agentNameRegistry = useAppState(s_1 => s_1.agentNameRegistry); const agentNameRegistry = useAppState(s => s.agentNameRegistry)
const coordinatorTaskIndex = useAppState(s_2 => s_2.coordinatorTaskIndex); const coordinatorTaskIndex = useAppState(s => s.coordinatorTaskIndex)
const tasksSelected = useAppState(s_3 => s_3.footerSelection === 'tasks'); const tasksSelected = useAppState(s => s.footerSelection === 'tasks')
const selectedIndex = tasksSelected ? coordinatorTaskIndex : undefined; const selectedIndex = tasksSelected ? coordinatorTaskIndex : undefined
const setAppState = useSetAppState(); const setAppState = useSetAppState()
const visibleTasks = getVisibleAgentTasks(tasks);
const hasTasks = Object.values(tasks).some(isPanelAgentTask); const visibleTasks = getVisibleAgentTasks(tasks)
const hasTasks = Object.values(tasks).some(isPanelAgentTask)
// 1s tick: re-render for elapsed time + evict tasks past their deadline. // 1s tick: re-render for elapsed time + evict tasks past their deadline.
// The eviction deletes from prev.tasks, which makes useCoordinatorTaskCount // The eviction deletes from prev.tasks, which makes useCoordinatorTaskCount
// (and other consumers) see the updated count without their own tick. // (and other consumers) see the updated count without their own tick.
const tasksRef = React.useRef(tasks); const tasksRef = React.useRef(tasks)
tasksRef.current = tasks; tasksRef.current = tasks
const [, setTick] = React.useState(0); const [, setTick] = React.useState(0)
React.useEffect(() => { React.useEffect(() => {
if (!hasTasks) return; if (!hasTasks) return
const interval = setInterval((tasksRef_0, setAppState_0, setTick_0) => { const interval = setInterval(
const now = Date.now(); (tasksRef, setAppState, setTick) => {
for (const t of Object.values(tasksRef_0.current)) { const now = Date.now()
if (isPanelAgentTask(t) && (t.evictAfter ?? Infinity) <= now) { for (const t of Object.values(tasksRef.current)) {
evictTerminalTask(t.id, setAppState_0); if (isPanelAgentTask(t) && (t.evictAfter ?? Infinity) <= now) {
evictTerminalTask(t.id, setAppState)
}
} }
} setTick((prev: number) => prev + 1)
setTick_0((prev: number) => prev + 1); },
}, 1000, tasksRef, setAppState, setTick); 1000,
return () => clearInterval(interval); tasksRef,
}, [hasTasks, setAppState]); setAppState,
setTick,
)
return () => clearInterval(interval)
}, [hasTasks, setAppState])
const nameByAgentId = React.useMemo(() => { const nameByAgentId = React.useMemo(() => {
const inv = new Map<string, string>(); const inv = new Map<string, string>()
for (const [n, id] of agentNameRegistry) inv.set(id, n); for (const [n, id] of agentNameRegistry) inv.set(id, n)
return inv; return inv
}, [agentNameRegistry]); }, [agentNameRegistry])
if (visibleTasks.length === 0) { if (visibleTasks.length === 0) {
return null; return null
} }
return <Box flexDirection="column" marginTop={1}>
<MainLine isSelected={selectedIndex === 0} isViewed={viewingAgentTaskId === undefined} onClick={() => exitTeammateView(setAppState)} /> return (
{visibleTasks.map((task, i) => <AgentLine key={task.id} task={task} name={nameByAgentId.get(task.id)} isSelected={selectedIndex === i + 1} isViewed={viewingAgentTaskId === task.id} onClick={() => enterTeammateView(task.id, setAppState)} />)} <Box flexDirection="column" marginTop={1}>
</Box>; <MainLine
isSelected={selectedIndex === 0}
isViewed={viewingAgentTaskId === undefined}
onClick={() => exitTeammateView(setAppState)}
/>
{visibleTasks.map((task, i) => (
<AgentLine
key={task.id}
task={task}
name={nameByAgentId.get(task.id)}
isSelected={selectedIndex === i + 1}
isViewed={viewingAgentTaskId === task.id}
onClick={() => enterTeammateView(task.id, setAppState)}
/>
))}
</Box>
)
} }
/** /**
@@ -80,193 +121,137 @@ export function CoordinatorTaskPanel(): React.ReactNode {
* The panel's 1s tick evicts expired tasks from prev.tasks, so this count * The panel's 1s tick evicts expired tasks from prev.tasks, so this count
* stays accurate without needing its own tick. * stays accurate without needing its own tick.
*/ */
export function useCoordinatorTaskCount() { export function useCoordinatorTaskCount(): number {
const tasks = useAppState(_temp); const tasks = useAppState(s => s.tasks)
let t0; return React.useMemo(() => {
t0 = 0; if ("external" !== 'ant') return 0
return t0; const count = getVisibleAgentTasks(tasks).length
return count > 0 ? count + 1 : 0
}, [tasks])
} }
function _temp(s) {
return s.tasks; function MainLine({
} isSelected,
function MainLine(t0) { isViewed,
const $ = _c(10); onClick,
const { }: {
isSelected, isSelected?: boolean
isViewed, isViewed?: boolean
onClick onClick: () => void
} = t0; }): React.ReactNode {
const [hover, setHover] = React.useState(false); const [hover, setHover] = React.useState(false)
const prefix = isSelected || hover ? figures.pointer + " " : " "; const prefix = isSelected || hover ? figures.pointer + ' ' : ' '
const bullet = isViewed ? BLACK_CIRCLE : figures.circle; const bullet = isViewed ? BLACK_CIRCLE : figures.circle
let t1; return (
let t2; <Box
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { onClick={onClick}
t1 = () => setHover(true); onMouseEnter={() => setHover(true)}
t2 = () => setHover(false); onMouseLeave={() => setHover(false)}
$[0] = t1; >
$[1] = t2; <Text dimColor={!isSelected && !isViewed && !hover} bold={isViewed}>
} else { {prefix}
t1 = $[0]; {bullet} main
t2 = $[1]; </Text>
} </Box>
const t3 = !isSelected && !isViewed && !hover; )
let t4;
if ($[2] !== bullet || $[3] !== isViewed || $[4] !== prefix || $[5] !== t3) {
t4 = <Text dimColor={t3} bold={isViewed}>{prefix}{bullet} main</Text>;
$[2] = bullet;
$[3] = isViewed;
$[4] = prefix;
$[5] = t3;
$[6] = t4;
} else {
t4 = $[6];
}
let t5;
if ($[7] !== onClick || $[8] !== t4) {
t5 = <Box onClick={onClick} onMouseEnter={t1} onMouseLeave={t2}>{t4}</Box>;
$[7] = onClick;
$[8] = t4;
$[9] = t5;
} else {
t5 = $[9];
}
return t5;
} }
type AgentLineProps = { type AgentLineProps = {
task: LocalAgentTaskState; task: LocalAgentTaskState
name?: string; name?: string
isSelected?: boolean; isSelected?: boolean
isViewed?: boolean; isViewed?: boolean
onClick?: () => void; onClick?: () => void
}; }
function AgentLine(t0) {
const $ = _c(32); function AgentLine({
const { task,
task, name,
name, isSelected,
isSelected, isViewed,
isViewed, onClick,
onClick }: AgentLineProps): React.ReactNode {
} = t0; const { columns } = useTerminalSize()
const { const [hover, setHover] = React.useState(false)
columns const isRunning = !isTerminalStatus(task.status)
} = useTerminalSize(); const pausedMs = task.totalPausedMs ?? 0
const [hover, setHover] = React.useState(false); const elapsedMs = Math.max(
const isRunning = !isTerminalStatus(task.status); 0,
const pausedMs = task.totalPausedMs ?? 0; isRunning
const elapsedMs = Math.max(0, isRunning ? Date.now() - task.startTime - pausedMs : (task.endTime ?? task.startTime) - task.startTime - pausedMs); ? Date.now() - task.startTime - pausedMs
let t1; : (task.endTime ?? task.startTime) - task.startTime - pausedMs,
if ($[0] !== elapsedMs) { )
t1 = formatDuration(elapsedMs);
$[0] = elapsedMs; const elapsed = formatDuration(elapsedMs)
$[1] = t1; const tokenCount = task.progress?.tokenCount
} else {
t1 = $[1]; // Derive direction arrow from activity state, same logic as Spinner
} const lastActivity = task.progress?.lastActivity
const elapsed = t1; const arrow = lastActivity ? figures.arrowDown : figures.arrowUp
const tokenCount = task.progress?.tokenCount;
const lastActivity = task.progress?.lastActivity; const tokenText =
const arrow = lastActivity ? figures.arrowDown : figures.arrowUp; tokenCount !== undefined && tokenCount > 0
let t2; ? ` · ${arrow} ${formatNumber(tokenCount)} tokens`
if ($[2] !== arrow || $[3] !== tokenCount) { : ''
t2 = tokenCount !== undefined && tokenCount > 0 ? ` · ${arrow} ${formatNumber(tokenCount)} tokens` : "";
$[2] = arrow; const queuedCount = task.pendingMessages.length
$[3] = tokenCount; const queuedText = queuedCount > 0 ? ` · ${queuedCount} queued` : ''
$[4] = t2;
} else { // Precedence: AI summary > static description (no tool-call activity noise)
t2 = $[4]; const displayDescription = task.progress?.summary || task.description
}
const tokenText = t2; const highlighted = isSelected || hover
const queuedCount = task.pendingMessages.length; const prefix = highlighted ? figures.pointer + ' ' : ' '
const queuedText = queuedCount > 0 ? ` · ${queuedCount} queued` : ""; const bullet = isViewed ? BLACK_CIRCLE : figures.circle
const displayDescription = task.progress?.summary || task.description; const dim = !highlighted && !isViewed
const highlighted = isSelected || hover;
const prefix = highlighted ? figures.pointer + " " : " "; const sep = isRunning ? PLAY_ICON : PAUSE_ICON
const bullet = isViewed ? BLACK_CIRCLE : figures.circle; // Name is the steering handle — kept out of truncation and undimmed so it
const dim = !highlighted && !isViewed; // stays readable even when the row is inactive. Short by convention (the
const sep = isRunning ? PLAY_ICON : PAUSE_ICON; // Agent tool prompt asks for "one or two words, lowercase").
const namePart = name ? `${name}: ` : ""; const namePart = name ? `${name}: ` : ''
const hintPart = isSelected && !isViewed ? ` · x to ${isRunning ? "stop" : "clear"}` : ""; const hintPart =
const suffixPart = ` ${sep} ${elapsed}${tokenText}${queuedText}${hintPart}`; isSelected && !isViewed ? ` · x to ${isRunning ? 'stop' : 'clear'}` : ''
const availableForDesc = columns - stringWidth(prefix) - stringWidth(`${bullet} `) - stringWidth(namePart) - stringWidth(suffixPart); const suffixPart = ` ${sep} ${elapsed}${tokenText}${queuedText}${hintPart}`
const t3 = Math.max(0, availableForDesc); const availableForDesc =
let t4; columns -
if ($[5] !== displayDescription || $[6] !== t3) { stringWidth(prefix) -
t4 = wrapText(displayDescription, t3, "truncate-end"); stringWidth(`${bullet} `) -
$[5] = displayDescription; stringWidth(namePart) -
$[6] = t3; stringWidth(suffixPart)
$[7] = t4; const truncated = wrapText(
} else { displayDescription,
t4 = $[7]; Math.max(0, availableForDesc),
} 'truncate-end',
const truncated = t4; )
let t5;
if ($[8] !== name) { const line = (
t5 = name && <><Text dimColor={false} bold={true}>{name}</Text>{": "}</>; <Text dimColor={dim} bold={isViewed}>
$[8] = name; {prefix}
$[9] = t5; {bullet}{' '}
} else { {name && (
t5 = $[9]; <>
} <Text dimColor={false} bold>
let t6; {name}
if ($[10] !== queuedCount || $[11] !== queuedText) { </Text>
t6 = queuedCount > 0 && <Text color="warning">{queuedText}</Text>; {': '}
$[10] = queuedCount; </>
$[11] = queuedText; )}
$[12] = t6; {truncated} {sep} {elapsed}
} else { {tokenText}
t6 = $[12]; {queuedCount > 0 && <Text color="warning">{queuedText}</Text>}
} {hintPart && <Text dimColor>{hintPart}</Text>}
let t7; </Text>
if ($[13] !== hintPart) { )
t7 = hintPart && <Text dimColor={true}>{hintPart}</Text>;
$[13] = hintPart; if (!onClick) return line
$[14] = t7; return (
} else { <Box
t7 = $[14]; onClick={onClick}
} onMouseEnter={() => setHover(true)}
let t8; onMouseLeave={() => setHover(false)}
if ($[15] !== bullet || $[16] !== dim || $[17] !== elapsed || $[18] !== isViewed || $[19] !== prefix || $[20] !== sep || $[21] !== t5 || $[22] !== t6 || $[23] !== t7 || $[24] !== tokenText || $[25] !== truncated) { >
t8 = <Text dimColor={dim} bold={isViewed}>{prefix}{bullet}{" "}{t5}{truncated} {sep} {elapsed}{tokenText}{t6}{t7}</Text>; {line}
$[15] = bullet; </Box>
$[16] = dim; )
$[17] = elapsed;
$[18] = isViewed;
$[19] = prefix;
$[20] = sep;
$[21] = t5;
$[22] = t6;
$[23] = t7;
$[24] = tokenText;
$[25] = truncated;
$[26] = t8;
} else {
t8 = $[26];
}
const line = t8;
if (!onClick) {
return line;
}
let t10;
let t9;
if ($[27] === Symbol.for("react.memo_cache_sentinel")) {
t9 = () => setHover(true);
t10 = () => setHover(false);
$[27] = t10;
$[28] = t9;
} else {
t10 = $[27];
t9 = $[28];
}
let t11;
if ($[29] !== line || $[30] !== onClick) {
t11 = <Box onClick={onClick} onMouseEnter={t9} onMouseLeave={t10}>{line}</Box>;
$[29] = line;
$[30] = onClick;
$[31] = t11;
} else {
t11 = $[31];
}
return t11;
} }

View File

@@ -1,49 +1,31 @@
import { c as _c } from "react/compiler-runtime"; import React from 'react'
import React from 'react'; import { Box, Link, Text } from '../ink.js'
import { Box, Link, Text } from '../ink.js'; import { Select } from './CustomSelect/index.js'
import { Select } from './CustomSelect/index.js'; import { Dialog } from './design-system/Dialog.js'
import { Dialog } from './design-system/Dialog.js';
type Props = { type Props = {
onDone: () => void; onDone: () => void
}; }
export function CostThresholdDialog(t0) {
const $ = _c(7); export function CostThresholdDialog({ onDone }: Props): React.ReactNode {
const { return (
onDone <Dialog
} = t0; title="You've spent $5 on the Anthropic API this session."
let t1; onCancel={onDone}
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { >
t1 = <Box flexDirection="column"><Text>Learn more about how to monitor your spending:</Text><Link url="https://code.claude.com/docs/en/costs" /></Box>; <Box flexDirection="column">
$[0] = t1; <Text>Learn more about how to monitor your spending:</Text>
} else { <Link url="https://code.claude.com/docs/en/costs" />
t1 = $[0]; </Box>
} <Select
let t2; options={[
if ($[1] === Symbol.for("react.memo_cache_sentinel")) { {
t2 = [{ value: 'ok',
value: "ok", label: 'Got it, thanks!',
label: "Got it, thanks!" },
}]; ]}
$[1] = t2; onChange={onDone}
} else { />
t2 = $[1]; </Dialog>
} )
let t3;
if ($[2] !== onDone) {
t3 = <Select options={t2} onChange={onDone} />;
$[2] = onDone;
$[3] = t3;
} else {
t3 = $[3];
}
let t4;
if ($[4] !== onDone || $[5] !== t3) {
t4 = <Dialog title="You've spent $5 on the Anthropic API this session." onCancel={onDone}>{t1}{t3}</Dialog>;
$[4] = onDone;
$[5] = t3;
$[6] = t4;
} else {
t4 = $[6];
}
return t4;
} }

View File

@@ -1,50 +1,49 @@
import { c as _c } from "react/compiler-runtime"; import chalk from 'chalk'
import chalk from 'chalk'; import React, { useContext } from 'react'
import React, { useContext } from 'react'; import { Text } from '../ink.js'
import { Text } from '../ink.js'; import { getShortcutDisplay } from '../keybindings/shortcutFormat.js'
import { getShortcutDisplay } from '../keybindings/shortcutFormat.js'; import { useShortcutDisplay } from '../keybindings/useShortcutDisplay.js'
import { useShortcutDisplay } from '../keybindings/useShortcutDisplay.js'; import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js'
import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js'; import { InVirtualListContext } from './messageActions.js'
import { InVirtualListContext } from './messageActions.js';
// Context to track if we're inside a sub agent // Context to track if we're inside a sub agent
// Similar to MessageResponseContext, this helps us avoid showing // Similar to MessageResponseContext, this helps us avoid showing
// too many "(ctrl+o to expand)" hints in sub agent output // too many "(ctrl+o to expand)" hints in sub agent output
const SubAgentContext = React.createContext(false); const SubAgentContext = React.createContext(false)
export function SubAgentProvider(t0) {
const $ = _c(2); export function SubAgentProvider({
const { children,
children }: {
} = t0; children: React.ReactNode
let t1; }): React.ReactNode {
if ($[0] !== children) { return (
t1 = <SubAgentContext.Provider value={true}>{children}</SubAgentContext.Provider>; <SubAgentContext.Provider value={true}>{children}</SubAgentContext.Provider>
$[0] = children; )
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
} }
export function CtrlOToExpand() {
const $ = _c(2); export function CtrlOToExpand(): React.ReactNode {
const isInSubAgent = useContext(SubAgentContext); const isInSubAgent = useContext(SubAgentContext)
const inVirtualList = useContext(InVirtualListContext); const inVirtualList = useContext(InVirtualListContext)
const expandShortcut = useShortcutDisplay("app:toggleTranscript", "Global", "ctrl+o"); const expandShortcut = useShortcutDisplay(
'app:toggleTranscript',
'Global',
'ctrl+o',
)
if (isInSubAgent || inVirtualList) { if (isInSubAgent || inVirtualList) {
return null; return null
} }
let t0; return (
if ($[0] !== expandShortcut) { <Text dimColor>
t0 = <Text dimColor={true}><KeyboardShortcutHint shortcut={expandShortcut} action="expand" parens={true} /></Text>; <KeyboardShortcutHint shortcut={expandShortcut} action="expand" parens />
$[0] = expandShortcut; </Text>
$[1] = t0; )
} else {
t0 = $[1];
}
return t0;
} }
export function ctrlOToExpand(): string { export function ctrlOToExpand(): string {
const shortcut = getShortcutDisplay('app:toggleTranscript', 'Global', 'ctrl+o'); const shortcut = getShortcutDisplay(
return chalk.dim(`(${shortcut} to expand)`); 'app:toggleTranscript',
'Global',
'ctrl+o',
)
return chalk.dim(`(${shortcut} to expand)`)
} }

View File

@@ -1,69 +1,97 @@
import { c as _c } from "react/compiler-runtime"; import figures from 'figures'
import figures from 'figures'; import React from 'react'
import React from 'react'; import { Box, Text } from '../../ink.js'
import { Box, Text } from '../../ink.js'; import type { PastedContent } from '../../utils/config.js'
import type { PastedContent } from '../../utils/config.js'; import type { ImageDimensions } from '../../utils/imageResizer.js'
import type { ImageDimensions } from '../../utils/imageResizer.js'; import type { OptionWithDescription } from './select.js'
import type { OptionWithDescription } from './select.js'; import { SelectInputOption } from './select-input-option.js'
import { SelectInputOption } from './select-input-option.js'; import { SelectOption } from './select-option.js'
import { SelectOption } from './select-option.js'; import { useMultiSelectState } from './use-multi-select-state.js'
import { useMultiSelectState } from './use-multi-select-state.js';
export type SelectMultiProps<T> = { export type SelectMultiProps<T> = {
readonly isDisabled?: boolean; readonly isDisabled?: boolean
readonly visibleOptionCount?: number; readonly visibleOptionCount?: number
readonly options: OptionWithDescription<T>[]; readonly options: OptionWithDescription<T>[]
readonly defaultValue?: T[]; readonly defaultValue?: T[]
readonly onCancel: () => void; readonly onCancel: () => void
readonly onChange?: (values: T[]) => void; readonly onChange?: (values: T[]) => void
readonly onFocus?: (value: T) => void; readonly onFocus?: (value: T) => void
readonly focusValue?: T; readonly focusValue?: T
/** /**
* Text for the submit button. When provided, a submit button is shown and * Text for the submit button. When provided, a submit button is shown and
* Enter toggles selection (submit only fires when the button is focused). * Enter toggles selection (submit only fires when the button is focused).
* When omitted, Enter submits directly and Space toggles selection. * When omitted, Enter submits directly and Space toggles selection.
*/ */
readonly submitButtonText?: string; readonly submitButtonText?: string
/** /**
* Callback when user submits. Receives the currently selected values. * Callback when user submits. Receives the currently selected values.
*/ */
readonly onSubmit?: (values: T[]) => void; readonly onSubmit?: (values: T[]) => void
/** /**
* When true, hides the numeric indexes next to each option. * When true, hides the numeric indexes next to each option.
*/ */
readonly hideIndexes?: boolean; readonly hideIndexes?: boolean
/** /**
* Callback when user presses down from the last item (submit button). * Callback when user presses down from the last item (submit button).
* If provided, navigation will not wrap to the first item. * If provided, navigation will not wrap to the first item.
*/ */
readonly onDownFromLastItem?: () => void; readonly onDownFromLastItem?: () => void
/** /**
* Callback when user presses up from the first item. * Callback when user presses up from the first item.
* If provided, navigation will not wrap to the last item. * If provided, navigation will not wrap to the last item.
*/ */
readonly onUpFromFirstItem?: () => void; readonly onUpFromFirstItem?: () => void
/** /**
* Focus the last option initially instead of the first. * Focus the last option initially instead of the first.
*/ */
readonly initialFocusLast?: boolean; readonly initialFocusLast?: boolean
/** /**
* Callback to open external editor for editing input option values. * Callback to open external editor for editing input option values.
* When provided, ctrl+g will trigger this callback in input options * When provided, ctrl+g will trigger this callback in input options
* with the current value and a setter function to update the internal state. * with the current value and a setter function to update the internal state.
*/ */
readonly onOpenEditor?: (currentValue: string, setValue: (value: string) => void) => void; readonly onOpenEditor?: (
readonly onImagePaste?: (base64Image: string, mediaType?: string, filename?: string, dimensions?: ImageDimensions, sourcePath?: string) => void; currentValue: string,
readonly pastedContents?: Record<number, PastedContent>; setValue: (value: string) => void,
readonly onRemoveImage?: (id: number) => void; ) => void
}; readonly onImagePaste?: (
export function SelectMulti(t0) { base64Image: string,
const $ = _c(44); mediaType?: string,
const { filename?: string,
isDisabled: t1, dimensions?: ImageDimensions,
visibleOptionCount: t2, sourcePath?: string,
) => void
readonly pastedContents?: Record<number, PastedContent>
readonly onRemoveImage?: (id: number) => void
}
export function SelectMulti<T>({
isDisabled = false,
visibleOptionCount = 5,
options,
defaultValue = [],
onCancel,
onChange,
onFocus,
focusValue,
submitButtonText,
onSubmit,
onDownFromLastItem,
onUpFromFirstItem,
initialFocusLast,
onOpenEditor,
hideIndexes = false,
onImagePaste,
pastedContents,
onRemoveImage,
}: SelectMultiProps<T>): React.ReactNode {
const state = useMultiSelectState<T>({
isDisabled,
visibleOptionCount,
options, options,
defaultValue: t3, defaultValue,
onCancel,
onChange, onChange,
onCancel,
onFocus, onFocus,
focusValue, focusValue,
submitButtonText, submitButtonText,
@@ -71,142 +99,111 @@ export function SelectMulti(t0) {
onDownFromLastItem, onDownFromLastItem,
onUpFromFirstItem, onUpFromFirstItem,
initialFocusLast, initialFocusLast,
onOpenEditor, hideIndexes,
hideIndexes: t4, })
onImagePaste,
pastedContents, const maxIndexWidth = options.length.toString().length
onRemoveImage
} = t0; return (
const isDisabled = t1 === undefined ? false : t1; <Box flexDirection="column">
const visibleOptionCount = t2 === undefined ? 5 : t2; <Box flexDirection="column">
let t5; {state.visibleOptions.map((option, index) => {
if ($[0] !== t3) { const isOptionFocused =
t5 = t3 === undefined ? [] : t3; !isDisabled &&
$[0] = t3; state.focusedValue === option.value &&
$[1] = t5; !state.isSubmitFocused
} else { const isSelected = state.selectedValues.includes(option.value)
t5 = $[1];
} const isFirstVisibleOption = option.index === state.visibleFromIndex
const defaultValue = t5; const isLastVisibleOption = option.index === state.visibleToIndex - 1
const hideIndexes = t4 === undefined ? false : t4; const areMoreOptionsBelow = state.visibleToIndex < options.length
let t6; const areMoreOptionsAbove = state.visibleFromIndex > 0
if ($[2] !== defaultValue || $[3] !== focusValue || $[4] !== hideIndexes || $[5] !== initialFocusLast || $[6] !== isDisabled || $[7] !== onCancel || $[8] !== onChange || $[9] !== onDownFromLastItem || $[10] !== onFocus || $[11] !== onSubmit || $[12] !== onUpFromFirstItem || $[13] !== options || $[14] !== submitButtonText || $[15] !== visibleOptionCount) {
t6 = { const i = state.visibleFromIndex + index + 1
isDisabled,
visibleOptionCount, if (option.type === 'input') {
options, const inputValue = state.inputValues.get(option.value) || ''
defaultValue,
onChange, return (
onCancel, <Box key={String(option.value)} gap={1}>
onFocus, <SelectInputOption
focusValue, option={option}
submitButtonText, isFocused={isOptionFocused}
onSubmit, isSelected={
onDownFromLastItem, false /* We show selection state differently for multi-select */
onUpFromFirstItem, }
initialFocusLast, shouldShowDownArrow={
hideIndexes areMoreOptionsBelow && isLastVisibleOption
}; }
$[2] = defaultValue; shouldShowUpArrow={
$[3] = focusValue; areMoreOptionsAbove && isFirstVisibleOption
$[4] = hideIndexes; }
$[5] = initialFocusLast; maxIndexWidth={maxIndexWidth}
$[6] = isDisabled; index={i}
$[7] = onCancel; inputValue={inputValue}
$[8] = onChange; onInputChange={value => {
$[9] = onDownFromLastItem; state.updateInputValue(option.value, value)
$[10] = onFocus; }}
$[11] = onSubmit; onSubmit={() => {}} /* We handle submit higher up */
$[12] = onUpFromFirstItem; onExit={() => {
$[13] = options; onCancel()
$[14] = submitButtonText; }}
$[15] = visibleOptionCount; layout="compact"
$[16] = t6; onOpenEditor={onOpenEditor}
} else { onImagePaste={onImagePaste}
t6 = $[16]; pastedContents={pastedContents}
} onRemoveImage={onRemoveImage}
const state = useMultiSelectState(t6); >
let T0; <Text color={isSelected ? 'success' : undefined}>
let T1; [{isSelected ? figures.tick : ' '}]{' '}
let t7; </Text>
let t8; </SelectInputOption>
let t9; </Box>
if ($[17] !== hideIndexes || $[18] !== isDisabled || $[19] !== onCancel || $[20] !== onImagePaste || $[21] !== onOpenEditor || $[22] !== onRemoveImage || $[23] !== options.length || $[24] !== pastedContents || $[25] !== state) { )
const maxIndexWidth = options.length.toString().length; }
T1 = Box;
t9 = "column"; return (
T0 = Box; <Box key={String(option.value)} gap={1}>
t7 = "column"; <SelectOption
t8 = state.visibleOptions.map((option, index) => { isFocused={isOptionFocused}
const isOptionFocused = !isDisabled && state.focusedValue === option.value && !state.isSubmitFocused; isSelected={
const isSelected = state.selectedValues.includes(option.value); false /* We show selection state differently for multi-select */
const isFirstVisibleOption = option.index === state.visibleFromIndex; }
const isLastVisibleOption = option.index === state.visibleToIndex - 1; shouldShowDownArrow={areMoreOptionsBelow && isLastVisibleOption}
const areMoreOptionsBelow = state.visibleToIndex < options.length; shouldShowUpArrow={areMoreOptionsAbove && isFirstVisibleOption}
const areMoreOptionsAbove = state.visibleFromIndex > 0; description={option.description}
const i = state.visibleFromIndex + index + 1; >
if (option.type === "input") { {!hideIndexes && (
const inputValue = state.inputValues.get(option.value) || ""; <Text dimColor>{`${i}.`.padEnd(maxIndexWidth)}</Text>
return <Box key={String(option.value)} gap={1}><SelectInputOption option={option} isFocused={isOptionFocused} isSelected={false} shouldShowDownArrow={areMoreOptionsBelow && isLastVisibleOption} shouldShowUpArrow={areMoreOptionsAbove && isFirstVisibleOption} maxIndexWidth={maxIndexWidth} index={i} inputValue={inputValue} onInputChange={value => { )}
state.updateInputValue(option.value, value); <Text color={isSelected ? 'success' : undefined}>
}} onSubmit={_temp} onExit={() => { [{isSelected ? figures.tick : ' '}]
onCancel(); </Text>
}} layout="compact" onOpenEditor={onOpenEditor} onImagePaste={onImagePaste} pastedContents={pastedContents} onRemoveImage={onRemoveImage}><Text color={isSelected ? "success" : undefined}>[{isSelected ? figures.tick : " "}]{" "}</Text></SelectInputOption></Box>; <Text color={isOptionFocused ? 'suggestion' : undefined}>
} {option.label}
return <Box key={String(option.value)} gap={1}><SelectOption isFocused={isOptionFocused} isSelected={false} shouldShowDownArrow={areMoreOptionsBelow && isLastVisibleOption} shouldShowUpArrow={areMoreOptionsAbove && isFirstVisibleOption} description={option.description}>{!hideIndexes && <Text dimColor={true}>{`${i}.`.padEnd(maxIndexWidth)}</Text>}<Text color={isSelected ? "success" : undefined}>[{isSelected ? figures.tick : " "}]</Text><Text color={isOptionFocused ? "suggestion" : undefined}>{option.label}</Text></SelectOption></Box>; </Text>
}); </SelectOption>
$[17] = hideIndexes; </Box>
$[18] = isDisabled; )
$[19] = onCancel; })}
$[20] = onImagePaste; </Box>
$[21] = onOpenEditor; {submitButtonText && onSubmit && (
$[22] = onRemoveImage; <Box marginTop={0} gap={1}>
$[23] = options.length; {state.isSubmitFocused ? (
$[24] = pastedContents; <Text color="suggestion">{figures.pointer}</Text>
$[25] = state; ) : (
$[26] = T0; <Text> </Text>
$[27] = T1; )}
$[28] = t7; <Box marginLeft={3}>
$[29] = t8; <Text
$[30] = t9; color={state.isSubmitFocused ? 'suggestion' : undefined}
} else { bold={true}
T0 = $[26]; >
T1 = $[27]; {submitButtonText}
t7 = $[28]; </Text>
t8 = $[29]; </Box>
t9 = $[30]; </Box>
} )}
let t10; </Box>
if ($[31] !== T0 || $[32] !== t7 || $[33] !== t8) { )
t10 = <T0 flexDirection={t7}>{t8}</T0>;
$[31] = T0;
$[32] = t7;
$[33] = t8;
$[34] = t10;
} else {
t10 = $[34];
}
let t11;
if ($[35] !== onSubmit || $[36] !== state.isSubmitFocused || $[37] !== submitButtonText) {
t11 = submitButtonText && onSubmit && <Box marginTop={0} gap={1}>{state.isSubmitFocused ? <Text color="suggestion">{figures.pointer}</Text> : <Text> </Text>}<Box marginLeft={3}><Text color={state.isSubmitFocused ? "suggestion" : undefined} bold={true}>{submitButtonText}</Text></Box></Box>;
$[35] = onSubmit;
$[36] = state.isSubmitFocused;
$[37] = submitButtonText;
$[38] = t11;
} else {
t11 = $[38];
}
let t12;
if ($[39] !== T1 || $[40] !== t10 || $[41] !== t11 || $[42] !== t9) {
t12 = <T1 flexDirection={t9}>{t10}{t11}</T1>;
$[39] = T1;
$[40] = t10;
$[41] = t11;
$[42] = t9;
$[43] = t12;
} else {
t12 = $[43];
}
return t12;
} }
function _temp() {}

View File

@@ -1,487 +1,412 @@
import { c as _c } from "react/compiler-runtime"; import React, { type ReactNode, useEffect, useRef, useState } from 'react'
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 // 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 '../../ink.js'
import { useKeybinding, useKeybindings } from '../../keybindings/useKeybinding.js'; import {
import type { PastedContent } from '../../utils/config.js'; useKeybinding,
import { getImageFromClipboard } from '../../utils/imagePaste.js'; useKeybindings,
import type { ImageDimensions } from '../../utils/imageResizer.js'; } from '../../keybindings/useKeybinding.js'
import { ClickableImageRef } from '../ClickableImageRef.js'; import type { PastedContent } from '../../utils/config.js'
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'; import { getImageFromClipboard } from '../../utils/imagePaste.js'
import { Byline } from '../design-system/Byline.js'; import type { ImageDimensions } from '../../utils/imageResizer.js'
import TextInput from '../TextInput.js'; import { ClickableImageRef } from '../ClickableImageRef.js'
import type { OptionWithDescription } from './select.js'; import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
import { SelectOption } from './select-option.js'; import { Byline } from '../design-system/Byline.js'
import TextInput from '../TextInput.js'
import type { OptionWithDescription } from './select.js'
import { SelectOption } from './select-option.js'
type Props<T> = { type Props<T> = {
option: Extract<OptionWithDescription<T>, { option: Extract<OptionWithDescription<T>, { type: 'input' }>
type: 'input'; isFocused: boolean
}>; isSelected: boolean
isFocused: boolean; shouldShowDownArrow: boolean
isSelected: boolean; shouldShowUpArrow: boolean
shouldShowDownArrow: boolean; maxIndexWidth: number
shouldShowUpArrow: boolean; index: number
maxIndexWidth: number; inputValue: string
index: number; onInputChange: (value: string) => void
inputValue: string; onSubmit: (value: string) => void
onInputChange: (value: string) => void; onExit?: () => void
onSubmit: (value: string) => void; layout: 'compact' | 'expanded'
onExit?: () => void; children?: ReactNode
layout: 'compact' | 'expanded';
children?: ReactNode;
/** /**
* When true, shows the label before the input field. * When true, shows the label before the input field.
* When false (default), uses the label as the placeholder. * When false (default), uses the label as the placeholder.
*/ */
showLabel?: boolean; showLabel?: boolean
/** /**
* Callback to open external editor for editing the input value. * Callback to open external editor for editing the input value.
* When provided, ctrl+g will trigger this callback with the current value * When provided, ctrl+g will trigger this callback with the current value
* and a setter function to update the internal state. * and a setter function to update the internal state.
*/ */
onOpenEditor?: (currentValue: string, setValue: (value: string) => void) => void; onOpenEditor?: (
currentValue: string,
setValue: (value: string) => void,
) => void
/** /**
* When true, automatically reset cursor to end of line when: * When true, automatically reset cursor to end of line when:
* - Option becomes focused * - Option becomes focused
* - Input value changes * - Input value changes
* This prevents cursor position bugs when the input value updates asynchronously. * This prevents cursor position bugs when the input value updates asynchronously.
*/ */
resetCursorOnUpdate?: boolean; resetCursorOnUpdate?: boolean
/** /**
* Optional callback when an image is pasted into the input. * Optional callback when an image is pasted into the input.
*/ */
onImagePaste?: (base64Image: string, mediaType?: string, filename?: string, dimensions?: ImageDimensions, sourcePath?: string) => void; onImagePaste?: (
base64Image: string,
mediaType?: string,
filename?: string,
dimensions?: ImageDimensions,
sourcePath?: string,
) => void
/** /**
* Pasted content to display inline above the input when focused. * Pasted content to display inline above the input when focused.
*/ */
pastedContents?: Record<number, PastedContent>; pastedContents?: Record<number, PastedContent>
/** /**
* Callback to remove a pasted image by its ID. * Callback to remove a pasted image by its ID.
*/ */
onRemoveImage?: (id: number) => void; onRemoveImage?: (id: number) => void
/** /**
* Whether image selection mode is active. * Whether image selection mode is active.
*/ */
imagesSelected?: boolean; imagesSelected?: boolean
/** /**
* Currently selected image index within the image attachments array. * Currently selected image index within the image attachments array.
*/ */
selectedImageIndex?: number; selectedImageIndex?: number
/** /**
* Callback to set image selection mode on/off. * Callback to set image selection mode on/off.
*/ */
onImagesSelectedChange?: (selected: boolean) => void; onImagesSelectedChange?: (selected: boolean) => void
/** /**
* Callback to change the selected image index. * Callback to change the selected image index.
*/ */
onSelectedImageIndexChange?: (index: number) => void; onSelectedImageIndexChange?: (index: number) => void
}; }
export function SelectInputOption(t0) {
const $ = _c(100); export function SelectInputOption<T>({
const { option,
option, isFocused,
isFocused, isSelected,
isSelected, shouldShowDownArrow,
shouldShowDownArrow, shouldShowUpArrow,
shouldShowUpArrow, maxIndexWidth,
maxIndexWidth, index,
index, inputValue,
inputValue, onInputChange,
onInputChange, onSubmit,
onSubmit, onExit,
onExit, layout,
layout, children,
children, showLabel: showLabelProp = false,
showLabel: t1, onOpenEditor,
onOpenEditor, resetCursorOnUpdate = false,
resetCursorOnUpdate: t2, onImagePaste,
onImagePaste, pastedContents,
pastedContents, onRemoveImage,
onRemoveImage, imagesSelected,
imagesSelected, selectedImageIndex = 0,
selectedImageIndex: t3, onImagesSelectedChange,
onImagesSelectedChange, onSelectedImageIndexChange,
onSelectedImageIndexChange }: Props<T>): React.ReactNode {
} = t0; const imageAttachments = pastedContents
const showLabelProp = t1 === undefined ? false : t1; ? Object.values(pastedContents).filter(c => c.type === 'image')
const resetCursorOnUpdate = t2 === undefined ? false : t2; : []
const selectedImageIndex = t3 === undefined ? 0 : t3;
let t4; // Allow individual options to force showing the label via showLabelWithValue
if ($[0] !== pastedContents) { const showLabel = showLabelProp || option.showLabelWithValue === true
t4 = pastedContents ? Object.values(pastedContents).filter(_temp) : []; const [cursorOffset, setCursorOffset] = useState(inputValue.length)
$[0] = pastedContents;
$[1] = t4; // Track whether the latest inputValue change was from user typing/pasting,
} else { // so we can skip resetting cursor to end on user-initiated changes.
t4 = $[1]; const isUserEditing = useRef(false)
}
const imageAttachments = t4; // Reset cursor to end of line when:
const showLabel = showLabelProp || option.showLabelWithValue === true; // 1. Option becomes focused (user navigates to it)
const [cursorOffset, setCursorOffset] = useState(inputValue.length); // 2. Input value changes externally (e.g., async classifier description updates)
const isUserEditing = useRef(false); // Skip reset when the change was from user typing (which sets isUserEditing ref)
let t5; // Only enabled when resetCursorOnUpdate prop is true
if ($[2] !== inputValue.length || $[3] !== isFocused || $[4] !== resetCursorOnUpdate) { useEffect(() => {
t5 = () => { if (resetCursorOnUpdate && isFocused) {
if (resetCursorOnUpdate && isFocused) { if (isUserEditing.current) {
if (isUserEditing.current) { isUserEditing.current = false
isUserEditing.current = false; } else {
} else { setCursorOffset(inputValue.length)
setCursorOffset(inputValue.length);
}
} }
}; }
$[2] = inputValue.length; }, [resetCursorOnUpdate, isFocused, inputValue])
$[3] = isFocused;
$[4] = resetCursorOnUpdate; // ctrl+g to open external editor (reuses chat:externalEditor keybinding)
$[5] = t5; useKeybinding(
} else { 'chat:externalEditor',
t5 = $[5]; () => {
} onOpenEditor?.(inputValue, onInputChange)
let t6; },
if ($[6] !== inputValue || $[7] !== isFocused || $[8] !== resetCursorOnUpdate) { { context: 'Chat', isActive: isFocused && !!onOpenEditor },
t6 = [resetCursorOnUpdate, isFocused, inputValue]; )
$[6] = inputValue;
$[7] = isFocused; // ctrl+v to paste image from clipboard (same as PromptInput)
$[8] = resetCursorOnUpdate; useKeybinding(
$[9] = t6; 'chat:imagePaste',
} else { () => {
t6 = $[9]; if (!onImagePaste) return
} void getImageFromClipboard().then(imageData => {
useEffect(t5, t6);
let t7;
if ($[10] !== inputValue || $[11] !== onInputChange || $[12] !== onOpenEditor) {
t7 = () => {
onOpenEditor?.(inputValue, onInputChange);
};
$[10] = inputValue;
$[11] = onInputChange;
$[12] = onOpenEditor;
$[13] = t7;
} else {
t7 = $[13];
}
const t8 = isFocused && !!onOpenEditor;
let t9;
if ($[14] !== t8) {
t9 = {
context: "Chat",
isActive: t8
};
$[14] = t8;
$[15] = t9;
} else {
t9 = $[15];
}
useKeybinding("chat:externalEditor", t7, t9);
let t10;
if ($[16] !== onImagePaste) {
t10 = () => {
if (!onImagePaste) {
return;
}
getImageFromClipboard().then(imageData => {
if (imageData) { if (imageData) {
onImagePaste(imageData.base64, imageData.mediaType, undefined, imageData.dimensions); onImagePaste(
imageData.base64,
imageData.mediaType,
undefined,
imageData.dimensions,
)
} }
}); })
}; },
$[16] = onImagePaste; { context: 'Chat', isActive: isFocused && !!onImagePaste },
$[17] = t10; )
} else {
t10 = $[17]; // Backspace with empty input removes the last pasted image (non-image-selection mode)
} useKeybinding(
const t11 = isFocused && !!onImagePaste; 'attachments:remove',
let t12; () => {
if ($[18] !== t11) {
t12 = {
context: "Chat",
isActive: t11
};
$[18] = t11;
$[19] = t12;
} else {
t12 = $[19];
}
useKeybinding("chat:imagePaste", t10, t12);
let t13;
if ($[20] !== imageAttachments || $[21] !== onRemoveImage) {
t13 = () => {
if (imageAttachments.length > 0 && onRemoveImage) { if (imageAttachments.length > 0 && onRemoveImage) {
onRemoveImage(imageAttachments.at(-1).id); onRemoveImage(imageAttachments.at(-1)!.id)
} }
}; },
$[20] = imageAttachments; {
$[21] = onRemoveImage; context: 'Attachments',
$[22] = t13; isActive:
} else { isFocused &&
t13 = $[22]; !imagesSelected &&
} inputValue === '' &&
const t14 = isFocused && !imagesSelected && inputValue === "" && imageAttachments.length > 0 && !!onRemoveImage; imageAttachments.length > 0 &&
let t15; !!onRemoveImage,
if ($[23] !== t14) { },
t15 = { )
context: "Attachments",
isActive: t14 // Image selection mode keybindings — reuses existing Attachments actions
}; useKeybindings(
$[23] = t14; {
$[24] = t15; 'attachments:next': () => {
} else { if (imageAttachments.length > 1) {
t15 = $[24]; onSelectedImageIndexChange?.(
} (selectedImageIndex + 1) % imageAttachments.length,
useKeybinding("attachments:remove", t13, t15); )
let t16;
let t17;
if ($[25] !== imageAttachments.length || $[26] !== onSelectedImageIndexChange || $[27] !== selectedImageIndex) {
t16 = () => {
if (imageAttachments.length > 1) {
onSelectedImageIndexChange?.((selectedImageIndex + 1) % imageAttachments.length);
}
};
t17 = () => {
if (imageAttachments.length > 1) {
onSelectedImageIndexChange?.((selectedImageIndex - 1 + imageAttachments.length) % imageAttachments.length);
}
};
$[25] = imageAttachments.length;
$[26] = onSelectedImageIndexChange;
$[27] = selectedImageIndex;
$[28] = t16;
$[29] = t17;
} else {
t16 = $[28];
t17 = $[29];
}
let t18;
if ($[30] !== imageAttachments || $[31] !== onImagesSelectedChange || $[32] !== onRemoveImage || $[33] !== onSelectedImageIndexChange || $[34] !== selectedImageIndex) {
t18 = () => {
const img = imageAttachments[selectedImageIndex];
if (img && onRemoveImage) {
onRemoveImage(img.id);
if (imageAttachments.length <= 1) {
onImagesSelectedChange?.(false);
} else {
onSelectedImageIndexChange?.(Math.min(selectedImageIndex, imageAttachments.length - 2));
} }
} },
}; 'attachments:previous': () => {
$[30] = imageAttachments; if (imageAttachments.length > 1) {
$[31] = onImagesSelectedChange; onSelectedImageIndexChange?.(
$[32] = onRemoveImage; (selectedImageIndex - 1 + imageAttachments.length) %
$[33] = onSelectedImageIndexChange; imageAttachments.length,
$[34] = selectedImageIndex; )
$[35] = t18; }
} else { },
t18 = $[35]; 'attachments:remove': () => {
} const img = imageAttachments[selectedImageIndex]
let t19; if (img && onRemoveImage) {
if ($[36] !== onImagesSelectedChange) { onRemoveImage(img.id)
t19 = () => { // If no images left after removal, exit image selection
onImagesSelectedChange?.(false); if (imageAttachments.length <= 1) {
}; onImagesSelectedChange?.(false)
$[36] = onImagesSelectedChange; } else {
$[37] = t19; // Adjust index if we deleted the last image
} else { onSelectedImageIndexChange?.(
t19 = $[37]; Math.min(selectedImageIndex, imageAttachments.length - 2),
} )
let t20; }
if ($[38] !== t16 || $[39] !== t17 || $[40] !== t18 || $[41] !== t19) { }
t20 = { },
"attachments:next": t16, 'attachments:exit': () => {
"attachments:previous": t17, onImagesSelectedChange?.(false)
"attachments:remove": t18, },
"attachments:exit": t19 },
}; { context: 'Attachments', isActive: isFocused && !!imagesSelected },
$[38] = t16; )
$[39] = t17;
$[40] = t18; // UP arrow exits image selection mode (UP isn't bound to attachments:exit)
$[41] = t19; useInput(
$[42] = t20; (_input, key) => {
} else {
t20 = $[42];
}
const t21 = isFocused && !!imagesSelected;
let t22;
if ($[43] !== t21) {
t22 = {
context: "Attachments",
isActive: t21
};
$[43] = t21;
$[44] = t22;
} else {
t22 = $[44];
}
useKeybindings(t20, t22);
let t23;
if ($[45] !== onImagesSelectedChange) {
t23 = (_input, key) => {
if (key.upArrow) { if (key.upArrow) {
onImagesSelectedChange?.(false); onImagesSelectedChange?.(false)
} }
}; },
$[45] = onImagesSelectedChange; { isActive: isFocused && !!imagesSelected },
$[46] = t23; )
} else {
t23 = $[46]; // Exit image mode when option loses focus
} useEffect(() => {
const t24 = isFocused && !!imagesSelected; if (!isFocused && imagesSelected) {
let t25; onImagesSelectedChange?.(false)
if ($[47] !== t24) { }
t25 = { }, [isFocused, imagesSelected, onImagesSelectedChange])
isActive: t24
}; const descriptionPaddingLeft =
$[47] = t24; layout === 'expanded' ? maxIndexWidth + 3 : maxIndexWidth + 4
$[48] = t25;
} else { return (
t25 = $[48]; <Box flexDirection="column" flexShrink={0}>
} <SelectOption
useInput(t23, t25); isFocused={isFocused}
let t26; isSelected={isSelected}
let t27; shouldShowDownArrow={shouldShowDownArrow}
if ($[49] !== imagesSelected || $[50] !== isFocused || $[51] !== onImagesSelectedChange) { shouldShowUpArrow={shouldShowUpArrow}
t26 = () => { declareCursor={false}
if (!isFocused && imagesSelected) { >
onImagesSelectedChange?.(false); <Box
} flexDirection="row"
}; flexShrink={layout === 'compact' ? 0 : undefined}
t27 = [isFocused, imagesSelected, onImagesSelectedChange]; >
$[49] = imagesSelected; <Text dimColor>{`${index}.`.padEnd(maxIndexWidth + 2)}</Text>
$[50] = isFocused; {children}
$[51] = onImagesSelectedChange; {showLabel ? (
$[52] = t26; <>
$[53] = t27; <Text color={isFocused ? 'suggestion' : undefined}>
} else { {option.label}
t26 = $[52]; </Text>
t27 = $[53]; {isFocused ? (
} <>
useEffect(t26, t27); <Text color="suggestion">
const descriptionPaddingLeft = layout === "expanded" ? maxIndexWidth + 3 : maxIndexWidth + 4; {option.labelValueSeparator ?? ', '}
const t28 = layout === "compact" ? 0 : undefined; </Text>
const t29 = `${index}.`; <TextInput
let t30; value={inputValue}
if ($[54] !== maxIndexWidth || $[55] !== t29) { onChange={value => {
t30 = t29.padEnd(maxIndexWidth + 2); isUserEditing.current = true
$[54] = maxIndexWidth; onInputChange(value)
$[55] = t29; option.onChange(value)
$[56] = t30; }}
} else { onSubmit={onSubmit}
t30 = $[56]; onExit={onExit}
} placeholder={option.placeholder}
let t31; focus={!imagesSelected}
if ($[57] !== t30) { showCursor={true}
t31 = <Text dimColor={true}>{t30}</Text>; multiline={true}
$[57] = t30; cursorOffset={cursorOffset}
$[58] = t31; onChangeCursorOffset={setCursorOffset}
} else { columns={80}
t31 = $[58]; onImagePaste={onImagePaste}
} onPaste={(pastedText: string) => {
let t32; isUserEditing.current = true
if ($[59] !== cursorOffset || $[60] !== imagesSelected || $[61] !== inputValue || $[62] !== isFocused || $[63] !== onExit || $[64] !== onImagePaste || $[65] !== onInputChange || $[66] !== onSubmit || $[67] !== option || $[68] !== showLabel) { const before = inputValue.slice(0, cursorOffset)
t32 = showLabel ? <><Text color={isFocused ? "suggestion" : undefined}>{option.label}</Text>{isFocused ? <><Text color="suggestion">{option.labelValueSeparator ?? ", "}</Text><TextInput value={inputValue} onChange={value => { const after = inputValue.slice(cursorOffset)
isUserEditing.current = true; const newValue = before + pastedText + after
onInputChange(value); onInputChange(newValue)
option.onChange(value); option.onChange(newValue)
}} onSubmit={onSubmit} onExit={onExit} placeholder={option.placeholder} focus={!imagesSelected} showCursor={true} multiline={true} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} columns={80} onImagePaste={onImagePaste} onPaste={pastedText => { setCursorOffset(before.length + pastedText.length)
isUserEditing.current = true; }}
const before = inputValue.slice(0, cursorOffset); />
const after = inputValue.slice(cursorOffset); </>
const newValue = before + pastedText + after; ) : (
onInputChange(newValue); inputValue && (
option.onChange(newValue); <Text>
setCursorOffset(before.length + pastedText.length); {option.labelValueSeparator ?? ', '}
}} /></> : inputValue && <Text>{option.labelValueSeparator ?? ", "}{inputValue}</Text>}</> : isFocused ? <TextInput value={inputValue} onChange={value_0 => { {inputValue}
isUserEditing.current = true; </Text>
onInputChange(value_0); )
option.onChange(value_0); )}
}} onSubmit={onSubmit} onExit={onExit} placeholder={option.placeholder || (typeof option.label === "string" ? option.label : undefined)} focus={!imagesSelected} showCursor={true} multiline={true} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} columns={80} onImagePaste={onImagePaste} onPaste={pastedText_0 => { </>
isUserEditing.current = true; ) : isFocused ? (
const before_0 = inputValue.slice(0, cursorOffset); <TextInput
const after_0 = inputValue.slice(cursorOffset); value={inputValue}
const newValue_0 = before_0 + pastedText_0 + after_0; onChange={value => {
onInputChange(newValue_0); isUserEditing.current = true
option.onChange(newValue_0); onInputChange(value)
setCursorOffset(before_0.length + pastedText_0.length); option.onChange(value)
}} /> : <Text color={inputValue ? undefined : "inactive"}>{inputValue || option.placeholder || option.label}</Text>; }}
$[59] = cursorOffset; onSubmit={onSubmit}
$[60] = imagesSelected; onExit={onExit}
$[61] = inputValue; placeholder={
$[62] = isFocused; option.placeholder ||
$[63] = onExit; (typeof option.label === 'string' ? option.label : undefined)
$[64] = onImagePaste; }
$[65] = onInputChange; focus={!imagesSelected}
$[66] = onSubmit; showCursor={true}
$[67] = option; multiline={true}
$[68] = showLabel; cursorOffset={cursorOffset}
$[69] = t32; onChangeCursorOffset={setCursorOffset}
} else { columns={80}
t32 = $[69]; onImagePaste={onImagePaste}
} onPaste={(pastedText: string) => {
let t33; isUserEditing.current = true
if ($[70] !== children || $[71] !== t28 || $[72] !== t31 || $[73] !== t32) { const before = inputValue.slice(0, cursorOffset)
t33 = <Box flexDirection="row" flexShrink={t28}>{t31}{children}{t32}</Box>; const after = inputValue.slice(cursorOffset)
$[70] = children; const newValue = before + pastedText + after
$[71] = t28; onInputChange(newValue)
$[72] = t31; option.onChange(newValue)
$[73] = t32; setCursorOffset(before.length + pastedText.length)
$[74] = t33; }}
} else { />
t33 = $[74]; ) : (
} <Text color={inputValue ? undefined : 'inactive'}>
let t34; {inputValue || option.placeholder || option.label}
if ($[75] !== isFocused || $[76] !== isSelected || $[77] !== shouldShowDownArrow || $[78] !== shouldShowUpArrow || $[79] !== t33) { </Text>
t34 = <SelectOption isFocused={isFocused} isSelected={isSelected} shouldShowDownArrow={shouldShowDownArrow} shouldShowUpArrow={shouldShowUpArrow} declareCursor={false}>{t33}</SelectOption>; )}
$[75] = isFocused; </Box>
$[76] = isSelected; </SelectOption>
$[77] = shouldShowDownArrow; {option.description && (
$[78] = shouldShowUpArrow; <Box paddingLeft={descriptionPaddingLeft}>
$[79] = t33; <Text
$[80] = t34; dimColor={option.dimDescription !== false}
} else { color={
t34 = $[80]; isSelected ? 'success' : isFocused ? 'suggestion' : undefined
} }
let t35; >
if ($[81] !== descriptionPaddingLeft || $[82] !== isFocused || $[83] !== isSelected || $[84] !== option.description || $[85] !== option.dimDescription) { {option.description}
t35 = option.description && <Box paddingLeft={descriptionPaddingLeft}><Text dimColor={option.dimDescription !== false} color={isSelected ? "success" : isFocused ? "suggestion" : undefined}>{option.description}</Text></Box>; </Text>
$[81] = descriptionPaddingLeft; </Box>
$[82] = isFocused; )}
$[83] = isSelected; {imageAttachments.length > 0 && (
$[84] = option.description; <Box flexDirection="row" gap={1} paddingLeft={descriptionPaddingLeft}>
$[85] = option.dimDescription; {imageAttachments.map((img, idx) => (
$[86] = t35; <ClickableImageRef
} else { key={img.id}
t35 = $[86]; imageId={img.id}
} isSelected={!!imagesSelected && idx === selectedImageIndex}
let t36; />
if ($[87] !== descriptionPaddingLeft || $[88] !== imageAttachments || $[89] !== imagesSelected || $[90] !== isFocused || $[91] !== selectedImageIndex) { ))}
t36 = imageAttachments.length > 0 && <Box flexDirection="row" gap={1} paddingLeft={descriptionPaddingLeft}>{imageAttachments.map((img_0, idx) => <ClickableImageRef key={img_0.id} imageId={img_0.id} isSelected={!!imagesSelected && idx === selectedImageIndex} />)}<Box flexGrow={1} justifyContent="flex-start" flexDirection="row"><Text dimColor={true}>{imagesSelected ? <Byline>{imageAttachments.length > 1 && <><ConfigurableShortcutHint action="attachments:next" context="Attachments" fallback={"\u2192"} description="next" /><ConfigurableShortcutHint action="attachments:previous" context="Attachments" fallback={"\u2190"} description="prev" /></>}<ConfigurableShortcutHint action="attachments:remove" context="Attachments" fallback="backspace" description="remove" /><ConfigurableShortcutHint action="attachments:exit" context="Attachments" fallback="esc" description="cancel" /></Byline> : isFocused ? "(\u2193 to select)" : null}</Text></Box></Box>; <Box flexGrow={1} justifyContent="flex-start" flexDirection="row">
$[87] = descriptionPaddingLeft; <Text dimColor>
$[88] = imageAttachments; {imagesSelected ? (
$[89] = imagesSelected; <Byline>
$[90] = isFocused; {imageAttachments.length > 1 && (
$[91] = selectedImageIndex; <>
$[92] = t36; <ConfigurableShortcutHint
} else { action="attachments:next"
t36 = $[92]; context="Attachments"
} fallback="→"
let t37; description="next"
if ($[93] !== layout) { />
t37 = layout === "expanded" && <Text> </Text>; <ConfigurableShortcutHint
$[93] = layout; action="attachments:previous"
$[94] = t37; context="Attachments"
} else { fallback="←"
t37 = $[94]; description="prev"
} />
let t38; </>
if ($[95] !== t34 || $[96] !== t35 || $[97] !== t36 || $[98] !== t37) { )}
t38 = <Box flexDirection="column" flexShrink={0}>{t34}{t35}{t36}{t37}</Box>; <ConfigurableShortcutHint
$[95] = t34; action="attachments:remove"
$[96] = t35; context="Attachments"
$[97] = t36; fallback="backspace"
$[98] = t37; description="remove"
$[99] = t38; />
} else { <ConfigurableShortcutHint
t38 = $[99]; action="attachments:exit"
} context="Attachments"
return t38; fallback="esc"
} description="cancel"
function _temp(c) { />
return c.type === "image"; </Byline>
) : isFocused ? (
'(↓ to select)'
) : null}
</Text>
</Box>
</Box>
)}
{layout === 'expanded' && <Text> </Text>}
</Box>
)
} }

View File

@@ -1,67 +1,64 @@
import { c as _c } from "react/compiler-runtime"; import React, { type ReactNode } from 'react'
import React, { type ReactNode } from 'react'; import { ListItem } from '../design-system/ListItem.js'
import { ListItem } from '../design-system/ListItem.js';
export type SelectOptionProps = { export type SelectOptionProps = {
/** /**
* Determines if option is focused. * Determines if option is focused.
*/ */
readonly isFocused: boolean; readonly isFocused: boolean
/** /**
* Determines if option is selected. * Determines if option is selected.
*/ */
readonly isSelected: boolean; readonly isSelected: boolean
/** /**
* Option label. * Option label.
*/ */
readonly children: ReactNode; readonly children: ReactNode
/** /**
* Optional description to display below the label. * Optional description to display below the label.
*/ */
readonly description?: string; readonly description?: string
/** /**
* Determines if the down arrow should be shown. * Determines if the down arrow should be shown.
*/ */
readonly shouldShowDownArrow?: boolean; readonly shouldShowDownArrow?: boolean
/** /**
* Determines if the up arrow should be shown. * Determines if the up arrow should be shown.
*/ */
readonly shouldShowUpArrow?: boolean; readonly shouldShowUpArrow?: boolean
/** /**
* Whether ListItem should declare the terminal cursor position. * Whether ListItem should declare the terminal cursor position.
* Set false when a child declares its own cursor (e.g. BaseTextInput). * Set false when a child declares its own cursor (e.g. BaseTextInput).
*/ */
readonly declareCursor?: boolean; readonly declareCursor?: boolean
}; }
export function SelectOption(t0) {
const $ = _c(8); export function SelectOption({
const { isFocused,
isFocused, isSelected,
isSelected, children,
children, description,
description, shouldShowDownArrow,
shouldShowDownArrow, shouldShowUpArrow,
shouldShowUpArrow, declareCursor,
declareCursor }: SelectOptionProps): React.ReactNode {
} = t0; return (
let t1; <ListItem
if ($[0] !== children || $[1] !== declareCursor || $[2] !== description || $[3] !== isFocused || $[4] !== isSelected || $[5] !== shouldShowDownArrow || $[6] !== shouldShowUpArrow) { isFocused={isFocused}
t1 = <ListItem isFocused={isFocused} isSelected={isSelected} description={description} showScrollDown={shouldShowDownArrow} showScrollUp={shouldShowUpArrow} styled={false} declareCursor={declareCursor}>{children}</ListItem>; isSelected={isSelected}
$[0] = children; description={description}
$[1] = declareCursor; showScrollDown={shouldShowDownArrow}
$[2] = description; showScrollUp={shouldShowUpArrow}
$[3] = isFocused; styled={false}
$[4] = isSelected; declareCursor={declareCursor}
$[5] = shouldShowDownArrow; >
$[6] = shouldShowUpArrow; {children}
$[7] = t1; </ListItem>
} else { )
t1 = $[7];
}
return t1;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,192 +1,151 @@
import { c as _c } from "react/compiler-runtime"; import React, { useEffect, useState } from 'react'
import React, { useEffect, useState } from 'react'; import type { CommandResultDisplay } from '../commands.js'
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 // 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 } from '../ink.js'
import { openBrowser } from '../utils/browser.js'; import { openBrowser } from '../utils/browser.js'
import { getDesktopInstallStatus, openCurrentSessionInDesktop } from '../utils/desktopDeepLink.js'; import {
import { errorMessage } from '../utils/errors.js'; getDesktopInstallStatus,
import { gracefulShutdown } from '../utils/gracefulShutdown.js'; openCurrentSessionInDesktop,
import { flushSessionStorage } from '../utils/sessionStorage.js'; } from '../utils/desktopDeepLink.js'
import { LoadingState } from './design-system/LoadingState.js'; import { errorMessage } from '../utils/errors.js'
const DESKTOP_DOCS_URL = 'https://clau.de/desktop'; 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'
export function getDownloadUrl(): string { export function getDownloadUrl(): string {
switch (process.platform) { switch (process.platform) {
case 'win32': case 'win32':
return 'https://claude.ai/api/desktop/win32/x64/exe/latest/redirect'; return 'https://claude.ai/api/desktop/win32/x64/exe/latest/redirect'
default: default:
return 'https://claude.ai/api/desktop/darwin/universal/dmg/latest/redirect'; return 'https://claude.ai/api/desktop/darwin/universal/dmg/latest/redirect'
} }
} }
type DesktopHandoffState = 'checking' | 'prompt-download' | 'flushing' | 'opening' | 'success' | 'error';
type DesktopHandoffState =
| 'checking'
| 'prompt-download'
| 'flushing'
| 'opening'
| 'success'
| 'error'
type Props = { type Props = {
onDone: (result?: string, options?: { onDone: (
display?: CommandResultDisplay; result?: string,
}) => void; options?: { display?: CommandResultDisplay },
}; ) => void
export function DesktopHandoff(t0) {
const $ = _c(20);
const {
onDone
} = t0;
const [state, setState] = useState("checking");
const [error, setError] = useState(null);
const [downloadMessage, setDownloadMessage] = useState("");
let t1;
if ($[0] !== error || $[1] !== onDone || $[2] !== state) {
t1 = input => {
if (state === "error") {
onDone(error ?? "Unknown error", {
display: "system"
});
return;
}
if (state === "prompt-download") {
if (input === "y" || input === "Y") {
openBrowser(getDownloadUrl()).catch(_temp);
onDone(`Starting download. Re-run /desktop once you\u2019ve installed the app.\nLearn more at ${DESKTOP_DOCS_URL}`, {
display: "system"
});
} else {
if (input === "n" || input === "N") {
onDone(`The desktop app is required for /desktop. Learn more at ${DESKTOP_DOCS_URL}`, {
display: "system"
});
}
}
}
};
$[0] = error;
$[1] = onDone;
$[2] = state;
$[3] = t1;
} else {
t1 = $[3];
}
useInput(t1);
let t2;
let t3;
if ($[4] !== onDone) {
t2 = () => {
const performHandoff = async function performHandoff() {
setState("checking");
const installStatus = await getDesktopInstallStatus();
if (installStatus.status === "not-installed") {
setDownloadMessage("Claude Desktop is not installed.");
setState("prompt-download");
return;
}
if (installStatus.status === "version-too-old") {
setDownloadMessage(`Claude Desktop needs to be updated (found v${installStatus.version}, need v1.1.2396+).`);
setState("prompt-download");
return;
}
setState("flushing");
await flushSessionStorage();
setState("opening");
const result = await openCurrentSessionInDesktop();
if (!result.success) {
setError(result.error ?? "Failed to open Claude Desktop");
setState("error");
return;
}
setState("success");
setTimeout(_temp2, 500, onDone);
};
performHandoff().catch(err => {
setError(errorMessage(err));
setState("error");
});
};
t3 = [onDone];
$[4] = onDone;
$[5] = t2;
$[6] = t3;
} else {
t2 = $[5];
t3 = $[6];
}
useEffect(t2, t3);
if (state === "error") {
let t4;
if ($[7] !== error) {
t4 = <Text color="error">Error: {error}</Text>;
$[7] = error;
$[8] = t4;
} else {
t4 = $[8];
}
let t5;
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
t5 = <Text dimColor={true}>Press any key to continue</Text>;
$[9] = t5;
} else {
t5 = $[9];
}
let t6;
if ($[10] !== t4) {
t6 = <Box flexDirection="column" paddingX={2}>{t4}{t5}</Box>;
$[10] = t4;
$[11] = t6;
} else {
t6 = $[11];
}
return t6;
}
if (state === "prompt-download") {
let t4;
if ($[12] !== downloadMessage) {
t4 = <Text>{downloadMessage}</Text>;
$[12] = downloadMessage;
$[13] = t4;
} else {
t4 = $[13];
}
let t5;
if ($[14] === Symbol.for("react.memo_cache_sentinel")) {
t5 = <Text>Download now? (y/n)</Text>;
$[14] = t5;
} else {
t5 = $[14];
}
let t6;
if ($[15] !== t4) {
t6 = <Box flexDirection="column" paddingX={2}>{t4}{t5}</Box>;
$[15] = t4;
$[16] = t6;
} else {
t6 = $[16];
}
return t6;
}
let t4;
if ($[17] === Symbol.for("react.memo_cache_sentinel")) {
t4 = {
checking: "Checking for Claude Desktop\u2026",
flushing: "Saving session\u2026",
opening: "Opening Claude Desktop\u2026",
success: "Opening in Claude Desktop\u2026"
};
$[17] = t4;
} else {
t4 = $[17];
}
const messages = t4;
const t5 = messages[state];
let t6;
if ($[18] !== t5) {
t6 = <LoadingState message={t5} />;
$[18] = t5;
$[19] = t6;
} else {
t6 = $[19];
}
return t6;
} }
async function _temp2(onDone_0) {
onDone_0("Session transferred to Claude Desktop", { export function DesktopHandoff({ onDone }: Props): React.ReactNode {
display: "system" const [state, setState] = useState<DesktopHandoffState>('checking')
}); const [error, setError] = useState<string | null>(null)
await gracefulShutdown(0, "other"); const [downloadMessage, setDownloadMessage] = useState<string>('')
// Handle keyboard input for error and prompt-download states
useInput(input => {
if (state === 'error') {
onDone(error ?? 'Unknown error', { display: 'system' })
return
}
if (state === 'prompt-download') {
if (input === 'y' || input === 'Y') {
openBrowser(getDownloadUrl()).catch(() => {})
onDone(
`Starting download. Re-run /desktop once you\u2019ve installed the app.\nLearn more at ${DESKTOP_DOCS_URL}`,
{ display: 'system' },
)
} else if (input === 'n' || input === 'N') {
onDone(
`The desktop app is required for /desktop. Learn more at ${DESKTOP_DOCS_URL}`,
{ display: 'system' },
)
}
}
})
useEffect(() => {
async function performHandoff(): Promise<void> {
// Check Desktop install status
setState('checking')
const installStatus = await getDesktopInstallStatus()
if (installStatus.status === 'not-installed') {
setDownloadMessage('Claude Desktop is not installed.')
setState('prompt-download')
return
}
if (installStatus.status === 'version-too-old') {
setDownloadMessage(
`Claude Desktop needs to be updated (found v${installStatus.version}, need v1.1.2396+).`,
)
setState('prompt-download')
return
}
// Flush session storage to ensure transcript is fully written
setState('flushing')
await flushSessionStorage()
// Open the deep link (uses claude-dev:// in dev mode)
setState('opening')
const result = await openCurrentSessionInDesktop()
if (!result.success) {
setError(result.error ?? 'Failed to open Claude Desktop')
setState('error')
return
}
// Success - exit the CLI
setState('success')
// Give the user a moment to see the success message
setTimeout(
async (onDone: Props['onDone']) => {
onDone('Session transferred to Claude Desktop', { display: 'system' })
await gracefulShutdown(0, 'other')
},
500,
onDone,
)
}
performHandoff().catch(err => {
setError(errorMessage(err))
setState('error')
})
}, [onDone])
if (state === 'error') {
return (
<Box flexDirection="column" paddingX={2}>
<Text color="error">Error: {error}</Text>
<Text dimColor>Press any key to continue</Text>
</Box>
)
}
if (state === 'prompt-download') {
return (
<Box flexDirection="column" paddingX={2}>
<Text>{downloadMessage}</Text>
<Text>Download now? (y/n)</Text>
</Box>
)
}
const messages: Record<
Exclude<DesktopHandoffState, 'error' | 'prompt-download'>,
string
> = {
checking: 'Checking for Claude Desktop…',
flushing: 'Saving session…',
opening: 'Opening Claude Desktop…',
success: 'Opening in Claude Desktop…',
}
return <LoadingState message={messages[state]} />
} }
function _temp() {}

View File

@@ -1,170 +1,108 @@
import { c as _c } from "react/compiler-runtime"; import * as React from 'react'
import * as React from 'react'; import { useEffect, useState } from 'react'
import { useEffect, useState } from 'react'; import { Box, Text } from '../../ink.js'
import { Box, Text } from '../../ink.js'; import { getDynamicConfig_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
import { getDynamicConfig_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'; import { logEvent } from '../../services/analytics/index.js'
import { logEvent } from '../../services/analytics/index.js'; import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'; import { Select } from '../CustomSelect/select.js'
import { Select } from '../CustomSelect/select.js'; import { DesktopHandoff } from '../DesktopHandoff.js'
import { DesktopHandoff } from '../DesktopHandoff.js'; import { PermissionDialog } from '../permissions/PermissionDialog.js'
import { PermissionDialog } from '../permissions/PermissionDialog.js';
type DesktopUpsellConfig = { type DesktopUpsellConfig = {
enable_shortcut_tip: boolean; enable_shortcut_tip: boolean
enable_startup_dialog: boolean; enable_startup_dialog: boolean
}; }
const DESKTOP_UPSELL_DEFAULT: DesktopUpsellConfig = { const DESKTOP_UPSELL_DEFAULT: DesktopUpsellConfig = {
enable_shortcut_tip: false, enable_shortcut_tip: false,
enable_startup_dialog: false enable_startup_dialog: false,
}; }
export function getDesktopUpsellConfig(): DesktopUpsellConfig { export function getDesktopUpsellConfig(): DesktopUpsellConfig {
return getDynamicConfig_CACHED_MAY_BE_STALE('tengu_desktop_upsell', DESKTOP_UPSELL_DEFAULT); return getDynamicConfig_CACHED_MAY_BE_STALE(
'tengu_desktop_upsell',
DESKTOP_UPSELL_DEFAULT,
)
} }
function isSupportedPlatform(): boolean { function isSupportedPlatform(): boolean {
return process.platform === 'darwin' || process.platform === 'win32' && process.arch === 'x64'; return (
process.platform === 'darwin' ||
(process.platform === 'win32' && process.arch === 'x64')
)
} }
export function shouldShowDesktopUpsellStartup(): boolean { export function shouldShowDesktopUpsellStartup(): boolean {
if (!isSupportedPlatform()) return false; if (!isSupportedPlatform()) return false
if (!getDesktopUpsellConfig().enable_startup_dialog) return false; if (!getDesktopUpsellConfig().enable_startup_dialog) return false
const config = getGlobalConfig(); const config = getGlobalConfig()
if (config.desktopUpsellDismissed) return false; if (config.desktopUpsellDismissed) return false
if ((config.desktopUpsellSeenCount ?? 0) >= 3) return false; if ((config.desktopUpsellSeenCount ?? 0) >= 3) return false
return true; return true
} }
type DesktopUpsellSelection = 'try' | 'not-now' | 'never';
type DesktopUpsellSelection = 'try' | 'not-now' | 'never'
type Props = { type Props = {
onDone: () => void; onDone: () => void
}; }
export function DesktopUpsellStartup(t0) {
const $ = _c(14); export function DesktopUpsellStartup({ onDone }: Props): React.ReactNode {
const { const [showHandoff, setShowHandoff] = useState(false)
onDone
} = t0; // Increment seen count on mount (guard in updater for StrictMode safety)
const [showHandoff, setShowHandoff] = useState(false); useEffect(() => {
let t1; const newCount = (getGlobalConfig().desktopUpsellSeenCount ?? 0) + 1
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { saveGlobalConfig(prev => {
t1 = []; if ((prev.desktopUpsellSeenCount ?? 0) >= newCount) return prev
$[0] = t1; return { ...prev, desktopUpsellSeenCount: newCount }
} else { })
t1 = $[0]; logEvent('tengu_desktop_upsell_shown', { seen_count: newCount })
} }, [])
useEffect(_temp, t1);
if (showHandoff) { if (showHandoff) {
let t2; return <DesktopHandoff onDone={() => onDone()} />
if ($[1] !== onDone) { }
t2 = <DesktopHandoff onDone={() => onDone()} />;
$[1] = onDone; function handleSelect(value: DesktopUpsellSelection): void {
$[2] = t2; switch (value) {
} else { case 'try':
t2 = $[2]; setShowHandoff(true)
return
case 'never':
saveGlobalConfig(prev => {
if (prev.desktopUpsellDismissed) return prev
return { ...prev, desktopUpsellDismissed: true }
})
onDone()
return
case 'not-now':
onDone()
return
} }
return t2;
} }
let t2;
if ($[3] !== onDone) { const options = [
t2 = function handleSelect(value) { { label: 'Open in Claude Code Desktop', value: 'try' as const },
switch (value) { { label: 'Not now', value: 'not-now' as const },
case "try": { label: "Don't ask again", value: 'never' as const },
{ ]
setShowHandoff(true);
return; return (
} <PermissionDialog title="Try Claude Code Desktop">
case "never": <Box flexDirection="column" paddingX={2} paddingY={1}>
{ <Box marginBottom={1}>
saveGlobalConfig(_temp2); <Text>
onDone(); Same Claude Code with visual diffs, live app preview, parallel
return; sessions, and more.
} </Text>
case "not-now": </Box>
{ <Select
onDone(); options={options}
return; onChange={handleSelect}
} onCancel={() => handleSelect('not-now')}
} />
}; </Box>
$[3] = onDone; </PermissionDialog>
$[4] = t2; )
} else {
t2 = $[4];
}
const handleSelect = t2;
let t3;
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
t3 = {
label: "Open in Claude Code Desktop",
value: "try" as const
};
$[5] = t3;
} else {
t3 = $[5];
}
let t4;
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
t4 = {
label: "Not now",
value: "not-now" as const
};
$[6] = t4;
} else {
t4 = $[6];
}
let t5;
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
t5 = [t3, t4, {
label: "Don't ask again",
value: "never" as const
}];
$[7] = t5;
} else {
t5 = $[7];
}
const options = t5;
let t6;
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
t6 = <Box marginBottom={1}><Text>Same Claude Code with visual diffs, live app preview, parallel sessions, and more.</Text></Box>;
$[8] = t6;
} else {
t6 = $[8];
}
let t7;
if ($[9] !== handleSelect) {
t7 = () => handleSelect("not-now");
$[9] = handleSelect;
$[10] = t7;
} else {
t7 = $[10];
}
let t8;
if ($[11] !== handleSelect || $[12] !== t7) {
t8 = <PermissionDialog title="Try Claude Code Desktop"><Box flexDirection="column" paddingX={2} paddingY={1}>{t6}<Select options={options} onChange={handleSelect} onCancel={t7} /></Box></PermissionDialog>;
$[11] = handleSelect;
$[12] = t7;
$[13] = t8;
} else {
t8 = $[13];
}
return t8;
}
function _temp2(prev_0) {
if (prev_0.desktopUpsellDismissed) {
return prev_0;
}
return {
...prev_0,
desktopUpsellDismissed: true
};
}
function _temp() {
const newCount = (getGlobalConfig().desktopUpsellSeenCount ?? 0) + 1;
saveGlobalConfig(prev => {
if ((prev.desktopUpsellSeenCount ?? 0) >= newCount) {
return prev;
}
return {
...prev,
desktopUpsellSeenCount: newCount
};
});
logEvent("tengu_desktop_upsell_shown", {
seen_count: newCount
});
} }

View File

@@ -1,48 +1,46 @@
import { c as _c } from "react/compiler-runtime"; import * as React from 'react'
import * as React from 'react'; import { useState } from 'react'
import { useState } from 'react'; import { getSlowOperations } from '../bootstrap/state.js'
import { getSlowOperations } from '../bootstrap/state.js'; import { Text, useInterval } from '../ink.js'
import { Text, useInterval } from '../ink.js';
// Show DevBar for dev builds or all ants // Show DevBar for dev builds or all ants
function shouldShowDevBar(): boolean { function shouldShowDevBar(): boolean {
return ("production" as string) === 'development' || (process.env.USER_TYPE) === 'ant'; return (
"production" === 'development' || process.env.USER_TYPE === 'ant'
)
} }
export function DevBar() {
const $ = _c(5); export function DevBar(): React.ReactNode {
const [slowOps, setSlowOps] = useState(getSlowOperations); const [slowOps, setSlowOps] =
let t0; useState<
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { ReadonlyArray<{
t0 = () => { operation: string
setSlowOps(getSlowOperations()); durationMs: number
}; timestamp: number
$[0] = t0; }>
} else { >(getSlowOperations)
t0 = $[0];
} useInterval(
useInterval(t0, shouldShowDevBar() ? 500 : null); () => {
setSlowOps(getSlowOperations())
},
shouldShowDevBar() ? 500 : null,
)
// Only show when there's something to display
if (!shouldShowDevBar() || slowOps.length === 0) { if (!shouldShowDevBar() || slowOps.length === 0) {
return null; return null
} }
let t1;
if ($[1] !== slowOps) { // Single-line format so short terminals don't lose rows to dev noise.
t1 = slowOps.slice(-3).map(_temp).join(" \xB7 "); const recentOps = slowOps
$[1] = slowOps; .slice(-3)
$[2] = t1; .map(op => `${op.operation} (${Math.round(op.durationMs)}ms)`)
} else { .join(' · ')
t1 = $[2];
} return (
const recentOps = t1; <Text wrap="truncate-end" color="warning">
let t2; [ANT-ONLY] slow sync: {recentOps}
if ($[3] !== recentOps) { </Text>
t2 = <Text wrap="truncate-end" color="warning">[ANT-ONLY] slow sync: {recentOps}</Text>; )
$[3] = recentOps;
$[4] = t2;
} else {
t2 = $[4];
}
return t2;
}
function _temp(op) {
return `${op.operation} (${Math.round(op.durationMs)}ms)`;
} }

View File

@@ -1,104 +1,66 @@
import { c as _c } from "react/compiler-runtime"; import React, { useCallback } from 'react'
import React, { useCallback } from 'react'; import type { ChannelEntry } from '../bootstrap/state.js'
import type { ChannelEntry } from '../bootstrap/state.js'; import { Box, Text } from '../ink.js'
import { Box, Text } from '../ink.js'; import { gracefulShutdownSync } from '../utils/gracefulShutdown.js'
import { gracefulShutdownSync } from '../utils/gracefulShutdown.js'; import { Select } from './CustomSelect/index.js'
import { Select } from './CustomSelect/index.js'; import { Dialog } from './design-system/Dialog.js'
import { Dialog } from './design-system/Dialog.js';
type Props = { type Props = {
channels: ChannelEntry[]; channels: ChannelEntry[]
onAccept(): void; onAccept(): void
};
export function DevChannelsDialog(t0) {
const $ = _c(14);
const {
channels,
onAccept
} = t0;
let t1;
if ($[0] !== onAccept) {
t1 = function onChange(value) {
bb2: switch (value) {
case "accept":
{
onAccept();
break bb2;
}
case "exit":
{
gracefulShutdownSync(1);
}
}
};
$[0] = onAccept;
$[1] = t1;
} else {
t1 = $[1];
}
const onChange = t1;
const handleEscape = _temp;
let t2;
let t3;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t2 = <Text>--dangerously-load-development-channels is for local channel development only. Do not use this option to run channels you have downloaded off the internet.</Text>;
t3 = <Text>Please use --channels to run a list of approved channels.</Text>;
$[2] = t2;
$[3] = t3;
} else {
t2 = $[2];
t3 = $[3];
}
let t4;
if ($[4] !== channels) {
t4 = channels.map(_temp2).join(", ");
$[4] = channels;
$[5] = t4;
} else {
t4 = $[5];
}
let t5;
if ($[6] !== t4) {
t5 = <Box flexDirection="column" gap={1}>{t2}{t3}<Text dimColor={true}>Channels:{" "}{t4}</Text></Box>;
$[6] = t4;
$[7] = t5;
} else {
t5 = $[7];
}
let t6;
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
t6 = [{
label: "I am using this for local development",
value: "accept"
}, {
label: "Exit",
value: "exit"
}];
$[8] = t6;
} else {
t6 = $[8];
}
let t7;
if ($[9] !== onChange) {
t7 = <Select options={t6} onChange={value_0 => onChange(value_0 as 'accept' | 'exit')} />;
$[9] = onChange;
$[10] = t7;
} else {
t7 = $[10];
}
let t8;
if ($[11] !== t5 || $[12] !== t7) {
t8 = <Dialog title="WARNING: Loading development channels" color="error" onCancel={handleEscape}>{t5}{t7}</Dialog>;
$[11] = t5;
$[12] = t7;
$[13] = t8;
} else {
t8 = $[13];
}
return t8;
} }
function _temp2(c) {
return c.kind === "plugin" ? `plugin:${c.name}@${c.marketplace}` : `server:${c.name}`; export function DevChannelsDialog({
} channels,
function _temp() { onAccept,
gracefulShutdownSync(0); }: Props): React.ReactNode {
function onChange(value: 'accept' | 'exit') {
switch (value) {
case 'accept':
onAccept()
break
case 'exit':
gracefulShutdownSync(1)
break
}
}
const handleEscape = useCallback(() => {
gracefulShutdownSync(0)
}, [])
return (
<Dialog
title="WARNING: Loading development channels"
color="error"
onCancel={handleEscape}
>
<Box flexDirection="column" gap={1}>
<Text>
--dangerously-load-development-channels is for local channel
development only. Do not use this option to run channels you have
downloaded off the internet.
</Text>
<Text>Please use --channels to run a list of approved channels.</Text>
<Text dimColor>
Channels:{' '}
{channels
.map(c =>
c.kind === 'plugin'
? `plugin:${c.name}@${c.marketplace}`
: `server:${c.name}`,
)
.join(', ')}
</Text>
</Box>
<Select
options={[
{ label: 'I am using this for local development', value: 'accept' },
{ label: 'Exit', value: 'exit' },
]}
onChange={value => onChange(value as 'accept' | 'exit')}
/>
</Dialog>
)
} }

View File

@@ -1,94 +1,91 @@
import { c as _c } from "react/compiler-runtime"; import { relative } from 'path'
import { relative } from 'path'; import React from 'react'
import React from 'react'; import { Box, Text } from '../ink.js'
import { Box, Text } from '../ink.js'; import { DiagnosticTrackingService } from '../services/diagnosticTracking.js'
import { DiagnosticTrackingService } from '../services/diagnosticTracking.js'; import type { Attachment } from '../utils/attachments.js'
import type { Attachment } from '../utils/attachments.js'; import { getCwd } from '../utils/cwd.js'
import { getCwd } from '../utils/cwd.js'; import { CtrlOToExpand } from './CtrlOToExpand.js'
import { CtrlOToExpand } from './CtrlOToExpand.js'; import { MessageResponse } from './MessageResponse.js'
import { MessageResponse } from './MessageResponse.js';
type DiagnosticsAttachment = Extract<Attachment, { type DiagnosticsAttachment = Extract<Attachment, { type: 'diagnostics' }>
type: 'diagnostics';
}>;
type DiagnosticsDisplayProps = { type DiagnosticsDisplayProps = {
attachment: DiagnosticsAttachment; attachment: DiagnosticsAttachment
verbose: boolean; verbose: boolean
}; }
export function DiagnosticsDisplay(t0) {
const $ = _c(14); export function DiagnosticsDisplay({
const { attachment,
attachment, verbose,
verbose }: DiagnosticsDisplayProps): React.ReactNode {
} = t0; // Only show if there are diagnostics to report
if (attachment.files.length === 0) { if (attachment.files.length === 0) return null
return null;
} // Count total issues
let t1; const totalIssues = attachment.files.reduce(
if ($[0] !== attachment.files) { (sum, file) => sum + file.diagnostics.length,
t1 = attachment.files.reduce(_temp, 0); 0,
$[0] = attachment.files; )
$[1] = t1;
} else { const fileCount = attachment.files.length
t1 = $[1];
}
const totalIssues = t1;
const fileCount = attachment.files.length;
if (verbose) { if (verbose) {
let t2; // Show all diagnostics in verbose mode (ctrl+o)
if ($[2] !== attachment.files) { return (
t2 = attachment.files.map(_temp3); <Box flexDirection="column">
$[2] = attachment.files; {attachment.files.map((file, fileIndex) => (
$[3] = t2; <React.Fragment key={fileIndex}>
} else { <MessageResponse>
t2 = $[3]; <Text dimColor wrap="wrap">
} <Text bold>
let t3; {relative(
if ($[4] !== t2) { getCwd(),
t3 = <Box flexDirection="column">{t2}</Box>; file.uri
$[4] = t2; .replace('file://', '')
$[5] = t3; .replace('_claude_fs_right:', ''),
} else { )}
t3 = $[5]; </Text>{' '}
} <Text dimColor>
return t3; {file.uri.startsWith('file://')
? '(file://)'
: file.uri.startsWith('_claude_fs_right:')
? '(claude_fs_right)'
: `(${file.uri.split(':')[0]})`}
</Text>
:
</Text>
</MessageResponse>
{file.diagnostics.map((diagnostic, diagIndex) => (
<MessageResponse key={diagIndex}>
<Text dimColor wrap="wrap">
{' '}
{DiagnosticTrackingService.getSeveritySymbol(
diagnostic.severity,
)}
{' [Line '}
{diagnostic.range.start.line + 1}:
{diagnostic.range.start.character + 1}
{'] '}
{diagnostic.message}
{diagnostic.code ? ` [${diagnostic.code}]` : ''}
{diagnostic.source ? ` (${diagnostic.source})` : ''}
</Text>
</MessageResponse>
))}
</React.Fragment>
))}
</Box>
)
} else { } else {
let t2; // Show summary in normal mode
if ($[6] !== totalIssues) { return (
t2 = <Text bold={true}>{totalIssues}</Text>; <MessageResponse>
$[6] = totalIssues; <Text dimColor wrap="wrap">
$[7] = t2; Found <Text bold>{totalIssues}</Text> new diagnostic{' '}
} else { {totalIssues === 1 ? 'issue' : 'issues'} in {fileCount}{' '}
t2 = $[7]; {fileCount === 1 ? 'file' : 'files'} <CtrlOToExpand />
} </Text>
const t3 = totalIssues === 1 ? "issue" : "issues"; </MessageResponse>
const t4 = fileCount === 1 ? "file" : "files"; )
let t5;
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
t5 = <CtrlOToExpand />;
$[8] = t5;
} else {
t5 = $[8];
}
let t6;
if ($[9] !== fileCount || $[10] !== t2 || $[11] !== t3 || $[12] !== t4) {
t6 = <MessageResponse><Text dimColor={true} wrap="wrap">Found {t2} new diagnostic{" "}{t3} in {fileCount}{" "}{t4} {t5}</Text></MessageResponse>;
$[9] = fileCount;
$[10] = t2;
$[11] = t3;
$[12] = t4;
$[13] = t6;
} else {
t6 = $[13];
}
return t6;
} }
} }
function _temp3(file_0, fileIndex) {
return <React.Fragment key={fileIndex}><MessageResponse><Text dimColor={true} wrap="wrap"><Text bold={true}>{relative(getCwd(), file_0.uri.replace("file://", "").replace("_claude_fs_right:", ""))}</Text>{" "}<Text dimColor={true}>{file_0.uri.startsWith("file://") ? "(file://)" : file_0.uri.startsWith("_claude_fs_right:") ? "(claude_fs_right)" : `(${file_0.uri.split(":")[0]})`}</Text>:</Text></MessageResponse>{file_0.diagnostics.map(_temp2)}</React.Fragment>;
}
function _temp2(diagnostic, diagIndex) {
return <MessageResponse key={diagIndex}><Text dimColor={true} wrap="wrap">{" "}{DiagnosticTrackingService.getSeveritySymbol(diagnostic.severity)}{" [Line "}{diagnostic.range.start.line + 1}:{diagnostic.range.start.character + 1}{"] "}{diagnostic.message}{diagnostic.code ? ` [${diagnostic.code}]` : ""}{diagnostic.source ? ` (${diagnostic.source})` : ""}</Text></MessageResponse>;
}
function _temp(sum, file) {
return sum + file.diagnostics.length;
}

View File

@@ -1,211 +1,125 @@
import { c as _c } from "react/compiler-runtime"; import React, { useCallback, useEffect, useRef } from 'react'
import React, { useCallback, useEffect, useRef } from 'react'; import { Box, Text } from '../ink.js'
import { Box, Text } from '../ink.js'; import {
import { isMaxSubscriber, isProSubscriber, isTeamSubscriber } from '../utils/auth.js'; isMaxSubscriber,
import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js'; isProSubscriber,
import type { EffortLevel } from '../utils/effort.js'; isTeamSubscriber,
import { convertEffortValueToLevel, getDefaultEffortForModel, getOpusDefaultEffortConfig, toPersistableEffort } from '../utils/effort.js'; } from '../utils/auth.js'
import { parseUserSpecifiedModel } from '../utils/model/model.js'; import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js'
import { updateSettingsForSource } from '../utils/settings/settings.js'; import type { EffortLevel } from '../utils/effort.js'
import type { OptionWithDescription } from './CustomSelect/select.js'; import {
import { Select } from './CustomSelect/select.js'; convertEffortValueToLevel,
import { effortLevelToSymbol } from './EffortIndicator.js'; getDefaultEffortForModel,
import { PermissionDialog } from './permissions/PermissionDialog.js'; getOpusDefaultEffortConfig,
type EffortCalloutSelection = EffortLevel | undefined | 'dismiss'; toPersistableEffort,
} from '../utils/effort.js'
import { parseUserSpecifiedModel } from '../utils/model/model.js'
import { updateSettingsForSource } from '../utils/settings/settings.js'
import type { OptionWithDescription } from './CustomSelect/select.js'
import { Select } from './CustomSelect/select.js'
import { effortLevelToSymbol } from './EffortIndicator.js'
import { PermissionDialog } from './permissions/PermissionDialog.js'
type EffortCalloutSelection = EffortLevel | undefined | 'dismiss'
type Props = { type Props = {
model: string; model: string
onDone: (selection: EffortCalloutSelection) => void; onDone: (selection: EffortCalloutSelection) => void
}; }
const AUTO_DISMISS_MS = 30_000;
export function EffortCallout(t0) { const AUTO_DISMISS_MS = 30_000
const $ = _c(18);
const { export function EffortCallout({ model, onDone }: Props): React.ReactNode {
model, const defaultEffortConfig = getOpusDefaultEffortConfig()
onDone // Latest-ref pattern — write via effect so React Compiler can memoize.
} = t0; const onDoneRef = useRef(onDone)
let t1; useEffect(() => {
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { onDoneRef.current = onDone
t1 = getOpusDefaultEffortConfig(); })
$[0] = t1;
} else { const handleCancel = useCallback((): void => {
t1 = $[0]; onDoneRef.current('dismiss')
} }, [])
const defaultEffortConfig = t1;
const onDoneRef = useRef(onDone); // Permanently dismiss on mount so it only shows once
let t2; useEffect(() => {
if ($[1] !== onDone) { markV2Dismissed()
t2 = () => { }, [])
onDoneRef.current = onDone;
}; // 30-second auto-dismiss timer
$[1] = onDone; useEffect(() => {
$[2] = t2; const timeoutId = setTimeout(handleCancel, AUTO_DISMISS_MS)
} else { return () => clearTimeout(timeoutId)
t2 = $[2]; }, [handleCancel])
}
useEffect(t2); const defaultEffort = getDefaultEffortForModel(model)
let t3; const defaultLevel = defaultEffort
if ($[3] === Symbol.for("react.memo_cache_sentinel")) { ? convertEffortValueToLevel(defaultEffort)
t3 = () => { : 'high'
onDoneRef.current("dismiss");
}; const handleSelect = useCallback(
$[3] = t3; (value: EffortLevel): void => {
} else { const effortLevel = value === defaultLevel ? undefined : value
t3 = $[3]; updateSettingsForSource('userSettings', {
} effortLevel: toPersistableEffort(effortLevel),
const handleCancel = t3; })
let t4; onDoneRef.current(value)
if ($[4] === Symbol.for("react.memo_cache_sentinel")) { },
t4 = []; [defaultLevel],
$[4] = t4; )
} else {
t4 = $[4]; const options: OptionWithDescription<EffortLevel>[] = [
} {
useEffect(_temp, t4);
let t5;
let t6;
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
t5 = () => {
const timeoutId = setTimeout(handleCancel, AUTO_DISMISS_MS);
return () => clearTimeout(timeoutId);
};
t6 = [handleCancel];
$[5] = t5;
$[6] = t6;
} else {
t5 = $[5];
t6 = $[6];
}
useEffect(t5, t6);
let t7;
if ($[7] !== model) {
const defaultEffort = getDefaultEffortForModel(model);
t7 = defaultEffort ? convertEffortValueToLevel(defaultEffort) : "high";
$[7] = model;
$[8] = t7;
} else {
t7 = $[8];
}
const defaultLevel = t7;
let t8;
if ($[9] !== defaultLevel) {
t8 = value => {
const effortLevel = value === defaultLevel ? undefined : value;
updateSettingsForSource("userSettings", {
effortLevel: toPersistableEffort(effortLevel)
});
onDoneRef.current(value);
};
$[9] = defaultLevel;
$[10] = t8;
} else {
t8 = $[10];
}
const handleSelect = t8;
let t9;
if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
t9 = [{
label: <EffortOptionLabel level="medium" text="Medium (recommended)" />, label: <EffortOptionLabel level="medium" text="Medium (recommended)" />,
value: "medium" value: 'medium',
}, { },
label: <EffortOptionLabel level="high" text="High" />, { label: <EffortOptionLabel level="high" text="High" />, value: 'high' },
value: "high" { label: <EffortOptionLabel level="low" text="Low" />, value: 'low' },
}, { ]
label: <EffortOptionLabel level="low" text="Low" />,
value: "low" return (
}]; <PermissionDialog title={defaultEffortConfig.dialogTitle}>
$[11] = t9; <Box flexDirection="column" paddingX={2} paddingY={1}>
} else { <Box marginBottom={1} flexDirection="column">
t9 = $[11]; <Text>{defaultEffortConfig.dialogDescription}</Text>
} </Box>
const options = t9; <Box marginBottom={1}>
let t10; <Text dimColor>
if ($[12] === Symbol.for("react.memo_cache_sentinel")) { <EffortIndicatorSymbol level="low" /> low {'·'}{' '}
t10 = <Box marginBottom={1} flexDirection="column"><Text>{defaultEffortConfig.dialogDescription}</Text></Box>; <EffortIndicatorSymbol level="medium" /> medium {'·'}{' '}
$[12] = t10; <EffortIndicatorSymbol level="high" /> high
} else { </Text>
t10 = $[12]; </Box>
} <Select
let t11; options={options}
if ($[13] === Symbol.for("react.memo_cache_sentinel")) { onChange={handleSelect}
t11 = <EffortIndicatorSymbol level="low" />; onCancel={handleCancel}
$[13] = t11; />
} else { </Box>
t11 = $[13]; </PermissionDialog>
} )
let t12;
if ($[14] === Symbol.for("react.memo_cache_sentinel")) {
t12 = <EffortIndicatorSymbol level="medium" />;
$[14] = t12;
} else {
t12 = $[14];
}
let t13;
if ($[15] === Symbol.for("react.memo_cache_sentinel")) {
t13 = <Box marginBottom={1}><Text dimColor={true}>{t11} low {"\xB7"}{" "}{t12} medium {"\xB7"}{" "}<EffortIndicatorSymbol level="high" /> high</Text></Box>;
$[15] = t13;
} else {
t13 = $[15];
}
let t14;
if ($[16] !== handleSelect) {
t14 = <PermissionDialog title={defaultEffortConfig.dialogTitle}><Box flexDirection="column" paddingX={2} paddingY={1}>{t10}{t13}<Select options={options} onChange={handleSelect} onCancel={handleCancel} /></Box></PermissionDialog>;
$[16] = handleSelect;
$[17] = t14;
} else {
t14 = $[17];
}
return t14;
} }
function _temp() {
markV2Dismissed(); function EffortIndicatorSymbol({
level,
}: {
level: EffortLevel
}): React.ReactNode {
return <Text color="suggestion">{effortLevelToSymbol(level)}</Text>
} }
function EffortIndicatorSymbol(t0) {
const $ = _c(4); function EffortOptionLabel({
const { level,
level text,
} = t0; }: {
let t1; level: EffortLevel
if ($[0] !== level) { text: string
t1 = effortLevelToSymbol(level); }): React.ReactNode {
$[0] = level; return (
$[1] = t1; <>
} else { <EffortIndicatorSymbol level={level} /> {text}
t1 = $[1]; </>
} )
let t2;
if ($[2] !== t1) {
t2 = <Text color="suggestion">{t1}</Text>;
$[2] = t1;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
function EffortOptionLabel(t0) {
const $ = _c(5);
const {
level,
text
} = t0;
let t1;
if ($[0] !== level) {
t1 = <EffortIndicatorSymbol level={level} />;
$[0] = level;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] !== t1 || $[3] !== text) {
t2 = <>{t1} {text}</>;
$[2] = t1;
$[3] = text;
$[4] = t2;
} else {
t2 = $[4];
}
return t2;
} }
/** /**
@@ -218,47 +132,46 @@ function EffortOptionLabel(t0) {
*/ */
export function shouldShowEffortCallout(model: string): boolean { export function shouldShowEffortCallout(model: string): boolean {
// Only show for Opus 4.6 for now // Only show for Opus 4.6 for now
const parsed = parseUserSpecifiedModel(model); const parsed = parseUserSpecifiedModel(model)
if (!parsed.toLowerCase().includes('opus-4-6')) { if (!parsed.toLowerCase().includes('opus-4-6')) {
return false; return false
} }
const config = getGlobalConfig();
if (config.effortCalloutV2Dismissed) return false; const config = getGlobalConfig()
if (config.effortCalloutV2Dismissed) return false
// Don't show to brand-new users — they never knew the old default, so this // Don't show to brand-new users — they never knew the old default, so this
// isn't a change for them. Mark as dismissed so it stays suppressed. // isn't a change for them. Mark as dismissed so it stays suppressed.
if (config.numStartups <= 1) { if (config.numStartups <= 1) {
markV2Dismissed(); markV2Dismissed()
return false; return false
} }
// Pro users already had medium default before this PR. Show the new copy, // Pro users already had medium default before this PR. Show the new copy,
// but skip if they already saw the v1 dialog — no point nagging twice. // but skip if they already saw the v1 dialog — no point nagging twice.
if (isProSubscriber()) { if (isProSubscriber()) {
if (config.effortCalloutDismissed) { if (config.effortCalloutDismissed) {
markV2Dismissed(); markV2Dismissed()
return false; return false
} }
return getOpusDefaultEffortConfig().enabled; return getOpusDefaultEffortConfig().enabled
} }
// Max/Team are the target of the tengu_grey_step2 config. // Max/Team are the target of the tengu_grey_step2 config.
// Don't mark dismissed when config is disabled — they should see the dialog // Don't mark dismissed when config is disabled — they should see the dialog
// once it's enabled for them. // once it's enabled for them.
if (isMaxSubscriber() || isTeamSubscriber()) { if (isMaxSubscriber() || isTeamSubscriber()) {
return getOpusDefaultEffortConfig().enabled; return getOpusDefaultEffortConfig().enabled
} }
// Everyone else (free tier, API key, non-subscribers): not in scope. // Everyone else (free tier, API key, non-subscribers): not in scope.
markV2Dismissed(); markV2Dismissed()
return false; return false
} }
function markV2Dismissed(): void { function markV2Dismissed(): void {
saveGlobalConfig(current => { saveGlobalConfig(current => {
if (current.effortCalloutV2Dismissed) return current; if (current.effortCalloutV2Dismissed) return current
return { return { ...current, effortCalloutV2Dismissed: true }
...current, })
effortCalloutV2Dismissed: true
};
});
} }

View File

@@ -1,47 +1,33 @@
import { c as _c } from "react/compiler-runtime"; import sample from 'lodash-es/sample.js'
import sample from 'lodash-es/sample.js'; import React from 'react'
import React from 'react'; import { gracefulShutdown } from '../utils/gracefulShutdown.js'
import { gracefulShutdown } from '../utils/gracefulShutdown.js'; import { WorktreeExitDialog } from './WorktreeExitDialog.js'
import { WorktreeExitDialog } from './WorktreeExitDialog.js';
const GOODBYE_MESSAGES = ['Goodbye!', 'See ya!', 'Bye!', 'Catch you later!']; const GOODBYE_MESSAGES = ['Goodbye!', 'See ya!', 'Bye!', 'Catch you later!']
function getRandomGoodbyeMessage(): string { function getRandomGoodbyeMessage(): string {
return sample(GOODBYE_MESSAGES) ?? 'Goodbye!'; return sample(GOODBYE_MESSAGES) ?? 'Goodbye!'
} }
type Props = { type Props = {
onDone: (message?: string) => void; onDone: (message?: string) => void
onCancel?: () => void; onCancel?: () => void
showWorktree: boolean; showWorktree: boolean
}; }
export function ExitFlow(t0) {
const $ = _c(5); export function ExitFlow({
const { showWorktree,
showWorktree, onDone,
onDone, onCancel,
onCancel }: Props): React.ReactNode {
} = t0; async function onExit(resultMessage?: string) {
let t1; onDone(resultMessage ?? getRandomGoodbyeMessage())
if ($[0] !== onDone) { await gracefulShutdown(0, 'prompt_input_exit')
t1 = async function onExit(resultMessage) { }
onDone(resultMessage ?? getRandomGoodbyeMessage());
await gracefulShutdown(0, "prompt_input_exit"); if (showWorktree) {
}; return <WorktreeExitDialog onDone={onExit} onCancel={onCancel} />
$[0] = onDone; }
$[1] = t1;
} else { return null
t1 = $[1];
}
const onExit = t1;
if (showWorktree) {
let t2;
if ($[2] !== onCancel || $[3] !== onExit) {
t2 = <WorktreeExitDialog onDone={onExit} onCancel={onCancel} />;
$[2] = onCancel;
$[3] = onExit;
$[4] = t2;
} else {
t2 = $[4];
}
return t2;
}
return null;
} }

View File

@@ -1,127 +1,173 @@
import { join } from 'path'; import { join } from 'path'
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react'
import type { ExitState } from '../hooks/useExitOnCtrlCDWithKeybindings.js'; import type { ExitState } from '../hooks/useExitOnCtrlCDWithKeybindings.js'
import { useTerminalSize } from '../hooks/useTerminalSize.js'; import { useTerminalSize } from '../hooks/useTerminalSize.js'
import { setClipboard } from '../ink/termio/osc.js'; import { setClipboard } from '../ink/termio/osc.js'
import { Box, Text } from '../ink.js'; import { Box, Text } from '../ink.js'
import { useKeybinding } from '../keybindings/useKeybinding.js'; import { useKeybinding } from '../keybindings/useKeybinding.js'
import { getCwd } from '../utils/cwd.js'; import { getCwd } from '../utils/cwd.js'
import { writeFileSync_DEPRECATED } from '../utils/slowOperations.js'; import { writeFileSync_DEPRECATED } from '../utils/slowOperations.js'
import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js'; import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js'
import { Select } from './CustomSelect/select.js'; import { Select } from './CustomSelect/select.js'
import { Byline } from './design-system/Byline.js'; import { Byline } from './design-system/Byline.js'
import { Dialog } from './design-system/Dialog.js'; import { Dialog } from './design-system/Dialog.js'
import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js'; import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js'
import TextInput from './TextInput.js'; import TextInput from './TextInput.js'
type ExportDialogProps = { type ExportDialogProps = {
content: string; content: string
defaultFilename: string; defaultFilename: string
onDone: (result: { onDone: (result: { success: boolean; message: string }) => void
success: boolean; }
message: string;
}) => void; type ExportOption = 'clipboard' | 'file'
};
type ExportOption = 'clipboard' | 'file';
export function ExportDialog({ export function ExportDialog({
content, content,
defaultFilename, defaultFilename,
onDone onDone,
}: ExportDialogProps): React.ReactNode { }: ExportDialogProps): React.ReactNode {
const [, setSelectedOption] = useState<ExportOption | null>(null); const [, setSelectedOption] = useState<ExportOption | null>(null)
const [filename, setFilename] = useState<string>(defaultFilename); const [filename, setFilename] = useState<string>(defaultFilename)
const [cursorOffset, setCursorOffset] = useState<number>(defaultFilename.length); const [cursorOffset, setCursorOffset] = useState<number>(
const [showFilenameInput, setShowFilenameInput] = useState(false); defaultFilename.length,
const { )
columns const [showFilenameInput, setShowFilenameInput] = useState(false)
} = useTerminalSize(); const { columns } = useTerminalSize()
// Handle going back from filename input to option selection // Handle going back from filename input to option selection
const handleGoBack = useCallback(() => { const handleGoBack = useCallback(() => {
setShowFilenameInput(false); setShowFilenameInput(false)
setSelectedOption(null); setSelectedOption(null)
}, []); }, [])
const handleSelectOption = async (value: string): Promise<void> => { const handleSelectOption = async (value: string): Promise<void> => {
if (value === 'clipboard') { if (value === 'clipboard') {
// Copy to clipboard immediately // Copy to clipboard immediately
const raw = await setClipboard(content); const raw = await setClipboard(content)
if (raw) process.stdout.write(raw); if (raw) process.stdout.write(raw)
onDone({ onDone({ success: true, message: 'Conversation copied to clipboard' })
success: true,
message: 'Conversation copied to clipboard'
});
} else if (value === 'file') { } else if (value === 'file') {
setSelectedOption('file'); setSelectedOption('file')
setShowFilenameInput(true); setShowFilenameInput(true)
} }
}; }
const handleFilenameSubmit = () => { const handleFilenameSubmit = () => {
const finalFilename = filename.endsWith('.txt') ? filename : filename.replace(/\.[^.]+$/, '') + '.txt'; const finalFilename = filename.endsWith('.txt')
const filepath = join(getCwd(), finalFilename); ? filename
: filename.replace(/\.[^.]+$/, '') + '.txt'
const filepath = join(getCwd(), finalFilename)
try { try {
writeFileSync_DEPRECATED(filepath, content, { writeFileSync_DEPRECATED(filepath, content, {
encoding: 'utf-8', encoding: 'utf-8',
flush: true flush: true,
}); })
onDone({ onDone({
success: true, success: true,
message: `Conversation exported to: ${filepath}` message: `Conversation exported to: ${filepath}`,
}); })
} catch (error) { } catch (error) {
onDone({ onDone({
success: false, success: false,
message: `Failed to export conversation: ${error instanceof Error ? error.message : 'Unknown error'}` message: `Failed to export conversation: ${error instanceof Error ? error.message : 'Unknown error'}`,
}); })
} }
}; }
// Dialog calls onCancel when Escape is pressed. If we are in the filename // Dialog calls onCancel when Escape is pressed. If we are in the filename
// input sub-screen, go back to the option list instead of closing entirely. // input sub-screen, go back to the option list instead of closing entirely.
const handleCancel = useCallback(() => { const handleCancel = useCallback(() => {
if (showFilenameInput) { if (showFilenameInput) {
handleGoBack(); handleGoBack()
} else { } else {
onDone({ onDone({ success: false, message: 'Export cancelled' })
success: false,
message: 'Export cancelled'
});
} }
}, [showFilenameInput, handleGoBack, onDone]); }, [showFilenameInput, handleGoBack, onDone])
const options = [{
label: 'Copy to clipboard', const options = [
value: 'clipboard', {
description: 'Copy the conversation to your system clipboard' label: 'Copy to clipboard',
}, { value: 'clipboard',
label: 'Save to file', description: 'Copy the conversation to your system clipboard',
value: 'file', },
description: 'Save the conversation to a file in the current directory' {
}]; label: 'Save to file',
value: 'file',
description: 'Save the conversation to a file in the current directory',
},
]
// Custom input guide that changes based on dialog state // Custom input guide that changes based on dialog state
function renderInputGuide(exitState: ExitState): React.ReactNode { function renderInputGuide(exitState: ExitState): React.ReactNode {
if (showFilenameInput) { if (showFilenameInput) {
return <Byline> return (
<Byline>
<KeyboardShortcutHint shortcut="Enter" action="save" /> <KeyboardShortcutHint shortcut="Enter" action="save" />
<ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" /> <ConfigurableShortcutHint
</Byline>; action="confirm:no"
context="Confirmation"
fallback="Esc"
description="go back"
/>
</Byline>
)
} }
if (exitState.pending) { if (exitState.pending) {
return <Text>Press {exitState.keyName} again to exit</Text>; return <Text>Press {exitState.keyName} again to exit</Text>
} }
return <ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="cancel" />;
return (
<ConfigurableShortcutHint
action="confirm:no"
context="Confirmation"
fallback="Esc"
description="cancel"
/>
)
} }
// Use Settings context so 'n' key doesn't cancel (allows typing 'n' in filename input) // Use Settings context so 'n' key doesn't cancel (allows typing 'n' in filename input)
useKeybinding('confirm:no', handleCancel, { useKeybinding('confirm:no', handleCancel, {
context: 'Settings', context: 'Settings',
isActive: showFilenameInput isActive: showFilenameInput,
}); })
return <Dialog title="Export Conversation" subtitle="Select export method:" color="permission" onCancel={handleCancel} inputGuide={renderInputGuide} isCancelActive={!showFilenameInput}>
{!showFilenameInput ? <Select options={options} onChange={handleSelectOption} onCancel={handleCancel} /> : <Box flexDirection="column"> return (
<Dialog
title="Export Conversation"
subtitle="Select export method:"
color="permission"
onCancel={handleCancel}
inputGuide={renderInputGuide}
isCancelActive={!showFilenameInput}
>
{!showFilenameInput ? (
<Select
options={options}
onChange={handleSelectOption}
onCancel={handleCancel}
/>
) : (
<Box flexDirection="column">
<Text>Enter filename:</Text> <Text>Enter filename:</Text>
<Box flexDirection="row" gap={1} marginTop={1}> <Box flexDirection="row" gap={1} marginTop={1}>
<Text>&gt;</Text> <Text>&gt;</Text>
<TextInput value={filename} onChange={setFilename} onSubmit={handleFilenameSubmit} focus={true} showCursor={true} columns={columns} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} /> <TextInput
value={filename}
onChange={setFilename}
onSubmit={handleFilenameSubmit}
focus={true}
showCursor={true}
columns={columns}
cursorOffset={cursorOffset}
onChangeCursorOffset={setCursorOffset}
/>
</Box> </Box>
</Box>} </Box>
</Dialog>; )}
</Dialog>
)
} }

View File

@@ -1,115 +1,79 @@
import { c as _c } from "react/compiler-runtime"; import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/messages/messages.mjs'
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/messages/messages.mjs'; import * as React from 'react'
import * as React from 'react'; import { stripUnderlineAnsi } from 'src/components/shell/OutputLine.js'
import { stripUnderlineAnsi } from 'src/components/shell/OutputLine.js'; import { extractTag } from 'src/utils/messages.js'
import { extractTag } from 'src/utils/messages.js'; import { removeSandboxViolationTags } from 'src/utils/sandbox/sandbox-ui-utils.js'
import { removeSandboxViolationTags } from 'src/utils/sandbox/sandbox-ui-utils.js'; import { Box, Text } from '../ink.js'
import { Box, Text } from '../ink.js'; import { useShortcutDisplay } from '../keybindings/useShortcutDisplay.js'
import { useShortcutDisplay } from '../keybindings/useShortcutDisplay.js'; import { countCharInString } from '../utils/stringUtils.js'
import { countCharInString } from '../utils/stringUtils.js'; import { MessageResponse } from './MessageResponse.js'
import { MessageResponse } from './MessageResponse.js';
const MAX_RENDERED_LINES = 10; const MAX_RENDERED_LINES = 10
type Props = { type Props = {
result: ToolResultBlockParam['content']; result: ToolResultBlockParam['content']
verbose: boolean; verbose: boolean
}; }
export function FallbackToolUseErrorMessage(t0) {
const $ = _c(25); export function FallbackToolUseErrorMessage({
const { result,
result, verbose,
verbose }: Props): React.ReactNode {
} = t0; const transcriptShortcut = useShortcutDisplay(
const transcriptShortcut = useShortcutDisplay("app:toggleTranscript", "Global", "ctrl+o"); 'app:toggleTranscript',
let T0; 'Global',
let T1; 'ctrl+o',
let T2; )
let plusLines; let error: string
let t1;
let t2; if (typeof result !== 'string') {
let t3; error = 'Tool execution failed'
if ($[0] !== result || $[1] !== verbose) { } else {
let error; const extractedError = extractTag(result, 'tool_use_error') ?? result
if (typeof result !== "string") { // Remove sandbox_violations tags from error display (Claude still sees them in the tool result)
error = "Tool execution failed"; const withoutSandboxViolations = removeSandboxViolationTags(extractedError)
} else { // Strip <error> tags but keep their content (tags are for the model, not the UI)
const extractedError = extractTag(result, "tool_use_error") ?? result; const withoutErrorTags = withoutSandboxViolations.replace(/<\/?error>/g, '')
const withoutSandboxViolations = removeSandboxViolationTags(extractedError); const trimmed = withoutErrorTags.trim()
const withoutErrorTags = withoutSandboxViolations.replace(/<\/?error>/g, ""); if (!verbose && trimmed.includes('InputValidationError: ')) {
const trimmed = withoutErrorTags.trim(); error = 'Invalid tool parameters'
if (!verbose && trimmed.includes("InputValidationError: ")) { } else if (
error = "Invalid tool parameters"; trimmed.startsWith('Error: ') ||
} else { trimmed.startsWith('Cancelled: ')
if (trimmed.startsWith("Error: ") || trimmed.startsWith("Cancelled: ")) { ) {
error = trimmed; error = trimmed
} else { } else {
error = `Error: ${trimmed}`; error = `Error: ${trimmed}`
} }
} }
}
plusLines = countCharInString(error, "\n") + 1 - MAX_RENDERED_LINES; const plusLines = countCharInString(error, '\n') + 1 - MAX_RENDERED_LINES
T2 = MessageResponse;
T1 = Box; return (
t3 = "column"; <MessageResponse>
T0 = Text; <Box flexDirection="column">
t1 = "error"; <Text color="error">
t2 = stripUnderlineAnsi(verbose ? error : error.split("\n").slice(0, MAX_RENDERED_LINES).join("\n")); {stripUnderlineAnsi(
$[0] = result; verbose
$[1] = verbose; ? error
$[2] = T0; : error.split('\n').slice(0, MAX_RENDERED_LINES).join('\n'),
$[3] = T1; )}
$[4] = T2; </Text>
$[5] = plusLines; {!verbose && plusLines > 0 && (
$[6] = t1; // The careful <Text> layout is a workaround for the dim-bold
$[7] = t2; // rendering bug
$[8] = t3; <Box>
} else { <Text dimColor>
T0 = $[2]; +{plusLines} {plusLines === 1 ? 'line' : 'lines'} (
T1 = $[3]; </Text>
T2 = $[4]; <Text dimColor bold>
plusLines = $[5]; {transcriptShortcut}
t1 = $[6]; </Text>
t2 = $[7]; <Text> </Text>
t3 = $[8]; <Text dimColor>to see all)</Text>
} </Box>
let t4; )}
if ($[9] !== T0 || $[10] !== t1 || $[11] !== t2) { </Box>
t4 = <T0 color={t1}>{t2}</T0>; </MessageResponse>
$[9] = T0; )
$[10] = t1;
$[11] = t2;
$[12] = t4;
} else {
t4 = $[12];
}
let t5;
if ($[13] !== plusLines || $[14] !== transcriptShortcut || $[15] !== verbose) {
t5 = !verbose && plusLines > 0 && <Box><Text dimColor={true}> +{plusLines} {plusLines === 1 ? "line" : "lines"} (</Text><Text dimColor={true} bold={true}>{transcriptShortcut}</Text><Text> </Text><Text dimColor={true}>to see all)</Text></Box>;
$[13] = plusLines;
$[14] = transcriptShortcut;
$[15] = verbose;
$[16] = t5;
} else {
t5 = $[16];
}
let t6;
if ($[17] !== T1 || $[18] !== t3 || $[19] !== t4 || $[20] !== t5) {
t6 = <T1 flexDirection={t3}>{t4}{t5}</T1>;
$[17] = T1;
$[18] = t3;
$[19] = t4;
$[20] = t5;
$[21] = t6;
} else {
t6 = $[21];
}
let t7;
if ($[22] !== T2 || $[23] !== t6) {
t7 = <T2>{t6}</T2>;
$[22] = T2;
$[23] = t6;
$[24] = t7;
} else {
t7 = $[24];
}
return t7;
} }

View File

@@ -1,15 +1,11 @@
import { c as _c } from "react/compiler-runtime"; import * as React from 'react'
import * as React from 'react'; import { InterruptedByUser } from './InterruptedByUser.js'
import { InterruptedByUser } from './InterruptedByUser.js'; import { MessageResponse } from './MessageResponse.js'
import { MessageResponse } from './MessageResponse.js';
export function FallbackToolUseRejectedMessage() { export function FallbackToolUseRejectedMessage(): React.ReactNode {
const $ = _c(1); return (
let t0; <MessageResponse height={1}>
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { <InterruptedByUser />
t0 = <MessageResponse height={1}><InterruptedByUser /></MessageResponse>; </MessageResponse>
$[0] = t0; )
} else {
t0 = $[0];
}
return t0;
} }

View File

@@ -1,45 +1,33 @@
import { c as _c } from "react/compiler-runtime"; import chalk from 'chalk'
import chalk from 'chalk'; import * as React from 'react'
import * as React from 'react'; import { LIGHTNING_BOLT } from '../constants/figures.js'
import { LIGHTNING_BOLT } from '../constants/figures.js'; import { Text } from '../ink.js'
import { Text } from '../ink.js'; import { getGlobalConfig } from '../utils/config.js'
import { getGlobalConfig } from '../utils/config.js'; import { resolveThemeSetting } from '../utils/systemTheme.js'
import { resolveThemeSetting } from '../utils/systemTheme.js'; import { color } from './design-system/color.js'
import { color } from './design-system/color.js';
type Props = { type Props = {
cooldown?: boolean; cooldown?: boolean
};
export function FastIcon(t0) {
const $ = _c(2);
const {
cooldown
} = t0;
if (cooldown) {
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <Text color="promptBorder" dimColor={true}>{LIGHTNING_BOLT}</Text>;
$[0] = t1;
} else {
t1 = $[0];
}
return t1;
}
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <Text color="fastMode">{LIGHTNING_BOLT}</Text>;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
} }
export function FastIcon({ cooldown }: Props): React.ReactNode {
if (cooldown) {
return (
<Text color="promptBorder" dimColor>
{LIGHTNING_BOLT}
</Text>
)
}
return <Text color="fastMode">{LIGHTNING_BOLT}</Text>
}
export function getFastIconString(applyColor = true, cooldown = false): string { export function getFastIconString(applyColor = true, cooldown = false): string {
if (!applyColor) { if (!applyColor) {
return LIGHTNING_BOLT; return LIGHTNING_BOLT
} }
const themeName = resolveThemeSetting(getGlobalConfig().theme); const themeName = resolveThemeSetting(getGlobalConfig().theme)
if (cooldown) { if (cooldown) {
return chalk.dim(color('promptBorder', themeName)(LIGHTNING_BOLT)); return chalk.dim(color('promptBorder', themeName)(LIGHTNING_BOLT))
} }
return color('fastMode', themeName)(LIGHTNING_BOLT); return color('fastMode', themeName)(LIGHTNING_BOLT)
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,180 +1,180 @@
import { c as _c } from "react/compiler-runtime"; import type { StructuredPatchHunk } from 'diff'
import type { StructuredPatchHunk } from 'diff'; import * as React from 'react'
import * as React from 'react'; import { Suspense, use, useState } from 'react'
import { Suspense, use, useState } from 'react'; import { useTerminalSize } from '../hooks/useTerminalSize.js'
import { useTerminalSize } from '../hooks/useTerminalSize.js'; import { Box, Text } from '../ink.js'
import { Box, Text } from '../ink.js'; import type { FileEdit } from '../tools/FileEditTool/types.js'
import type { FileEdit } from '../tools/FileEditTool/types.js'; import {
import { findActualString, preserveQuoteStyle } from '../tools/FileEditTool/utils.js'; findActualString,
import { adjustHunkLineNumbers, CONTEXT_LINES, getPatchForDisplay } from '../utils/diff.js'; preserveQuoteStyle,
import { logError } from '../utils/log.js'; } from '../tools/FileEditTool/utils.js'
import { CHUNK_SIZE, openForScan, readCapped, scanForContext } from '../utils/readEditContext.js'; import {
import { firstLineOf } from '../utils/stringUtils.js'; adjustHunkLineNumbers,
import { StructuredDiffList } from './StructuredDiffList.js'; CONTEXT_LINES,
getPatchForDisplay,
} from '../utils/diff.js'
import { logError } from '../utils/log.js'
import {
CHUNK_SIZE,
openForScan,
readCapped,
scanForContext,
} from '../utils/readEditContext.js'
import { firstLineOf } from '../utils/stringUtils.js'
import { StructuredDiffList } from './StructuredDiffList.js'
type Props = { type Props = {
file_path: string; file_path: string
edits: FileEdit[]; edits: FileEdit[]
}; }
type DiffData = { type DiffData = {
patch: StructuredPatchHunk[]; patch: StructuredPatchHunk[]
firstLine: string | null; firstLine: string | null
fileContent: string | undefined; fileContent: string | undefined
};
export function FileEditToolDiff(props) {
const $ = _c(7);
let t0;
if ($[0] !== props.edits || $[1] !== props.file_path) {
t0 = () => loadDiffData(props.file_path, props.edits);
$[0] = props.edits;
$[1] = props.file_path;
$[2] = t0;
} else {
t0 = $[2];
}
const [dataPromise] = useState(t0);
let t1;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <DiffFrame placeholder={true} />;
$[3] = t1;
} else {
t1 = $[3];
}
let t2;
if ($[4] !== dataPromise || $[5] !== props.file_path) {
t2 = <Suspense fallback={t1}><DiffBody promise={dataPromise} file_path={props.file_path} /></Suspense>;
$[4] = dataPromise;
$[5] = props.file_path;
$[6] = t2;
} else {
t2 = $[6];
}
return t2;
} }
function DiffBody(t0: { promise: Promise<DiffData>; file_path: string }) {
const $ = _c(6); export function FileEditToolDiff(props: Props): React.ReactNode {
const { // Snapshot on mount — the diff must stay consistent even if the file changes
promise, // while the dialog is open. useMemo on props.edits would re-read the file on
file_path // every render because callers pass fresh array literals.
} = t0; const [dataPromise] = useState(() =>
const { loadDiffData(props.file_path, props.edits),
patch, )
firstLine, return (
fileContent <Suspense fallback={<DiffFrame placeholder />}>
} = use(promise); <DiffBody promise={dataPromise} file_path={props.file_path} />
const { </Suspense>
columns )
} = useTerminalSize();
let t1;
if ($[0] !== columns || $[1] !== fileContent || $[2] !== file_path || $[3] !== firstLine || $[4] !== patch) {
t1 = <DiffFrame><StructuredDiffList hunks={patch} dim={false} width={columns} filePath={file_path} firstLine={firstLine} fileContent={fileContent} /></DiffFrame>;
$[0] = columns;
$[1] = fileContent;
$[2] = file_path;
$[3] = firstLine;
$[4] = patch;
$[5] = t1;
} else {
t1 = $[5];
}
return t1;
} }
function DiffFrame(t0) {
const $ = _c(5); function DiffBody({
const { promise,
children, file_path,
placeholder }: {
} = t0; promise: Promise<DiffData>
let t1; file_path: string
if ($[0] !== children || $[1] !== placeholder) { }): React.ReactNode {
t1 = placeholder ? <Text dimColor={true}></Text> : children; const { patch, firstLine, fileContent } = use(promise)
$[0] = children; const { columns } = useTerminalSize()
$[1] = placeholder; return (
$[2] = t1; <DiffFrame>
} else { <StructuredDiffList
t1 = $[2]; hunks={patch}
} dim={false}
let t2; width={columns}
if ($[3] !== t1) { filePath={file_path}
t2 = <Box flexDirection="column"><Box borderColor="subtle" borderStyle="dashed" flexDirection="column" borderLeft={false} borderRight={false}>{t1}</Box></Box>; firstLine={firstLine}
$[3] = t1; fileContent={fileContent}
$[4] = t2; />
} else { </DiffFrame>
t2 = $[4]; )
}
return t2;
} }
async function loadDiffData(file_path: string, edits: FileEdit[]): Promise<DiffData> {
const valid = edits.filter(e => e.old_string != null && e.new_string != null); function DiffFrame({
const single = valid.length === 1 ? valid[0]! : undefined; children,
placeholder,
}: {
children?: React.ReactNode
placeholder?: boolean
}): React.ReactNode {
return (
<Box flexDirection="column">
<Box
borderColor="subtle"
borderStyle="dashed"
flexDirection="column"
borderLeft={false}
borderRight={false}
>
{placeholder ? <Text dimColor></Text> : children}
</Box>
</Box>
)
}
async function loadDiffData(
file_path: string,
edits: FileEdit[],
): Promise<DiffData> {
const valid = edits.filter(e => e.old_string != null && e.new_string != null)
const single = valid.length === 1 ? valid[0]! : undefined
// SedEditPermissionRequest passes the entire file as old_string. Scanning for // SedEditPermissionRequest passes the entire file as old_string. Scanning for
// a needle ≥ CHUNK_SIZE allocates O(needle) for the overlap buffer — skip the // a needle ≥ CHUNK_SIZE allocates O(needle) for the overlap buffer — skip the
// file read entirely and diff the inputs we already have. // file read entirely and diff the inputs we already have.
if (single && single.old_string.length >= CHUNK_SIZE) { if (single && single.old_string.length >= CHUNK_SIZE) {
return diffToolInputsOnly(file_path, [single]); return diffToolInputsOnly(file_path, [single])
} }
try { try {
const handle = await openForScan(file_path); const handle = await openForScan(file_path)
if (handle === null) return diffToolInputsOnly(file_path, valid); if (handle === null) return diffToolInputsOnly(file_path, valid)
try { try {
// Multi-edit and empty old_string genuinely need full-file for sequential // Multi-edit and empty old_string genuinely need full-file for sequential
// replacements — structuredPatch needs before/after strings. replace_all // replacements — structuredPatch needs before/after strings. replace_all
// routes through the chunked path below (shows first-occurrence window; // routes through the chunked path below (shows first-occurrence window;
// matches within the slice still replace via edit.replace_all). // matches within the slice still replace via edit.replace_all).
if (!single || single.old_string === '') { if (!single || single.old_string === '') {
const file = await readCapped(handle); const file = await readCapped(handle)
if (file === null) return diffToolInputsOnly(file_path, valid); if (file === null) return diffToolInputsOnly(file_path, valid)
const normalized = valid.map(e => normalizeEdit(file, e)); const normalized = valid.map(e => normalizeEdit(file, e))
return { return {
patch: getPatchForDisplay({ patch: getPatchForDisplay({
filePath: file_path, filePath: file_path,
fileContents: file, fileContents: file,
edits: normalized edits: normalized,
}), }),
firstLine: firstLineOf(file), firstLine: firstLineOf(file),
fileContent: file fileContent: file,
}; }
} }
const ctx = await scanForContext(handle, single.old_string, CONTEXT_LINES);
const ctx = await scanForContext(handle, single.old_string, CONTEXT_LINES)
if (ctx.truncated || ctx.content === '') { if (ctx.truncated || ctx.content === '') {
return diffToolInputsOnly(file_path, [single]); return diffToolInputsOnly(file_path, [single])
} }
const normalized = normalizeEdit(ctx.content, single); const normalized = normalizeEdit(ctx.content, single)
const hunks = getPatchForDisplay({ const hunks = getPatchForDisplay({
filePath: file_path, filePath: file_path,
fileContents: ctx.content, fileContents: ctx.content,
edits: [normalized] edits: [normalized],
}); })
return { return {
patch: adjustHunkLineNumbers(hunks, ctx.lineOffset - 1), patch: adjustHunkLineNumbers(hunks, ctx.lineOffset - 1),
firstLine: ctx.lineOffset === 1 ? firstLineOf(ctx.content) : null, firstLine: ctx.lineOffset === 1 ? firstLineOf(ctx.content) : null,
fileContent: ctx.content fileContent: ctx.content,
}; }
} finally { } finally {
await handle.close(); await handle.close()
} }
} catch (e) { } catch (e) {
logError(e as Error); logError(e as Error)
return diffToolInputsOnly(file_path, valid); return diffToolInputsOnly(file_path, valid)
} }
} }
function diffToolInputsOnly(filePath: string, edits: FileEdit[]): DiffData { function diffToolInputsOnly(filePath: string, edits: FileEdit[]): DiffData {
return { return {
patch: edits.flatMap(e => getPatchForDisplay({ patch: edits.flatMap(e =>
filePath, getPatchForDisplay({
fileContents: e.old_string, filePath,
edits: [e] fileContents: e.old_string,
})), edits: [e],
}),
),
firstLine: null, firstLine: null,
fileContent: undefined fileContent: undefined,
}; }
} }
function normalizeEdit(fileContent: string, edit: FileEdit): FileEdit { function normalizeEdit(fileContent: string, edit: FileEdit): FileEdit {
const actualOld = findActualString(fileContent, edit.old_string) || edit.old_string; const actualOld =
const actualNew = preserveQuoteStyle(edit.old_string, actualOld, edit.new_string); findActualString(fileContent, edit.old_string) || edit.old_string
return { const actualNew = preserveQuoteStyle(
...edit, edit.old_string,
old_string: actualOld, actualOld,
new_string: actualNew edit.new_string,
}; )
return { ...edit, old_string: actualOld, new_string: actualNew }
} }

View File

@@ -1,123 +1,86 @@
import { c as _c } from "react/compiler-runtime"; import type { StructuredPatchHunk } from 'diff'
import type { StructuredPatchHunk } from 'diff'; import * as React from 'react'
import * as React from 'react'; import { useTerminalSize } from '../hooks/useTerminalSize.js'
import { useTerminalSize } from '../hooks/useTerminalSize.js'; import { Box, Text } from '../ink.js'
import { Box, Text } from '../ink.js'; import { count } from '../utils/array.js'
import { count } from '../utils/array.js'; import { MessageResponse } from './MessageResponse.js'
import { MessageResponse } from './MessageResponse.js'; import { StructuredDiffList } from './StructuredDiffList.js'
import { StructuredDiffList } from './StructuredDiffList.js';
type Props = { type Props = {
filePath: string; filePath: string
structuredPatch: StructuredPatchHunk[]; structuredPatch: StructuredPatchHunk[]
firstLine: string | null; firstLine: string | null
fileContent?: string; fileContent?: string
style?: 'condensed'; style?: 'condensed'
verbose: boolean; verbose: boolean
previewHint?: string; previewHint?: string
}; }
export function FileEditToolUpdatedMessage(t0) {
const $ = _c(22); export function FileEditToolUpdatedMessage({
const { filePath,
filePath, structuredPatch,
structuredPatch, firstLine,
firstLine, fileContent,
fileContent, style,
style, verbose,
verbose, previewHint,
previewHint }: Props): React.ReactNode {
} = t0; const { columns } = useTerminalSize()
const { const numAdditions = structuredPatch.reduce(
columns (acc, hunk) => acc + count(hunk.lines, _ => _.startsWith('+')),
} = useTerminalSize(); 0,
const numAdditions = structuredPatch.reduce(_temp2, 0); )
const numRemovals = structuredPatch.reduce(_temp4, 0); const numRemovals = structuredPatch.reduce(
let t1; (acc, hunk) => acc + count(hunk.lines, _ => _.startsWith('-')),
if ($[0] !== numAdditions) { 0,
t1 = numAdditions > 0 ? <>Added <Text bold={true}>{numAdditions}</Text>{" "}{numAdditions > 1 ? "lines" : "line"}</> : null; )
$[0] = numAdditions;
$[1] = t1; const text = (
} else { <Text>
t1 = $[1]; {numAdditions > 0 ? (
} <>
const t2 = numAdditions > 0 && numRemovals > 0 ? ", " : null; Added <Text bold>{numAdditions}</Text>{' '}
let t3; {numAdditions > 1 ? 'lines' : 'line'}
if ($[2] !== numAdditions || $[3] !== numRemovals) { </>
t3 = numRemovals > 0 ? <>{numAdditions === 0 ? "R" : "r"}emoved <Text bold={true}>{numRemovals}</Text>{" "}{numRemovals > 1 ? "lines" : "line"}</> : null; ) : null}
$[2] = numAdditions; {numAdditions > 0 && numRemovals > 0 ? ', ' : null}
$[3] = numRemovals; {numRemovals > 0 ? (
$[4] = t3; <>
} else { {numAdditions === 0 ? 'R' : 'r'}emoved <Text bold>{numRemovals}</Text>{' '}
t3 = $[4]; {numRemovals > 1 ? 'lines' : 'line'}
} </>
let t4; ) : null}
if ($[5] !== t1 || $[6] !== t2 || $[7] !== t3) { </Text>
t4 = <Text>{t1}{t2}{t3}</Text>; )
$[5] = t1;
$[6] = t2; // Plan files: invert condensed behavior
$[7] = t3; // - Regular mode: just show the hint (user can type /plan to see full content)
$[8] = t4; // - Condensed mode (subagent view): show the diff
} else {
t4 = $[8];
}
const text = t4;
if (previewHint) { if (previewHint) {
if (style !== "condensed" && !verbose) { if (style !== 'condensed' && !verbose) {
let t5; return (
if ($[9] !== previewHint) { <MessageResponse>
t5 = <MessageResponse><Text dimColor={true}>{previewHint}</Text></MessageResponse>; <Text dimColor>{previewHint}</Text>
$[9] = previewHint; </MessageResponse>
$[10] = t5; )
} else {
t5 = $[10];
}
return t5;
}
} else {
if (style === "condensed" && !verbose) {
return text;
} }
} else if (style === 'condensed' && !verbose) {
return text
} }
let t5;
if ($[11] !== text) { return (
t5 = <Text>{text}</Text>; <MessageResponse>
$[11] = text; <Box flexDirection="column">
$[12] = t5; <Text>{text}</Text>
} else { <StructuredDiffList
t5 = $[12]; hunks={structuredPatch}
} dim={false}
const t6 = columns - 12; width={columns - 12}
let t7; filePath={filePath}
if ($[13] !== fileContent || $[14] !== filePath || $[15] !== firstLine || $[16] !== structuredPatch || $[17] !== t6) { firstLine={firstLine}
t7 = <StructuredDiffList hunks={structuredPatch} dim={false} width={t6} filePath={filePath} firstLine={firstLine} fileContent={fileContent} />; fileContent={fileContent}
$[13] = fileContent; />
$[14] = filePath; </Box>
$[15] = firstLine; </MessageResponse>
$[16] = structuredPatch; )
$[17] = t6;
$[18] = t7;
} else {
t7 = $[18];
}
let t8;
if ($[19] !== t5 || $[20] !== t7) {
t8 = <MessageResponse><Box flexDirection="column">{t5}{t7}</Box></MessageResponse>;
$[19] = t5;
$[20] = t7;
$[21] = t8;
} else {
t8 = $[21];
}
return t8;
}
function _temp4(acc_0, hunk_0) {
return acc_0 + count(hunk_0.lines, _temp3);
}
function _temp3(__0) {
return __0.startsWith("-");
}
function _temp2(acc, hunk) {
return acc + count(hunk.lines, _temp);
}
function _temp(_) {
return _.startsWith("+");
} }

View File

@@ -1,169 +1,98 @@
import { c as _c } from "react/compiler-runtime"; import type { StructuredPatchHunk } from 'diff'
import type { StructuredPatchHunk } from 'diff'; import { relative } from 'path'
import { relative } from 'path'; import * as React from 'react'
import * as React from 'react'; import { useTerminalSize } from 'src/hooks/useTerminalSize.js'
import { useTerminalSize } from 'src/hooks/useTerminalSize.js'; import { getCwd } from 'src/utils/cwd.js'
import { getCwd } from 'src/utils/cwd.js'; import { Box, Text } from '../ink.js'
import { Box, Text } from '../ink.js'; import { HighlightedCode } from './HighlightedCode.js'
import { HighlightedCode } from './HighlightedCode.js'; import { MessageResponse } from './MessageResponse.js'
import { MessageResponse } from './MessageResponse.js'; import { StructuredDiffList } from './StructuredDiffList.js'
import { StructuredDiffList } from './StructuredDiffList.js';
const MAX_LINES_TO_RENDER = 10; const MAX_LINES_TO_RENDER = 10
type Props = { type Props = {
file_path: string; file_path: string
operation: 'write' | 'update'; operation: 'write' | 'update'
// For updates - show diff // For updates - show diff
patch?: StructuredPatchHunk[]; patch?: StructuredPatchHunk[]
firstLine: string | null; firstLine: string | null
fileContent?: string; fileContent?: string
// For new file creation - show content preview // For new file creation - show content preview
content?: string; content?: string
style?: 'condensed'; style?: 'condensed'
verbose: boolean; verbose: boolean
}; }
export function FileEditToolUseRejectedMessage(t0) {
const $ = _c(38); export function FileEditToolUseRejectedMessage({
const { file_path,
file_path, operation,
operation, patch,
patch, firstLine,
firstLine, fileContent,
fileContent, content,
content, style,
style, verbose,
verbose }: Props): React.ReactNode {
} = t0; const { columns } = useTerminalSize()
const { const text = (
columns <Box flexDirection="row">
} = useTerminalSize(); <Text color="subtle">User rejected {operation} to </Text>
let t1; <Text bold color="subtle">
if ($[0] !== operation) { {verbose ? file_path : relative(getCwd(), file_path)}
t1 = <Text color="subtle">User rejected {operation} to </Text>; </Text>
$[0] = operation; </Box>
$[1] = t1; )
} else {
t1 = $[1]; // For condensed style, just show the text
} if (style === 'condensed' && !verbose) {
let t2; return <MessageResponse>{text}</MessageResponse>
if ($[2] !== file_path || $[3] !== verbose) { }
t2 = verbose ? file_path : relative(getCwd(), file_path);
$[2] = file_path; // For new file creation, show content preview (dimmed)
$[3] = verbose; if (operation === 'write' && content !== undefined) {
$[4] = t2; const lines = content.split('\n')
} else { const numLines = lines.length
t2 = $[4]; const plusLines = numLines - MAX_LINES_TO_RENDER
} const truncatedContent = verbose
let t3; ? content
if ($[5] !== t2) { : lines.slice(0, MAX_LINES_TO_RENDER).join('\n')
t3 = <Text bold={true} color="subtle">{t2}</Text>;
$[5] = t2; return (
$[6] = t3; <MessageResponse>
} else { <Box flexDirection="column">
t3 = $[6]; {text}
} <HighlightedCode
let t4; code={truncatedContent || '(No content)'}
if ($[7] !== t1 || $[8] !== t3) { filePath={file_path}
t4 = <Box flexDirection="row">{t1}{t3}</Box>; width={columns - 12}
$[7] = t1; dim
$[8] = t3; />
$[9] = t4; {!verbose && plusLines > 0 && (
} else { <Text dimColor> +{plusLines} lines</Text>
t4 = $[9]; )}
} </Box>
const text = t4; </MessageResponse>
if (style === "condensed" && !verbose) { )
let t5; }
if ($[10] !== text) {
t5 = <MessageResponse>{text}</MessageResponse>; // For updates, show diff
$[10] = text; if (!patch || patch.length === 0) {
$[11] = t5; return <MessageResponse>{text}</MessageResponse>
} else { }
t5 = $[11];
} return (
return t5; <MessageResponse>
} <Box flexDirection="column">
if (operation === "write" && content !== undefined) { {text}
let plusLines; <StructuredDiffList
let t5; hunks={patch}
if ($[12] !== content || $[13] !== verbose) { dim
const lines = content.split("\n"); width={columns - 12}
const numLines = lines.length; filePath={file_path}
plusLines = numLines - MAX_LINES_TO_RENDER; firstLine={firstLine}
t5 = verbose ? content : lines.slice(0, MAX_LINES_TO_RENDER).join("\n"); fileContent={fileContent}
$[12] = content; />
$[13] = verbose; </Box>
$[14] = plusLines; </MessageResponse>
$[15] = t5; )
} else {
plusLines = $[14];
t5 = $[15];
}
const truncatedContent = t5;
const t6 = truncatedContent || "(No content)";
const t7 = columns - 12;
let t8;
if ($[16] !== file_path || $[17] !== t6 || $[18] !== t7) {
t8 = <HighlightedCode code={t6} filePath={file_path} width={t7} dim={true} />;
$[16] = file_path;
$[17] = t6;
$[18] = t7;
$[19] = t8;
} else {
t8 = $[19];
}
let t9;
if ($[20] !== plusLines || $[21] !== verbose) {
t9 = !verbose && plusLines > 0 && <Text dimColor={true}> +{plusLines} lines</Text>;
$[20] = plusLines;
$[21] = verbose;
$[22] = t9;
} else {
t9 = $[22];
}
let t10;
if ($[23] !== t8 || $[24] !== t9 || $[25] !== text) {
t10 = <MessageResponse><Box flexDirection="column">{text}{t8}{t9}</Box></MessageResponse>;
$[23] = t8;
$[24] = t9;
$[25] = text;
$[26] = t10;
} else {
t10 = $[26];
}
return t10;
}
if (!patch || patch.length === 0) {
let t5;
if ($[27] !== text) {
t5 = <MessageResponse>{text}</MessageResponse>;
$[27] = text;
$[28] = t5;
} else {
t5 = $[28];
}
return t5;
}
const t5 = columns - 12;
let t6;
if ($[29] !== fileContent || $[30] !== file_path || $[31] !== firstLine || $[32] !== patch || $[33] !== t5) {
t6 = <StructuredDiffList hunks={patch} dim={true} width={t5} filePath={file_path} firstLine={firstLine} fileContent={fileContent} />;
$[29] = fileContent;
$[30] = file_path;
$[31] = firstLine;
$[32] = patch;
$[33] = t5;
$[34] = t6;
} else {
t6 = $[34];
}
let t7;
if ($[35] !== t6 || $[36] !== text) {
t7 = <MessageResponse><Box flexDirection="column">{text}{t6}</Box></MessageResponse>;
$[35] = t6;
$[36] = text;
$[37] = t7;
} else {
t7 = $[37];
}
return t7;
} }

View File

@@ -1,42 +1,19 @@
import { c as _c } from "react/compiler-runtime"; import React from 'react'
import React from 'react'; import { pathToFileURL } from 'url'
import { pathToFileURL } from 'url'; import Link from '../ink/components/Link.js'
import Link from '../ink/components/Link.js';
type Props = { type Props = {
/** The absolute file path */ /** The absolute file path */
filePath: string; filePath: string
/** Optional display text (defaults to filePath) */ /** Optional display text (defaults to filePath) */
children?: React.ReactNode; children?: React.ReactNode
}; }
/** /**
* Renders a file path as an OSC 8 hyperlink. * Renders a file path as an OSC 8 hyperlink.
* This helps terminals like iTerm correctly identify file paths * This helps terminals like iTerm correctly identify file paths
* even when they appear inside parentheses or other text. * even when they appear inside parentheses or other text.
*/ */
export function FilePathLink(t0) { export function FilePathLink({ filePath, children }: Props): React.ReactNode {
const $ = _c(5); return <Link url={pathToFileURL(filePath).href}>{children ?? filePath}</Link>
const {
filePath,
children
} = t0;
let t1;
if ($[0] !== filePath) {
t1 = pathToFileURL(filePath);
$[0] = filePath;
$[1] = t1;
} else {
t1 = $[1];
}
const t2 = children ?? filePath;
let t3;
if ($[2] !== t1.href || $[3] !== t2) {
t3 = <Link url={t1.href}>{t2}</Link>;
$[2] = t1.href;
$[3] = t2;
$[4] = t3;
} else {
t3 = $[4];
}
return t3;
} }

View File

@@ -1,70 +1,83 @@
import { c as _c } from "react/compiler-runtime"; import figures from 'figures'
import figures from 'figures'; import React, {
import React, { createContext, type ReactNode, type RefObject, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'; createContext,
import { fileURLToPath } from 'url'; type ReactNode,
import { ModalContext } from '../context/modalContext.js'; type RefObject,
import { PromptOverlayProvider, usePromptOverlay, usePromptOverlayDialog } from '../context/promptOverlayContext.js'; useCallback,
import { useTerminalSize } from '../hooks/useTerminalSize.js'; useEffect,
import ScrollBox, { type ScrollBoxHandle } from '../ink/components/ScrollBox.js'; useLayoutEffect,
import instances from '../ink/instances.js'; useMemo,
import { Box, Text } from '../ink.js'; useRef,
import type { Message } from '../types/message.js'; useState,
import { openBrowser, openPath } from '../utils/browser.js'; useSyncExternalStore,
import { isFullscreenEnvEnabled } from '../utils/fullscreen.js'; } from 'react'
import { plural } from '../utils/stringUtils.js'; import { fileURLToPath } from 'url'
import { isNullRenderingAttachment } from './messages/nullRenderingAttachments.js'; import { ModalContext } from '../context/modalContext.js'
import PromptInputFooterSuggestions from './PromptInput/PromptInputFooterSuggestions.js'; import {
import type { StickyPrompt } from './VirtualMessageList.js'; PromptOverlayProvider,
usePromptOverlay,
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 type { Message } from '../types/message.js'
import { openBrowser, openPath } from '../utils/browser.js'
import { isFullscreenEnvEnabled } from '../utils/fullscreen.js'
import { plural } from '../utils/stringUtils.js'
import { isNullRenderingAttachment } from './messages/nullRenderingAttachments.js'
import PromptInputFooterSuggestions from './PromptInput/PromptInputFooterSuggestions.js'
import type { StickyPrompt } from './VirtualMessageList.js'
/** Rows of transcript context kept visible above the modal pane's ▔ divider. */ /** Rows of transcript context kept visible above the modal pane's ▔ divider. */
const MODAL_TRANSCRIPT_PEEK = 2; const MODAL_TRANSCRIPT_PEEK = 2
/** Context for scroll-derived chrome (sticky header, pill). StickyTracker /** Context for scroll-derived chrome (sticky header, pill). StickyTracker
* in VirtualMessageList writes via this instead of threading a callback * in VirtualMessageList writes via this instead of threading a callback
* up through Messages → REPL → FullscreenLayout. The setter is stable so * up through Messages → REPL → FullscreenLayout. The setter is stable so
* consuming this context never causes re-renders. */ * consuming this context never causes re-renders. */
export const ScrollChromeContext = createContext<{ export const ScrollChromeContext = createContext<{
setStickyPrompt: (p: StickyPrompt | null) => void; setStickyPrompt: (p: StickyPrompt | null) => void
}>({ }>({ setStickyPrompt: () => {} })
setStickyPrompt: () => {}
});
type Props = { type Props = {
/** Content that scrolls (messages, tool output) */ /** Content that scrolls (messages, tool output) */
scrollable: ReactNode; scrollable: ReactNode
/** Content pinned to the bottom (spinner, prompt, permissions) */ /** Content pinned to the bottom (spinner, prompt, permissions) */
bottom: ReactNode; bottom: ReactNode
/** Content rendered inside the ScrollBox after messages — user can scroll /** Content rendered inside the ScrollBox after messages — user can scroll
* up to see context while it's showing (used by PermissionRequest). */ * up to see context while it's showing (used by PermissionRequest). */
overlay?: ReactNode; overlay?: ReactNode
/** Absolute-positioned content anchored at the bottom-right of the /** Absolute-positioned content anchored at the bottom-right of the
* ScrollBox area, floating over scrollback. Rendered inside the flexGrow * ScrollBox area, floating over scrollback. Rendered inside the flexGrow
* region (not the bottom slot) so the overflowY:hidden cap doesn't clip * region (not the bottom slot) so the overflowY:hidden cap doesn't clip
* it. Fullscreen only — used for the companion speech bubble. */ * it. Fullscreen only — used for the companion speech bubble. */
bottomFloat?: ReactNode; bottomFloat?: ReactNode
/** Slash-command dialog content. Rendered in an absolute-positioned /** Slash-command dialog content. Rendered in an absolute-positioned
* bottom-anchored pane (▔ divider, paddingX=2) that paints over the * bottom-anchored pane (▔ divider, paddingX=2) that paints over the
* ScrollBox AND bottom slot. Provides ModalContext so Pane/Dialog inside * ScrollBox AND bottom slot. Provides ModalContext so Pane/Dialog inside
* skip their own frame. Fullscreen only; inline after overlay otherwise. */ * skip their own frame. Fullscreen only; inline after overlay otherwise. */
modal?: ReactNode; modal?: ReactNode
/** Ref passed via ModalContext so Tabs (or any scroll-owning descendant) /** Ref passed via ModalContext so Tabs (or any scroll-owning descendant)
* can attach it to their own ScrollBox for tall content. */ * can attach it to their own ScrollBox for tall content. */
modalScrollRef?: React.RefObject<ScrollBoxHandle | null>; modalScrollRef?: React.RefObject<ScrollBoxHandle | null>
/** Ref to the scroll box for keyboard scrolling. RefObject (not Ref) so /** Ref to the scroll box for keyboard scrolling. RefObject (not Ref) so
* pillVisible's useSyncExternalStore can subscribe to scroll changes. */ * pillVisible's useSyncExternalStore can subscribe to scroll changes. */
scrollRef?: RefObject<ScrollBoxHandle | null>; scrollRef?: RefObject<ScrollBoxHandle | null>
/** Y-position (scrollHeight at snapshot) of the unseen-divider. Pill /** Y-position (scrollHeight at snapshot) of the unseen-divider. Pill
* shows while viewport bottom hasn't reached this. Ref so REPL doesn't * shows while viewport bottom hasn't reached this. Ref so REPL doesn't
* re-render on the one-shot snapshot write. */ * re-render on the one-shot snapshot write. */
dividerYRef?: RefObject<number | null>; dividerYRef?: RefObject<number | null>
/** Force-hide the pill (e.g. viewing a sub-agent task). */ /** Force-hide the pill (e.g. viewing a sub-agent task). */
hidePill?: boolean; hidePill?: boolean
/** Force-hide the sticky prompt header (e.g. viewing a teammate task). */ /** Force-hide the sticky prompt header (e.g. viewing a teammate task). */
hideSticky?: boolean; hideSticky?: boolean
/** Count for the pill text. 0 → "Jump to bottom", >0 → "N new messages". */ /** Count for the pill text. 0 → "Jump to bottom", >0 → "N new messages". */
newMessageCount?: number; newMessageCount?: number
/** Called when the user clicks the "N new" pill. */ /** Called when the user clicks the "N new" pill. */
onPillClick?: () => void; onPillClick?: () => void
}; }
/** /**
* Tracks the in-transcript "N new messages" divider position while the * Tracks the in-transcript "N new messages" divider position while the
@@ -87,41 +100,43 @@ export function useUnseenDivider(messageCount: number): {
/** Index into messages[] where the divider line renders. Cleared on /** Index into messages[] where the divider line renders. Cleared on
* sticky-resume (scroll back to bottom) so the "N new" line doesn't * sticky-resume (scroll back to bottom) so the "N new" line doesn't
* linger once everything is visible. */ * linger once everything is visible. */
dividerIndex: number | null; dividerIndex: number | null
/** scrollHeight snapshot at first scroll-away — the divider's y-position. /** scrollHeight snapshot at first scroll-away — the divider's y-position.
* FullscreenLayout subscribes to ScrollBox and compares viewport bottom * FullscreenLayout subscribes to ScrollBox and compares viewport bottom
* against this for pillVisible. Ref so writes don't re-render REPL. */ * against this for pillVisible. Ref so writes don't re-render REPL. */
dividerYRef: RefObject<number | null>; dividerYRef: RefObject<number | null>
onScrollAway: (handle: ScrollBoxHandle) => void; onScrollAway: (handle: ScrollBoxHandle) => void
onRepin: () => void; onRepin: () => void
/** Scroll the handle so the divider line is at the top of the viewport. */ /** Scroll the handle so the divider line is at the top of the viewport. */
jumpToNew: (handle: ScrollBoxHandle | null) => void; jumpToNew: (handle: ScrollBoxHandle | null) => void
/** Shift dividerIndex and dividerYRef when messages are prepended /** Shift dividerIndex and dividerYRef when messages are prepended
* (infinite scroll-back). indexDelta = number of messages prepended; * (infinite scroll-back). indexDelta = number of messages prepended;
* heightDelta = content height growth in rows. */ * heightDelta = content height growth in rows. */
shiftDivider: (indexDelta: number, heightDelta: number) => void; shiftDivider: (indexDelta: number, heightDelta: number) => void
} { } {
const [dividerIndex, setDividerIndex] = useState<number | null>(null); const [dividerIndex, setDividerIndex] = useState<number | null>(null)
// Ref holds the current count for onScrollAway to snapshot. Written in // Ref holds the current count for onScrollAway to snapshot. Written in
// the render body (not useEffect) so wheel events arriving between a // the render body (not useEffect) so wheel events arriving between a
// message-append render and its effect flush don't capture a stale // message-append render and its effect flush don't capture a stale
// count (off-by-one in the baseline). React Compiler bails out here — // count (off-by-one in the baseline). React Compiler bails out here —
// acceptable for a hook instantiated once in REPL. // acceptable for a hook instantiated once in REPL.
const countRef = useRef(messageCount); const countRef = useRef(messageCount)
countRef.current = messageCount; countRef.current = messageCount
// scrollHeight snapshot — the divider's y in content coords. Ref-only: // scrollHeight snapshot — the divider's y in content coords. Ref-only:
// read synchronously in onScrollAway (setState is batched, can't // read synchronously in onScrollAway (setState is batched, can't
// read-then-write in the same callback) AND by FullscreenLayout's // read-then-write in the same callback) AND by FullscreenLayout's
// pillVisible subscription. null = pinned to bottom. // pillVisible subscription. null = pinned to bottom.
const dividerYRef = useRef<number | null>(null); const dividerYRef = useRef<number | null>(null)
const onRepin = useCallback(() => { const onRepin = useCallback(() => {
// Don't clear dividerYRef here — a trackpad momentum wheel event // Don't clear dividerYRef here — a trackpad momentum wheel event
// racing in the same stdin batch would see null and re-snapshot, // racing in the same stdin batch would see null and re-snapshot,
// overriding the setDividerIndex(null) below. The useEffect below // overriding the setDividerIndex(null) below. The useEffect below
// clears the ref after React commits the null dividerIndex, so the // clears the ref after React commits the null dividerIndex, so the
// ref stays non-null until the state settles. // ref stays non-null until the state settles.
setDividerIndex(null); setDividerIndex(null)
}, []); }, [])
const onScrollAway = useCallback((handle: ScrollBoxHandle) => { const onScrollAway = useCallback((handle: ScrollBoxHandle) => {
// Nothing below the viewport → nothing to jump to. Covers both: // Nothing below the viewport → nothing to jump to. Covers both:
// • empty/short session: scrollUp calls scrollTo(0) which breaks sticky // • empty/short session: scrollUp calls scrollTo(0) which breaks sticky
@@ -132,20 +147,24 @@ export function useUnseenDivider(messageCount: number): {
// at max (Sarah Deaton, #claude-code-feedback 2026-03-15) // at max (Sarah Deaton, #claude-code-feedback 2026-03-15)
// pendingDelta: scrollBy accumulates without updating scrollTop. Without // pendingDelta: scrollBy accumulates without updating scrollTop. Without
// it, wheeling up from max would see scrollTop==max and suppress the pill. // it, wheeling up from max would see scrollTop==max and suppress the pill.
const max = Math.max(0, handle.getScrollHeight() - handle.getViewportHeight()); const max = Math.max(
if (handle.getScrollTop() + handle.getPendingDelta() >= max) return; 0,
handle.getScrollHeight() - handle.getViewportHeight(),
)
if (handle.getScrollTop() + handle.getPendingDelta() >= max) return
// Snapshot only on the FIRST scroll-away. onScrollAway fires on EVERY // Snapshot only on the FIRST scroll-away. onScrollAway fires on EVERY
// scroll action (not just the initial break from sticky) — this guard // scroll action (not just the initial break from sticky) — this guard
// preserves the original baseline so the count doesn't reset on the // preserves the original baseline so the count doesn't reset on the
// second PageUp. Subsequent calls are ref-only no-ops (no REPL re-render). // second PageUp. Subsequent calls are ref-only no-ops (no REPL re-render).
if (dividerYRef.current === null) { if (dividerYRef.current === null) {
dividerYRef.current = handle.getScrollHeight(); dividerYRef.current = handle.getScrollHeight()
// New scroll-away session → move the divider here (replaces old one) // New scroll-away session → move the divider here (replaces old one)
setDividerIndex(countRef.current); setDividerIndex(countRef.current)
} }
}, []); }, [])
const jumpToNew = useCallback((handle_0: ScrollBoxHandle | null) => {
if (!handle_0) return; const jumpToNew = useCallback((handle: ScrollBoxHandle | null) => {
if (!handle) return
// scrollToBottom (not scrollTo(dividerY)): sets stickyScroll=true so // scrollToBottom (not scrollTo(dividerY)): sets stickyScroll=true so
// useVirtualScroll mounts the tail and render-node-to-output pins // useVirtualScroll mounts the tail and render-node-to-output pins
// scrollTop=maxScroll. scrollTo sets stickyScroll=false → the clamp // scrollTop=maxScroll. scrollTo sets stickyScroll=false → the clamp
@@ -153,8 +172,8 @@ export function useUnseenDivider(messageCount: number): {
// back, stopping short. The divider stays rendered (dividerIndex // back, stopping short. The divider stays rendered (dividerIndex
// unchanged) so users see where new messages started; the clear on // unchanged) so users see where new messages started; the clear on
// next submit/explicit scroll-to-bottom handles cleanup. // next submit/explicit scroll-to-bottom handles cleanup.
handle_0.scrollToBottom(); handle.scrollToBottom()
}, []); }, [])
// Sync dividerYRef with dividerIndex. When onRepin fires (submit, // Sync dividerYRef with dividerIndex. When onRepin fires (submit,
// scroll-to-bottom), it sets dividerIndex=null but leaves the ref // scroll-to-bottom), it sets dividerIndex=null but leaves the ref
@@ -167,26 +186,31 @@ export function useUnseenDivider(messageCount: number): {
// below the divider index, the divider would point at nothing. // below the divider index, the divider would point at nothing.
useEffect(() => { useEffect(() => {
if (dividerIndex === null) { if (dividerIndex === null) {
dividerYRef.current = null; dividerYRef.current = null
} else if (messageCount < dividerIndex) { } else if (messageCount < dividerIndex) {
dividerYRef.current = null; dividerYRef.current = null
setDividerIndex(null); setDividerIndex(null)
} }
}, [messageCount, dividerIndex]); }, [messageCount, dividerIndex])
const shiftDivider = useCallback((indexDelta: number, heightDelta: number) => {
setDividerIndex(idx => idx === null ? null : idx + indexDelta); const shiftDivider = useCallback(
if (dividerYRef.current !== null) { (indexDelta: number, heightDelta: number) => {
dividerYRef.current += heightDelta; setDividerIndex(idx => (idx === null ? null : idx + indexDelta))
} if (dividerYRef.current !== null) {
}, []); dividerYRef.current += heightDelta
}
},
[],
)
return { return {
dividerIndex, dividerIndex,
dividerYRef, dividerYRef,
onScrollAway, onScrollAway,
onRepin, onRepin,
jumpToNew, jumpToNew,
shiftDivider shiftDivider,
}; }
} }
/** /**
@@ -197,35 +221,37 @@ export function useUnseenDivider(messageCount: number): {
* carry text — tool-use-only entries are skipped (like progress messages) * carry text — tool-use-only entries are skipped (like progress messages)
* so "⏺ Searched for 13 patterns, read 6 files" doesn't tick the pill. * so "⏺ Searched for 13 patterns, read 6 files" doesn't tick the pill.
*/ */
export function countUnseenAssistantTurns(messages: readonly Message[], dividerIndex: number): number { export function countUnseenAssistantTurns(
let count = 0; messages: readonly Message[],
let prevWasAssistant = false; dividerIndex: number,
): number {
let count = 0
let prevWasAssistant = false
for (let i = dividerIndex; i < messages.length; i++) { for (let i = dividerIndex; i < messages.length; i++) {
const m = messages[i]!; const m = messages[i]!
if (m.type === 'progress') continue; if (m.type === 'progress') continue
// Tool-use-only assistant entries aren't "new messages" to the user — // Tool-use-only assistant entries aren't "new messages" to the user —
// skip them the same way we skip progress. prevWasAssistant is NOT // skip them the same way we skip progress. prevWasAssistant is NOT
// updated, so a text block immediately following still counts as the // updated, so a text block immediately following still counts as the
// same turn (tool_use + text from one API response = 1). // same turn (tool_use + text from one API response = 1).
if (m.type === 'assistant' && !assistantHasVisibleText(m)) continue; if (m.type === 'assistant' && !assistantHasVisibleText(m)) continue
const isAssistant = m.type === 'assistant'; const isAssistant = m.type === 'assistant'
if (isAssistant && !prevWasAssistant) count++; if (isAssistant && !prevWasAssistant) count++
prevWasAssistant = isAssistant; prevWasAssistant = isAssistant
} }
return count; return count
} }
function assistantHasVisibleText(m: Message): boolean { function assistantHasVisibleText(m: Message): boolean {
if (m.type !== 'assistant') return false; if (m.type !== 'assistant') return false
if (!Array.isArray(m.message.content)) return false; if (!Array.isArray(m.message.content)) return false
for (const b of m.message.content) { for (const b of m.message.content) {
if (typeof b !== 'string' && b.type === 'text' && b.text.trim() !== '') return true; if (typeof b !== 'string' && b.type === 'text' && b.text.trim() !== '') return true
} }
return false; return false
} }
export type UnseenDivider = {
firstUnseenUuid: Message['uuid']; export type UnseenDivider = { firstUnseenUuid: Message['uuid']; count: number }
count: number;
};
/** /**
* Builds the unseenDivider object REPL passes to Messages + the pill. * Builds the unseenDivider object REPL passes to Messages + the pill.
@@ -237,23 +263,27 @@ export type UnseenDivider = {
* the pill stays "Jump to bottom" through an entire tool-call sequence * the pill stays "Jump to bottom" through an entire tool-call sequence
* until Claude's text response lands. * until Claude's text response lands.
*/ */
export function computeUnseenDivider(messages: readonly Message[], dividerIndex: number | null): UnseenDivider | undefined { export function computeUnseenDivider(
if (dividerIndex === null) return undefined; messages: readonly Message[],
dividerIndex: number | null,
): UnseenDivider | undefined {
if (dividerIndex === null) return undefined
// Skip progress and null-rendering attachments when picking the divider // Skip progress and null-rendering attachments when picking the divider
// anchor — Messages.tsx filters these out of renderableMessages before the // anchor — Messages.tsx filters these out of renderableMessages before the
// dividerBeforeIndex search, so their UUID wouldn't be found (CC-724). // dividerBeforeIndex search, so their UUID wouldn't be found (CC-724).
// Hook attachments use randomUUID() so nothing shares their 24-char prefix. // Hook attachments use randomUUID() so nothing shares their 24-char prefix.
let anchorIdx = dividerIndex; let anchorIdx = dividerIndex
while (anchorIdx < messages.length && (messages[anchorIdx]?.type === 'progress' || isNullRenderingAttachment(messages[anchorIdx]!))) { while (
anchorIdx++; anchorIdx < messages.length &&
(messages[anchorIdx]?.type === 'progress' ||
isNullRenderingAttachment(messages[anchorIdx]!))
) {
anchorIdx++
} }
const uuid = messages[anchorIdx]?.uuid; const uuid = messages[anchorIdx]?.uuid
if (!uuid) return undefined; if (!uuid) return undefined
const count = countUnseenAssistantTurns(messages, dividerIndex); const count = countUnseenAssistantTurns(messages, dividerIndex)
return { return { firstUnseenUuid: uuid, count: Math.max(1, count) }
firstUnseenUuid: uuid,
count: Math.max(1, count)
};
} }
/** /**
@@ -268,195 +298,198 @@ export function computeUnseenDivider(messages: readonly Message[], dividerIndex:
* (alt buffer + mouse tracking + height constraint) lives at REPL's root * (alt buffer + mouse tracking + height constraint) lives at REPL's root
* so nothing can accidentally render outside it. * so nothing can accidentally render outside it.
*/ */
export function FullscreenLayout(t0) { export function FullscreenLayout({
const $ = _c(47); scrollable,
const { bottom,
scrollable, overlay,
bottom, bottomFloat,
overlay, modal,
bottomFloat, modalScrollRef,
modal, scrollRef,
modalScrollRef, dividerYRef,
scrollRef, hidePill = false,
dividerYRef, hideSticky = false,
hidePill: t1, newMessageCount = 0,
hideSticky: t2, onPillClick,
newMessageCount: t3, }: Props): React.ReactNode {
onPillClick const { rows: terminalRows, columns } = useTerminalSize()
} = t0; // Scroll-derived chrome state lives HERE, not in REPL. StickyTracker
const hidePill = t1 === undefined ? false : t1; // writes via ScrollChromeContext; pillVisible subscribes directly to
const hideSticky = t2 === undefined ? false : t2; // ScrollBox. Both change rarely (pill flips once per threshold crossing,
const newMessageCount = t3 === undefined ? 0 : t3; // sticky changes ~5-20×/transcript) — re-rendering FullscreenLayout on
const { // those is fine; re-rendering the 6966-line REPL + its 22+ useAppState
rows: terminalRows, // selectors per-scroll-frame was not.
columns const [stickyPrompt, setStickyPrompt] = useState<StickyPrompt | null>(null)
} = useTerminalSize(); const chromeCtx = useMemo(() => ({ setStickyPrompt }), [])
const [stickyPrompt, setStickyPrompt] = useState(null); // Boolean-quantized scroll subscription. Snapshot is "is viewport bottom
let t4; // above the divider y?" — Object.is on a boolean → FullscreenLayout only
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { // re-renders when the pill should actually flip, not per-frame.
t4 = { const subscribe = useCallback(
setStickyPrompt (listener: () => void) =>
}; scrollRef?.current?.subscribe(listener) ?? (() => {}),
$[0] = t4; [scrollRef],
} else { )
t4 = $[0]; const pillVisible = useSyncExternalStore(subscribe, () => {
} const s = scrollRef?.current
const chromeCtx = t4; const dividerY = dividerYRef?.current
let t5; if (!s || dividerY == null) return false
if ($[1] !== scrollRef) { return (
t5 = listener => scrollRef?.current?.subscribe(listener) ?? _temp; s.getScrollTop() + s.getPendingDelta() + s.getViewportHeight() < dividerY
$[1] = scrollRef; )
$[2] = t5; })
} else { // Wire up hyperlink click handling — in fullscreen mode, mouse tracking
t5 = $[2]; // intercepts clicks before the terminal can open OSC 8 links natively.
} useLayoutEffect(() => {
const subscribe = t5; if (!isFullscreenEnvEnabled()) return
let t6; const ink = instances.get(process.stdout)
if ($[3] !== dividerYRef || $[4] !== scrollRef) { if (!ink) return
t6 = () => { ink.onHyperlinkClick = url => {
const s = scrollRef?.current; // Most OSC 8 links emitted by Claude Code are file:// URLs from
const dividerY = dividerYRef?.current; // FilePathLink (FileEdit/FileWrite/FileRead tool output). openBrowser
if (!s || dividerY == null) { // rejects non-http(s) protocols — route file: to openPath instead.
return false; if (url.startsWith('file:')) {
try {
void openPath(fileURLToPath(url))
} catch {
// Malformed file: URLs (e.g. file://host/path from plain-text
// detection) cause fileURLToPath to throw — ignore silently.
}
} else {
void openBrowser(url)
} }
return s.getScrollTop() + s.getPendingDelta() + s.getViewportHeight() < dividerY; }
}; return () => {
$[3] = dividerYRef; ink.onHyperlinkClick = undefined
$[4] = scrollRef; }
$[5] = t6; }, [])
} else {
t6 = $[5];
}
const pillVisible = useSyncExternalStore(subscribe, t6);
let t7;
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
t7 = [];
$[6] = t7;
} else {
t7 = $[6];
}
useLayoutEffect(_temp3, t7);
if (isFullscreenEnvEnabled()) { if (isFullscreenEnvEnabled()) {
const sticky = hideSticky ? null : stickyPrompt; // Overlay renders BELOW messages inside the same ScrollBox — user can
const headerPrompt = sticky != null && sticky !== "clicked" && overlay == null ? sticky : null; // scroll up to see prior context while a permission dialog is showing.
const padCollapsed = sticky != null && overlay == null; // The ScrollBox never unmounts across overlay transitions, so scroll
let t8; // position is preserved without save/restore. stickyScroll auto-scrolls
if ($[7] !== headerPrompt) { // to the appended overlay when it mounts (if user was already at
t8 = headerPrompt && <StickyPromptHeader text={headerPrompt.text} onClick={headerPrompt.scrollTo} />; // bottom); REPL re-pins on the overlay appear/dismiss transition for
$[7] = headerPrompt; // the case where sticky was broken. Tall dialogs (FileEdit diffs) still
$[8] = t8; // get PgUp/PgDn/wheel — same scrollRef drives the same ScrollBox.
} else { // Three sticky states: null (at bottom), {text,scrollTo} (scrolled up,
t8 = $[8]; // header shows), 'clicked' (just clicked header — hide it so the
} // content takes row 0). padCollapsed covers the latter two: once
const t9 = padCollapsed ? 0 : 1; // scrolled away from bottom, padding drops to 0 and stays there until
let t10; // repin. headerVisible is only the middle state. After click:
if ($[9] !== scrollable) { // scrollBox_y=0 (header gone) + padding=0 → viewportTop=0 → at
t10 = <ScrollChromeContext value={chromeCtx}>{scrollable}</ScrollChromeContext>; // row 0. On next scroll the onChange fires with a fresh {text} and
$[9] = scrollable; // header comes back (viewportTop 0→1, a single 1-row shift —
$[10] = t10; // acceptable since user explicitly scrolled).
} else { const sticky = hideSticky ? null : stickyPrompt
t10 = $[10]; const headerPrompt =
} sticky != null && sticky !== 'clicked' && overlay == null ? sticky : null
let t11; const padCollapsed = sticky != null && overlay == null
if ($[11] !== overlay || $[12] !== scrollRef || $[13] !== t10 || $[14] !== t9) { return (
t11 = <ScrollBox ref={scrollRef} flexGrow={1} flexDirection="column" paddingTop={t9} stickyScroll={true}>{t10}{overlay}</ScrollBox>; <PromptOverlayProvider>
$[11] = overlay; <Box flexGrow={1} flexDirection="column" overflow="hidden">
$[12] = scrollRef; {headerPrompt && (
$[13] = t10; <StickyPromptHeader
$[14] = t9; text={headerPrompt.text}
$[15] = t11; onClick={headerPrompt.scrollTo}
} else { />
t11 = $[15]; )}
} <ScrollBox
let t12; ref={scrollRef}
if ($[16] !== hidePill || $[17] !== newMessageCount || $[18] !== onPillClick || $[19] !== overlay || $[20] !== pillVisible) { flexGrow={1}
t12 = !hidePill && pillVisible && overlay == null && <NewMessagesPill count={newMessageCount} onClick={onPillClick} />; flexDirection="column"
$[16] = hidePill; paddingTop={padCollapsed ? 0 : 1}
$[17] = newMessageCount; stickyScroll
$[18] = onPillClick; >
$[19] = overlay; <ScrollChromeContext value={chromeCtx}>
$[20] = pillVisible; {scrollable}
$[21] = t12; </ScrollChromeContext>
} else { {overlay}
t12 = $[21]; </ScrollBox>
} {!hidePill && pillVisible && overlay == null && (
let t13; <NewMessagesPill count={newMessageCount} onClick={onPillClick} />
if ($[22] !== bottomFloat) { )}
t13 = bottomFloat != null && <Box position="absolute" bottom={0} right={0} opaque={true}>{bottomFloat}</Box>; {bottomFloat != null && (
$[22] = bottomFloat; <Box position="absolute" bottom={0} right={0} opaque>
$[23] = t13; {bottomFloat}
} else { </Box>
t13 = $[23]; )}
} </Box>
let t14; <Box flexDirection="column" flexShrink={0} width="100%" maxHeight="50%">
if ($[24] !== t11 || $[25] !== t12 || $[26] !== t13 || $[27] !== t8) { <SuggestionsOverlay />
t14 = <Box flexGrow={1} flexDirection="column" overflow="hidden">{t8}{t11}{t12}{t13}</Box>; <DialogOverlay />
$[24] = t11; <Box
$[25] = t12; flexDirection="column"
$[26] = t13; width="100%"
$[27] = t8; flexGrow={1}
$[28] = t14; overflowY="hidden"
} else { >
t14 = $[28]; {bottom}
} </Box>
let t15; </Box>
let t16; {modal != null && (
if ($[29] === Symbol.for("react.memo_cache_sentinel")) { <ModalContext
t15 = <SuggestionsOverlay />; value={{
t16 = <DialogOverlay />; rows: terminalRows - MODAL_TRANSCRIPT_PEEK - 1,
$[29] = t15; columns: columns - 4,
$[30] = t16; scrollRef: modalScrollRef ?? null,
} else { }}
t15 = $[29]; >
t16 = $[30]; {/* Bottom-anchored, grows upward to fit content. maxHeight keeps a
} few rows of transcript peek above the ▔ divider. Short modals
let t17; (/model) sit small at the bottom with lots of transcript above;
if ($[31] !== bottom) { tall modals (/buddy Card) grow as needed, clipped by overflow.
t17 = <Box flexDirection="column" flexShrink={0} width="100%" maxHeight="50%">{t15}{t16}<Box flexDirection="column" width="100%" flexGrow={1} overflowY="hidden">{bottom}</Box></Box>; Previously fixed-height (top+bottom anchored) — any fixed cap
$[31] = bottom; either clipped tall content or left short content floating in
$[32] = t17; a mostly-empty pane.
} else {
t17 = $[32]; flexShrink=0 on the inner Box is load-bearing: with Shrink=1,
} yoga squeezes deep children to h=0 when content > maxHeight,
let t18; and sibling Texts land on the same row → ghost overlap
if ($[33] !== columns || $[34] !== modal || $[35] !== modalScrollRef || $[36] !== terminalRows) { ("5 serversP servers"). Clipping at the outer Box's maxHeight
t18 = modal != null && <ModalContext value={{ keeps children at natural size.
rows: terminalRows - MODAL_TRANSCRIPT_PEEK - 1,
columns: columns - 4, Divider wrapped in flexShrink=0: when the inner box overflows
scrollRef: modalScrollRef ?? null (tall /config option list), yoga shrinks the divider Text to
}}><Box position="absolute" bottom={0} left={0} right={0} maxHeight={terminalRows - MODAL_TRANSCRIPT_PEEK} flexDirection="column" overflow="hidden" opaque={true}><Box flexShrink={0}><Text color="permission">{"\u2594".repeat(columns)}</Text></Box><Box flexDirection="column" paddingX={2} flexShrink={0} overflow="hidden">{modal}</Box></Box></ModalContext>; h=0 to absorb the deficit — it's the only shrinkable sibling.
$[33] = columns; The wrapper keeps it at 1 row; overflow past maxHeight is
$[34] = modal; clipped at the bottom by overflow=hidden instead. */}
$[35] = modalScrollRef; <Box
$[36] = terminalRows; position="absolute"
$[37] = t18; bottom={0}
} else { left={0}
t18 = $[37]; right={0}
} maxHeight={terminalRows - MODAL_TRANSCRIPT_PEEK}
let t19; flexDirection="column"
if ($[38] !== t14 || $[39] !== t17 || $[40] !== t18) { overflow="hidden"
t19 = <PromptOverlayProvider>{t14}{t17}{t18}</PromptOverlayProvider>; opaque
$[38] = t14; >
$[39] = t17; <Box flexShrink={0}>
$[40] = t18; <Text color="permission">{'▔'.repeat(columns)}</Text>
$[41] = t19; </Box>
} else { <Box
t19 = $[41]; flexDirection="column"
} paddingX={2}
return t19; flexShrink={0}
overflow="hidden"
>
{modal}
</Box>
</Box>
</ModalContext>
)}
</PromptOverlayProvider>
)
} }
let t8;
if ($[42] !== bottom || $[43] !== modal || $[44] !== overlay || $[45] !== scrollable) { return (
t8 = <>{scrollable}{bottom}{overlay}{modal}</>; <>
$[42] = bottom; {scrollable}
$[43] = modal; {bottom}
$[44] = overlay; {overlay}
$[45] = scrollable; {modal}
$[46] = t8; </>
} else { )
t8 = $[46];
}
return t8;
} }
// Slack-style pill. Absolute overlay at bottom={0} of the scrollwrap — floats // Slack-style pill. Absolute overlay at bottom={0} of the scrollwrap — floats
@@ -466,75 +499,42 @@ export function FullscreenLayout(t0) {
// (absoluteRectsPrev third-pass in render-node-to-output.ts, #23939). Shows // (absoluteRectsPrev third-pass in render-node-to-output.ts, #23939). Shows
// "Jump to bottom" when count is 0 (scrolled away but no new messages yet — // "Jump to bottom" when count is 0 (scrolled away but no new messages yet —
// the dead zone where users previously thought chat stalled). // the dead zone where users previously thought chat stalled).
function _temp3() { function NewMessagesPill({
if (!isFullscreenEnvEnabled()) { count,
return; onClick,
} }: {
const ink = instances.get(process.stdout); count: number
if (!ink) { onClick?: () => void
return; }): React.ReactNode {
} const [hover, setHover] = useState(false)
ink.onHyperlinkClick = _temp2; return (
return () => { <Box
ink.onHyperlinkClick = undefined; position="absolute"
}; bottom={0}
} left={0}
function _temp2(url) { right={0}
if (url.startsWith("file:")) { justifyContent="center"
try { >
openPath(fileURLToPath(url)); <Box
} catch {} onClick={onClick}
} else { onMouseEnter={() => setHover(true)}
openBrowser(url); onMouseLeave={() => setHover(false)}
} >
} <Text
function _temp() {} backgroundColor={
function NewMessagesPill(t0) { hover ? 'userMessageBackgroundHover' : 'userMessageBackground'
const $ = _c(10); }
const { dimColor
count, >
onClick {' '}
} = t0; {count > 0
const [hover, setHover] = useState(false); ? `${count} new ${plural(count, 'message')}`
let t1; : 'Jump to bottom'}{' '}
let t2; {figures.arrowDown}{' '}
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { </Text>
t1 = () => setHover(true); </Box>
t2 = () => setHover(false); </Box>
$[0] = t1; )
$[1] = t2;
} else {
t1 = $[0];
t2 = $[1];
}
const t3 = hover ? "userMessageBackgroundHover" : "userMessageBackground";
let t4;
if ($[2] !== count) {
t4 = count > 0 ? `${count} new ${plural(count, "message")}` : "Jump to bottom";
$[2] = count;
$[3] = t4;
} else {
t4 = $[3];
}
let t5;
if ($[4] !== t3 || $[5] !== t4) {
t5 = <Text backgroundColor={t3} dimColor={true}>{" "}{t4}{" "}{figures.arrowDown}{" "}</Text>;
$[4] = t3;
$[5] = t4;
$[6] = t5;
} else {
t5 = $[6];
}
let t6;
if ($[7] !== onClick || $[8] !== t5) {
t6 = <Box position="absolute" bottom={0} left={0} right={0} justifyContent="center"><Box onClick={onClick} onMouseEnter={t1} onMouseLeave={t2}>{t5}</Box></Box>;
$[7] = onClick;
$[8] = t5;
$[9] = t6;
} else {
t6 = $[9];
}
return t6;
} }
// Context breadcrumb: when scrolled up into history, pin the current // Context breadcrumb: when scrolled up into history, pin the current
@@ -549,44 +549,32 @@ function NewMessagesPill(t0) {
// even with scrollTop unchanged (the DECSTBM region top shifts with the // even with scrollTop unchanged (the DECSTBM region top shifts with the
// ScrollBox, and the diff engine sees "everything moved"). Fixed height // ScrollBox, and the diff engine sees "everything moved"). Fixed height
// keeps the ScrollBox anchored; only the header TEXT changes, not its box. // keeps the ScrollBox anchored; only the header TEXT changes, not its box.
function StickyPromptHeader(t0) { function StickyPromptHeader({
const $ = _c(8); text,
const { onClick,
text, }: {
onClick text: string
} = t0; onClick: () => void
const [hover, setHover] = useState(false); }): React.ReactNode {
const t1 = hover ? "userMessageBackgroundHover" : "userMessageBackground"; const [hover, setHover] = useState(false)
let t2; return (
let t3; <Box
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { flexShrink={0}
t2 = () => setHover(true); width="100%"
t3 = () => setHover(false); height={1}
$[0] = t2; paddingRight={1}
$[1] = t3; backgroundColor={
} else { hover ? 'userMessageBackgroundHover' : 'userMessageBackground'
t2 = $[0]; }
t3 = $[1]; onClick={onClick}
} onMouseEnter={() => setHover(true)}
let t4; onMouseLeave={() => setHover(false)}
if ($[2] !== text) { >
t4 = <Text color="subtle" wrap="truncate-end">{figures.pointer} {text}</Text>; <Text color="subtle" wrap="truncate-end">
$[2] = text; {figures.pointer} {text}
$[3] = t4; </Text>
} else { </Box>
t4 = $[3]; )
}
let t5;
if ($[4] !== onClick || $[5] !== t1 || $[6] !== t4) {
t5 = <Box flexShrink={0} width="100%" height={1} paddingRight={1} backgroundColor={t1} onClick={onClick} onMouseEnter={t2} onMouseLeave={t3}>{t4}</Box>;
$[4] = onClick;
$[5] = t1;
$[6] = t4;
$[7] = t5;
} else {
t5 = $[7];
}
return t5;
} }
// Slash-command suggestion overlay — see promptOverlayContext.tsx for why // Slash-command suggestion overlay — see promptOverlayContext.tsx for why
@@ -597,41 +585,39 @@ function StickyPromptHeader(t0) {
// even when the overlay extends above the viewport. We omit minHeight and // even when the overlay extends above the viewport. We omit minHeight and
// flex-end here: they would create empty padding rows that shift visible // flex-end here: they would create empty padding rows that shift visible
// items down into the prompt area when the list has fewer items than max. // items down into the prompt area when the list has fewer items than max.
function SuggestionsOverlay() { function SuggestionsOverlay(): React.ReactNode {
const $ = _c(4); const data = usePromptOverlay()
const data = usePromptOverlay(); if (!data || data.suggestions.length === 0) return null
if (!data || data.suggestions.length === 0) { return (
return null; <Box
} position="absolute"
let t0; bottom="100%"
if ($[0] !== data.maxColumnWidth || $[1] !== data.selectedSuggestion || $[2] !== data.suggestions) { left={0}
t0 = <Box position="absolute" bottom="100%" left={0} right={0} paddingX={2} paddingTop={1} flexDirection="column" opaque={true}><PromptInputFooterSuggestions suggestions={data.suggestions} selectedSuggestion={data.selectedSuggestion} maxColumnWidth={data.maxColumnWidth} overlay={true} /></Box>; right={0}
$[0] = data.maxColumnWidth; paddingX={2}
$[1] = data.selectedSuggestion; paddingTop={1}
$[2] = data.suggestions; flexDirection="column"
$[3] = t0; opaque
} else { >
t0 = $[3]; <PromptInputFooterSuggestions
} suggestions={data.suggestions}
return t0; selectedSuggestion={data.selectedSuggestion}
maxColumnWidth={data.maxColumnWidth}
overlay
/>
</Box>
)
} }
// Dialog portaled from PromptInput (AutoModeOptInDialog) — same clip-escape // Dialog portaled from PromptInput (AutoModeOptInDialog) — same clip-escape
// pattern as SuggestionsOverlay. Renders later in tree order so it paints // pattern as SuggestionsOverlay. Renders later in tree order so it paints
// over suggestions if both are ever up (they shouldn't be). // over suggestions if both are ever up (they shouldn't be).
function DialogOverlay() { function DialogOverlay(): React.ReactNode {
const $ = _c(2); const node = usePromptOverlayDialog()
const node = usePromptOverlayDialog(); if (!node) return null
if (!node) { return (
return null; <Box position="absolute" bottom="100%" left={0} right={0} opaque>
} {node}
let t0; </Box>
if ($[0] !== node) { )
t0 = <Box position="absolute" bottom="100%" left={0} right={0} opaque={true}>{node}</Box>;
$[0] = node;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
} }

View File

@@ -1,324 +1,308 @@
import { c as _c } from "react/compiler-runtime"; import { resolve as resolvePath } from 'path'
import { resolve as resolvePath } from 'path'; import * as React from 'react'
import * as React from 'react'; import { useEffect, useRef, useState } from 'react'
import { useEffect, useRef, useState } from 'react'; import { useRegisterOverlay } from '../context/overlayContext.js'
import { useRegisterOverlay } from '../context/overlayContext.js'; import { useTerminalSize } from '../hooks/useTerminalSize.js'
import { useTerminalSize } from '../hooks/useTerminalSize.js'; import { Text } from '../ink.js'
import { Text } from '../ink.js'; import { logEvent } from '../services/analytics/index.js'
import { logEvent } from '../services/analytics/index.js'; import { getCwd } from '../utils/cwd.js'
import { getCwd } from '../utils/cwd.js'; import { openFileInExternalEditor } from '../utils/editor.js'
import { openFileInExternalEditor } from '../utils/editor.js'; import { truncatePathMiddle, truncateToWidth } from '../utils/format.js'
import { truncatePathMiddle, truncateToWidth } from '../utils/format.js'; import { highlightMatch } from '../utils/highlightMatch.js'
import { highlightMatch } from '../utils/highlightMatch.js'; import { relativePath } from '../utils/permissions/filesystem.js'
import { relativePath } from '../utils/permissions/filesystem.js'; import { readFileInRange } from '../utils/readFileInRange.js'
import { readFileInRange } from '../utils/readFileInRange.js'; import { ripGrepStream } from '../utils/ripgrep.js'
import { ripGrepStream } from '../utils/ripgrep.js'; import { FuzzyPicker } from './design-system/FuzzyPicker.js'
import { FuzzyPicker } from './design-system/FuzzyPicker.js'; import { LoadingState } from './design-system/LoadingState.js'
import { LoadingState } from './design-system/LoadingState.js';
type Props = { type Props = {
onDone: () => void; onDone: () => void
onInsert: (text: string) => void; onInsert: (text: string) => void
}; }
type Match = { type Match = {
file: string; file: string
line: number; line: number
text: string; text: string
}; }
const VISIBLE_RESULTS = 12;
const DEBOUNCE_MS = 100; const VISIBLE_RESULTS = 12
const PREVIEW_CONTEXT_LINES = 4; const DEBOUNCE_MS = 100
const PREVIEW_CONTEXT_LINES = 4
// rg -m is per-file; we also cap the parsed array to keep memory bounded. // rg -m is per-file; we also cap the parsed array to keep memory bounded.
const MAX_MATCHES_PER_FILE = 10; const MAX_MATCHES_PER_FILE = 10
const MAX_TOTAL_MATCHES = 500; const MAX_TOTAL_MATCHES = 500
/** /**
* Global Search dialog (ctrl+shift+f / cmd+shift+f). * Global Search dialog (ctrl+shift+f / cmd+shift+f).
* Debounced ripgrep search across the workspace. * Debounced ripgrep search across the workspace.
*/ */
export function GlobalSearchDialog(t0) { export function GlobalSearchDialog({
const $ = _c(40); onDone,
const { onInsert,
onDone, }: Props): React.ReactNode {
onInsert useRegisterOverlay('global-search')
} = t0; const { columns, rows } = useTerminalSize()
useRegisterOverlay("global-search", undefined); const previewOnRight = columns >= 140
const { // Chrome (title + search + matchLabel + hints + pane border + gaps) eats
columns, // ~14 rows. Shrink the list on short terminals so the dialog doesn't clip.
rows const visibleResults = Math.min(VISIBLE_RESULTS, Math.max(4, rows - 14))
} = useTerminalSize();
const previewOnRight = columns >= 140; const [matches, setMatches] = useState<Match[]>([])
const visibleResults = Math.min(VISIBLE_RESULTS, Math.max(4, rows - 14)); const [truncated, setTruncated] = useState(false)
let t1; const [isSearching, setIsSearching] = useState(false)
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { const [query, setQuery] = useState('')
t1 = []; const [focused, setFocused] = useState<Match | undefined>(undefined)
$[0] = t1; const [preview, setPreview] = useState<{
} else { file: string
t1 = $[0]; line: number
} content: string
const [matches, setMatches] = useState(t1); } | null>(null)
const [truncated, setTruncated] = useState(false); const abortRef = useRef<AbortController | null>(null)
const [isSearching, setIsSearching] = useState(false); const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const [query, setQuery] = useState("");
const [focused, setFocused] = useState(undefined); useEffect(() => {
const [preview, setPreview] = useState(null); return () => {
const abortRef = useRef(null); if (timeoutRef.current) clearTimeout(timeoutRef.current)
const timeoutRef = useRef(null); abortRef.current?.abort()
let t2; }
let t3; }, [])
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t2 = () => () => { // Load context lines around the focused match. AbortController prevents
if (timeoutRef.current) { // holding ↓ from piling up reads.
clearTimeout(timeoutRef.current); useEffect(() => {
} if (!focused) {
abortRef.current?.abort(); setPreview(null)
}; return
t3 = []; }
$[1] = t2; const controller = new AbortController()
$[2] = t3; const absolute = resolvePath(getCwd(), focused.file)
} else { const start = Math.max(0, focused.line - PREVIEW_CONTEXT_LINES - 1)
t2 = $[1]; void readFileInRange(
t3 = $[2]; absolute,
} start,
useEffect(t2, t3); PREVIEW_CONTEXT_LINES * 2 + 1,
let t4; undefined,
let t5; controller.signal,
if ($[3] !== focused) { )
t4 = () => { .then(r => {
if (!focused) { if (controller.signal.aborted) return
setPreview(null);
return;
}
const controller = new AbortController();
const absolute = resolvePath(getCwd(), focused.file);
const start = Math.max(0, focused.line - PREVIEW_CONTEXT_LINES - 1);
readFileInRange(absolute, start, PREVIEW_CONTEXT_LINES * 2 + 1, undefined, controller.signal).then(r => {
if (controller.signal.aborted) {
return;
}
setPreview({ setPreview({
file: focused.file, file: focused.file,
line: focused.line, line: focused.line,
content: r.content content: r.content,
}); })
}).catch(() => { })
if (controller.signal.aborted) { .catch(() => {
return; if (controller.signal.aborted) return
}
setPreview({ setPreview({
file: focused.file, file: focused.file,
line: focused.line, line: focused.line,
content: "(preview unavailable)" content: '(preview unavailable)',
}); })
}); })
return () => controller.abort(); return () => controller.abort()
}; }, [focused])
t5 = [focused];
$[3] = focused; const handleQueryChange = (q: string) => {
$[4] = t4; setQuery(q)
$[5] = t5; if (timeoutRef.current) clearTimeout(timeoutRef.current)
} else { abortRef.current?.abort()
t4 = $[4];
t5 = $[5]; if (!q.trim()) {
setMatches(m => (m.length ? [] : m))
setIsSearching(false)
setTruncated(false)
return
}
const controller = new AbortController()
abortRef.current = controller
setIsSearching(true)
setTruncated(false)
// Client-filter existing results while rg walks — keeps something on
// screen instead of flashing blank. rg results are merged in (deduped by
// file:line) rather than replaced, so the count is monotonic within a
// query: it only grows as rg streams, never dips to the first chunk's
// size. Narrowing (new query extends old): filter is exact — any line
// that matched the old -F -i literal contains the new one iff its text
// includes the new query lowered. Non-narrowing (broadening/different):
// filter is best-effort — may briefly show a subset until rg fills in
// the rest.
const queryLower = q.toLowerCase()
setMatches(m => {
const filtered = m.filter(match =>
match.text.toLowerCase().includes(queryLower),
)
return filtered.length === m.length ? m : filtered
})
timeoutRef.current = setTimeout(
(query, controller, setMatches, setTruncated, setIsSearching) => {
// ripgrep outputs absolute paths when given an absolute target, so
// relativize against cwd to preserve directory context in the truncated
// display (otherwise the cwd prefix eats the width budget).
// relativePath() returns POSIX-normalized output so truncatePathMiddle
// (which uses lastIndexOf('/')) works on Windows too.
const cwd = getCwd()
let collected = 0
void ripGrepStream(
// -e disambiguates pattern from options when the query starts with '-'
// (e.g. searching for "--verbose" or "-rf"). See GrepTool.ts for the
// same precaution.
[
'-n',
'--no-heading',
'-i',
'-m',
String(MAX_MATCHES_PER_FILE),
'-F',
'-e',
query,
],
cwd,
controller.signal,
lines => {
if (controller.signal.aborted) return
const parsed: Match[] = []
for (const line of lines) {
const m = parseRipgrepLine(line)
if (!m) continue
const rel = relativePath(cwd, m.file)
parsed.push({ ...m, file: rel.startsWith('..') ? m.file : rel })
}
if (!parsed.length) return
collected += parsed.length
setMatches(prev => {
// Append+dedupe instead of replace: prev may hold client-
// filtered results that are valid matches for this query.
// Replacing would drop the count to this chunk's size then
// grow it back — visible as a flicker.
const seen = new Set(prev.map(matchKey))
const fresh = parsed.filter(p => !seen.has(matchKey(p)))
if (!fresh.length) return prev
const next = prev.concat(fresh)
return next.length > MAX_TOTAL_MATCHES
? next.slice(0, MAX_TOTAL_MATCHES)
: next
})
if (collected >= MAX_TOTAL_MATCHES) {
controller.abort()
setTruncated(true)
setIsSearching(false)
}
},
)
.catch(() => {})
// Stream closed with zero chunks — clear stale results so
// "No matches" renders instead of the previous query's list.
.finally(() => {
if (controller.signal.aborted) return
if (collected === 0) setMatches(m => (m.length ? [] : m))
setIsSearching(false)
})
},
DEBOUNCE_MS,
q,
controller,
setMatches,
setTruncated,
setIsSearching,
)
} }
useEffect(t4, t5);
let t6; const listWidth = previewOnRight
if ($[6] === Symbol.for("react.memo_cache_sentinel")) { ? Math.floor((columns - 10) * 0.5)
t6 = q => { : columns - 8
setQuery(q); const maxPathWidth = Math.max(20, Math.floor(listWidth * 0.4))
if (timeoutRef.current) { const maxTextWidth = Math.max(20, listWidth - maxPathWidth - 4)
clearTimeout(timeoutRef.current); const previewWidth = previewOnRight
? Math.max(40, columns - listWidth - 14)
: columns - 6
const handleOpen = (m: Match) => {
const opened = openFileInExternalEditor(
resolvePath(getCwd(), m.file),
m.line,
)
logEvent('tengu_global_search_select', {
result_count: matches.length,
opened_editor: opened,
})
onDone()
}
const handleInsert = (m: Match, mention: boolean) => {
onInsert(mention ? `@${m.file}#L${m.line} ` : `${m.file}:${m.line} `)
logEvent('tengu_global_search_insert', {
result_count: matches.length,
mention,
})
onDone()
}
// Always pass a non-empty string so the line is reserved — prevents the
// searchBox from bouncing when the count appears/disappears.
const matchLabel =
matches.length > 0
? `${matches.length}${truncated ? '+' : ''} matches${isSearching ? '…' : ''}`
: ' '
return (
<FuzzyPicker
title="Global Search"
placeholder="Type to search…"
items={matches}
getKey={matchKey}
visibleCount={visibleResults}
direction="up"
previewPosition={previewOnRight ? 'right' : 'bottom'}
onQueryChange={handleQueryChange}
onFocus={setFocused}
onSelect={handleOpen}
onTab={{ action: 'mention', handler: m => handleInsert(m, true) }}
onShiftTab={{
action: 'insert path',
handler: m => handleInsert(m, false),
}}
onCancel={onDone}
emptyMessage={q =>
isSearching ? 'Searching…' : q ? 'No matches' : 'Type to search…'
} }
abortRef.current?.abort(); matchLabel={matchLabel}
if (!q.trim()) { selectAction="open in editor"
setMatches(_temp); renderItem={(m, isFocused) => (
setIsSearching(false); <Text color={isFocused ? 'suggestion' : undefined}>
setTruncated(false); <Text dimColor>
return; {truncatePathMiddle(m.file, maxPathWidth)}:{m.line}
</Text>{' '}
{highlightMatch(
truncateToWidth(m.text.trimStart(), maxTextWidth),
query,
)}
</Text>
)}
renderPreview={m =>
preview?.file === m.file && preview.line === m.line ? (
<>
<Text dimColor>
{truncatePathMiddle(m.file, previewWidth)}:{m.line}
</Text>
{preview.content.split('\n').map((line, i) => (
<Text key={i}>
{highlightMatch(truncateToWidth(line, previewWidth), query)}
</Text>
))}
</>
) : (
<LoadingState message="Loading…" dimColor />
)
} }
const controller_0 = new AbortController(); />
abortRef.current = controller_0; )
setIsSearching(true);
setTruncated(false);
const queryLower = q.toLowerCase();
setMatches(m_0 => {
const filtered = m_0.filter(match => match.text.toLowerCase().includes(queryLower));
return filtered.length === m_0.length ? m_0 : filtered;
});
timeoutRef.current = setTimeout(_temp4, DEBOUNCE_MS, q, controller_0, setMatches, setTruncated, setIsSearching);
};
$[6] = t6;
} else {
t6 = $[6];
}
const handleQueryChange = t6;
const listWidth = previewOnRight ? Math.floor((columns - 10) * 0.5) : columns - 8;
const maxPathWidth = Math.max(20, Math.floor(listWidth * 0.4));
const maxTextWidth = Math.max(20, listWidth - maxPathWidth - 4);
const previewWidth = previewOnRight ? Math.max(40, columns - listWidth - 14) : columns - 6;
let t7;
if ($[7] !== matches.length || $[8] !== onDone) {
t7 = m_3 => {
const opened = openFileInExternalEditor(resolvePath(getCwd(), m_3.file), m_3.line);
logEvent("tengu_global_search_select", {
result_count: matches.length,
opened_editor: opened
});
onDone();
};
$[7] = matches.length;
$[8] = onDone;
$[9] = t7;
} else {
t7 = $[9];
}
const handleOpen = t7;
let t8;
if ($[10] !== matches.length || $[11] !== onDone || $[12] !== onInsert) {
t8 = (m_4, mention) => {
onInsert(mention ? `@${m_4.file}#L${m_4.line} ` : `${m_4.file}:${m_4.line} `);
logEvent("tengu_global_search_insert", {
result_count: matches.length,
mention
});
onDone();
};
$[10] = matches.length;
$[11] = onDone;
$[12] = onInsert;
$[13] = t8;
} else {
t8 = $[13];
}
const handleInsert = t8;
const matchLabel = matches.length > 0 ? `${matches.length}${truncated ? "+" : ""} matches${isSearching ? "\u2026" : ""}` : " ";
const t9 = previewOnRight ? "right" : "bottom";
let t10;
if ($[14] !== handleInsert) {
t10 = {
action: "mention",
handler: m_5 => handleInsert(m_5, true)
};
$[14] = handleInsert;
$[15] = t10;
} else {
t10 = $[15];
}
let t11;
if ($[16] !== handleInsert) {
t11 = {
action: "insert path",
handler: m_6 => handleInsert(m_6, false)
};
$[16] = handleInsert;
$[17] = t11;
} else {
t11 = $[17];
}
let t12;
if ($[18] !== isSearching) {
t12 = q_0 => isSearching ? "Searching\u2026" : q_0 ? "No matches" : "Type to search\u2026";
$[18] = isSearching;
$[19] = t12;
} else {
t12 = $[19];
}
let t13;
if ($[20] !== maxPathWidth || $[21] !== maxTextWidth || $[22] !== query) {
t13 = (m_7, isFocused) => <Text color={isFocused ? "suggestion" : undefined}><Text dimColor={true}>{truncatePathMiddle(m_7.file, maxPathWidth)}:{m_7.line}</Text>{" "}{highlightMatch(truncateToWidth(m_7.text.trimStart(), maxTextWidth), query)}</Text>;
$[20] = maxPathWidth;
$[21] = maxTextWidth;
$[22] = query;
$[23] = t13;
} else {
t13 = $[23];
}
let t14;
if ($[24] !== preview || $[25] !== previewWidth || $[26] !== query) {
t14 = m_8 => preview?.file === m_8.file && preview.line === m_8.line ? <><Text dimColor={true}>{truncatePathMiddle(m_8.file, previewWidth)}:{m_8.line}</Text>{preview.content.split("\n").map((line_0, i) => <Text key={i}>{highlightMatch(truncateToWidth(line_0, previewWidth), query)}</Text>)}</> : <LoadingState message={"Loading\u2026"} dimColor={true} />;
$[24] = preview;
$[25] = previewWidth;
$[26] = query;
$[27] = t14;
} else {
t14 = $[27];
}
let t15;
if ($[28] !== handleOpen || $[29] !== matchLabel || $[30] !== matches || $[31] !== onDone || $[32] !== t10 || $[33] !== t11 || $[34] !== t12 || $[35] !== t13 || $[36] !== t14 || $[37] !== t9 || $[38] !== visibleResults) {
t15 = <FuzzyPicker title="Global Search" placeholder={"Type to search\u2026"} items={matches} getKey={matchKey} visibleCount={visibleResults} direction="up" previewPosition={t9} onQueryChange={handleQueryChange} onFocus={setFocused} onSelect={handleOpen} onTab={t10} onShiftTab={t11} onCancel={onDone} emptyMessage={t12} matchLabel={matchLabel} selectAction="open in editor" renderItem={t13} renderPreview={t14} />;
$[28] = handleOpen;
$[29] = matchLabel;
$[30] = matches;
$[31] = onDone;
$[32] = t10;
$[33] = t11;
$[34] = t12;
$[35] = t13;
$[36] = t14;
$[37] = t9;
$[38] = visibleResults;
$[39] = t15;
} else {
t15 = $[39];
}
return t15;
}
function _temp4(query_0, controller_1, setMatches_0, setTruncated_0, setIsSearching_0) {
const cwd = getCwd();
let collected = 0;
ripGrepStream(["-n", "--no-heading", "-i", "-m", String(MAX_MATCHES_PER_FILE), "-F", "-e", query_0], cwd, controller_1.signal, lines => {
if (controller_1.signal.aborted) {
return;
}
const parsed = [];
for (const line of lines) {
const m_1 = parseRipgrepLine(line);
if (!m_1) {
continue;
}
const rel = relativePath(cwd, m_1.file);
parsed.push({
...m_1,
file: rel.startsWith("..") ? m_1.file : rel
});
}
if (!parsed.length) {
return;
}
collected = collected + parsed.length;
collected;
setMatches_0(prev => {
const seen = new Set(prev.map(matchKey));
const fresh = parsed.filter(p => !seen.has(matchKey(p)));
if (!fresh.length) {
return prev;
}
const next = prev.concat(fresh);
return next.length > MAX_TOTAL_MATCHES ? next.slice(0, MAX_TOTAL_MATCHES) : next;
});
if (collected >= MAX_TOTAL_MATCHES) {
controller_1.abort();
setTruncated_0(true);
setIsSearching_0(false);
}
}).catch(_temp2).finally(() => {
if (controller_1.signal.aborted) {
return;
}
if (collected === 0) {
setMatches_0(_temp3);
}
setIsSearching_0(false);
});
}
function _temp3(m_2) {
return m_2.length ? [] : m_2;
}
function _temp2() {}
function _temp(m) {
return m.length ? [] : m;
} }
function matchKey(m: Match): string { function matchKey(m: Match): string {
return `${m.file}:${m.line}`; return `${m.file}:${m.line}`
} }
/** /**
@@ -329,14 +313,10 @@ function matchKey(m: Match): string {
* @internal exported for testing * @internal exported for testing
*/ */
export function parseRipgrepLine(line: string): Match | null { export function parseRipgrepLine(line: string): Match | null {
const m = /^(.*?):(\d+):(.*)$/.exec(line); const m = /^(.*?):(\d+):(.*)$/.exec(line)
if (!m) return null; if (!m) return null
const [, file, lineStr, text] = m; const [, file, lineStr, text] = m
const lineNum = Number(lineStr); const lineNum = Number(lineStr)
if (!file || !Number.isFinite(lineNum)) return null; if (!file || !Number.isFinite(lineNum)) return null
return { return { file, line: lineNum, text: text ?? '' }
file,
line: lineNum,
text: text ?? ''
};
} }

View File

@@ -1,81 +1,71 @@
import { c as _c } from "react/compiler-runtime"; import * as React from 'react'
import * as React from 'react'; import { useMemo } from 'react'
import { useMemo } from 'react'; import { type Command, formatDescriptionWithSource } from '../../commands.js'
import { type Command, formatDescriptionWithSource } from '../../commands.js'; import { Box, Text } from '../../ink.js'
import { Box, Text } from '../../ink.js'; import { truncate } from '../../utils/format.js'
import { truncate } from '../../utils/format.js'; import { Select } from '../CustomSelect/select.js'
import { Select } from '../CustomSelect/select.js'; import { useTabHeaderFocus } from '../design-system/Tabs.js'
import { useTabHeaderFocus } from '../design-system/Tabs.js';
type Props = { type Props = {
commands: Command[]; commands: Command[]
maxHeight: number; maxHeight: number
columns: number; columns: number
title: string; title: string
onCancel: () => void; onCancel: () => void
emptyMessage?: string; emptyMessage?: string
};
export function Commands(t0) {
const $ = _c(14);
const {
commands,
maxHeight,
columns,
title,
onCancel,
emptyMessage
} = t0;
const {
headerFocused,
focusHeader
} = useTabHeaderFocus();
const maxWidth = Math.max(1, columns - 10);
const visibleCount = Math.max(1, Math.floor((maxHeight - 10) / 2));
let t1;
if ($[0] !== commands || $[1] !== maxWidth) {
const seen = new Set();
let t2;
if ($[3] !== maxWidth) {
t2 = cmd_0 => ({
label: `/${cmd_0.name}`,
value: cmd_0.name,
description: truncate(formatDescriptionWithSource(cmd_0), maxWidth, true)
});
$[3] = maxWidth;
$[4] = t2;
} else {
t2 = $[4];
}
t1 = commands.filter(cmd => {
if (seen.has(cmd.name)) {
return false;
}
seen.add(cmd.name);
return true;
}).sort(_temp).map(t2);
$[0] = commands;
$[1] = maxWidth;
$[2] = t1;
} else {
t1 = $[2];
}
const options = t1;
let t2;
if ($[5] !== commands.length || $[6] !== emptyMessage || $[7] !== focusHeader || $[8] !== headerFocused || $[9] !== onCancel || $[10] !== options || $[11] !== title || $[12] !== visibleCount) {
t2 = <Box flexDirection="column" paddingY={1}>{commands.length === 0 && emptyMessage ? <Text dimColor={true}>{emptyMessage}</Text> : <><Text>{title}</Text><Box marginTop={1}><Select options={options} visibleOptionCount={visibleCount} onCancel={onCancel} disableSelection={true} hideIndexes={true} layout="compact-vertical" onUpFromFirstItem={focusHeader} isDisabled={headerFocused} /></Box></>}</Box>;
$[5] = commands.length;
$[6] = emptyMessage;
$[7] = focusHeader;
$[8] = headerFocused;
$[9] = onCancel;
$[10] = options;
$[11] = title;
$[12] = visibleCount;
$[13] = t2;
} else {
t2 = $[13];
}
return t2;
} }
function _temp(a, b) {
return a.name.localeCompare(b.name); export function Commands({
commands,
maxHeight,
columns,
title,
onCancel,
emptyMessage,
}: Props): React.ReactNode {
const { headerFocused, focusHeader } = useTabHeaderFocus()
const maxWidth = Math.max(1, columns - 10)
const visibleCount = Math.max(1, Math.floor((maxHeight - 10) / 2))
const options = useMemo(() => {
// Custom commands can appear more than once (e.g. same name at user and
// project scope). Dedupe by name to avoid React key collisions in Select.
const seen = new Set<string>()
return commands
.filter(cmd => {
if (seen.has(cmd.name)) return false
seen.add(cmd.name)
return true
})
.sort((a, b) => a.name.localeCompare(b.name))
.map(cmd => ({
label: `/${cmd.name}`,
value: cmd.name,
description: truncate(formatDescriptionWithSource(cmd), maxWidth, true),
}))
}, [commands, maxWidth])
return (
<Box flexDirection="column" paddingY={1}>
{commands.length === 0 && emptyMessage ? (
<Text dimColor>{emptyMessage}</Text>
) : (
<>
<Text>{title}</Text>
<Box marginTop={1}>
<Select
options={options}
visibleOptionCount={visibleCount}
onCancel={onCancel}
disableSelection
hideIndexes
layout="compact-vertical"
onUpFromFirstItem={focusHeader}
isDisabled={headerFocused}
/>
</Box>
</>
)}
</Box>
)
} }

View File

@@ -1,22 +1,22 @@
import { c as _c } from "react/compiler-runtime"; import * as React from 'react'
import * as React from 'react'; import { Box, Text } from '../../ink.js'
import { Box, Text } from '../../ink.js'; import { PromptInputHelpMenu } from '../PromptInput/PromptInputHelpMenu.js'
import { PromptInputHelpMenu } from '../PromptInput/PromptInputHelpMenu.js';
export function General() { export function General(): React.ReactNode {
const $ = _c(2); return (
let t0; <Box flexDirection="column" paddingY={1} gap={1}>
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { <Box>
t0 = <Box><Text>Claude understands your codebase, makes edits with your permission, and executes commands right from your terminal.</Text></Box>; <Text>
$[0] = t0; Claude understands your codebase, makes edits with your permission,
} else { and executes commands right from your terminal.
t0 = $[0]; </Text>
} </Box>
let t1; <Box flexDirection="column">
if ($[1] === Symbol.for("react.memo_cache_sentinel")) { <Box>
t1 = <Box flexDirection="column" paddingY={1} gap={1}>{t0}<Box flexDirection="column"><Box><Text bold={true}>Shortcuts</Text></Box><PromptInputHelpMenu gap={2} fixedWidth={true} /></Box></Box>; <Text bold>Shortcuts</Text>
$[1] = t1; </Box>
} else { <PromptInputHelpMenu gap={2} fixedWidth={true} />
t1 = $[1]; </Box>
} </Box>
return t1; )
} }

View File

@@ -1,183 +1,138 @@
import { c as _c } from "react/compiler-runtime"; import * as React from 'react'
import * as React from 'react'; import { useExitOnCtrlCDWithKeybindings } from 'src/hooks/useExitOnCtrlCDWithKeybindings.js'
import { useExitOnCtrlCDWithKeybindings } from 'src/hooks/useExitOnCtrlCDWithKeybindings.js'; import { useShortcutDisplay } from 'src/keybindings/useShortcutDisplay.js'
import { useShortcutDisplay } from 'src/keybindings/useShortcutDisplay.js'; import {
import { builtInCommandNames, type Command, type CommandResultDisplay, INTERNAL_ONLY_COMMANDS } from '../../commands.js'; builtInCommandNames,
import { useIsInsideModal } from '../../context/modalContext.js'; type Command,
import { useTerminalSize } from '../../hooks/useTerminalSize.js'; type CommandResultDisplay,
import { Box, Link, Text } from '../../ink.js'; INTERNAL_ONLY_COMMANDS,
import { useKeybinding } from '../../keybindings/useKeybinding.js'; } from '../../commands.js'
import { Pane } from '../design-system/Pane.js'; import { useIsInsideModal } from '../../context/modalContext.js'
import { Tab, Tabs } from '../design-system/Tabs.js'; import { useTerminalSize } from '../../hooks/useTerminalSize.js'
import { Commands } from './Commands.js'; import { Box, Link, Text } from '../../ink.js'
import { General } from './General.js'; 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'
type Props = { type Props = {
onClose: (result?: string, options?: { onClose: (
display?: CommandResultDisplay; result?: string,
}) => void; options?: { display?: CommandResultDisplay },
commands: Command[]; ) => void
}; commands: Command[]
export function HelpV2(t0) { }
const $ = _c(44);
const { export function HelpV2({ onClose, commands }: Props): React.ReactNode {
onClose, const { rows, columns } = useTerminalSize()
commands const maxHeight = Math.floor(rows / 2)
} = t0; // Inside the modal slot, FullscreenLayout already caps height and Pane/Tabs
const { // use flexShrink=0 (see #23592) — our own height= constraint would clip the
rows, // footer since Tabs won't shrink to fit. Let the modal slot handle sizing.
columns const insideModal = useIsInsideModal()
} = useTerminalSize();
const maxHeight = Math.floor(rows / 2); const close = () => onClose('Help dialog dismissed', { display: 'system' })
const insideModal = useIsInsideModal(); useKeybinding('help:dismiss', close, { context: 'Help' })
let t1; const exitState = useExitOnCtrlCDWithKeybindings(close)
if ($[0] !== onClose) { const dismissShortcut = useShortcutDisplay('help:dismiss', 'Help', 'esc')
t1 = () => onClose("Help dialog dismissed", {
display: "system" const builtinNames = builtInCommandNames()
}); let builtinCommands = commands.filter(
$[0] = onClose; cmd => builtinNames.has(cmd.name) && !cmd.isHidden,
$[1] = t1; )
} else { let antOnlyCommands: Command[] = []
t1 = $[1];
} // We have to do this in an `if` to help treeshaking
const close = t1; if (process.env.USER_TYPE === 'ant') {
let t2; const internalOnlyNames = new Set(INTERNAL_ONLY_COMMANDS.map(_ => _.name))
if ($[2] === Symbol.for("react.memo_cache_sentinel")) { builtinCommands = builtinCommands.filter(
t2 = { cmd => !internalOnlyNames.has(cmd.name),
context: "Help" )
}; antOnlyCommands = commands.filter(
$[2] = t2; cmd => internalOnlyNames.has(cmd.name) && !cmd.isHidden,
} else { )
t2 = $[2]; }
}
useKeybinding("help:dismiss", close, t2); const customCommands = commands.filter(
const exitState = useExitOnCtrlCDWithKeybindings(close); cmd => !builtinNames.has(cmd.name) && !cmd.isHidden,
const dismissShortcut = useShortcutDisplay("help:dismiss", "Help", "esc"); )
let antOnlyCommands;
let builtinCommands; const tabs = [
let t3; <Tab key="general" title="general">
if ($[3] !== commands) { <General />
const builtinNames = builtInCommandNames(); </Tab>,
builtinCommands = commands.filter(cmd => builtinNames.has(cmd.name) && !cmd.isHidden); ]
let t4;
if ($[7] === Symbol.for("react.memo_cache_sentinel")) { tabs.push(
t4 = []; <Tab key="commands" title="commands">
$[7] = t4; <Commands
} else { commands={builtinCommands}
t4 = $[7]; maxHeight={maxHeight}
} columns={columns}
antOnlyCommands = t4; title="Browse default commands:"
t3 = commands.filter(cmd_2 => !builtinNames.has(cmd_2.name) && !cmd_2.isHidden); onCancel={close}
$[3] = commands; />
$[4] = antOnlyCommands; </Tab>,
$[5] = builtinCommands; )
$[6] = t3;
} else { tabs.push(
antOnlyCommands = $[4]; <Tab key="custom" title="custom-commands">
builtinCommands = $[5]; <Commands
t3 = $[6]; commands={customCommands}
} maxHeight={maxHeight}
const customCommands = t3; columns={columns}
let t4; title="Browse custom commands:"
if ($[8] === Symbol.for("react.memo_cache_sentinel")) { emptyMessage="No custom commands found"
t4 = <Tab key="general" title="general"><General /></Tab>; onCancel={close}
$[8] = t4; />
} else { </Tab>,
t4 = $[8]; )
}
let tabs; if (process.env.USER_TYPE === 'ant' && antOnlyCommands.length > 0) {
if ($[9] !== antOnlyCommands || $[10] !== builtinCommands || $[11] !== close || $[12] !== columns || $[13] !== customCommands || $[14] !== maxHeight) { tabs.push(
tabs = [t4]; <Tab key="ant-only" title="[ant-only]">
let t5; <Commands
if ($[16] !== builtinCommands || $[17] !== close || $[18] !== columns || $[19] !== maxHeight) { commands={antOnlyCommands}
t5 = <Tab key="commands" title="commands"><Commands commands={builtinCommands} maxHeight={maxHeight} columns={columns} title="Browse default commands:" onCancel={close} /></Tab>; maxHeight={maxHeight}
$[16] = builtinCommands; columns={columns}
$[17] = close; title="Browse ant-only commands:"
$[18] = columns; onCancel={close}
$[19] = maxHeight; />
$[20] = t5; </Tab>,
} else { )
t5 = $[20]; }
}
tabs.push(t5); return (
let t6; <Box flexDirection="column" height={insideModal ? undefined : maxHeight}>
if ($[21] !== close || $[22] !== columns || $[23] !== customCommands || $[24] !== maxHeight) { <Pane color="professionalBlue">
t6 = <Tab key="custom" title="custom-commands"><Commands commands={customCommands} maxHeight={maxHeight} columns={columns} title="Browse custom commands:" emptyMessage="No custom commands found" onCancel={close} /></Tab>; <Tabs
$[21] = close; title={
$[22] = columns; process.env.USER_TYPE === 'ant'
$[23] = customCommands; ? '/help'
$[24] = maxHeight; : `Claude Code v${MACRO.VERSION}`
$[25] = t6; }
} else { color="professionalBlue"
t6 = $[25]; defaultTab="general"
} >
tabs.push(t6); {tabs}
if (false && antOnlyCommands.length > 0) { </Tabs>
let t7; <Box marginTop={1}>
if ($[26] !== antOnlyCommands || $[27] !== close || $[28] !== columns || $[29] !== maxHeight) { <Text>
t7 = <Tab key="ant-only" title="[ant-only]"><Commands commands={antOnlyCommands} maxHeight={maxHeight} columns={columns} title="Browse ant-only commands:" onCancel={close} /></Tab>; For more help:{' '}
$[26] = antOnlyCommands; <Link url="https://code.claude.com/docs/en/overview" />
$[27] = close; </Text>
$[28] = columns; </Box>
$[29] = maxHeight; <Box marginTop={1}>
$[30] = t7; <Text dimColor>
} else { {exitState.pending ? (
t7 = $[30]; <>Press {exitState.keyName} again to exit</>
} ) : (
tabs.push(t7); <Text italic>{dismissShortcut} to cancel</Text>
} )}
$[9] = antOnlyCommands; </Text>
$[10] = builtinCommands; </Box>
$[11] = close; </Pane>
$[12] = columns; </Box>
$[13] = customCommands; )
$[14] = maxHeight;
$[15] = tabs;
} else {
tabs = $[15];
}
const t5 = insideModal ? undefined : maxHeight;
let t6;
if ($[31] !== tabs) {
t6 = <Tabs title={false ? "/help" : `Claude Code v${MACRO.VERSION}`} color="professionalBlue" defaultTab="general">{tabs}</Tabs>;
$[31] = tabs;
$[32] = t6;
} else {
t6 = $[32];
}
let t7;
if ($[33] === Symbol.for("react.memo_cache_sentinel")) {
t7 = <Box marginTop={1}><Text>For more help:{" "}<Link url="https://code.claude.com/docs/en/overview" /></Text></Box>;
$[33] = t7;
} else {
t7 = $[33];
}
let t8;
if ($[34] !== dismissShortcut || $[35] !== exitState.keyName || $[36] !== exitState.pending) {
t8 = <Box marginTop={1}><Text dimColor={true}>{exitState.pending ? <>Press {exitState.keyName} again to exit</> : <Text italic={true}>{dismissShortcut} to cancel</Text>}</Text></Box>;
$[34] = dismissShortcut;
$[35] = exitState.keyName;
$[36] = exitState.pending;
$[37] = t8;
} else {
t8 = $[37];
}
let t9;
if ($[38] !== t6 || $[39] !== t8) {
t9 = <Pane color="professionalBlue">{t6}{t7}{t8}</Pane>;
$[38] = t6;
$[39] = t8;
$[40] = t9;
} else {
t9 = $[40];
}
let t10;
if ($[41] !== t5 || $[42] !== t9) {
t10 = <Box flexDirection="column" height={t5}>{t9}</Box>;
$[41] = t5;
$[42] = t9;
$[43] = t10;
} else {
t10 = $[43];
}
return t10;
} }

View File

@@ -1,189 +1,127 @@
import { c as _c } from "react/compiler-runtime"; import * as React from 'react'
import * as React from 'react'; import { memo, useEffect, useMemo, useRef, useState } from 'react'
import { memo, useEffect, useMemo, useRef, useState } from 'react'; import { useSettings } from '../hooks/useSettings.js'
import { useSettings } from '../hooks/useSettings.js'; import {
import { Ansi, Box, type DOMElement, measureElement, NoSelect, Text, useTheme } from '../ink.js'; Ansi,
import { isFullscreenEnvEnabled } from '../utils/fullscreen.js'; Box,
import sliceAnsi from '../utils/sliceAnsi.js'; type DOMElement,
import { countCharInString } from '../utils/stringUtils.js'; measureElement,
import { HighlightedCodeFallback } from './HighlightedCode/Fallback.js'; NoSelect,
import { expectColorFile } from './StructuredDiff/colorDiff.js'; Text,
useTheme,
} from '../ink.js'
import { isFullscreenEnvEnabled } from '../utils/fullscreen.js'
import sliceAnsi from '../utils/sliceAnsi.js'
import { countCharInString } from '../utils/stringUtils.js'
import { HighlightedCodeFallback } from './HighlightedCode/Fallback.js'
import { expectColorFile } from './StructuredDiff/colorDiff.js'
type Props = { type Props = {
code: string; code: string
filePath: string; filePath: string
width?: number; width?: number
dim?: boolean; dim?: boolean
}; }
const DEFAULT_WIDTH = 80;
export const HighlightedCode = memo(function HighlightedCode(t0: Props) { const DEFAULT_WIDTH = 80
const $ = _c(21);
const { export const HighlightedCode = memo(function HighlightedCode({
code, code,
filePath, filePath,
width, width,
dim: t1 dim = false,
} = t0; }: Props): React.ReactElement {
const dim = t1 === undefined ? false : t1; const ref = useRef<DOMElement>(null)
const ref = useRef(null); const [measuredWidth, setMeasuredWidth] = useState(width || DEFAULT_WIDTH)
const [measuredWidth, setMeasuredWidth] = useState(width || DEFAULT_WIDTH); const [theme] = useTheme()
const [theme] = useTheme(); const settings = useSettings()
const settings = useSettings(); const syntaxHighlightingDisabled =
const syntaxHighlightingDisabled = settings.syntaxHighlightingDisabled ?? false; settings.syntaxHighlightingDisabled ?? false
let t2;
bb0: { const colorFile = useMemo(() => {
if (syntaxHighlightingDisabled) { if (syntaxHighlightingDisabled) {
t2 = null; return null
break bb0; }
} const ColorFile = expectColorFile()
let t3; if (!ColorFile) {
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { return null
t3 = expectColorFile(); }
$[0] = t3; return new ColorFile(code, filePath)
} else { }, [code, filePath, syntaxHighlightingDisabled])
t3 = $[0];
} useEffect(() => {
const ColorFile = t3; if (!width && ref.current) {
if (!ColorFile) { const { width: elementWidth } = measureElement(ref.current)
t2 = null; if (elementWidth > 0) {
break bb0; setMeasuredWidth(elementWidth - 2)
} }
let t4; }
if ($[1] !== code || $[2] !== filePath) { }, [width])
t4 = new ColorFile(code, filePath);
$[1] = code; const lines = useMemo(() => {
$[2] = filePath; if (colorFile === null) {
$[3] = t4; return null
} else { }
t4 = $[3]; return colorFile.render(theme, measuredWidth, dim)
} }, [colorFile, theme, measuredWidth, dim])
t2 = t4;
} // Gutter width matches ColorFile's layout in lib.rs: space + right-aligned
const colorFile = t2; // line number (max_digits = lineCount.toString().length) + space. No marker
let t3; // column like the diff path. Wrap in <NoSelect> so fullscreen selection
let t4; // yields clean code without line numbers. Only split in fullscreen mode
if ($[4] !== width) { // (~4× DOM nodes + sliceAnsi cost); non-fullscreen uses terminal-native
t3 = () => { // selection where noSelect is meaningless.
if (!width && ref.current) { const gutterWidth = useMemo(() => {
const { if (!isFullscreenEnvEnabled()) return 0
width: elementWidth const lineCount = countCharInString(code, '\n') + 1
} = measureElement(ref.current); return lineCount.toString().length + 2
if (elementWidth > 0) { }, [code])
setMeasuredWidth(elementWidth - 2);
} return (
} <Box ref={ref}>
}; {lines ? (
t4 = [width]; <Box flexDirection="column">
$[4] = width; {lines.map((line, i) =>
$[5] = t3; gutterWidth > 0 ? (
$[6] = t4; <CodeLine key={i} line={line} gutterWidth={gutterWidth} />
} else { ) : (
t3 = $[5]; <Text key={i}>
t4 = $[6]; <Ansi>{line}</Ansi>
} </Text>
useEffect(t3, t4); ),
let t5; )}
bb1: { </Box>
if (colorFile === null) { ) : (
t5 = null; <HighlightedCodeFallback
break bb1; code={code}
} filePath={filePath}
let t6; dim={dim}
if ($[7] !== colorFile || $[8] !== dim || $[9] !== measuredWidth || $[10] !== theme) { skipColoring={syntaxHighlightingDisabled}
t6 = colorFile.render(theme, measuredWidth, dim); />
$[7] = colorFile; )}
$[8] = dim; </Box>
$[9] = measuredWidth; )
$[10] = theme; })
$[11] = t6;
} else { function CodeLine({
t6 = $[11]; line,
} gutterWidth,
t5 = t6; }: {
} line: string
const lines = t5; gutterWidth: number
let t6; }): React.ReactNode {
bb2: { const gutter = sliceAnsi(line, 0, gutterWidth)
if (!isFullscreenEnvEnabled()) { const content = sliceAnsi(line, gutterWidth)
t6 = 0; return (
break bb2; <Box flexDirection="row">
} <NoSelect fromLeftEdge>
const lineCount = countCharInString(code, "\n") + 1; <Text>
let t7; <Ansi>{gutter}</Ansi>
if ($[12] !== lineCount) { </Text>
t7 = lineCount.toString(); </NoSelect>
$[12] = lineCount; <Text>
$[13] = t7; <Ansi>{content}</Ansi>
} else { </Text>
t7 = $[13]; </Box>
} )
t6 = t7.length + 2;
}
const gutterWidth = t6;
let t7;
if ($[14] !== code || $[15] !== dim || $[16] !== filePath || $[17] !== gutterWidth || $[18] !== lines || $[19] !== syntaxHighlightingDisabled) {
t7 = <Box ref={ref}>{lines ? <Box flexDirection="column">{lines.map((line, i) => gutterWidth > 0 ? <CodeLine key={i} line={line} gutterWidth={gutterWidth} /> : <Text key={i}><Ansi>{line}</Ansi></Text>)}</Box> : <HighlightedCodeFallback code={code} filePath={filePath} dim={dim} skipColoring={syntaxHighlightingDisabled} />}</Box>;
$[14] = code;
$[15] = dim;
$[16] = filePath;
$[17] = gutterWidth;
$[18] = lines;
$[19] = syntaxHighlightingDisabled;
$[20] = t7;
} else {
t7 = $[20];
}
return t7;
});
function CodeLine(t0) {
const $ = _c(13);
const {
line,
gutterWidth
} = t0;
let t1;
if ($[0] !== gutterWidth || $[1] !== line) {
t1 = sliceAnsi(line, 0, gutterWidth);
$[0] = gutterWidth;
$[1] = line;
$[2] = t1;
} else {
t1 = $[2];
}
const gutter = t1;
let t2;
if ($[3] !== gutterWidth || $[4] !== line) {
t2 = sliceAnsi(line, gutterWidth);
$[3] = gutterWidth;
$[4] = line;
$[5] = t2;
} else {
t2 = $[5];
}
const content = t2;
let t3;
if ($[6] !== gutter) {
t3 = <NoSelect fromLeftEdge={true}><Text><Ansi>{gutter}</Ansi></Text></NoSelect>;
$[6] = gutter;
$[7] = t3;
} else {
t3 = $[7];
}
let t4;
if ($[8] !== content) {
t4 = <Text><Ansi>{content}</Ansi></Text>;
$[8] = content;
$[9] = t4;
} else {
t4 = $[9];
}
let t5;
if ($[10] !== t3 || $[11] !== t4) {
t5 = <Box flexDirection="row">{t3}{t4}</Box>;
$[10] = t3;
$[11] = t4;
$[12] = t5;
} else {
t5 = $[12];
}
return t5;
} }

View File

@@ -1,192 +1,99 @@
import { c as _c } from "react/compiler-runtime"; import { extname } from 'path'
import { extname } from 'path'; import React, { Suspense, use, useMemo } from 'react'
import React, { Suspense, use, useMemo } from 'react'; import { Ansi, Text } from '../../ink.js'
import { Ansi, Text } from '../../ink.js'; import { getCliHighlightPromise } from '../../utils/cliHighlight.js'
import { getCliHighlightPromise } from '../../utils/cliHighlight.js'; import { logForDebugging } from '../../utils/debug.js'
import { logForDebugging } from '../../utils/debug.js'; import { convertLeadingTabsToSpaces } from '../../utils/file.js'
import { convertLeadingTabsToSpaces } from '../../utils/file.js'; import { hashPair } from '../../utils/hash.js'
import { hashPair } from '../../utils/hash.js';
type Props = { type Props = {
code: string; code: string
filePath: string; filePath: string
dim?: boolean; dim?: boolean
skipColoring?: boolean; skipColoring?: boolean
}; }
// Module-level highlight cache — hl.highlight() is the hot cost on virtual- // Module-level highlight cache — hl.highlight() is the hot cost on virtual-
// scroll remounts. useMemo doesn't survive unmount→remount. Keyed by hash // scroll remounts. useMemo doesn't survive unmount→remount. Keyed by hash
// of code+language to avoid retaining full source strings (#24180 RSS fix). // of code+language to avoid retaining full source strings (#24180 RSS fix).
const HL_CACHE_MAX = 500; const HL_CACHE_MAX = 500
const hlCache = new Map<string, string>(); const hlCache = new Map<string, string>()
function cachedHighlight(hl: NonNullable<Awaited<ReturnType<typeof getCliHighlightPromise>>>, code: string, language: string): string { function cachedHighlight(
const key = hashPair(language, code); hl: NonNullable<Awaited<ReturnType<typeof getCliHighlightPromise>>>,
const hit = hlCache.get(key); code: string,
language: string,
): string {
const key = hashPair(language, code)
const hit = hlCache.get(key)
if (hit !== undefined) { if (hit !== undefined) {
hlCache.delete(key); hlCache.delete(key)
hlCache.set(key, hit); hlCache.set(key, hit)
return hit; return hit
} }
const out = hl.highlight(code, { const out = hl.highlight(code, { language })
language
});
if (hlCache.size >= HL_CACHE_MAX) { if (hlCache.size >= HL_CACHE_MAX) {
const first = hlCache.keys().next().value; const first = hlCache.keys().next().value
if (first !== undefined) hlCache.delete(first); if (first !== undefined) hlCache.delete(first)
} }
hlCache.set(key, out); hlCache.set(key, out)
return out; return out
} }
export function HighlightedCodeFallback(t0) {
const $ = _c(20); export function HighlightedCodeFallback({
const { code,
code, filePath,
filePath, dim = false,
dim: t1, skipColoring = false,
skipColoring: t2 }: Props): React.ReactElement {
} = t0; const codeWithSpaces = convertLeadingTabsToSpaces(code)
const dim = t1 === undefined ? false : t1;
const skipColoring = t2 === undefined ? false : t2;
let t3;
if ($[0] !== code) {
t3 = convertLeadingTabsToSpaces(code);
$[0] = code;
$[1] = t3;
} else {
t3 = $[1];
}
const codeWithSpaces = t3;
if (skipColoring) { if (skipColoring) {
let t4; return (
if ($[2] !== codeWithSpaces) { <Text dimColor={dim}>
t4 = <Ansi>{codeWithSpaces}</Ansi>; <Ansi>{codeWithSpaces}</Ansi>
$[2] = codeWithSpaces; </Text>
$[3] = t4; )
} else {
t4 = $[3];
}
let t5;
if ($[4] !== dim || $[5] !== t4) {
t5 = <Text dimColor={dim}>{t4}</Text>;
$[4] = dim;
$[5] = t4;
$[6] = t5;
} else {
t5 = $[6];
}
return t5;
} }
let t4; const language = extname(filePath).slice(1)
if ($[7] !== filePath) { return (
t4 = extname(filePath).slice(1); <Text dimColor={dim}>
$[7] = filePath; <Suspense fallback={<Ansi>{codeWithSpaces}</Ansi>}>
$[8] = t4; <Highlighted codeWithSpaces={codeWithSpaces} language={language} />
} else { </Suspense>
t4 = $[8]; </Text>
} )
const language = t4;
let t5;
if ($[9] !== codeWithSpaces) {
t5 = <Ansi>{codeWithSpaces}</Ansi>;
$[9] = codeWithSpaces;
$[10] = t5;
} else {
t5 = $[10];
}
let t6;
if ($[11] !== codeWithSpaces || $[12] !== language) {
t6 = <Highlighted codeWithSpaces={codeWithSpaces} language={language} />;
$[11] = codeWithSpaces;
$[12] = language;
$[13] = t6;
} else {
t6 = $[13];
}
let t7;
if ($[14] !== t5 || $[15] !== t6) {
t7 = <Suspense fallback={t5}>{t6}</Suspense>;
$[14] = t5;
$[15] = t6;
$[16] = t7;
} else {
t7 = $[16];
}
let t8;
if ($[17] !== dim || $[18] !== t7) {
t8 = <Text dimColor={dim}>{t7}</Text>;
$[17] = dim;
$[18] = t7;
$[19] = t8;
} else {
t8 = $[19];
}
return t8;
} }
function Highlighted(t0) {
const $ = _c(10); function Highlighted({
const { codeWithSpaces,
codeWithSpaces, language,
language }: {
} = t0; codeWithSpaces: string
let t1; language: string
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { }): React.ReactElement {
t1 = getCliHighlightPromise(); const hl = use(getCliHighlightPromise())
$[0] = t1; const out = useMemo(() => {
} else { if (!hl) return codeWithSpaces
t1 = $[0]; let highlightLang = 'markdown'
} if (language) {
const hl = use(t1) as NonNullable<Awaited<ReturnType<typeof getCliHighlightPromise>>> | null; if (hl.supportsLanguage(language)) {
let t2; highlightLang = language
if ($[1] !== codeWithSpaces || $[2] !== hl || $[3] !== language) { } else {
bb0: { logForDebugging(
if (!hl) { `Language not supported while highlighting code, falling back to markdown: ${language}`,
t2 = codeWithSpaces; )
break bb0;
}
let highlightLang = "markdown";
if (language) {
if (hl.supportsLanguage(language)) {
highlightLang = language;
} else {
logForDebugging(`Language not supported while highlighting code, falling back to markdown: ${language}`);
}
}
;
try {
t2 = cachedHighlight(hl, codeWithSpaces, highlightLang);
} catch (t3) {
const e = t3;
if (e instanceof Error && e.message.includes("Unknown language")) {
logForDebugging(`Language not supported while highlighting code, falling back to markdown: ${e}`);
let t4;
if ($[5] !== codeWithSpaces || $[6] !== hl) {
t4 = cachedHighlight(hl, codeWithSpaces, "markdown");
$[5] = codeWithSpaces;
$[6] = hl;
$[7] = t4;
} else {
t4 = $[7];
}
t2 = t4;
break bb0;
}
t2 = codeWithSpaces;
} }
} }
$[1] = codeWithSpaces; try {
$[2] = hl; return cachedHighlight(hl, codeWithSpaces, highlightLang)
$[3] = language; } catch (e) {
$[4] = t2; if (e instanceof Error && e.message.includes('Unknown language')) {
} else { logForDebugging(
t2 = $[4]; `Language not supported while highlighting code, falling back to markdown: ${e}`,
} )
const out = t2; return cachedHighlight(hl, codeWithSpaces, 'markdown')
let t3; }
if ($[8] !== out) { return codeWithSpaces
t3 = <Ansi>{out}</Ansi>; }
$[8] = out; }, [codeWithSpaces, language, hl])
$[9] = t3; return <Ansi>{out}</Ansi>
} else {
t3 = $[9];
}
return t3;
} }

View File

@@ -1,117 +1,170 @@
import * as React from 'react'; import * as React from 'react'
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react'
import { useRegisterOverlay } from '../context/overlayContext.js'; import { useRegisterOverlay } from '../context/overlayContext.js'
import { getTimestampedHistory, type TimestampedHistoryEntry } from '../history.js'; import {
import { useTerminalSize } from '../hooks/useTerminalSize.js'; getTimestampedHistory,
import { stringWidth } from '../ink/stringWidth.js'; type TimestampedHistoryEntry,
import { wrapAnsi } from '../ink/wrapAnsi.js'; } from '../history.js'
import { Box, Text } from '../ink.js'; import { useTerminalSize } from '../hooks/useTerminalSize.js'
import { logEvent } from '../services/analytics/index.js'; import { stringWidth } from '../ink/stringWidth.js'
import type { HistoryEntry } from '../utils/config.js'; import { wrapAnsi } from '../ink/wrapAnsi.js'
import { formatRelativeTimeAgo, truncateToWidth } from '../utils/format.js'; import { Box, Text } from '../ink.js'
import { FuzzyPicker } from './design-system/FuzzyPicker.js'; 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'
type Props = { type Props = {
initialQuery?: string; initialQuery?: string
onSelect: (entry: HistoryEntry) => void; onSelect: (entry: HistoryEntry) => void
onCancel: () => void; onCancel: () => void
}; }
const PREVIEW_ROWS = 6;
const AGE_WIDTH = 8; const PREVIEW_ROWS = 6
const AGE_WIDTH = 8
type Item = { type Item = {
entry: TimestampedHistoryEntry; entry: TimestampedHistoryEntry
display: string; display: string
lower: string; lower: string
firstLine: string; firstLine: string
age: string; age: string
}; }
export function HistorySearchDialog({ export function HistorySearchDialog({
initialQuery, initialQuery,
onSelect, onSelect,
onCancel onCancel,
}: Props): React.ReactNode { }: Props): React.ReactNode {
useRegisterOverlay('history-search', undefined); useRegisterOverlay('history-search')
const { const { columns } = useTerminalSize()
columns
} = useTerminalSize(); const [items, setItems] = useState<Item[] | null>(null)
const [items, setItems] = useState<Item[] | null>(null); const [query, setQuery] = useState(initialQuery ?? '')
const [query, setQuery] = useState(initialQuery ?? '');
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false
void (async () => { void (async () => {
const reader = getTimestampedHistory(); const reader = getTimestampedHistory()
const loaded: Item[] = []; const loaded: Item[] = []
for await (const entry of reader) { for await (const entry of reader) {
if (cancelled) { if (cancelled) {
void reader.return(undefined); void reader.return(undefined)
return; return
} }
const display = entry.display; const display = entry.display
const nl = display.indexOf('\n'); const nl = display.indexOf('\n')
const age = formatRelativeTimeAgo(new Date(entry.timestamp)); const age = formatRelativeTimeAgo(new Date(entry.timestamp))
loaded.push({ loaded.push({
entry, entry,
display, display,
lower: display.toLowerCase(), lower: display.toLowerCase(),
firstLine: nl === -1 ? display : display.slice(0, nl), firstLine: nl === -1 ? display : display.slice(0, nl),
age: age + ' '.repeat(Math.max(0, AGE_WIDTH - stringWidth(age))) age: age + ' '.repeat(Math.max(0, AGE_WIDTH - stringWidth(age))),
}); })
} }
if (!cancelled) setItems(loaded); if (!cancelled) setItems(loaded)
})(); })()
return () => { return () => {
cancelled = true; cancelled = true
}; }
}, []); }, [])
const filtered = useMemo(() => { const filtered = useMemo(() => {
if (!items) return []; if (!items) return []
const q = query.trim().toLowerCase(); const q = query.trim().toLowerCase()
if (!q) return items; if (!q) return items
const exact: Item[] = []; const exact: Item[] = []
const fuzzy: Item[] = []; const fuzzy: Item[] = []
for (const item of items) { for (const item of items) {
if (item.lower.includes(q)) { if (item.lower.includes(q)) {
exact.push(item); exact.push(item)
} else if (isSubsequence(item.lower, q)) { } else if (isSubsequence(item.lower, q)) {
fuzzy.push(item); fuzzy.push(item)
} }
} }
return exact.concat(fuzzy); return exact.concat(fuzzy)
}, [items, query]); }, [items, query])
const previewOnRight = columns >= 100;
const listWidth = previewOnRight ? Math.floor((columns - 6) * 0.5) : columns - 6; const previewOnRight = columns >= 100
const rowWidth = Math.max(20, listWidth - AGE_WIDTH - 1); const listWidth = previewOnRight
const previewWidth = previewOnRight ? Math.max(20, columns - listWidth - 12) : Math.max(20, columns - 10); ? Math.floor((columns - 6) * 0.5)
return <FuzzyPicker title="Search prompts" placeholder="Filter history…" initialQuery={initialQuery} items={filtered} getKey={item_0 => String(item_0.entry.timestamp)} onQueryChange={setQuery} onSelect={item_1 => { : columns - 6
logEvent('tengu_history_picker_select', { const rowWidth = Math.max(20, listWidth - AGE_WIDTH - 1)
result_count: filtered.length, const previewWidth = previewOnRight
query_length: query.length ? Math.max(20, columns - listWidth - 12)
}); : Math.max(20, columns - 10)
void item_1.entry.resolve().then(onSelect);
}} onCancel={onCancel} emptyMessage={q_0 => items === null ? 'Loading…' : q_0 ? 'No matching prompts' : 'No history yet'} selectAction="use" direction="up" previewPosition={previewOnRight ? 'right' : 'bottom'} renderItem={(item_2, isFocused) => <Text> return (
<Text dimColor>{item_2.age}</Text> <FuzzyPicker
title="Search prompts"
placeholder="Filter history…"
initialQuery={initialQuery}
items={filtered}
getKey={item => String(item.entry.timestamp)}
onQueryChange={setQuery}
onSelect={item => {
logEvent('tengu_history_picker_select', {
result_count: filtered.length,
query_length: query.length,
})
void item.entry.resolve().then(onSelect)
}}
onCancel={onCancel}
emptyMessage={q =>
items === null
? 'Loading…'
: q
? 'No matching prompts'
: 'No history yet'
}
selectAction="use"
direction="up"
previewPosition={previewOnRight ? 'right' : 'bottom'}
renderItem={(item, isFocused) => (
<Text>
<Text dimColor>{item.age}</Text>
<Text color={isFocused ? 'suggestion' : undefined}> <Text color={isFocused ? 'suggestion' : undefined}>
{' '} {' '}
{truncateToWidth(item_2.firstLine, rowWidth)} {truncateToWidth(item.firstLine, rowWidth)}
</Text> </Text>
</Text>} renderPreview={item_3 => { </Text>
const wrapped = wrapAnsi(item_3.display, previewWidth, { )}
hard: true renderPreview={item => {
}).split('\n').filter(l => l.trim() !== ''); const wrapped = wrapAnsi(item.display, previewWidth, { hard: true })
const overflow = wrapped.length > PREVIEW_ROWS; .split('\n')
const shown = wrapped.slice(0, overflow ? PREVIEW_ROWS - 1 : PREVIEW_ROWS); .filter(l => l.trim() !== '')
const more = wrapped.length - shown.length; const overflow = wrapped.length > PREVIEW_ROWS
return <Box flexDirection="column" borderStyle="round" borderDimColor paddingX={1} height={PREVIEW_ROWS + 2}> const shown = wrapped.slice(
{shown.map((row, i) => <Text key={i} dimColor> 0,
overflow ? PREVIEW_ROWS - 1 : PREVIEW_ROWS,
)
const more = wrapped.length - shown.length
return (
<Box
flexDirection="column"
borderStyle="round"
borderDimColor
paddingX={1}
height={PREVIEW_ROWS + 2}
>
{shown.map((row, i) => (
<Text key={i} dimColor>
{row} {row}
</Text>)} </Text>
))}
{more > 0 && <Text dimColor>{`… +${more} more lines`}</Text>} {more > 0 && <Text dimColor>{`… +${more} more lines`}</Text>}
</Box>; </Box>
}} />; )
}}
/>
)
} }
function isSubsequence(text: string, query: string): boolean { function isSubsequence(text: string, query: string): boolean {
let j = 0; let j = 0
for (let i = 0; i < text.length && j < query.length; i++) { for (let i = 0; i < text.length && j < query.length; i++) {
if (text[i] === query[j]) j++; if (text[i] === query[j]) j++
} }
return j === query.length; return j === query.length
} }

View File

@@ -1,153 +1,106 @@
import { c as _c } from "react/compiler-runtime"; import React, { useCallback } from 'react'
import React, { useCallback } from 'react'; import { Text } from '../ink.js'
import { Text } from '../ink.js'; import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js'
import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js'; import { isSupportedTerminal } from '../utils/ide.js'
import { isSupportedTerminal } from '../utils/ide.js'; import { Select } from './CustomSelect/index.js'
import { Select } from './CustomSelect/index.js'; import { Dialog } from './design-system/Dialog.js'
import { Dialog } from './design-system/Dialog.js';
type IdeAutoConnectDialogProps = { type IdeAutoConnectDialogProps = {
onComplete: () => void; onComplete: () => void
}; }
export function IdeAutoConnectDialog(t0) {
const $ = _c(9); export function IdeAutoConnectDialog({
const { onComplete,
onComplete }: IdeAutoConnectDialogProps): React.ReactNode {
} = t0; const handleSelect = useCallback(
let t1; async (value: string) => {
if ($[0] !== onComplete) { const autoConnect = value === 'yes'
t1 = async value => {
const autoConnect = value === "yes"; // Save the preference and mark dialog as shown
saveGlobalConfig(current => ({ saveGlobalConfig(current => ({
...current, ...current,
autoConnectIde: autoConnect, autoConnectIde: autoConnect,
hasIdeAutoConnectDialogBeenShown: true hasIdeAutoConnectDialogBeenShown: true,
})); }))
onComplete();
}; onComplete()
$[0] = onComplete; },
$[1] = t1; [onComplete],
} else { )
t1 = $[1];
} const options = [
const handleSelect = t1; { label: 'Yes', value: 'yes' },
let t2; { label: 'No', value: 'no' },
if ($[2] === Symbol.for("react.memo_cache_sentinel")) { ]
t2 = [{
label: "Yes", return (
value: "yes" <Dialog
}, { title="Do you wish to enable auto-connect to IDE?"
label: "No", color="ide"
value: "no" onCancel={onComplete}
}]; >
$[2] = t2; <Select options={options} onChange={handleSelect} defaultValue={'yes'} />
} else { <Text dimColor>
t2 = $[2]; You can also configure this in /config or with the --ide flag
} </Text>
const options = t2; </Dialog>
let t3; )
if ($[3] !== handleSelect) {
t3 = <Select options={options} onChange={handleSelect} defaultValue="yes" />;
$[3] = handleSelect;
$[4] = t3;
} else {
t3 = $[4];
}
let t4;
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
t4 = <Text dimColor={true}>You can also configure this in /config or with the --ide flag</Text>;
$[5] = t4;
} else {
t4 = $[5];
}
let t5;
if ($[6] !== onComplete || $[7] !== t3) {
t5 = <Dialog title="Do you wish to enable auto-connect to IDE?" color="ide" onCancel={onComplete}>{t3}{t4}</Dialog>;
$[6] = onComplete;
$[7] = t3;
$[8] = t5;
} else {
t5 = $[8];
}
return t5;
} }
export function shouldShowAutoConnectDialog(): boolean { export function shouldShowAutoConnectDialog(): boolean {
const config = getGlobalConfig(); const config = getGlobalConfig()
return !isSupportedTerminal() && config.autoConnectIde !== true && config.hasIdeAutoConnectDialogBeenShown !== true; return (
!isSupportedTerminal() &&
config.autoConnectIde !== true &&
config.hasIdeAutoConnectDialogBeenShown !== true
)
} }
type IdeDisableAutoConnectDialogProps = { type IdeDisableAutoConnectDialogProps = {
onComplete: (disableAutoConnect: boolean) => void; onComplete: (disableAutoConnect: boolean) => void
}; }
export function IdeDisableAutoConnectDialog(t0) {
const $ = _c(10); export function IdeDisableAutoConnectDialog({
const { onComplete,
onComplete }: IdeDisableAutoConnectDialogProps): React.ReactNode {
} = t0; const handleSelect = useCallback(
let t1; (value: string) => {
if ($[0] !== onComplete) { const disableAutoConnect = value === 'yes'
t1 = value => {
const disableAutoConnect = value === "yes";
if (disableAutoConnect) { if (disableAutoConnect) {
saveGlobalConfig(_temp); saveGlobalConfig(current => ({
...current,
autoConnectIde: false,
}))
} }
onComplete(disableAutoConnect);
}; onComplete(disableAutoConnect)
$[0] = onComplete; },
$[1] = t1; [onComplete],
} else { )
t1 = $[1];
} const handleCancel = useCallback(() => {
const handleSelect = t1; onComplete(false)
let t2; }, [onComplete])
if ($[2] !== onComplete) {
t2 = () => { const options = [
onComplete(false); { label: 'No', value: 'no' },
}; { label: 'Yes', value: 'yes' },
$[2] = onComplete; ]
$[3] = t2;
} else { return (
t2 = $[3]; <Dialog
} title="Do you wish to disable auto-connect to IDE?"
const handleCancel = t2; subtitle="You can also configure this in /config"
let t3; onCancel={handleCancel}
if ($[4] === Symbol.for("react.memo_cache_sentinel")) { color="ide"
t3 = [{ >
label: "No", <Select options={options} onChange={handleSelect} defaultValue={'no'} />
value: "no" </Dialog>
}, { )
label: "Yes",
value: "yes"
}];
$[4] = t3;
} else {
t3 = $[4];
}
const options = t3;
let t4;
if ($[5] !== handleSelect) {
t4 = <Select options={options} onChange={handleSelect} defaultValue="no" />;
$[5] = handleSelect;
$[6] = t4;
} else {
t4 = $[6];
}
let t5;
if ($[7] !== handleCancel || $[8] !== t4) {
t5 = <Dialog title="Do you wish to disable auto-connect to IDE?" subtitle="You can also configure this in /config" onCancel={handleCancel} color="ide">{t4}</Dialog>;
$[7] = handleCancel;
$[8] = t4;
$[9] = t5;
} else {
t5 = $[9];
}
return t5;
}
function _temp(current) {
return {
...current,
autoConnectIde: false
};
} }
export function shouldShowDisableAutoConnectDialog(): boolean { export function shouldShowDisableAutoConnectDialog(): boolean {
const config = getGlobalConfig(); const config = getGlobalConfig()
return !isSupportedTerminal() && config.autoConnectIde === true; return !isSupportedTerminal() && config.autoConnectIde === true
} }

View File

@@ -1,166 +1,108 @@
import { c as _c } from "react/compiler-runtime"; import React from 'react'
import React from 'react'; import { envDynamic } from 'src/utils/envDynamic.js'
import { envDynamic } from 'src/utils/envDynamic.js'; import { Box, Text } from '../ink.js'
import { Box, Text } from '../ink.js'; import { useKeybindings } from '../keybindings/useKeybinding.js'
import { useKeybindings } from '../keybindings/useKeybinding.js'; import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js'
import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js'; import { env } from '../utils/env.js'
import { env } from '../utils/env.js'; import {
import { getTerminalIdeType, type IDEExtensionInstallationStatus, isJetBrainsIde, toIDEDisplayName } from '../utils/ide.js'; getTerminalIdeType,
import { Dialog } from './design-system/Dialog.js'; type IDEExtensionInstallationStatus,
isJetBrainsIde,
toIDEDisplayName,
} from '../utils/ide.js'
import { Dialog } from './design-system/Dialog.js'
interface Props { interface Props {
onDone: () => void; onDone: () => void
installationStatus: IDEExtensionInstallationStatus | null; installationStatus: IDEExtensionInstallationStatus | null
} }
export function IdeOnboardingDialog(t0) {
const $ = _c(23); export function IdeOnboardingDialog({
const { onDone,
onDone, installationStatus,
installationStatus }: Props): React.ReactNode {
} = t0; markDialogAsShown()
markDialogAsShown();
let t1; // Handle Enter/Escape to dismiss
if ($[0] !== onDone) { useKeybindings(
t1 = { {
"confirm:yes": onDone, 'confirm:yes': onDone,
"confirm:no": onDone 'confirm:no': onDone,
}; },
$[0] = onDone; { context: 'Confirmation' },
$[1] = t1; )
} else {
t1 = $[1]; const ideType = installationStatus?.ideType ?? getTerminalIdeType()
} const isJetBrains = isJetBrainsIde(ideType)
let t2;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) { const ideName = toIDEDisplayName(ideType)
t2 = { const installedVersion = installationStatus?.installedVersion
context: "Confirmation" const pluginOrExtension = isJetBrains ? 'plugin' : 'extension'
}; const mentionShortcut =
$[2] = t2; env.platform === 'darwin' ? 'Cmd+Option+K' : 'Ctrl+Alt+K'
} else {
t2 = $[2]; return (
} <>
useKeybindings(t1, t2); <Dialog
let t3; title={
if ($[3] !== installationStatus?.ideType) { <>
t3 = installationStatus?.ideType ?? getTerminalIdeType(); <Text color="claude"> </Text>
$[3] = installationStatus?.ideType; <Text>Welcome to Claude Code for {ideName}</Text>
$[4] = t3; </>
} else { }
t3 = $[4]; subtitle={
} installedVersion
const ideType = t3; ? `installed ${pluginOrExtension} v${installedVersion}`
const isJetBrains = isJetBrainsIde(ideType); : undefined
let t4; }
if ($[5] !== ideType) { color="ide"
t4 = toIDEDisplayName(ideType); onCancel={onDone}
$[5] = ideType; hideInputGuide
$[6] = t4; >
} else { <Box flexDirection="column" gap={1}>
t4 = $[6]; <Text>
} Claude has context of <Text color="suggestion"> open files</Text>{' '}
const ideName = t4; and <Text color="suggestion"> selected lines</Text>
const installedVersion = installationStatus?.installedVersion; </Text>
const pluginOrExtension = isJetBrains ? "plugin" : "extension"; <Text>
const mentionShortcut = env.platform === "darwin" ? "Cmd+Option+K" : "Ctrl+Alt+K"; Review Claude Code&apos;s changes{' '}
let t5; <Text color="diffAddedWord">+11</Text>{' '}
if ($[7] === Symbol.for("react.memo_cache_sentinel")) { <Text color="diffRemovedWord">-22</Text> in the comfort of your IDE
t5 = <Text color="claude"> </Text>; </Text>
$[7] = t5; <Text>
} else { Cmd+Esc<Text dimColor> for Quick Launch</Text>
t5 = $[7]; </Text>
} <Text>
let t6; {mentionShortcut}
if ($[8] !== ideName) { <Text dimColor> to reference files or lines in your input</Text>
t6 = <>{t5}<Text>Welcome to Claude Code for {ideName}</Text></>; </Text>
$[8] = ideName; </Box>
$[9] = t6; </Dialog>
} else { <Box paddingX={1}>
t6 = $[9]; <Text dimColor italic>
} Press Enter to continue
const t7 = installedVersion ? `installed ${pluginOrExtension} v${installedVersion}` : undefined; </Text>
let t8; </Box>
if ($[10] === Symbol.for("react.memo_cache_sentinel")) { </>
t8 = <Text color="suggestion"> open files</Text>; )
$[10] = t8;
} else {
t8 = $[10];
}
let t9;
if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
t9 = <Text> Claude has context of {t8}{" "}and <Text color="suggestion"> selected lines</Text></Text>;
$[11] = t9;
} else {
t9 = $[11];
}
let t10;
if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
t10 = <Text color="diffAddedWord">+11</Text>;
$[12] = t10;
} else {
t10 = $[12];
}
let t11;
if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
t11 = <Text> Review Claude Code's changes{" "}{t10}{" "}<Text color="diffRemovedWord">-22</Text> in the comfort of your IDE</Text>;
$[13] = t11;
} else {
t11 = $[13];
}
let t12;
if ($[14] === Symbol.for("react.memo_cache_sentinel")) {
t12 = <Text>• Cmd+Esc<Text dimColor={true}> for Quick Launch</Text></Text>;
$[14] = t12;
} else {
t12 = $[14];
}
let t13;
if ($[15] === Symbol.for("react.memo_cache_sentinel")) {
t13 = <Box flexDirection="column" gap={1}>{t9}{t11}{t12}<Text>• {mentionShortcut}<Text dimColor={true}> to reference files or lines in your input</Text></Text></Box>;
$[15] = t13;
} else {
t13 = $[15];
}
let t14;
if ($[16] !== onDone || $[17] !== t6 || $[18] !== t7) {
t14 = <Dialog title={t6} subtitle={t7} color="ide" onCancel={onDone} hideInputGuide={true}>{t13}</Dialog>;
$[16] = onDone;
$[17] = t6;
$[18] = t7;
$[19] = t14;
} else {
t14 = $[19];
}
let t15;
if ($[20] === Symbol.for("react.memo_cache_sentinel")) {
t15 = <Box paddingX={1}><Text dimColor={true} italic={true}>Press Enter to continue</Text></Box>;
$[20] = t15;
} else {
t15 = $[20];
}
let t16;
if ($[21] !== t14) {
t16 = <>{t14}{t15}</>;
$[21] = t14;
$[22] = t16;
} else {
t16 = $[22];
}
return t16;
} }
export function hasIdeOnboardingDialogBeenShown(): boolean { export function hasIdeOnboardingDialogBeenShown(): boolean {
const config = getGlobalConfig(); const config = getGlobalConfig()
const terminal = envDynamic.terminal || 'unknown'; const terminal = envDynamic.terminal || 'unknown'
return config.hasIdeOnboardingBeenShown?.[terminal] === true; return config.hasIdeOnboardingBeenShown?.[terminal] === true
} }
function markDialogAsShown(): void { function markDialogAsShown(): void {
if (hasIdeOnboardingDialogBeenShown()) { if (hasIdeOnboardingDialogBeenShown()) {
return; return
} }
const terminal = envDynamic.terminal || 'unknown'; const terminal = envDynamic.terminal || 'unknown'
saveGlobalConfig(current => ({ saveGlobalConfig(current => ({
...current, ...current,
hasIdeOnboardingBeenShown: { hasIdeOnboardingBeenShown: {
...current.hasIdeOnboardingBeenShown, ...current.hasIdeOnboardingBeenShown,
[terminal]: true [terminal]: true,
} },
})); }))
} }

View File

@@ -1,57 +1,45 @@
import { c as _c } from "react/compiler-runtime"; import { basename } from 'path'
import { basename } from 'path'; import * as React from 'react'
import * as React from 'react'; import { useIdeConnectionStatus } from '../hooks/useIdeConnectionStatus.js'
import { useIdeConnectionStatus } from '../hooks/useIdeConnectionStatus.js'; import type { IDESelection } from '../hooks/useIdeSelection.js'
import type { IDESelection } from '../hooks/useIdeSelection.js'; import { Text } from '../ink.js'
import { Text } from '../ink.js'; import type { MCPServerConnection } from '../services/mcp/types.js'
import type { MCPServerConnection } from '../services/mcp/types.js';
type IdeStatusIndicatorProps = { type IdeStatusIndicatorProps = {
ideSelection: IDESelection | undefined; ideSelection: IDESelection | undefined
mcpClients?: MCPServerConnection[]; mcpClients?: MCPServerConnection[]
}; }
export function IdeStatusIndicator(t0) {
const $ = _c(7); export function IdeStatusIndicator({
const { ideSelection,
ideSelection, mcpClients,
mcpClients }: IdeStatusIndicatorProps): React.ReactNode {
} = t0; const { status: ideStatus } = useIdeConnectionStatus(mcpClients)
const {
status: ideStatus // Check if we should show the IDE selection indicator
} = useIdeConnectionStatus(mcpClients); const shouldShowIdeSelection =
const shouldShowIdeSelection = ideStatus === "connected" && (ideSelection?.filePath || ideSelection?.text && ideSelection.lineCount > 0); ideStatus === 'connected' &&
(ideSelection?.filePath ||
(ideSelection?.text && ideSelection.lineCount > 0))
if (ideStatus === null || !shouldShowIdeSelection || !ideSelection) { if (ideStatus === null || !shouldShowIdeSelection || !ideSelection) {
return null; return null
} }
if (ideSelection.text && ideSelection.lineCount > 0) { if (ideSelection.text && ideSelection.lineCount > 0) {
const t1 = ideSelection.lineCount === 1 ? "line" : "lines"; return (
let t2; <Text color="ide" key="selection-indicator" wrap="truncate">
if ($[0] !== ideSelection.lineCount || $[1] !== t1) { {ideSelection.lineCount}{' '}
t2 = <Text color="ide" key="selection-indicator" wrap="truncate"> {ideSelection.lineCount}{" "}{t1} selected</Text>; {ideSelection.lineCount === 1 ? 'line' : 'lines'} selected
$[0] = ideSelection.lineCount; </Text>
$[1] = t1; )
$[2] = t2;
} else {
t2 = $[2];
}
return t2;
} }
if (ideSelection.filePath) { if (ideSelection.filePath) {
let t1; return (
if ($[3] !== ideSelection.filePath) { <Text color="ide" key="selection-indicator" wrap="truncate">
t1 = basename(ideSelection.filePath); In {basename(ideSelection.filePath)}
$[3] = ideSelection.filePath; </Text>
$[4] = t1; )
} else {
t1 = $[4];
}
let t2;
if ($[5] !== t1) {
t2 = <Text color="ide" key="selection-indicator" wrap="truncate"> In {t1}</Text>;
$[5] = t1;
$[6] = t2;
} else {
t2 = $[6];
}
return t2;
} }
} }

View File

@@ -1,117 +1,67 @@
import { c as _c } from "react/compiler-runtime"; import React from 'react'
import React from 'react'; import { Box, Text } from '../ink.js'
import { Box, Text } from '../ink.js'; import { formatTokens } from '../utils/format.js'
import { formatTokens } from '../utils/format.js'; import { Select } from './CustomSelect/index.js'
import { Select } from './CustomSelect/index.js'; import { Dialog } from './design-system/Dialog.js'
import { Dialog } from './design-system/Dialog.js';
type IdleReturnAction = 'continue' | 'clear' | 'dismiss' | 'never'; type IdleReturnAction = 'continue' | 'clear' | 'dismiss' | 'never'
type Props = { type Props = {
idleMinutes: number; idleMinutes: number
totalInputTokens: number; totalInputTokens: number
onDone: (action: IdleReturnAction) => void; onDone: (action: IdleReturnAction) => void
};
export function IdleReturnDialog(t0) {
const $ = _c(16);
const {
idleMinutes,
totalInputTokens,
onDone
} = t0;
let t1;
if ($[0] !== idleMinutes) {
t1 = formatIdleDuration(idleMinutes);
$[0] = idleMinutes;
$[1] = t1;
} else {
t1 = $[1];
}
const formattedIdle = t1;
let t2;
if ($[2] !== totalInputTokens) {
t2 = formatTokens(totalInputTokens);
$[2] = totalInputTokens;
$[3] = t2;
} else {
t2 = $[3];
}
const formattedTokens = t2;
const t3 = `You've been away ${formattedIdle} and this conversation is ${formattedTokens} tokens.`;
let t4;
if ($[4] !== onDone) {
t4 = () => onDone("dismiss");
$[4] = onDone;
$[5] = t4;
} else {
t4 = $[5];
}
let t5;
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
t5 = <Box flexDirection="column"><Text>If this is a new task, clearing context will save usage and be faster.</Text></Box>;
$[6] = t5;
} else {
t5 = $[6];
}
let t6;
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
t6 = {
value: "continue" as const,
label: "Continue this conversation"
};
$[7] = t6;
} else {
t6 = $[7];
}
let t7;
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
t7 = {
value: "clear" as const,
label: "Send message as a new conversation"
};
$[8] = t7;
} else {
t7 = $[8];
}
let t8;
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
t8 = [t6, t7, {
value: "never" as const,
label: "Don't ask me again"
}];
$[9] = t8;
} else {
t8 = $[9];
}
let t9;
if ($[10] !== onDone) {
t9 = <Select options={t8} onChange={value => onDone(value)} />;
$[10] = onDone;
$[11] = t9;
} else {
t9 = $[11];
}
let t10;
if ($[12] !== t3 || $[13] !== t4 || $[14] !== t9) {
t10 = <Dialog title={t3} onCancel={t4}>{t5}{t9}</Dialog>;
$[12] = t3;
$[13] = t4;
$[14] = t9;
$[15] = t10;
} else {
t10 = $[15];
}
return t10;
} }
export function IdleReturnDialog({
idleMinutes,
totalInputTokens,
onDone,
}: Props): React.ReactNode {
const formattedIdle = formatIdleDuration(idleMinutes)
const formattedTokens = formatTokens(totalInputTokens)
return (
<Dialog
title={`You've been away ${formattedIdle} and this conversation is ${formattedTokens} tokens.`}
onCancel={() => onDone('dismiss')}
>
<Box flexDirection="column">
<Text>
If this is a new task, clearing context will save usage and be faster.
</Text>
</Box>
<Select
options={[
{
value: 'continue' as const,
label: 'Continue this conversation',
},
{
value: 'clear' as const,
label: 'Send message as a new conversation',
},
{
value: 'never' as const,
label: "Don't ask me again",
},
]}
onChange={(value: IdleReturnAction) => onDone(value)}
/>
</Dialog>
)
}
function formatIdleDuration(minutes: number): string { function formatIdleDuration(minutes: number): string {
if (minutes < 1) { if (minutes < 1) {
return '< 1m'; return '< 1m'
} }
if (minutes < 60) { if (minutes < 60) {
return `${Math.floor(minutes)}m`; return `${Math.floor(minutes)}m`
} }
const hours = Math.floor(minutes / 60); const hours = Math.floor(minutes / 60)
const remainingMinutes = Math.floor(minutes % 60); const remainingMinutes = Math.floor(minutes % 60)
if (remainingMinutes === 0) { if (remainingMinutes === 0) {
return `${hours}h`; return `${hours}h`
} }
return `${hours}h ${remainingMinutes}m`; return `${hours}h ${remainingMinutes}m`
} }

View File

@@ -1,14 +1,15 @@
import { c as _c } from "react/compiler-runtime"; import * as React from 'react'
import * as React from 'react'; import { Text } from '../ink.js'
import { Text } from '../ink.js';
export function InterruptedByUser() { export function InterruptedByUser(): React.ReactNode {
const $ = _c(1); return (
let t0; <>
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { <Text dimColor>Interrupted </Text>
t0 = <><Text dimColor={true}>Interrupted </Text>{false ? <Text dimColor={true}>· [ANT-ONLY] /issue to report a model issue</Text> : <Text dimColor={true}>· What should Claude do instead?</Text>}</>; {process.env.USER_TYPE === 'ant' ? (
$[0] = t0; <Text dimColor>· [ANT-ONLY] /issue to report a model issue</Text>
} else { ) : (
t0 = $[0]; <Text dimColor>· What should Claude do instead?</Text>
} )}
return t0; </>
)
} }

View File

@@ -1,155 +1,115 @@
import { c as _c } from "react/compiler-runtime"; import React from 'react'
import React from 'react'; import { Box, render, Text } from '../ink.js'
import { Box, render, Text } from '../ink.js'; import { KeybindingSetup } from '../keybindings/KeybindingProviderSetup.js'
import { KeybindingSetup } from '../keybindings/KeybindingProviderSetup.js'; import { AppStateProvider } from '../state/AppState.js'
import { AppStateProvider } from '../state/AppState.js'; import type { ConfigParseError } from '../utils/errors.js'
import type { ConfigParseError } from '../utils/errors.js'; import { getBaseRenderOptions } from '../utils/renderOptions.js'
import { getBaseRenderOptions } from '../utils/renderOptions.js'; import {
import { jsonStringify, writeFileSync_DEPRECATED } from '../utils/slowOperations.js'; jsonStringify,
import type { ThemeName } from '../utils/theme.js'; writeFileSync_DEPRECATED,
import { Select } from './CustomSelect/index.js'; } from '../utils/slowOperations.js'
import { Dialog } from './design-system/Dialog.js'; import type { ThemeName } from '../utils/theme.js'
import { Select } from './CustomSelect/index.js'
import { Dialog } from './design-system/Dialog.js'
interface InvalidConfigHandlerProps { interface InvalidConfigHandlerProps {
error: ConfigParseError; error: ConfigParseError
} }
interface InvalidConfigDialogProps { interface InvalidConfigDialogProps {
filePath: string; filePath: string
errorDescription: string; errorDescription: string
onExit: () => void; onExit: () => void
onReset: () => void; onReset: () => void
} }
/** /**
* Dialog shown when the Claude config file contains invalid JSON * Dialog shown when the Claude config file contains invalid JSON
*/ */
function InvalidConfigDialog(t0) { function InvalidConfigDialog({
const $ = _c(19); filePath,
const { errorDescription,
filePath, onExit,
errorDescription, onReset,
onExit, }: InvalidConfigDialogProps): React.ReactNode {
onReset // Handler for Select onChange
} = t0; const handleSelect = (value: string) => {
let t1; if (value === 'exit') {
if ($[0] !== onExit || $[1] !== onReset) { onExit()
t1 = value => { } else {
if (value === "exit") { onReset()
onExit(); }
} else {
onReset();
}
};
$[0] = onExit;
$[1] = onReset;
$[2] = t1;
} else {
t1 = $[2];
} }
const handleSelect = t1;
let t2; return (
if ($[3] !== filePath) { <Dialog title="Configuration Error" color="error" onCancel={onExit}>
t2 = <Text>The configuration file at <Text bold={true}>{filePath}</Text> contains invalid JSON.</Text>; <Box flexDirection="column" gap={1}>
$[3] = filePath; <Text>
$[4] = t2; The configuration file at <Text bold>{filePath}</Text> contains
} else { invalid JSON.
t2 = $[4]; </Text>
} <Text>{errorDescription}</Text>
let t3; </Box>
if ($[5] !== errorDescription) { <Box flexDirection="column">
t3 = <Text>{errorDescription}</Text>; <Text bold>Choose an option:</Text>
$[5] = errorDescription; <Select
$[6] = t3; options={[
} else { { label: 'Exit and fix manually', value: 'exit' },
t3 = $[6]; { label: 'Reset with default configuration', value: 'reset' },
} ]}
let t4; onChange={handleSelect}
if ($[7] !== t2 || $[8] !== t3) { onCancel={onExit}
t4 = <Box flexDirection="column" gap={1}>{t2}{t3}</Box>; />
$[7] = t2; </Box>
$[8] = t3; </Dialog>
$[9] = t4; )
} else {
t4 = $[9];
}
let t5;
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
t5 = <Text bold={true}>Choose an option:</Text>;
$[10] = t5;
} else {
t5 = $[10];
}
let t6;
if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
t6 = [{
label: "Exit and fix manually",
value: "exit"
}, {
label: "Reset with default configuration",
value: "reset"
}];
$[11] = t6;
} else {
t6 = $[11];
}
let t7;
if ($[12] !== handleSelect || $[13] !== onExit) {
t7 = <Box flexDirection="column">{t5}<Select options={t6} onChange={handleSelect} onCancel={onExit} /></Box>;
$[12] = handleSelect;
$[13] = onExit;
$[14] = t7;
} else {
t7 = $[14];
}
let t8;
if ($[15] !== onExit || $[16] !== t4 || $[17] !== t7) {
t8 = <Dialog title="Configuration Error" color="error" onCancel={onExit}>{t4}{t7}</Dialog>;
$[15] = onExit;
$[16] = t4;
$[17] = t7;
$[18] = t8;
} else {
t8 = $[18];
}
return t8;
} }
/** /**
* Safe fallback theme name for error dialogs to avoid circular dependency. * Safe fallback theme name for error dialogs to avoid circular dependency.
* Uses a hardcoded dark theme that doesn't require reading from config. * Uses a hardcoded dark theme that doesn't require reading from config.
*/ */
const SAFE_ERROR_THEME_NAME: ThemeName = 'dark'; const SAFE_ERROR_THEME_NAME: ThemeName = 'dark'
export async function showInvalidConfigDialog({ export async function showInvalidConfigDialog({
error error,
}: InvalidConfigHandlerProps): Promise<void> { }: InvalidConfigHandlerProps): Promise<void> {
// Extend RenderOptions with theme property for this specific usage // Extend RenderOptions with theme property for this specific usage
type SafeRenderOptions = Parameters<typeof render>[1] & { type SafeRenderOptions = Parameters<typeof render>[1] & { theme?: ThemeName }
theme?: ThemeName;
};
const renderOptions: SafeRenderOptions = { const renderOptions: SafeRenderOptions = {
...getBaseRenderOptions(false), ...getBaseRenderOptions(false),
// IMPORTANT: Use hardcoded theme name to avoid circular dependency with getGlobalConfig() // IMPORTANT: Use hardcoded theme name to avoid circular dependency with getGlobalConfig()
// This allows the error dialog to show even when config file has JSON syntax errors // This allows the error dialog to show even when config file has JSON syntax errors
theme: SAFE_ERROR_THEME_NAME theme: SAFE_ERROR_THEME_NAME,
}; }
await new Promise<void>(async resolve => { await new Promise<void>(async resolve => {
const { const { unmount } = await render(
unmount <AppStateProvider>
} = await render(<AppStateProvider>
<KeybindingSetup> <KeybindingSetup>
<InvalidConfigDialog filePath={error.filePath} errorDescription={error.message} onExit={() => { <InvalidConfigDialog
unmount(); filePath={error.filePath}
void resolve(); errorDescription={error.message}
process.exit(1); onExit={() => {
}} onReset={() => { unmount()
writeFileSync_DEPRECATED(error.filePath, jsonStringify(error.defaultConfig, null, 2), { void resolve()
flush: false, process.exit(1)
encoding: 'utf8' }}
}); onReset={() => {
unmount(); writeFileSync_DEPRECATED(
void resolve(); error.filePath,
process.exit(0); jsonStringify(error.defaultConfig, null, 2),
}} /> { flush: false, encoding: 'utf8' },
)
unmount()
void resolve()
process.exit(0)
}}
/>
</KeybindingSetup> </KeybindingSetup>
</AppStateProvider>, renderOptions); </AppStateProvider>,
}); renderOptions,
)
})
} }

View File

@@ -1,88 +1,49 @@
import { c as _c } from "react/compiler-runtime"; import React from 'react'
import React from 'react'; import { Text } from '../ink.js'
import { Text } from '../ink.js'; import type { ValidationError } from '../utils/settings/validation.js'
import type { ValidationError } from '../utils/settings/validation.js'; import { Select } from './CustomSelect/index.js'
import { Select } from './CustomSelect/index.js'; import { Dialog } from './design-system/Dialog.js'
import { Dialog } from './design-system/Dialog.js'; import { ValidationErrorsList } from './ValidationErrorsList.js'
import { ValidationErrorsList } from './ValidationErrorsList.js';
type Props = { type Props = {
settingsErrors: ValidationError[]; settingsErrors: ValidationError[]
onContinue: () => void; onContinue: () => void
onExit: () => void; onExit: () => void
}; }
/** /**
* Dialog shown when settings files have validation errors. * Dialog shown when settings files have validation errors.
* User must choose to continue (skipping invalid files) or exit to fix them. * User must choose to continue (skipping invalid files) or exit to fix them.
*/ */
export function InvalidSettingsDialog(t0) { export function InvalidSettingsDialog({
const $ = _c(13); settingsErrors,
const { onContinue,
settingsErrors, onExit,
onContinue, }: Props): React.ReactNode {
onExit function handleSelect(value: string): void {
} = t0; if (value === 'exit') {
let t1; onExit()
if ($[0] !== onContinue || $[1] !== onExit) { } else {
t1 = function handleSelect(value) { onContinue()
if (value === "exit") { }
onExit();
} else {
onContinue();
}
};
$[0] = onContinue;
$[1] = onExit;
$[2] = t1;
} else {
t1 = $[2];
} }
const handleSelect = t1;
let t2; return (
if ($[3] !== settingsErrors) { <Dialog title="Settings Error" onCancel={onExit} color="warning">
t2 = <ValidationErrorsList errors={settingsErrors} />; <ValidationErrorsList errors={settingsErrors} />
$[3] = settingsErrors; <Text dimColor>
$[4] = t2; Files with errors are skipped entirely, not just the invalid settings.
} else { </Text>
t2 = $[4]; <Select
} options={[
let t3; { label: 'Exit and fix manually', value: 'exit' },
if ($[5] === Symbol.for("react.memo_cache_sentinel")) { {
t3 = <Text dimColor={true}>Files with errors are skipped entirely, not just the invalid settings.</Text>; label: 'Continue without these settings',
$[5] = t3; value: 'continue',
} else { },
t3 = $[5]; ]}
} onChange={handleSelect}
let t4; />
if ($[6] === Symbol.for("react.memo_cache_sentinel")) { </Dialog>
t4 = [{ )
label: "Exit and fix manually",
value: "exit"
}, {
label: "Continue without these settings",
value: "continue"
}];
$[6] = t4;
} else {
t4 = $[6];
}
let t5;
if ($[7] !== handleSelect) {
t5 = <Select options={t4} onChange={handleSelect} />;
$[7] = handleSelect;
$[8] = t5;
} else {
t5 = $[8];
}
let t6;
if ($[9] !== onExit || $[10] !== t2 || $[11] !== t5) {
t6 = <Dialog title="Settings Error" onCancel={onExit} color="warning">{t2}{t3}{t5}</Dialog>;
$[9] = onExit;
$[10] = t2;
$[11] = t5;
$[12] = t6;
} else {
t6 = $[12];
}
return t6;
} }

View File

@@ -1,7 +1,10 @@
import { c as _c } from "react/compiler-runtime"; import React from 'react'
import React from 'react'; import { Box, Text } from '../ink.js'
import { Box, Text } from '../ink.js'; import {
import { getCachedKeybindingWarnings, getKeybindingsPath, isKeybindingCustomizationEnabled } from '../keybindings/loadUserBindings.js'; getCachedKeybindingWarnings,
getKeybindingsPath,
isKeybindingCustomizationEnabled,
} from '../keybindings/loadUserBindings.js'
/** /**
* Displays keybinding validation warnings in the UI. * Displays keybinding validation warnings in the UI.
@@ -10,45 +13,60 @@ import { getCachedKeybindingWarnings, getKeybindingsPath, isKeybindingCustomizat
* *
* Only shown when keybinding customization is enabled (ant users + feature gate). * Only shown when keybinding customization is enabled (ant users + feature gate).
*/ */
export function KeybindingWarnings() { export function KeybindingWarnings(): React.ReactNode {
const $ = _c(2); // Only show warnings when keybinding customization is enabled
if (!isKeybindingCustomizationEnabled()) { if (!isKeybindingCustomizationEnabled()) {
return null; return null
} }
let t0;
let t1; const warnings = getCachedKeybindingWarnings()
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = Symbol.for("react.early_return_sentinel"); if (warnings.length === 0) {
bb0: { return null
const warnings = getCachedKeybindingWarnings();
if (warnings.length === 0) {
t1 = null;
break bb0;
}
const errors = warnings.filter(_temp);
const warns = warnings.filter(_temp2);
t0 = <Box flexDirection="column" marginTop={1} marginBottom={1}><Text bold={true} color={errors.length > 0 ? "error" : "warning"}>Keybinding Configuration Issues</Text><Box><Text dimColor={true}>Location: </Text><Text dimColor={true}>{getKeybindingsPath()}</Text></Box><Box marginLeft={1} flexDirection="column" marginTop={1}>{errors.map(_temp3)}{warns.map(_temp4)}</Box></Box>;
}
$[0] = t0;
$[1] = t1;
} else {
t0 = $[0];
t1 = $[1];
} }
if (t1 !== Symbol.for("react.early_return_sentinel")) {
return t1; const errors = warnings.filter(w => w.severity === 'error')
} const warns = warnings.filter(w => w.severity === 'warning')
return t0;
} return (
function _temp4(warning, i_0) { <Box flexDirection="column" marginTop={1} marginBottom={1}>
return <Box key={`warning-${i_0}`} flexDirection="column"><Box><Text dimColor={true}> </Text><Text color="warning">[Warning]</Text><Text dimColor={true}> {warning.message}</Text></Box>{warning.suggestion && <Box marginLeft={3}><Text dimColor={true}> {warning.suggestion}</Text></Box>}</Box>; <Text bold color={errors.length > 0 ? 'error' : 'warning'}>
} Keybinding Configuration Issues
function _temp3(error, i) { </Text>
return <Box key={`error-${i}`} flexDirection="column"><Box><Text dimColor={true}> </Text><Text color="error">[Error]</Text><Text dimColor={true}> {error.message}</Text></Box>{error.suggestion && <Box marginLeft={3}><Text dimColor={true}> {error.suggestion}</Text></Box>}</Box>; <Box>
} <Text dimColor>Location: </Text>
function _temp2(w_0) { <Text dimColor>{getKeybindingsPath()}</Text>
return w_0.severity === "warning"; </Box>
} <Box marginLeft={1} flexDirection="column" marginTop={1}>
function _temp(w) { {errors.map((error, i) => (
return w.severity === "error"; <Box key={`error-${i}`} flexDirection="column">
<Box>
<Text dimColor> </Text>
<Text color="error">[Error]</Text>
<Text dimColor> {error.message}</Text>
</Box>
{error.suggestion && (
<Box marginLeft={3}>
<Text dimColor> {error.suggestion}</Text>
</Box>
)}
</Box>
))}
{warns.map((warning, i) => (
<Box key={`warning-${i}`} flexDirection="column">
<Box>
<Text dimColor> </Text>
<Text color="warning">[Warning]</Text>
<Text dimColor> {warning.message}</Text>
</Box>
{warning.suggestion && (
<Box marginLeft={3}>
<Text dimColor> {warning.suggestion}</Text>
</Box>
)}
</Box>
))}
</Box>
</Box>
)
} }

View File

@@ -1,85 +1,52 @@
import { c as _c } from "react/compiler-runtime"; import figures from 'figures'
import figures from 'figures'; import React, { useState } from 'react'
import React, { useState } from 'react'; import { Box, Text } from '../ink.js'
import { Box, Text } from '../ink.js'; import { useKeybinding } from '../keybindings/useKeybinding.js'
import { useKeybinding } from '../keybindings/useKeybinding.js'; import TextInput from './TextInput.js'
import TextInput from './TextInput.js';
type Props = { type Props = {
initialLanguage: string | undefined; initialLanguage: string | undefined
onComplete: (language: string | undefined) => void; onComplete: (language: string | undefined) => void
onCancel: () => void; onCancel: () => void
}; }
export function LanguagePicker(t0) {
const $ = _c(13); export function LanguagePicker({
const { initialLanguage,
initialLanguage, onComplete,
onComplete, onCancel,
onCancel }: Props): React.ReactNode {
} = t0; const [language, setLanguage] = useState(initialLanguage)
const [language, setLanguage] = useState(initialLanguage); const [cursorOffset, setCursorOffset] = useState(
const [cursorOffset, setCursorOffset] = useState((initialLanguage ?? "").length); (initialLanguage ?? '').length,
let t1; )
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = { // Use configurable keybinding for ESC to cancel
context: "Settings" // Use Settings context so 'n' key doesn't trigger cancel (allows typing 'n' in input)
}; useKeybinding('confirm:no', onCancel, { context: 'Settings' })
$[0] = t1;
} else { function handleSubmit(): void {
t1 = $[0]; const trimmed = language?.trim()
} onComplete(trimmed || undefined)
useKeybinding("confirm:no", onCancel, t1); }
let t2;
if ($[1] !== language || $[2] !== onComplete) { return (
t2 = function handleSubmit() { <Box flexDirection="column" gap={1}>
const trimmed = language?.trim(); <Text>Enter your preferred response and voice language:</Text>
onComplete(trimmed || undefined); <Box flexDirection="row" gap={1}>
}; <Text>{figures.pointer}</Text>
$[1] = language; <TextInput
$[2] = onComplete; value={language ?? ''}
$[3] = t2; onChange={setLanguage}
} else { onSubmit={handleSubmit}
t2 = $[3]; focus={true}
} showCursor={true}
const handleSubmit = t2; placeholder={`e.g., Japanese, 日本語, Español${figures.ellipsis}`}
let t3; columns={60}
if ($[4] === Symbol.for("react.memo_cache_sentinel")) { cursorOffset={cursorOffset}
t3 = <Text>Enter your preferred response and voice language:</Text>; onChangeCursorOffset={setCursorOffset}
$[4] = t3; />
} else { </Box>
t3 = $[4]; <Text dimColor>Leave empty for default (English)</Text>
} </Box>
let t4; )
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
t4 = <Text>{figures.pointer}</Text>;
$[5] = t4;
} else {
t4 = $[5];
}
const t5 = language ?? "";
let t6;
if ($[6] !== cursorOffset || $[7] !== handleSubmit || $[8] !== t5) {
t6 = <Box flexDirection="row" gap={1}>{t4}<TextInput value={t5} onChange={setLanguage} onSubmit={handleSubmit} focus={true} showCursor={true} placeholder={`e.g., Japanese, 日本語, Español${figures.ellipsis}`} columns={60} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} /></Box>;
$[6] = cursorOffset;
$[7] = handleSubmit;
$[8] = t5;
$[9] = t6;
} else {
t6 = $[9];
}
let t7;
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
t7 = <Text dimColor={true}>Leave empty for default (English)</Text>;
$[10] = t7;
} else {
t7 = $[10];
}
let t8;
if ($[11] !== t6) {
t8 = <Box flexDirection="column" gap={1}>{t3}{t6}{t7}</Box>;
$[11] = t6;
$[12] = t8;
} else {
t8 = $[12];
}
return t8;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,49 +1,57 @@
import * as React from 'react'; import * as React from 'react'
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react'
import { TEARDROP_ASTERISK } from '../../constants/figures.js'; import { TEARDROP_ASTERISK } from '../../constants/figures.js'
import { Box, Text, useAnimationFrame } from '../../ink.js'; import { Box, Text, useAnimationFrame } from '../../ink.js'
import { getInitialSettings } from '../../utils/settings/settings.js'; import { getInitialSettings } from '../../utils/settings/settings.js'
import { hueToRgb, toRGBColor } from '../Spinner/utils.js'; import { hueToRgb, toRGBColor } from '../Spinner/utils.js'
const SWEEP_DURATION_MS = 1500;
const SWEEP_COUNT = 2; const SWEEP_DURATION_MS = 1500
const TOTAL_ANIMATION_MS = SWEEP_DURATION_MS * SWEEP_COUNT; const SWEEP_COUNT = 2
const SETTLED_GREY = toRGBColor({ const TOTAL_ANIMATION_MS = SWEEP_DURATION_MS * SWEEP_COUNT
r: 153, const SETTLED_GREY = toRGBColor({ r: 153, g: 153, b: 153 })
g: 153,
b: 153
});
export function AnimatedAsterisk({ export function AnimatedAsterisk({
char = TEARDROP_ASTERISK char = TEARDROP_ASTERISK,
}: { }: {
char?: string; char?: string
}): React.ReactNode { }): React.ReactNode {
// Read prefersReducedMotion once at mount — no useSettings() subscription, // Read prefersReducedMotion once at mount — no useSettings() subscription,
// since that would re-render whenever settings change. // since that would re-render whenever settings change.
const [reducedMotion] = useState(() => getInitialSettings().prefersReducedMotion ?? false); const [reducedMotion] = useState(
const [done, setDone] = useState(reducedMotion); () => getInitialSettings().prefersReducedMotion ?? false,
)
const [done, setDone] = useState(reducedMotion)
// useAnimationFrame's clock is shared — capture our start offset so the // useAnimationFrame's clock is shared — capture our start offset so the
// sweep always begins at hue 0 regardless of when we mount. // sweep always begins at hue 0 regardless of when we mount.
const startTimeRef = useRef<number | null>(null); const startTimeRef = useRef<number | null>(null)
// Wire the ref so useAnimationFrame's viewport-pause kicks in: if the // Wire the ref so useAnimationFrame's viewport-pause kicks in: if the
// user submits a message before the sweep finishes, the clock stops // user submits a message before the sweep finishes, the clock stops
// automatically once this row enters scrollback (prevents flicker). // automatically once this row enters scrollback (prevents flicker).
const [ref, time] = useAnimationFrame(done ? null : 50); const [ref, time] = useAnimationFrame(done ? null : 50)
useEffect(() => { useEffect(() => {
if (done) return; if (done) return
const t = setTimeout(setDone, TOTAL_ANIMATION_MS, true); const t = setTimeout(setDone, TOTAL_ANIMATION_MS, true)
return () => clearTimeout(t); return () => clearTimeout(t)
}, [done]); }, [done])
if (done) { if (done) {
return <Box ref={ref}> return (
<Box ref={ref}>
<Text color={SETTLED_GREY}>{char}</Text> <Text color={SETTLED_GREY}>{char}</Text>
</Box>; </Box>
)
} }
if (startTimeRef.current === null) { if (startTimeRef.current === null) {
startTimeRef.current = time; startTimeRef.current = time
} }
const elapsed = time - startTimeRef.current; const elapsed = time - startTimeRef.current
const hue = elapsed / SWEEP_DURATION_MS * 360 % 360; const hue = ((elapsed / SWEEP_DURATION_MS) * 360) % 360
return <Box ref={ref}>
return (
<Box ref={ref}>
<Text color={toRGBColor(hueToRgb(hue))}>{char}</Text> <Text color={toRGBColor(hueToRgb(hue))}>{char}</Text>
</Box>; </Box>
)
} }

View File

@@ -1,22 +1,14 @@
import { c as _c } from "react/compiler-runtime"; import * as React from 'react'
import * as React from 'react'; import { useEffect, useRef, useState } from 'react'
import { useEffect, useRef, useState } from 'react'; import { Box } from '../../ink.js'
import { Box } from '../../ink.js'; import { getInitialSettings } from '../../utils/settings/settings.js'
import { getInitialSettings } from '../../utils/settings/settings.js'; import { Clawd, type ClawdPose } from './Clawd.js'
import { Clawd, type ClawdPose } from './Clawd.js';
type Frame = { type Frame = { pose: ClawdPose; offset: number }
pose: ClawdPose;
offset: number;
};
/** Hold a pose for n frames (60ms each). */ /** Hold a pose for n frames (60ms each). */
function hold(pose: ClawdPose, offset: number, frames: number): Frame[] { function hold(pose: ClawdPose, offset: number, frames: number): Frame[] {
return Array.from({ return Array.from({ length: frames }, () => ({ pose, offset }))
length: frames
}, () => ({
pose,
offset
}));
} }
// Offset semantics: marginTop in a fixed-height-3 container. 0 = normal, // Offset semantics: marginTop in a fixed-height-3 container. 0 = normal,
@@ -25,26 +17,28 @@ function hold(pose: ClawdPose, offset: number, frames: number): Frame[] {
// clipped — reads as "ducking below the frame" before springing back up. // clipped — reads as "ducking below the frame" before springing back up.
// Click animation: crouch, then spring up with both arms raised. Twice. // Click animation: crouch, then spring up with both arms raised. Twice.
const JUMP_WAVE: readonly Frame[] = [...hold('default', 1, 2), const JUMP_WAVE: readonly Frame[] = [
// crouch ...hold('default', 1, 2), // crouch
...hold('arms-up', 0, 3), ...hold('arms-up', 0, 3), // spring!
// spring! ...hold('default', 0, 1),
...hold('default', 0, 1), ...hold('default', 1, 2), ...hold('default', 1, 2), // crouch again
// crouch again ...hold('arms-up', 0, 3), // spring!
...hold('arms-up', 0, 3), ...hold('default', 0, 1),
// spring! ]
...hold('default', 0, 1)];
// Click animation: glance right, then left, then back. // Click animation: glance right, then left, then back.
const LOOK_AROUND: readonly Frame[] = [...hold('look-right', 0, 5), ...hold('look-left', 0, 5), ...hold('default', 0, 1)]; const LOOK_AROUND: readonly Frame[] = [
const CLICK_ANIMATIONS: readonly (readonly Frame[])[] = [JUMP_WAVE, LOOK_AROUND]; ...hold('look-right', 0, 5),
const IDLE: Frame = { ...hold('look-left', 0, 5),
pose: 'default', ...hold('default', 0, 1),
offset: 0 ]
};
const FRAME_MS = 60; const CLICK_ANIMATIONS: readonly (readonly Frame[])[] = [JUMP_WAVE, LOOK_AROUND]
const incrementFrame = (i: number) => i + 1;
const CLAWD_HEIGHT = 3; const IDLE: Frame = { pose: 'default', offset: 0 }
const FRAME_MS = 60
const incrementFrame = (i: number) => i + 1
const CLAWD_HEIGHT = 3
/** /**
* Clawd with click-triggered animations (crouch-jump with arms up, or * Clawd with click-triggered animations (crouch-jump with arms up, or
@@ -54,70 +48,49 @@ const CLAWD_HEIGHT = 3;
* mouse tracking is enabled (i.e. inside `<AlternateScreen>` / fullscreen); * mouse tracking is enabled (i.e. inside `<AlternateScreen>` / fullscreen);
* elsewhere this renders and behaves identically to plain `<Clawd />`. * elsewhere this renders and behaves identically to plain `<Clawd />`.
*/ */
export function AnimatedClawd() { export function AnimatedClawd(): React.ReactNode {
const $ = _c(8); const { pose, bounceOffset, onClick } = useClawdAnimation()
const { return (
pose, <Box height={CLAWD_HEIGHT} flexDirection="column" onClick={onClick}>
bounceOffset, <Box marginTop={bounceOffset} flexShrink={0}>
onClick <Clawd pose={pose} />
} = useClawdAnimation(); </Box>
let t0; </Box>
if ($[0] !== pose) { )
t0 = <Clawd pose={pose} />;
$[0] = pose;
$[1] = t0;
} else {
t0 = $[1];
}
let t1;
if ($[2] !== bounceOffset || $[3] !== t0) {
t1 = <Box marginTop={bounceOffset} flexShrink={0}>{t0}</Box>;
$[2] = bounceOffset;
$[3] = t0;
$[4] = t1;
} else {
t1 = $[4];
}
let t2;
if ($[5] !== onClick || $[6] !== t1) {
t2 = <Box height={CLAWD_HEIGHT} flexDirection="column" onClick={onClick}>{t1}</Box>;
$[5] = onClick;
$[6] = t1;
$[7] = t2;
} else {
t2 = $[7];
}
return t2;
} }
function useClawdAnimation(): { function useClawdAnimation(): {
pose: ClawdPose; pose: ClawdPose
bounceOffset: number; bounceOffset: number
onClick: () => void; onClick: () => void
} { } {
// Read once at mount — no useSettings() subscription, since that would // Read once at mount — no useSettings() subscription, since that would
// re-render on any settings change. // re-render on any settings change.
const [reducedMotion] = useState(() => getInitialSettings().prefersReducedMotion ?? false); const [reducedMotion] = useState(
const [frameIndex, setFrameIndex] = useState(-1); () => getInitialSettings().prefersReducedMotion ?? false,
const sequenceRef = useRef<readonly Frame[]>(JUMP_WAVE); )
const [frameIndex, setFrameIndex] = useState(-1)
const sequenceRef = useRef<readonly Frame[]>(JUMP_WAVE)
const onClick = () => { const onClick = () => {
if (reducedMotion || frameIndex !== -1) return; if (reducedMotion || frameIndex !== -1) return
sequenceRef.current = CLICK_ANIMATIONS[Math.floor(Math.random() * CLICK_ANIMATIONS.length)]!; sequenceRef.current =
setFrameIndex(0); CLICK_ANIMATIONS[Math.floor(Math.random() * CLICK_ANIMATIONS.length)]!
}; setFrameIndex(0)
}
useEffect(() => { useEffect(() => {
if (frameIndex === -1) return; if (frameIndex === -1) return
if (frameIndex >= sequenceRef.current.length) { if (frameIndex >= sequenceRef.current.length) {
setFrameIndex(-1); setFrameIndex(-1)
return; return
} }
const timer = setTimeout(setFrameIndex, FRAME_MS, incrementFrame); const timer = setTimeout(setFrameIndex, FRAME_MS, incrementFrame)
return () => clearTimeout(timer); return () => clearTimeout(timer)
}, [frameIndex]); }, [frameIndex])
const seq = sequenceRef.current;
const current = frameIndex >= 0 && frameIndex < seq.length ? seq[frameIndex]! : IDLE; const seq = sequenceRef.current
return { const current =
pose: current.pose, frameIndex >= 0 && frameIndex < seq.length ? seq[frameIndex]! : IDLE
bounceOffset: current.offset, return { pose: current.pose, bounceOffset: current.offset, onClick }
onClick
};
} }

View File

@@ -1,265 +1,208 @@
import { c as _c } from "react/compiler-runtime";
// Conditionally require()'d in LogoV2.tsx behind feature('KAIROS') || // Conditionally require()'d in LogoV2.tsx behind feature('KAIROS') ||
// feature('KAIROS_CHANNELS'). No feature() guard here — the whole file // feature('KAIROS_CHANNELS'). No feature() guard here — the whole file
// tree-shakes via the require pattern when both flags are false (see // tree-shakes via the require pattern when both flags are false (see
// docs/feature-gating.md). Do NOT import this module statically from // docs/feature-gating.md). Do NOT import this module statically from
// unguarded code. // unguarded code.
import * as React from 'react'; import * as React from 'react'
import { useState } from 'react'; import { useState } from 'react'
import { type ChannelEntry, getAllowedChannels, getHasDevChannels } from '../../bootstrap/state.js'; import {
import { Box, Text } from '../../ink.js'; type ChannelEntry,
import { isChannelsEnabled } from '../../services/mcp/channelAllowlist.js'; getAllowedChannels,
import { getEffectiveChannelAllowlist } from '../../services/mcp/channelNotification.js'; getHasDevChannels,
import { getMcpConfigsByScope } from '../../services/mcp/config.js'; } from '../../bootstrap/state.js'
import { getClaudeAIOAuthTokens, getSubscriptionType } from '../../utils/auth.js'; import { Box, Text } from '../../ink.js'
import { loadInstalledPluginsV2 } from '../../utils/plugins/installedPluginsManager.js'; import { isChannelsEnabled } from '../../services/mcp/channelAllowlist.js'
import { getSettingsForSource } from '../../utils/settings/settings.js'; import { getEffectiveChannelAllowlist } from '../../services/mcp/channelNotification.js'
export function ChannelsNotice() { import { getMcpConfigsByScope } from '../../services/mcp/config.js'
const $ = _c(32); import {
const [t0] = useState(_temp); getClaudeAIOAuthTokens,
const { getSubscriptionType,
channels, } from '../../utils/auth.js'
disabled, import { loadInstalledPluginsV2 } from '../../utils/plugins/installedPluginsManager.js'
noAuth, import { getSettingsForSource } from '../../utils/settings/settings.js'
policyBlocked,
list, export function ChannelsNotice(): React.ReactNode {
unmatched // Snapshot all reads at mount. This notice enters scrollback immediately
} = t0; // after the logo; any re-render past that point forces a full terminal
if (channels.length === 0) { // reset. getAllowedChannels (bootstrap state), getSettingsForSource
return null; // (session cache updated by background polling / /login), and
} // isChannelsEnabled (GrowthBook 5-min refresh) must be captured once
const hasNonDev = channels.some(_temp2); // so a later re-render cannot flip branches.
const flag = getHasDevChannels() && hasNonDev ? "Channels" : getHasDevChannels() ? "--dangerously-load-development-channels" : "--channels"; const [{ channels, disabled, noAuth, policyBlocked, list, unmatched }] =
useState(() => {
const ch = getAllowedChannels()
if (ch.length === 0)
return {
channels: ch,
disabled: false,
noAuth: false,
policyBlocked: false,
list: '',
unmatched: [] as Unmatched[],
}
const l = ch.map(formatEntry).join(', ')
const sub = getSubscriptionType()
const managed = sub === 'team' || sub === 'enterprise'
const policy = getSettingsForSource('policySettings')
const allowlist = getEffectiveChannelAllowlist(
sub,
policy?.allowedChannelPlugins,
)
return {
channels: ch,
disabled: !isChannelsEnabled(),
noAuth: !getClaudeAIOAuthTokens()?.accessToken,
policyBlocked: managed && policy?.channelsEnabled !== true,
list: l,
unmatched: findUnmatched(ch, allowlist),
}
})
if (channels.length === 0) return null
// When both flags are passed, the list mixes entries and a single flag
// name would be wrong for half of it. entry.dev distinguishes origin.
const hasNonDev = channels.some(c => !c.dev)
const flag =
getHasDevChannels() && hasNonDev
? 'Channels'
: getHasDevChannels()
? '--dangerously-load-development-channels'
: '--channels'
if (disabled) { if (disabled) {
let t1; return (
if ($[0] !== flag || $[1] !== list) { <Box paddingLeft={2} flexDirection="column">
t1 = <Text color="error">{flag} ignored ({list})</Text>; <Text color="error">
$[0] = flag; {flag} ignored ({list})
$[1] = list; </Text>
$[2] = t1; <Text dimColor>Channels are not currently available</Text>
} else { </Box>
t1 = $[2]; )
}
let t2;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t2 = <Text dimColor={true}>Channels are not currently available</Text>;
$[3] = t2;
} else {
t2 = $[3];
}
let t3;
if ($[4] !== t1) {
t3 = <Box paddingLeft={2} flexDirection="column">{t1}{t2}</Box>;
$[4] = t1;
$[5] = t3;
} else {
t3 = $[5];
}
return t3;
} }
if (noAuth) { if (noAuth) {
let t1; return (
if ($[6] !== flag || $[7] !== list) { <Box paddingLeft={2} flexDirection="column">
t1 = <Text color="error">{flag} ignored ({list})</Text>; <Text color="error">
$[6] = flag; {flag} ignored ({list})
$[7] = list; </Text>
$[8] = t1; <Text dimColor>
} else { Channels require claude.ai authentication · run /login, then restart
t1 = $[8]; </Text>
} </Box>
let t2; )
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
t2 = <Text dimColor={true}>Channels require claude.ai authentication · run /login, then restart</Text>;
$[9] = t2;
} else {
t2 = $[9];
}
let t3;
if ($[10] !== t1) {
t3 = <Box paddingLeft={2} flexDirection="column">{t1}{t2}</Box>;
$[10] = t1;
$[11] = t3;
} else {
t3 = $[11];
}
return t3;
} }
if (policyBlocked) { if (policyBlocked) {
let t1; return (
if ($[12] !== flag || $[13] !== list) { <Box paddingLeft={2} flexDirection="column">
t1 = <Text color="error">{flag} blocked by org policy ({list})</Text>; <Text color="error">
$[12] = flag; {flag} blocked by org policy ({list})
$[13] = list; </Text>
$[14] = t1; <Text dimColor>Inbound messages will be silently dropped</Text>
} else { <Text dimColor>
t1 = $[14]; Have an administrator set channelsEnabled: true in managed settings to
} enable
let t2; </Text>
let t3; {unmatched.map(u => (
if ($[15] === Symbol.for("react.memo_cache_sentinel")) { <Text key={`${formatEntry(u.entry)}:${u.why}`} color="warning">
t2 = <Text dimColor={true}>Inbound messages will be silently dropped</Text>; {formatEntry(u.entry)} · {u.why}
t3 = <Text dimColor={true}>Have an administrator set channelsEnabled: true in managed settings to enable</Text>; </Text>
$[15] = t2; ))}
$[16] = t3; </Box>
} else { )
t2 = $[15];
t3 = $[16];
}
let t4;
if ($[17] !== unmatched) {
t4 = unmatched.map(_temp3);
$[17] = unmatched;
$[18] = t4;
} else {
t4 = $[18];
}
let t5;
if ($[19] !== t1 || $[20] !== t4) {
t5 = <Box paddingLeft={2} flexDirection="column">{t1}{t2}{t3}{t4}</Box>;
$[19] = t1;
$[20] = t4;
$[21] = t5;
} else {
t5 = $[21];
}
return t5;
} }
let t1;
if ($[22] !== list) { // "Listening for" not "active" — at this point we only know the allowlist
t1 = <Text color="error">Listening for channel messages from: {list}</Text>; // was set. Server connection, capability declaration, and whether the name
$[22] = list; // even matches a configured MCP server are all still unknown.
$[23] = t1; return (
} else { <Box paddingLeft={2} flexDirection="column">
t1 = $[23]; <Text color="error">Listening for channel messages from: {list}</Text>
} <Text dimColor>
let t2; Experimental · inbound messages will be pushed into this session, this
if ($[24] !== flag) { carries prompt injection risks. Restart Claude Code without {flag} to
t2 = <Text dimColor={true}>Experimental · inbound messages will be pushed into this session, this carries prompt injection risks. Restart Claude Code without {flag} to disable.</Text>; disable.
$[24] = flag; </Text>
$[25] = t2; {unmatched.map(u => (
} else { <Text key={`${formatEntry(u.entry)}:${u.why}`} color="warning">
t2 = $[25]; {formatEntry(u.entry)} · {u.why}
} </Text>
let t3; ))}
if ($[26] !== unmatched) { </Box>
t3 = unmatched.map(_temp4); )
$[26] = unmatched;
$[27] = t3;
} else {
t3 = $[27];
}
let t4;
if ($[28] !== t1 || $[29] !== t2 || $[30] !== t3) {
t4 = <Box paddingLeft={2} flexDirection="column">{t1}{t2}{t3}</Box>;
$[28] = t1;
$[29] = t2;
$[30] = t3;
$[31] = t4;
} else {
t4 = $[31];
}
return t4;
}
function _temp4(u_0) {
return <Text key={`${formatEntry(u_0.entry)}:${u_0.why}`} color="warning">{formatEntry(u_0.entry)} · {u_0.why}</Text>;
}
function _temp3(u) {
return <Text key={`${formatEntry(u.entry)}:${u.why}`} color="warning">{formatEntry(u.entry)} · {u.why}</Text>;
}
function _temp2(c) {
return !c.dev;
}
function _temp() {
const ch = getAllowedChannels();
if (ch.length === 0) {
return {
channels: ch,
disabled: false,
noAuth: false,
policyBlocked: false,
list: "",
unmatched: [] as Unmatched[]
};
}
const l = ch.map(formatEntry).join(", ");
const sub = getSubscriptionType();
const managed = sub === "team" || sub === "enterprise";
const policy = getSettingsForSource("policySettings");
const allowlist = getEffectiveChannelAllowlist(sub, policy?.allowedChannelPlugins);
return {
channels: ch,
disabled: !isChannelsEnabled(),
noAuth: !getClaudeAIOAuthTokens()?.accessToken,
policyBlocked: managed && policy?.channelsEnabled !== true,
list: l,
unmatched: findUnmatched(ch, allowlist)
};
} }
function formatEntry(c: ChannelEntry): string { function formatEntry(c: ChannelEntry): string {
return c.kind === 'plugin' ? `plugin:${c.name}@${c.marketplace}` : `server:${c.name}`; return c.kind === 'plugin'
? `plugin:${c.name}@${c.marketplace}`
: `server:${c.name}`
} }
type Unmatched = {
entry: ChannelEntry; type Unmatched = { entry: ChannelEntry; why: string }
why: string;
}; function findUnmatched(
function findUnmatched(entries: readonly ChannelEntry[], allowlist: ReturnType<typeof getEffectiveChannelAllowlist>): Unmatched[] { entries: readonly ChannelEntry[],
allowlist: ReturnType<typeof getEffectiveChannelAllowlist>,
): Unmatched[] {
// Server-kind: build one Set from all scopes up front. getMcpConfigsByScope // Server-kind: build one Set from all scopes up front. getMcpConfigsByScope
// is not cached (project scope walks the dir tree); getMcpConfigByName would // is not cached (project scope walks the dir tree); getMcpConfigByName would
// redo that walk per entry. // redo that walk per entry.
const scopes = ['enterprise', 'user', 'project', 'local'] as const; const scopes = ['enterprise', 'user', 'project', 'local'] as const
const configured = new Set<string>(); const configured = new Set<string>()
for (const scope of scopes) { for (const scope of scopes) {
for (const name of Object.keys(getMcpConfigsByScope(scope).servers)) { for (const name of Object.keys(getMcpConfigsByScope(scope).servers)) {
configured.add(name); configured.add(name)
} }
} }
// Plugin-kind installed check: installed_plugins.json keys are // Plugin-kind installed check: installed_plugins.json keys are
// `name@marketplace`. loadInstalledPluginsV2 is cached. // `name@marketplace`. loadInstalledPluginsV2 is cached.
const installedPluginIds = new Set(Object.keys(loadInstalledPluginsV2().plugins)); const installedPluginIds = new Set(
Object.keys(loadInstalledPluginsV2().plugins),
)
// Plugin-kind allowlist check: same {marketplace, plugin} test as the // Plugin-kind allowlist check: same {marketplace, plugin} test as the
// gate at channelNotification.ts. entry.dev bypasses (dev flag opts out // gate at channelNotification.ts. entry.dev bypasses (dev flag opts out
// of the allowlist). Org list replaces ledger when set (team/enterprise). // of the allowlist). Org list replaces ledger when set (team/enterprise).
// GrowthBook _CACHED_MAY_BE_STALE — cold cache yields [] so every plugin // GrowthBook _CACHED_MAY_BE_STALE — cold cache yields [] so every plugin
// entry warns; same tradeoff the gate already accepts. // entry warns; same tradeoff the gate already accepts.
const { const { entries: allowed, source } = allowlist
entries: allowed,
source
} = allowlist;
// Independent ifs — a plugin entry that's both uninstalled AND // Independent ifs — a plugin entry that's both uninstalled AND
// unlisted shows two lines. Server kind checks config + dev flag. // unlisted shows two lines. Server kind checks config + dev flag.
const out: Unmatched[] = []; const out: Unmatched[] = []
for (const entry of entries) { for (const entry of entries) {
if (entry.kind === 'server') { if (entry.kind === 'server') {
if (!configured.has(entry.name)) { if (!configured.has(entry.name)) {
out.push({ out.push({ entry, why: 'no MCP server configured with that name' })
entry,
why: 'no MCP server configured with that name'
});
} }
if (!entry.dev) { if (!entry.dev) {
out.push({ out.push({
entry, entry,
why: 'server: entries need --dangerously-load-development-channels' why: 'server: entries need --dangerously-load-development-channels',
}); })
} }
continue; continue
} }
if (!installedPluginIds.has(`${entry.name}@${entry.marketplace}`)) { if (!installedPluginIds.has(`${entry.name}@${entry.marketplace}`)) {
out.push({ out.push({ entry, why: 'plugin not installed' })
entry,
why: 'plugin not installed'
});
} }
if (!entry.dev && !allowed.some(e => e.plugin === entry.name && e.marketplace === entry.marketplace)) { if (
!entry.dev &&
!allowed.some(
e => e.plugin === entry.name && e.marketplace === entry.marketplace,
)
) {
out.push({ out.push({
entry, entry,
why: source === 'org' ? "not on your org's approved channels list" : 'not on the approved channels allowlist' why:
}); source === 'org'
? "not on your org's approved channels list"
: 'not on the approved channels allowlist',
})
} }
} }
return out; return out
} }

View File

@@ -1,14 +1,16 @@
import { c as _c } from "react/compiler-runtime"; import * as React from 'react'
import * as React from 'react'; import { Box, Text } from '../../ink.js'
import { Box, Text } from '../../ink.js'; import { env } from '../../utils/env.js'
import { env } from '../../utils/env.js';
export type ClawdPose = 'default' | 'arms-up' // both arms raised (used during jump) export type ClawdPose =
| 'look-left' // both pupils shifted left | 'default'
| 'look-right'; // both pupils shifted right | 'arms-up' // both arms raised (used during jump)
| 'look-left' // both pupils shifted left
| 'look-right' // both pupils shifted right
type Props = { type Props = {
pose?: ClawdPose; pose?: ClawdPose
}; }
// Standard-terminal pose fragments. Each row is split into segments so we can // Standard-terminal pose fragments. Each row is split into segments so we can
// vary only the parts that change (eyes, arms) while keeping the body/bg spans // vary only the parts that change (eyes, arms) while keeping the body/bg spans
@@ -21,46 +23,23 @@ type Props = {
// default (▛/▜, bottom pupils) — otherwise only one eye would appear to move. // default (▛/▜, bottom pupils) — otherwise only one eye would appear to move.
type Segments = { type Segments = {
/** row 1 left (no bg): optional raised arm + side */ /** row 1 left (no bg): optional raised arm + side */
r1L: string; r1L: string
/** row 1 eyes (with bg): left-eye, forehead, right-eye */ /** row 1 eyes (with bg): left-eye, forehead, right-eye */
r1E: string; r1E: string
/** row 1 right (no bg): side + optional raised arm */ /** row 1 right (no bg): side + optional raised arm */
r1R: string; r1R: string
/** row 2 left (no bg): arm + body curve */ /** row 2 left (no bg): arm + body curve */
r2L: string; r2L: string
/** row 2 right (no bg): body curve + arm */ /** row 2 right (no bg): body curve + arm */
r2R: string; r2R: string
}; }
const POSES: Record<ClawdPose, Segments> = { const POSES: Record<ClawdPose, Segments> = {
default: { default: { r1L: ' ▐', r1E: '▛███▜', r1R: '▌', r2L: '▝▜', r2R: '▛▘' },
r1L: ' ▐', 'look-left': { r1L: ' ▐', r1E: '▟███▟', r1R: '▌', r2L: '▝▜', r2R: '▛▘' },
r1E: '███▜', 'look-right': { r1L: ' ▐', r1E: '███▙', r1R: '▌', r2L: '▝▜', r2R: '▛▘' },
r1R: '▌', 'arms-up': { r1L: '▗▟', r1E: '▛███▜', r1R: '▙▖', r2L: ' ▜', r2R: '▛ ' },
r2L: '▝▜', }
r2R: '▛▘'
},
'look-left': {
r1L: ' ▐',
r1E: '▟███▟',
r1R: '▌',
r2L: '▝▜',
r2R: '▛▘'
},
'look-right': {
r1L: ' ▐',
r1E: '▙███▙',
r1R: '▌',
r2L: '▝▜',
r2R: '▛▘'
},
'arms-up': {
r1L: '▗▟',
r1E: '▛███▜',
r1R: '▙▖',
r2L: ' ▜',
r2R: '▛ '
}
};
// Apple Terminal uses a bg-fill trick (see below), so only eye poses make // Apple Terminal uses a bg-fill trick (see below), so only eye poses make
// sense. Arm poses fall back to default. // sense. Arm poses fall back to default.
@@ -68,172 +47,52 @@ const APPLE_EYES: Record<ClawdPose, string> = {
default: ' ▗ ▖ ', default: ' ▗ ▖ ',
'look-left': ' ▘ ▘ ', 'look-left': ' ▘ ▘ ',
'look-right': ' ▝ ▝ ', 'look-right': ' ▝ ▝ ',
'arms-up': ' ▗ ▖ ' 'arms-up': ' ▗ ▖ ',
};
export function Clawd(t0) {
const $ = _c(26);
let t1;
if ($[0] !== t0) {
t1 = t0 === undefined ? {} : t0;
$[0] = t0;
$[1] = t1;
} else {
t1 = $[1];
}
const {
pose: t2
} = t1;
const pose = t2 === undefined ? "default" : t2;
if (env.terminal === "Apple_Terminal") {
let t3;
if ($[2] !== pose) {
t3 = <AppleTerminalClawd pose={pose} />;
$[2] = pose;
$[3] = t3;
} else {
t3 = $[3];
}
return t3;
}
const p = POSES[pose];
let t3;
if ($[4] !== p.r1L) {
t3 = <Text color="clawd_body">{p.r1L}</Text>;
$[4] = p.r1L;
$[5] = t3;
} else {
t3 = $[5];
}
let t4;
if ($[6] !== p.r1E) {
t4 = <Text color="clawd_body" backgroundColor="clawd_background">{p.r1E}</Text>;
$[6] = p.r1E;
$[7] = t4;
} else {
t4 = $[7];
}
let t5;
if ($[8] !== p.r1R) {
t5 = <Text color="clawd_body">{p.r1R}</Text>;
$[8] = p.r1R;
$[9] = t5;
} else {
t5 = $[9];
}
let t6;
if ($[10] !== t3 || $[11] !== t4 || $[12] !== t5) {
t6 = <Text>{t3}{t4}{t5}</Text>;
$[10] = t3;
$[11] = t4;
$[12] = t5;
$[13] = t6;
} else {
t6 = $[13];
}
let t7;
if ($[14] !== p.r2L) {
t7 = <Text color="clawd_body">{p.r2L}</Text>;
$[14] = p.r2L;
$[15] = t7;
} else {
t7 = $[15];
}
let t8;
if ($[16] === Symbol.for("react.memo_cache_sentinel")) {
t8 = <Text color="clawd_body" backgroundColor="clawd_background"></Text>;
$[16] = t8;
} else {
t8 = $[16];
}
let t9;
if ($[17] !== p.r2R) {
t9 = <Text color="clawd_body">{p.r2R}</Text>;
$[17] = p.r2R;
$[18] = t9;
} else {
t9 = $[18];
}
let t10;
if ($[19] !== t7 || $[20] !== t9) {
t10 = <Text>{t7}{t8}{t9}</Text>;
$[19] = t7;
$[20] = t9;
$[21] = t10;
} else {
t10 = $[21];
}
let t11;
if ($[22] === Symbol.for("react.memo_cache_sentinel")) {
t11 = <Text color="clawd_body">{" "} {" "}</Text>;
$[22] = t11;
} else {
t11 = $[22];
}
let t12;
if ($[23] !== t10 || $[24] !== t6) {
t12 = <Box flexDirection="column">{t6}{t10}{t11}</Box>;
$[23] = t10;
$[24] = t6;
$[25] = t12;
} else {
t12 = $[25];
}
return t12;
} }
function AppleTerminalClawd(t0) {
const $ = _c(10); export function Clawd({ pose = 'default' }: Props = {}): React.ReactNode {
const { if (env.terminal === 'Apple_Terminal') {
pose return <AppleTerminalClawd pose={pose} />
} = t0;
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <Text color="clawd_body"></Text>;
$[0] = t1;
} else {
t1 = $[0];
} }
const t2 = APPLE_EYES[pose]; const p = POSES[pose]
let t3; return (
if ($[1] !== t2) { <Box flexDirection="column">
t3 = <Text color="clawd_background" backgroundColor="clawd_body">{t2}</Text>; <Text>
$[1] = t2; <Text color="clawd_body">{p.r1L}</Text>
$[2] = t3; <Text color="clawd_body" backgroundColor="clawd_background">
} else { {p.r1E}
t3 = $[2]; </Text>
} <Text color="clawd_body">{p.r1R}</Text>
let t4; </Text>
if ($[3] === Symbol.for("react.memo_cache_sentinel")) { <Text>
t4 = <Text color="clawd_body"></Text>; <Text color="clawd_body">{p.r2L}</Text>
$[3] = t4; <Text color="clawd_body" backgroundColor="clawd_background">
} else {
t4 = $[3]; </Text>
} <Text color="clawd_body">{p.r2R}</Text>
let t5; </Text>
if ($[4] !== t3) { <Text color="clawd_body">
t5 = <Text>{t1}{t3}{t4}</Text>; {' '} {' '}
$[4] = t3; </Text>
$[5] = t5; </Box>
} else { )
t5 = $[5]; }
}
let t6; function AppleTerminalClawd({ pose }: { pose: ClawdPose }): React.ReactNode {
let t7; // Apple's Terminal renders vertical space between chars by default.
if ($[6] === Symbol.for("react.memo_cache_sentinel")) { // It does NOT render vertical space between background colors
t6 = <Text backgroundColor="clawd_body">{" ".repeat(7)}</Text>; // so we use background color to draw the main shape.
t7 = <Text color="clawd_body"> </Text>; return (
$[6] = t6; <Box flexDirection="column" alignItems="center">
$[7] = t7; <Text>
} else { <Text color="clawd_body"></Text>
t6 = $[6]; <Text color="clawd_background" backgroundColor="clawd_body">
t7 = $[7]; {APPLE_EYES[pose]}
} </Text>
let t8; <Text color="clawd_body"></Text>
if ($[8] !== t5) { </Text>
t8 = <Box flexDirection="column" alignItems="center">{t5}{t6}{t7}</Box>; <Text backgroundColor="clawd_body">{' '.repeat(7)}</Text>
$[8] = t5; <Text color="clawd_body"> </Text>
$[9] = t8; </Box>
} else { )
t8 = $[9];
}
return t8;
} }

View File

@@ -1,160 +1,119 @@
import { c as _c } from "react/compiler-runtime"; import * as React from 'react'
import * as React from 'react'; import { type ReactNode, useEffect } from 'react'
import { type ReactNode, useEffect } from 'react'; import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'
import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'; import { useTerminalSize } from '../../hooks/useTerminalSize.js'
import { useTerminalSize } from '../../hooks/useTerminalSize.js'; import { stringWidth } from '../../ink/stringWidth.js'
import { stringWidth } from '../../ink/stringWidth.js'; import { Box, Text } from '../../ink.js'
import { Box, Text } from '../../ink.js'; import { useAppState } from '../../state/AppState.js'
import { useAppState } from '../../state/AppState.js'; import { getEffortSuffix } from '../../utils/effort.js'
import { getEffortSuffix } from '../../utils/effort.js'; import { truncate } from '../../utils/format.js'
import { truncate } from '../../utils/format.js'; import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'
import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'; import {
import { formatModelAndBilling, getLogoDisplayData, truncatePath } from '../../utils/logoV2Utils.js'; formatModelAndBilling,
import { renderModelSetting } from '../../utils/model/model.js'; getLogoDisplayData,
import { OffscreenFreeze } from '../OffscreenFreeze.js'; truncatePath,
import { AnimatedClawd } from './AnimatedClawd.js'; } from '../../utils/logoV2Utils.js'
import { Clawd } from './Clawd.js'; import { renderModelSetting } from '../../utils/model/model.js'
import { GuestPassesUpsell, incrementGuestPassesSeenCount, useShowGuestPassesUpsell } from './GuestPassesUpsell.js'; import { OffscreenFreeze } from '../OffscreenFreeze.js'
import { incrementOverageCreditUpsellSeenCount, OverageCreditUpsell, useShowOverageCreditUpsell } from './OverageCreditUpsell.js'; import { AnimatedClawd } from './AnimatedClawd.js'
export function CondensedLogo() { import { Clawd } from './Clawd.js'
const $ = _c(29); import {
const { GuestPassesUpsell,
columns incrementGuestPassesSeenCount,
} = useTerminalSize(); useShowGuestPassesUpsell,
const agent = useAppState(_temp); } from './GuestPassesUpsell.js'
const effortValue = useAppState(_temp2); import {
const model = useMainLoopModel(); incrementOverageCreditUpsellSeenCount,
const modelDisplayName = renderModelSetting(model); OverageCreditUpsell,
const { useShowOverageCreditUpsell,
} from './OverageCreditUpsell.js'
export function CondensedLogo(): ReactNode {
const { columns } = useTerminalSize()
const agent = useAppState(s => s.agent)
const effortValue = useAppState(s => s.effortValue)
const model = useMainLoopModel()
const modelDisplayName = renderModelSetting(model)
const { version, cwd, billingType, agentName: agentNameFromSettings } = getLogoDisplayData()
// Prefer AppState.agent (set from --agent CLI flag) over settings
const agentName = agent ?? agentNameFromSettings
const showGuestPassesUpsell = useShowGuestPassesUpsell()
const showOverageCreditUpsell = useShowOverageCreditUpsell()
useEffect(() => {
if (showGuestPassesUpsell) {
incrementGuestPassesSeenCount()
}
}, [showGuestPassesUpsell])
useEffect(() => {
if (showOverageCreditUpsell && !showGuestPassesUpsell) {
incrementOverageCreditUpsellSeenCount()
}
}, [showOverageCreditUpsell, showGuestPassesUpsell])
// Calculate available width for text content
// Account for: condensed clawd width (11 chars) + gap (2) + padding (2) = 15 chars
const textWidth = Math.max(columns - 15, 20)
// Truncate version to fit within available width, accounting for "Claude Code v" prefix
const versionPrefix = 'Claude Code v'
const truncatedVersion = truncate(
version, version,
cwd, Math.max(textWidth - versionPrefix.length, 6),
billingType, )
agentName: agentNameFromSettings
} = getLogoDisplayData(); const effortSuffix = getEffortSuffix(model, effortValue)
const agentName = agent ?? agentNameFromSettings; const { shouldSplit, truncatedModel, truncatedBilling } =
const showGuestPassesUpsell = useShowGuestPassesUpsell(); formatModelAndBilling(
const showOverageCreditUpsell = useShowOverageCreditUpsell(); modelDisplayName + effortSuffix,
let t0; billingType,
let t1; textWidth,
if ($[0] !== showGuestPassesUpsell) { )
t0 = () => {
if (showGuestPassesUpsell) { // Truncate path, accounting for agent name if present
incrementGuestPassesSeenCount(); const separator = ' · '
} const atPrefix = '@'
}; const cwdAvailableWidth = agentName
t1 = [showGuestPassesUpsell]; ? textWidth - atPrefix.length - stringWidth(agentName) - separator.length
$[0] = showGuestPassesUpsell; : textWidth
$[1] = t0; const truncatedCwd = truncatePath(cwd, Math.max(cwdAvailableWidth, 10))
$[2] = t1;
} else { // OffscreenFreeze: the logo sits at the top of the message list and is the
t0 = $[1]; // first thing to enter scrollback. useMainLoopModel() subscribes to model
t1 = $[2]; // changes and getLogoDisplayData() reads getCwd()/subscription state — any
} // of which changing while in scrollback would force a full terminal reset.
useEffect(t0, t1); return (
let t2; <OffscreenFreeze>
let t3; <Box flexDirection="row" gap={2} alignItems="center">
if ($[3] !== showGuestPassesUpsell || $[4] !== showOverageCreditUpsell) { {isFullscreenEnvEnabled() ? <AnimatedClawd /> : <Clawd />}
t2 = () => {
if (showOverageCreditUpsell && !showGuestPassesUpsell) { {/* Info */}
incrementOverageCreditUpsellSeenCount(); <Box flexDirection="column">
} <Text>
}; <Text bold>Claude Code</Text>{' '}
t3 = [showOverageCreditUpsell, showGuestPassesUpsell]; <Text dimColor>v{truncatedVersion}</Text>
$[3] = showGuestPassesUpsell; </Text>
$[4] = showOverageCreditUpsell; {shouldSplit ? (
$[5] = t2; <>
$[6] = t3; <Text dimColor>{truncatedModel}</Text>
} else { <Text dimColor>{truncatedBilling}</Text>
t2 = $[5]; </>
t3 = $[6]; ) : (
} <Text dimColor>
useEffect(t2, t3); {truncatedModel} · {truncatedBilling}
const textWidth = Math.max(columns - 15, 20); </Text>
const truncatedVersion = truncate(version, Math.max(textWidth - 13, 6)); )}
const effortSuffix = getEffortSuffix(model, effortValue); <Text dimColor>
const { {agentName ? `@${agentName} · ${truncatedCwd}` : truncatedCwd}
shouldSplit, </Text>
truncatedModel, {showGuestPassesUpsell && <GuestPassesUpsell />}
truncatedBilling {!showGuestPassesUpsell && showOverageCreditUpsell && (
} = formatModelAndBilling(modelDisplayName + effortSuffix, billingType, textWidth); <OverageCreditUpsell maxWidth={textWidth} twoLine />
const cwdAvailableWidth = agentName ? textWidth - 1 - stringWidth(agentName) - 3 : textWidth; )}
const truncatedCwd = truncatePath(cwd, Math.max(cwdAvailableWidth, 10)); </Box>
let t4; </Box>
if ($[7] === Symbol.for("react.memo_cache_sentinel")) { </OffscreenFreeze>
t4 = isFullscreenEnvEnabled() ? <AnimatedClawd /> : <Clawd />; )
$[7] = t4;
} else {
t4 = $[7];
}
let t5;
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
t5 = <Text bold={true}>Claude Code</Text>;
$[8] = t5;
} else {
t5 = $[8];
}
let t6;
if ($[9] !== truncatedVersion) {
t6 = <Text>{t5}{" "}<Text dimColor={true}>v{truncatedVersion}</Text></Text>;
$[9] = truncatedVersion;
$[10] = t6;
} else {
t6 = $[10];
}
let t7;
if ($[11] !== shouldSplit || $[12] !== truncatedBilling || $[13] !== truncatedModel) {
t7 = shouldSplit ? <><Text dimColor={true}>{truncatedModel}</Text><Text dimColor={true}>{truncatedBilling}</Text></> : <Text dimColor={true}>{truncatedModel} · {truncatedBilling}</Text>;
$[11] = shouldSplit;
$[12] = truncatedBilling;
$[13] = truncatedModel;
$[14] = t7;
} else {
t7 = $[14];
}
const t8 = agentName ? `@${agentName} · ${truncatedCwd}` : truncatedCwd;
let t9;
if ($[15] !== t8) {
t9 = <Text dimColor={true}>{t8}</Text>;
$[15] = t8;
$[16] = t9;
} else {
t9 = $[16];
}
let t10;
if ($[17] !== showGuestPassesUpsell) {
t10 = showGuestPassesUpsell && <GuestPassesUpsell />;
$[17] = showGuestPassesUpsell;
$[18] = t10;
} else {
t10 = $[18];
}
let t11;
if ($[19] !== showGuestPassesUpsell || $[20] !== showOverageCreditUpsell || $[21] !== textWidth) {
t11 = !showGuestPassesUpsell && showOverageCreditUpsell && <OverageCreditUpsell maxWidth={textWidth} twoLine={true} />;
$[19] = showGuestPassesUpsell;
$[20] = showOverageCreditUpsell;
$[21] = textWidth;
$[22] = t11;
} else {
t11 = $[22];
}
let t12;
if ($[23] !== t10 || $[24] !== t11 || $[25] !== t6 || $[26] !== t7 || $[27] !== t9) {
t12 = <OffscreenFreeze><Box flexDirection="row" gap={2} alignItems="center">{t4}<Box flexDirection="column">{t6}{t7}{t9}{t10}{t11}</Box></Box></OffscreenFreeze>;
$[23] = t10;
$[24] = t11;
$[25] = t6;
$[26] = t7;
$[27] = t9;
$[28] = t12;
} else {
t12 = $[28];
}
return t12;
}
function _temp2(s_0) {
return s_0.effortValue;
}
function _temp(s) {
return s.agent;
} }

View File

@@ -1,57 +1,65 @@
import * as React from 'react'; import * as React from 'react'
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react'
import { Box, Text } from 'src/ink.js'; import { Box, Text } from 'src/ink.js'
import { getDynamicConfig_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'; import { getDynamicConfig_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
import { getGlobalConfig, saveGlobalConfig } from 'src/utils/config.js'; import { getGlobalConfig, saveGlobalConfig } from 'src/utils/config.js'
const CONFIG_NAME = 'tengu-top-of-feed-tip';
const CONFIG_NAME = 'tengu-top-of-feed-tip'
export function EmergencyTip(): React.ReactNode { export function EmergencyTip(): React.ReactNode {
const tip = useMemo(getTipOfFeed, []); const tip = useMemo(getTipOfFeed, [])
// Memoize to prevent re-reads after we save - we want the value at mount time // Memoize to prevent re-reads after we save - we want the value at mount time
const lastShownTip = useMemo(() => getGlobalConfig().lastShownEmergencyTip, []); const lastShownTip = useMemo(
() => getGlobalConfig().lastShownEmergencyTip,
[],
)
// Only show if this is a new/different tip // Only show if this is a new/different tip
const shouldShow = tip.tip && tip.tip !== lastShownTip; const shouldShow = tip.tip && tip.tip !== lastShownTip
// Save the tip we're showing so we don't show it again // Save the tip we're showing so we don't show it again
useEffect(() => { useEffect(() => {
if (shouldShow) { if (shouldShow) {
saveGlobalConfig(current => { saveGlobalConfig(current => {
if (current.lastShownEmergencyTip === tip.tip) return current; if (current.lastShownEmergencyTip === tip.tip) return current
return { return { ...current, lastShownEmergencyTip: tip.tip }
...current, })
lastShownEmergencyTip: tip.tip
};
});
} }
}, [shouldShow, tip.tip]); }, [shouldShow, tip.tip])
if (!shouldShow) { if (!shouldShow) {
return null; return null
} }
return <Box paddingLeft={2} flexDirection="column">
<Text {...tip.color === 'warning' ? { return (
color: 'warning' <Box paddingLeft={2} flexDirection="column">
} : tip.color === 'error' ? { <Text
color: 'error' {...(tip.color === 'warning'
} : { ? { color: 'warning' }
dimColor: true : tip.color === 'error'
}}> ? { color: 'error' }
: { dimColor: true })}
>
{tip.tip} {tip.tip}
</Text> </Text>
</Box>; </Box>
)
} }
type TipOfFeed = { type TipOfFeed = {
tip: string; tip: string
color?: 'dim' | 'warning' | 'error'; color?: 'dim' | 'warning' | 'error'
}; }
const DEFAULT_TIP: TipOfFeed = {
tip: '', const DEFAULT_TIP: TipOfFeed = { tip: '', color: 'dim' }
color: 'dim'
};
/** /**
* Get the tip of the feed from dynamic config with caching * Get the tip of the feed from dynamic config with caching
* Returns cached value immediately, updates in background * Returns cached value immediately, updates in background
*/ */
function getTipOfFeed(): TipOfFeed { function getTipOfFeed(): TipOfFeed {
return getDynamicConfig_CACHED_MAY_BE_STALE<TipOfFeed>(CONFIG_NAME, DEFAULT_TIP); return getDynamicConfig_CACHED_MAY_BE_STALE<TipOfFeed>(
CONFIG_NAME,
DEFAULT_TIP,
)
} }

View File

@@ -1,111 +1,113 @@
import { c as _c } from "react/compiler-runtime"; import * as React from 'react'
import * as React from 'react'; import { stringWidth } from '../../ink/stringWidth.js'
import { stringWidth } from '../../ink/stringWidth.js'; import { Box, Text } from '../../ink.js'
import { Box, Text } from '../../ink.js'; import { truncate } from '../../utils/format.js'
import { truncate } from '../../utils/format.js';
export type FeedLine = { export type FeedLine = {
text: string; text: string
timestamp?: string; timestamp?: string
}; }
export type FeedConfig = { export type FeedConfig = {
title: string; title: string
lines: FeedLine[]; lines: FeedLine[]
footer?: string; footer?: string
emptyMessage?: string; emptyMessage?: string
customContent?: { customContent?: { content: React.ReactNode; width: number }
content: React.ReactNode; }
width: number;
};
};
type FeedProps = { type FeedProps = {
config: FeedConfig; config: FeedConfig
actualWidth: number; actualWidth: number
}; }
export function calculateFeedWidth(config: FeedConfig): number { export function calculateFeedWidth(config: FeedConfig): number {
const { const { title, lines, footer, emptyMessage, customContent } = config
title,
lines, let maxWidth = stringWidth(title)
footer,
emptyMessage,
customContent
} = config;
let maxWidth = stringWidth(title);
if (customContent !== undefined) { if (customContent !== undefined) {
maxWidth = Math.max(maxWidth, customContent.width); maxWidth = Math.max(maxWidth, customContent.width)
} else if (lines.length === 0 && emptyMessage) { } else if (lines.length === 0 && emptyMessage) {
maxWidth = Math.max(maxWidth, stringWidth(emptyMessage)); maxWidth = Math.max(maxWidth, stringWidth(emptyMessage))
} else { } else {
const gap = ' '; const gap = ' '
const maxTimestampWidth = Math.max(0, ...lines.map(line => line.timestamp ? stringWidth(line.timestamp) : 0)); const maxTimestampWidth = Math.max(
0,
...lines.map(line => (line.timestamp ? stringWidth(line.timestamp) : 0)),
)
for (const line of lines) { for (const line of lines) {
const timestampWidth = maxTimestampWidth > 0 ? maxTimestampWidth : 0; const timestampWidth = maxTimestampWidth > 0 ? maxTimestampWidth : 0
const lineWidth = stringWidth(line.text) + (timestampWidth > 0 ? timestampWidth + gap.length : 0); const lineWidth =
maxWidth = Math.max(maxWidth, lineWidth); stringWidth(line.text) +
(timestampWidth > 0 ? timestampWidth + gap.length : 0)
maxWidth = Math.max(maxWidth, lineWidth)
} }
} }
if (footer) { if (footer) {
maxWidth = Math.max(maxWidth, stringWidth(footer)); maxWidth = Math.max(maxWidth, stringWidth(footer))
} }
return maxWidth;
return maxWidth
} }
export function Feed(t0) {
const $ = _c(15); export function Feed({ config, actualWidth }: FeedProps): React.ReactNode {
const { const { title, lines, footer, emptyMessage, customContent } = config
config,
actualWidth const gap = ' '
} = t0; const maxTimestampWidth = Math.max(
const { 0,
title, ...lines.map(line => (line.timestamp ? stringWidth(line.timestamp) : 0)),
lines, )
footer,
emptyMessage, return (
customContent <Box flexDirection="column" width={actualWidth}>
} = config; <Text bold color="claude">
let t1; {title}
if ($[0] !== lines) { </Text>
t1 = Math.max(0, ...lines.map(_temp)); {customContent ? (
$[0] = lines; <>
$[1] = t1; {customContent.content}
} else { {footer && (
t1 = $[1]; <Text dimColor italic>
} {truncate(footer, actualWidth)}
const maxTimestampWidth = t1; </Text>
let t2; )}
if ($[2] !== title) { </>
t2 = <Text bold={true} color="claude">{title}</Text>; ) : lines.length === 0 && emptyMessage ? (
$[2] = title; <Text dimColor>{truncate(emptyMessage, actualWidth)}</Text>
$[3] = t2; ) : (
} else { <>
t2 = $[3]; {lines.map((line, index) => {
} const textWidth = Math.max(
let t3; 10,
if ($[4] !== actualWidth || $[5] !== customContent || $[6] !== emptyMessage || $[7] !== footer || $[8] !== lines || $[9] !== maxTimestampWidth) { actualWidth -
t3 = customContent ? <>{customContent.content}{footer && <Text dimColor={true} italic={true}>{truncate(footer, actualWidth)}</Text>}</> : lines.length === 0 && emptyMessage ? <Text dimColor={true}>{truncate(emptyMessage, actualWidth)}</Text> : <>{lines.map((line_0, index) => { (maxTimestampWidth > 0 ? maxTimestampWidth + gap.length : 0),
const textWidth = Math.max(10, actualWidth - (maxTimestampWidth > 0 ? maxTimestampWidth + 2 : 0)); )
return <Text key={index}>{maxTimestampWidth > 0 && <><Text dimColor={true}>{(line_0.timestamp || "").padEnd(maxTimestampWidth)}</Text>{" "}</>}<Text>{truncate(line_0.text, textWidth)}</Text></Text>;
})}{footer && <Text dimColor={true} italic={true}>{truncate(footer, actualWidth)}</Text>}</>; return (
$[4] = actualWidth; <Text key={index}>
$[5] = customContent; {maxTimestampWidth > 0 && (
$[6] = emptyMessage; <>
$[7] = footer; <Text dimColor>
$[8] = lines; {(line.timestamp || '').padEnd(maxTimestampWidth)}
$[9] = maxTimestampWidth; </Text>
$[10] = t3; {gap}
} else { </>
t3 = $[10]; )}
} <Text>{truncate(line.text, textWidth)}</Text>
let t4; </Text>
if ($[11] !== actualWidth || $[12] !== t2 || $[13] !== t3) { )
t4 = <Box flexDirection="column" width={actualWidth}>{t2}{t3}</Box>; })}
$[11] = actualWidth; {footer && (
$[12] = t2; <Text dimColor italic>
$[13] = t3; {truncate(footer, actualWidth)}
$[14] = t4; </Text>
} else { )}
t4 = $[14]; </>
} )}
return t4; </Box>
} )
function _temp(line) {
return line.timestamp ? stringWidth(line.timestamp) : 0;
} }

View File

@@ -1,58 +1,32 @@
import { c as _c } from "react/compiler-runtime"; import * as React from 'react'
import * as React from 'react'; import { Box } from '../../ink.js'
import { Box } from '../../ink.js'; import { Divider } from '../design-system/Divider.js'
import { Divider } from '../design-system/Divider.js'; import type { FeedConfig } from './Feed.js'
import type { FeedConfig } from './Feed.js'; import { calculateFeedWidth, Feed } from './Feed.js'
import { calculateFeedWidth, Feed } from './Feed.js';
type FeedColumnProps = { type FeedColumnProps = {
feeds: FeedConfig[]; feeds: FeedConfig[]
maxWidth: number; maxWidth: number
};
export function FeedColumn(t0) {
const $ = _c(10);
const {
feeds,
maxWidth
} = t0;
let t1;
if ($[0] !== feeds) {
const feedWidths = feeds.map(_temp);
t1 = Math.max(...feedWidths);
$[0] = feeds;
$[1] = t1;
} else {
t1 = $[1];
}
const maxOfAllFeeds = t1;
const actualWidth = Math.min(maxOfAllFeeds, maxWidth);
let t2;
if ($[2] !== actualWidth || $[3] !== feeds) {
let t3;
if ($[5] !== actualWidth || $[6] !== feeds.length) {
t3 = (feed_0, index) => <React.Fragment key={index}><Feed config={feed_0} actualWidth={actualWidth} />{index < feeds.length - 1 && <Divider color="claude" width={actualWidth} />}</React.Fragment>;
$[5] = actualWidth;
$[6] = feeds.length;
$[7] = t3;
} else {
t3 = $[7];
}
t2 = feeds.map(t3);
$[2] = actualWidth;
$[3] = feeds;
$[4] = t2;
} else {
t2 = $[4];
}
let t3;
if ($[8] !== t2) {
t3 = <Box flexDirection="column">{t2}</Box>;
$[8] = t2;
$[9] = t3;
} else {
t3 = $[9];
}
return t3;
} }
function _temp(feed) {
return calculateFeedWidth(feed); export function FeedColumn({
feeds,
maxWidth,
}: FeedColumnProps): React.ReactNode {
const feedWidths = feeds.map(feed => calculateFeedWidth(feed))
const maxOfAllFeeds = Math.max(...feedWidths)
const actualWidth = Math.min(maxOfAllFeeds, maxWidth)
return (
<Box flexDirection="column">
{feeds.map((feed, index) => (
<React.Fragment key={index}>
<Feed config={feed} actualWidth={actualWidth} />
{index < feeds.length - 1 && (
<Divider color="claude" width={actualWidth} />
)}
</React.Fragment>
))}
</Box>
)
} }

View File

@@ -1,69 +1,73 @@
import { c as _c } from "react/compiler-runtime"; import * as React from 'react'
import * as React from 'react'; import { useState } from 'react'
import { useState } from 'react'; import { Text } from '../../ink.js'
import { Text } from '../../ink.js'; import { logEvent } from '../../services/analytics/index.js'
import { logEvent } from '../../services/analytics/index.js'; import {
import { checkCachedPassesEligibility, formatCreditAmount, getCachedReferrerReward, getCachedRemainingPasses } from '../../services/api/referral.js'; checkCachedPassesEligibility,
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'; formatCreditAmount,
getCachedReferrerReward,
getCachedRemainingPasses,
} from '../../services/api/referral.js'
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
function resetIfPassesRefreshed(): void { function resetIfPassesRefreshed(): void {
const remaining = getCachedRemainingPasses(); const remaining = getCachedRemainingPasses()
if (remaining == null || remaining <= 0) return; if (remaining == null || remaining <= 0) return
const config = getGlobalConfig(); const config = getGlobalConfig()
const lastSeen = config.passesLastSeenRemaining ?? 0; const lastSeen = config.passesLastSeenRemaining ?? 0
if (remaining > lastSeen) { if (remaining > lastSeen) {
saveGlobalConfig(prev => ({ saveGlobalConfig(prev => ({
...prev, ...prev,
passesUpsellSeenCount: 0, passesUpsellSeenCount: 0,
hasVisitedPasses: false, hasVisitedPasses: false,
passesLastSeenRemaining: remaining passesLastSeenRemaining: remaining,
})); }))
} }
} }
function shouldShowGuestPassesUpsell(): boolean { function shouldShowGuestPassesUpsell(): boolean {
const { const { eligible, hasCache } = checkCachedPassesEligibility()
eligible,
hasCache
} = checkCachedPassesEligibility();
// Only show if eligible and cache exists (don't block on fetch) // Only show if eligible and cache exists (don't block on fetch)
if (!eligible || !hasCache) return false; if (!eligible || !hasCache) return false
// Reset upsell counters if passes were refreshed (covers both campaign change and pass refresh) // Reset upsell counters if passes were refreshed (covers both campaign change and pass refresh)
resetIfPassesRefreshed(); resetIfPassesRefreshed()
const config = getGlobalConfig();
if ((config.passesUpsellSeenCount ?? 0) >= 3) return false; const config = getGlobalConfig()
if (config.hasVisitedPasses) return false; if ((config.passesUpsellSeenCount ?? 0) >= 3) return false
return true; if (config.hasVisitedPasses) return false
return true
} }
export function useShowGuestPassesUpsell() {
const [show] = useState(_temp); export function useShowGuestPassesUpsell(): boolean {
return show; const [show] = useState(() => shouldShowGuestPassesUpsell())
} return show
function _temp() {
return shouldShowGuestPassesUpsell();
} }
export function incrementGuestPassesSeenCount(): void { export function incrementGuestPassesSeenCount(): void {
let newCount = 0; let newCount = 0
saveGlobalConfig(prev => { saveGlobalConfig(prev => {
newCount = (prev.passesUpsellSeenCount ?? 0) + 1; newCount = (prev.passesUpsellSeenCount ?? 0) + 1
return { return {
...prev, ...prev,
passesUpsellSeenCount: newCount passesUpsellSeenCount: newCount,
}; }
}); })
logEvent('tengu_guest_passes_upsell_shown', { logEvent('tengu_guest_passes_upsell_shown', {
seen_count: newCount seen_count: newCount,
}); })
} }
// Condensed layout for mini welcome screen // Condensed layout for mini welcome screen
export function GuestPassesUpsell() { export function GuestPassesUpsell(): React.ReactNode {
const $ = _c(1); const reward = getCachedReferrerReward()
let t0; return (
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { <Text dimColor>
const reward = getCachedReferrerReward(); <Text color="claude">[]</Text> <Text color="claude">[]</Text>{' '}
t0 = <Text dimColor={true}><Text color="claude">[]</Text> <Text color="claude">[]</Text>{" "}<Text color="claude">[]</Text> ·{" "}{reward ? `Share Claude Code and earn ${formatCreditAmount(reward)} of extra usage · /passes` : "3 guest passes at /passes"}</Text>; <Text color="claude">[]</Text> ·{' '}
$[0] = t0; {reward
} else { ? `Share Claude Code and earn ${formatCreditAmount(reward)} of extra usage · /passes`
t0 = $[0]; : '3 guest passes at /passes'}
} </Text>
return t0; )
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,54 +1,41 @@
import { c as _c } from "react/compiler-runtime"; import * as React from 'react'
import * as React from 'react'; import { useEffect, useState } from 'react'
import { useEffect, useState } from 'react'; import { UP_ARROW } from '../../constants/figures.js'
import { UP_ARROW } from '../../constants/figures.js'; import { Box, Text } from '../../ink.js'
import { Box, Text } from '../../ink.js'; import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'; import { isOpus1mMergeEnabled } from '../../utils/model/model.js'
import { isOpus1mMergeEnabled } from '../../utils/model/model.js'; import { AnimatedAsterisk } from './AnimatedAsterisk.js'
import { AnimatedAsterisk } from './AnimatedAsterisk.js';
const MAX_SHOW_COUNT = 6; const MAX_SHOW_COUNT = 6
export function shouldShowOpus1mMergeNotice(): boolean { export function shouldShowOpus1mMergeNotice(): boolean {
return isOpus1mMergeEnabled() && (getGlobalConfig().opus1mMergeNoticeSeenCount ?? 0) < MAX_SHOW_COUNT; return (
isOpus1mMergeEnabled() &&
(getGlobalConfig().opus1mMergeNoticeSeenCount ?? 0) < MAX_SHOW_COUNT
)
} }
export function Opus1mMergeNotice() {
const $ = _c(4); export function Opus1mMergeNotice(): React.ReactNode {
const [show] = useState(shouldShowOpus1mMergeNotice); const [show] = useState(shouldShowOpus1mMergeNotice)
let t0;
let t1; useEffect(() => {
if ($[0] !== show) { if (!show) return
t0 = () => { const newCount = (getGlobalConfig().opus1mMergeNoticeSeenCount ?? 0) + 1
if (!show) { saveGlobalConfig(prev => {
return; if ((prev.opus1mMergeNoticeSeenCount ?? 0) >= newCount) return prev
} return { ...prev, opus1mMergeNoticeSeenCount: newCount }
const newCount = (getGlobalConfig().opus1mMergeNoticeSeenCount ?? 0) + 1; })
saveGlobalConfig(prev => { }, [show])
if ((prev.opus1mMergeNoticeSeenCount ?? 0) >= newCount) {
return prev; if (!show) return null
}
return { return (
...prev, <Box paddingLeft={2}>
opus1mMergeNoticeSeenCount: newCount <AnimatedAsterisk char={UP_ARROW} />
}; <Text dimColor>
}); {' '}
}; Opus now defaults to 1M context · 5x more room, same pricing
t1 = [show]; </Text>
$[0] = show; </Box>
$[1] = t0; )
$[2] = t1;
} else {
t0 = $[1];
t1 = $[2];
}
useEffect(t0, t1);
if (!show) {
return null;
}
let t2;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t2 = <Box paddingLeft={2}><AnimatedAsterisk char={UP_ARROW} /><Text dimColor={true}>{" "}Opus now defaults to 1M context · 5x more room, same pricing</Text></Box>;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
} }

View File

@@ -1,13 +1,17 @@
import { c as _c } from "react/compiler-runtime"; import * as React from 'react'
import * as React from 'react'; import { useState } from 'react'
import { useState } from 'react'; import { Text } from '../../ink.js'
import { Text } from '../../ink.js'; import { logEvent } from '../../services/analytics/index.js'
import { logEvent } from '../../services/analytics/index.js'; import {
import { formatGrantAmount, getCachedOverageCreditGrant, refreshOverageCreditGrantCache } from '../../services/api/overageCreditGrant.js'; formatGrantAmount,
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'; getCachedOverageCreditGrant,
import { truncate } from '../../utils/format.js'; refreshOverageCreditGrantCache,
import type { FeedConfig } from './Feed.js'; } from '../../services/api/overageCreditGrant.js'
const MAX_IMPRESSIONS = 3; import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
import { truncate } from '../../utils/format.js'
import type { FeedConfig } from './Feed.js'
const MAX_IMPRESSIONS = 3
/** /**
* Whether to show the overage credit upsell on any surface. * Whether to show the overage credit upsell on any surface.
@@ -25,16 +29,20 @@ const MAX_IMPRESSIONS = 3;
* (welcome feed, tips). * (welcome feed, tips).
*/ */
export function isEligibleForOverageCreditGrant(): boolean { export function isEligibleForOverageCreditGrant(): boolean {
const info = getCachedOverageCreditGrant(); const info = getCachedOverageCreditGrant()
if (!info || !info.available || info.granted) return false; if (!info || !info.available || info.granted) return false
return formatGrantAmount(info) !== null; return formatGrantAmount(info) !== null
} }
export function shouldShowOverageCreditUpsell(): boolean { export function shouldShowOverageCreditUpsell(): boolean {
if (!isEligibleForOverageCreditGrant()) return false; if (!isEligibleForOverageCreditGrant()) return false
const config = getGlobalConfig();
if (config.hasVisitedExtraUsage) return false; const config = getGlobalConfig()
if ((config.overageCreditUpsellSeenCount ?? 0) >= MAX_IMPRESSIONS) return false; if (config.hasVisitedExtraUsage) return false
return true; if ((config.overageCreditUpsellSeenCount ?? 0) >= MAX_IMPRESSIONS)
return false
return true
} }
/** /**
@@ -42,105 +50,78 @@ export function shouldShowOverageCreditUpsell(): boolean {
* unconditionally on mount — it no-ops if cache is fresh. * unconditionally on mount — it no-ops if cache is fresh.
*/ */
export function maybeRefreshOverageCreditCache(): void { export function maybeRefreshOverageCreditCache(): void {
if (getCachedOverageCreditGrant() !== null) return; if (getCachedOverageCreditGrant() !== null) return
void refreshOverageCreditGrantCache(); void refreshOverageCreditGrantCache()
} }
export function useShowOverageCreditUpsell() {
const [show] = useState(_temp); export function useShowOverageCreditUpsell(): boolean {
return show; const [show] = useState(() => {
} maybeRefreshOverageCreditCache()
function _temp() { return shouldShowOverageCreditUpsell()
maybeRefreshOverageCreditCache(); })
return shouldShowOverageCreditUpsell(); return show
} }
export function incrementOverageCreditUpsellSeenCount(): void { export function incrementOverageCreditUpsellSeenCount(): void {
let newCount = 0; let newCount = 0
saveGlobalConfig(prev => { saveGlobalConfig(prev => {
newCount = (prev.overageCreditUpsellSeenCount ?? 0) + 1; newCount = (prev.overageCreditUpsellSeenCount ?? 0) + 1
return { return {
...prev, ...prev,
overageCreditUpsellSeenCount: newCount overageCreditUpsellSeenCount: newCount,
}; }
}); })
logEvent('tengu_overage_credit_upsell_shown', { logEvent('tengu_overage_credit_upsell_shown', { seen_count: newCount })
seen_count: newCount
});
} }
// Copy from "OC & Bulk Overages copy" doc (#6 — CLI /usage) // Copy from "OC & Bulk Overages copy" doc (#6 — CLI /usage)
function getUsageText(amount: string): string { function getUsageText(amount: string): string {
return `${amount} in extra usage for third-party apps · /extra-usage`; return `${amount} in extra usage for third-party apps · /extra-usage`
} }
// Copy from "OC & Bulk Overages copy" doc (#4 — CLI Welcome screen). // Copy from "OC & Bulk Overages copy" doc (#4 — CLI Welcome screen).
// Char budgets: title ≤19, subtitle ≤48. // Char budgets: title ≤19, subtitle ≤48.
const FEED_SUBTITLE = 'On us. Works on third-party apps · /extra-usage'; const FEED_SUBTITLE = 'On us. Works on third-party apps · /extra-usage'
function getFeedTitle(amount: string): string { function getFeedTitle(amount: string): string {
return `${amount} in extra usage`; return `${amount} in extra usage`
} }
type Props = {
maxWidth?: number; type Props = { maxWidth?: number; twoLine?: boolean }
twoLine?: boolean;
}; export function OverageCreditUpsell({
export function OverageCreditUpsell(t0) { maxWidth,
const $ = _c(8); twoLine,
const { }: Props): React.ReactNode {
maxWidth, const info = getCachedOverageCreditGrant()
twoLine if (!info) return null
} = t0; const amount = formatGrantAmount(info)
let t1; if (!amount) return null
let t2;
if ($[0] !== maxWidth || $[1] !== twoLine) { if (twoLine) {
t2 = Symbol.for("react.early_return_sentinel"); const title = getFeedTitle(amount)
bb0: { return (
const info = getCachedOverageCreditGrant(); <>
if (!info) { <Text color="claude">
t2 = null; {maxWidth ? truncate(title, maxWidth) : title}
break bb0; </Text>
} <Text dimColor>
const amount = formatGrantAmount(info); {maxWidth ? truncate(FEED_SUBTITLE, maxWidth) : FEED_SUBTITLE}
if (!amount) { </Text>
t2 = null; </>
break bb0; )
}
if (twoLine) {
const title = getFeedTitle(amount);
let t3;
if ($[4] !== maxWidth) {
t3 = maxWidth ? truncate(FEED_SUBTITLE, maxWidth) : FEED_SUBTITLE;
$[4] = maxWidth;
$[5] = t3;
} else {
t3 = $[5];
}
let t4;
if ($[6] !== t3) {
t4 = <Text dimColor={true}>{t3}</Text>;
$[6] = t3;
$[7] = t4;
} else {
t4 = $[7];
}
t2 = <><Text color="claude">{maxWidth ? truncate(title, maxWidth) : title}</Text>{t4}</>;
break bb0;
}
const text = getUsageText(amount);
const display = maxWidth ? truncate(text, maxWidth) : text;
const highlightLen = Math.min(getFeedTitle(amount).length, display.length);
t1 = <Text dimColor={true}><Text color="claude">{display.slice(0, highlightLen)}</Text>{display.slice(highlightLen)}</Text>;
}
$[0] = maxWidth;
$[1] = twoLine;
$[2] = t1;
$[3] = t2;
} else {
t1 = $[2];
t2 = $[3];
} }
if (t2 !== Symbol.for("react.early_return_sentinel")) {
return t2; const text = getUsageText(amount)
} const display = maxWidth ? truncate(text, maxWidth) : text
return t1; const highlightLen = Math.min(getFeedTitle(amount).length, display.length)
return (
<Text dimColor>
<Text color="claude">{display.slice(0, highlightLen)}</Text>
{display.slice(highlightLen)}
</Text>
)
} }
/** /**
@@ -151,15 +132,15 @@ export function OverageCreditUpsell(t0) {
* Char budgets: title ≤19, subtitle ≤48. * Char budgets: title ≤19, subtitle ≤48.
*/ */
export function createOverageCreditFeed(): FeedConfig { export function createOverageCreditFeed(): FeedConfig {
const info = getCachedOverageCreditGrant(); const info = getCachedOverageCreditGrant()
const amount = info ? formatGrantAmount(info) : null; const amount = info ? formatGrantAmount(info) : null
const title = amount ? getFeedTitle(amount) : 'extra usage credit'; const title = amount ? getFeedTitle(amount) : 'extra usage credit'
return { return {
title, title,
lines: [], lines: [],
customContent: { customContent: {
content: <Text dimColor>{FEED_SUBTITLE}</Text>, content: <Text dimColor>{FEED_SUBTITLE}</Text>,
width: Math.max(title.length, FEED_SUBTITLE.length) width: Math.max(title.length, FEED_SUBTITLE.length),
} },
}; }
} }

View File

@@ -1,67 +1,51 @@
import { c as _c } from "react/compiler-runtime"; import { feature } from 'bun:bundle'
import { feature } from 'bun:bundle'; import * as React from 'react'
import * as React from 'react'; import { useEffect, useState } from 'react'
import { useEffect, useState } from 'react'; import { Box, Text } from '../../ink.js'
import { Box, Text } from '../../ink.js'; import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'; import { getInitialSettings } from '../../utils/settings/settings.js'
import { getInitialSettings } from '../../utils/settings/settings.js'; import { isVoiceModeEnabled } from '../../voice/voiceModeEnabled.js'
import { isVoiceModeEnabled } from '../../voice/voiceModeEnabled.js'; import { AnimatedAsterisk } from './AnimatedAsterisk.js'
import { AnimatedAsterisk } from './AnimatedAsterisk.js'; import { shouldShowOpus1mMergeNotice } from './Opus1mMergeNotice.js'
import { shouldShowOpus1mMergeNotice } from './Opus1mMergeNotice.js';
const MAX_SHOW_COUNT = 3; const MAX_SHOW_COUNT = 3
export function VoiceModeNotice() {
const $ = _c(1); export function VoiceModeNotice(): React.ReactNode {
let t0; // Positive ternary pattern — see docs/feature-gating.md.
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { // All strings must be inside the guarded branch for dead-code elimination.
t0 = feature("VOICE_MODE") ? <VoiceModeNoticeInner /> : null; return feature('VOICE_MODE') ? <VoiceModeNoticeInner /> : null
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
} }
function VoiceModeNoticeInner() {
const $ = _c(4); function VoiceModeNoticeInner(): React.ReactNode {
const [show] = useState(_temp); // Capture eligibility once at mount — no reactive subscriptions. This sits
let t0; // at the top of the message list and enters scrollback quickly; any
let t1; // re-render after it's in scrollback would force a full terminal reset.
if ($[0] !== show) { // If the user runs /voice this session, the notice stays visible; it won't
t0 = () => { // show next session since voiceEnabled will be true on disk.
if (!show) { const [show] = useState(
return; () =>
} isVoiceModeEnabled() &&
const newCount = (getGlobalConfig().voiceNoticeSeenCount ?? 0) + 1; getInitialSettings().voiceEnabled !== true &&
saveGlobalConfig(prev => { (getGlobalConfig().voiceNoticeSeenCount ?? 0) < MAX_SHOW_COUNT &&
if ((prev.voiceNoticeSeenCount ?? 0) >= newCount) { !shouldShowOpus1mMergeNotice(),
return prev; )
}
return { useEffect(() => {
...prev, if (!show) return
voiceNoticeSeenCount: newCount // Capture outside the updater so StrictMode's second invocation is a no-op.
}; const newCount = (getGlobalConfig().voiceNoticeSeenCount ?? 0) + 1
}); saveGlobalConfig(prev => {
}; if ((prev.voiceNoticeSeenCount ?? 0) >= newCount) return prev
t1 = [show]; return { ...prev, voiceNoticeSeenCount: newCount }
$[0] = show; })
$[1] = t0; }, [show])
$[2] = t1;
} else { if (!show) return null
t0 = $[1];
t1 = $[2]; return (
} <Box paddingLeft={2}>
useEffect(t0, t1); <AnimatedAsterisk />
if (!show) { <Text dimColor> Voice mode is now available · /voice to enable</Text>
return null; </Box>
} )
let t2;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t2 = <Box paddingLeft={2}><AnimatedAsterisk /><Text dimColor={true}> Voice mode is now available · /voice to enable</Text></Box>;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
function _temp() {
return isVoiceModeEnabled() && getInitialSettings().voiceEnabled !== true && (getGlobalConfig().voiceNoticeSeenCount ?? 0) < MAX_SHOW_COUNT && !shouldShowOpus1mMergeNotice();
} }

View File

@@ -1,432 +1,326 @@
import { c as _c } from "react/compiler-runtime"; import React from 'react'
import React from 'react'; import { Box, Text, useTheme } from 'src/ink.js'
import { Box, Text, useTheme } from 'src/ink.js'; import { env } from '../../utils/env.js'
import { env } from '../../utils/env.js';
const WELCOME_V2_WIDTH = 58; const WELCOME_V2_WIDTH = 58
export function WelcomeV2() {
const $ = _c(35); export function WelcomeV2(): React.ReactNode {
const [theme] = useTheme(); const [theme] = useTheme()
if (env.terminal === "Apple_Terminal") { const welcomeMessage = 'Welcome to Claude Code'
let t0;
if ($[0] !== theme) { if (env.terminal === 'Apple_Terminal') {
t0 = <AppleTerminalWelcomeV2 theme={theme} welcomeMessage="Welcome to Claude Code" />; return (
$[0] = theme; <AppleTerminalWelcomeV2 theme={theme} welcomeMessage={welcomeMessage} />
$[1] = t0; )
} else {
t0 = $[1];
}
return t0;
} }
if (["light", "light-daltonized", "light-ansi"].includes(theme)) {
let t0; if (['light', 'light-daltonized', 'light-ansi'].includes(theme)) {
let t1; return (
let t2; <Box width={WELCOME_V2_WIDTH}>
let t3; <Text>
let t4; <Text>
let t5; <Text color="claude">{welcomeMessage} </Text>
let t6; <Text dimColor>v{MACRO.VERSION} </Text>
let t7; </Text>
let t8; <Text>
if ($[2] === Symbol.for("react.memo_cache_sentinel")) { {'…………………………………………………………………………………………………………………………………………………………'}
t0 = <Text><Text color="claude">{"Welcome to Claude Code"} </Text><Text dimColor={true}>v{MACRO.VERSION} </Text></Text>; </Text>
t1 = <Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026"}</Text>; <Text>
t2 = <Text>{" "}</Text>; {' '}
t3 = <Text>{" "}</Text>; </Text>
t4 = <Text>{" "}</Text>; <Text>
t5 = <Text>{" \u2591\u2591\u2591\u2591\u2591\u2591 "}</Text>; {' '}
t6 = <Text>{" \u2591\u2591\u2591 \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 "}</Text>; </Text>
t7 = <Text>{" \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 "}</Text>; <Text>
t8 = <Text>{" "}</Text>; {' '}
$[2] = t0; </Text>
$[3] = t1; <Text>
$[4] = t2; {' ░░░░░░ '}
$[5] = t3; </Text>
$[6] = t4; <Text>
$[7] = t5; {' ░░░ ░░░░░░░░░░ '}
$[8] = t6; </Text>
$[9] = t7; <Text>
$[10] = t8; {' ░░░░░░░░░░░░░░░░░░░ '}
} else { </Text>
t0 = $[2]; <Text>
t1 = $[3]; {' '}
t2 = $[4]; </Text>
t3 = $[5]; <Text>
t4 = $[6]; <Text dimColor>{' ░░░░'}</Text>
t5 = $[7]; <Text>{' ██ '}</Text>
t6 = $[8]; </Text>
t7 = $[9]; <Text>
t8 = $[10]; <Text dimColor>{' ░░░░░░░░░░'}</Text>
} <Text>{' ██▒▒██ '}</Text>
let t9; </Text>
if ($[11] === Symbol.for("react.memo_cache_sentinel")) { <Text>
t9 = <Text><Text dimColor={true}>{" \u2591\u2591\u2591\u2591"}</Text><Text>{" \u2588\u2588 "}</Text></Text>; {' ▒▒ ██ ▒'}
$[11] = t9; </Text>
} else { <Text>
t9 = $[11]; {' '}
} <Text color="clawd_body"> </Text>
let t10; {' ▒▒░░▒▒ ▒ ▒▒'}
let t11; </Text>
if ($[12] === Symbol.for("react.memo_cache_sentinel")) { <Text>
t10 = <Text><Text dimColor={true}>{" \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591"}</Text><Text>{" \u2588\u2588\u2592\u2592\u2588\u2588 "}</Text></Text>; {' '}
t11 = <Text>{" \u2592\u2592 \u2588\u2588 \u2592"}</Text>; <Text color="clawd_body" backgroundColor="clawd_background">
$[12] = t10;
$[13] = t11; </Text>
} else { {' ▒▒ ▒▒ '}
t10 = $[12]; </Text>
t11 = $[13]; <Text>
} {' '}
let t12; <Text color="clawd_body"> </Text>
if ($[14] === Symbol.for("react.memo_cache_sentinel")) { {' ░ ▒ '}
t12 = <Text>{" "}<Text color="clawd_body"> </Text>{" \u2592\u2592\u2591\u2591\u2592\u2592 \u2592 \u2592\u2592"}</Text>; </Text>
$[14] = t12; <Text>
} else { {'…………………'}
t12 = $[14]; <Text color="clawd_body">{'█ █ █ █'}</Text>
} {'……………………………………………………………………░…………………………▒…………'}
let t13; </Text>
if ($[15] === Symbol.for("react.memo_cache_sentinel")) { </Text>
t13 = <Text>{" "}<Text color="clawd_body" backgroundColor="clawd_background"></Text>{" \u2592\u2592 \u2592\u2592 "}</Text>; </Box>
$[15] = t13; )
} else {
t13 = $[15];
}
let t14;
if ($[16] === Symbol.for("react.memo_cache_sentinel")) {
t14 = <Text>{" "}<Text color="clawd_body"> </Text>{" \u2591 \u2592 "}</Text>;
$[16] = t14;
} else {
t14 = $[16];
}
let t15;
if ($[17] === Symbol.for("react.memo_cache_sentinel")) {
t15 = <Box width={WELCOME_V2_WIDTH}><Text>{t0}{t1}{t2}{t3}{t4}{t5}{t6}{t7}{t8}{t9}{t10}{t11}{t12}{t13}{t14}<Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026"}<Text color="clawd_body">{"\u2588 \u2588 \u2588 \u2588"}</Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2591\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2592\u2026\u2026\u2026\u2026"}</Text></Text></Box>;
$[17] = t15;
} else {
t15 = $[17];
}
return t15;
} }
let t0;
let t1; return (
let t2; <Box width={WELCOME_V2_WIDTH}>
let t3; <Text>
let t4; <Text>
let t5; <Text color="claude">{welcomeMessage} </Text>
let t6; <Text dimColor>v{MACRO.VERSION} </Text>
if ($[18] === Symbol.for("react.memo_cache_sentinel")) { </Text>
t0 = <Text><Text color="claude">{"Welcome to Claude Code"} </Text><Text dimColor={true}>v{MACRO.VERSION} </Text></Text>; <Text>
t1 = <Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026"}</Text>; {'…………………………………………………………………………………………………………………………………………………………'}
t2 = <Text>{" "}</Text>; </Text>
t3 = <Text>{" * \u2588\u2588\u2588\u2588\u2588\u2593\u2593\u2591 "}</Text>; <Text>
t4 = <Text>{" * \u2588\u2588\u2588\u2593\u2591 \u2591\u2591 "}</Text>; {' '}
t5 = <Text>{" \u2591\u2591\u2591\u2591\u2591\u2591 \u2588\u2588\u2588\u2593\u2591 "}</Text>; </Text>
t6 = <Text>{" \u2591\u2591\u2591 \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 \u2588\u2588\u2588\u2593\u2591 "}</Text>; <Text>
$[18] = t0; {' * █████▓▓░ '}
$[19] = t1; </Text>
$[20] = t2; <Text>
$[21] = t3; {' * ███▓░ ░░ '}
$[22] = t4; </Text>
$[23] = t5; <Text>
$[24] = t6; {' ░░░░░░ ███▓░ '}
} else { </Text>
t0 = $[18]; <Text>
t1 = $[19]; {' ░░░ ░░░░░░░░░░ ███▓░ '}
t2 = $[20]; </Text>
t3 = $[21]; <Text>
t4 = $[22]; <Text>{' ░░░░░░░░░░░░░░░░░░░ '}</Text>
t5 = $[23]; <Text bold>*</Text>
t6 = $[24]; <Text>{' ██▓░░ ▓ '}</Text>
} </Text>
let t10; <Text>
let t11; {' ░▓▓███▓▓░ '}
let t7; </Text>
let t8; <Text dimColor>
let t9; {' * ░░░░ '}
if ($[25] === Symbol.for("react.memo_cache_sentinel")) { </Text>
t7 = <Text><Text>{" \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 "}</Text><Text bold={true}>*</Text><Text>{" \u2588\u2588\u2593\u2591\u2591 \u2593 "}</Text></Text>; <Text dimColor>
t8 = <Text>{" \u2591\u2593\u2593\u2588\u2588\u2588\u2593\u2593\u2591 "}</Text>; {' ░░░░░░░░ '}
t9 = <Text dimColor={true}>{" * \u2591\u2591\u2591\u2591 "}</Text>; </Text>
t10 = <Text dimColor={true}>{" \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 "}</Text>; <Text dimColor>
t11 = <Text dimColor={true}>{" \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 "}</Text>; {' ░░░░░░░░░░░░░░░░ '}
$[25] = t10; </Text>
$[26] = t11; <Text>
$[27] = t7; {' '}
$[28] = t8; <Text color="clawd_body"> </Text>
$[29] = t9; {' '}
} else { <Text dimColor>*</Text>
t10 = $[25]; <Text> </Text>
t11 = $[26]; </Text>
t7 = $[27]; <Text>
t8 = $[28]; {' '}
t9 = $[29]; <Text color="clawd_body"></Text>
} <Text>{' '}</Text>
let t12; <Text bold>*</Text>
if ($[30] === Symbol.for("react.memo_cache_sentinel")) { <Text>{' '}</Text>
t12 = <Text color="clawd_body"> </Text>; </Text>
$[30] = t12; <Text>
} else { {' '}
t12 = $[30]; <Text color="clawd_body"> </Text>
} {' * '}
let t13; </Text>
if ($[31] === Symbol.for("react.memo_cache_sentinel")) { <Text>
t13 = <Text>{" "}{t12}{" "}<Text dimColor={true}>*</Text><Text> </Text></Text>; {'…………………'}
$[31] = t13; <Text color="clawd_body">{'█ █ █ █'}</Text>
} else { {'………………………………………………………………………………………………………………'}
t13 = $[31]; </Text>
} </Text>
let t14; </Box>
if ($[32] === Symbol.for("react.memo_cache_sentinel")) { )
t14 = <Text>{" "}<Text color="clawd_body"></Text><Text>{" "}</Text><Text bold={true}>*</Text><Text>{" "}</Text></Text>;
$[32] = t14;
} else {
t14 = $[32];
}
let t15;
if ($[33] === Symbol.for("react.memo_cache_sentinel")) {
t15 = <Text>{" "}<Text color="clawd_body"> </Text>{" * "}</Text>;
$[33] = t15;
} else {
t15 = $[33];
}
let t16;
if ($[34] === Symbol.for("react.memo_cache_sentinel")) {
t16 = <Box width={WELCOME_V2_WIDTH}><Text>{t0}{t1}{t2}{t3}{t4}{t5}{t6}{t7}{t8}{t9}{t10}{t11}{t13}{t14}{t15}<Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026"}<Text color="clawd_body">{"\u2588 \u2588 \u2588 \u2588"}</Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026"}</Text></Text></Box>;
$[34] = t16;
} else {
t16 = $[34];
}
return t16;
} }
type AppleTerminalWelcomeV2Props = { type AppleTerminalWelcomeV2Props = {
theme: string; theme: string
welcomeMessage: string; welcomeMessage: string
}; }
function AppleTerminalWelcomeV2(t0) {
const $ = _c(44); function AppleTerminalWelcomeV2({
const { theme,
theme, welcomeMessage,
welcomeMessage }: AppleTerminalWelcomeV2Props): React.ReactNode {
} = t0; const isLightTheme = ['light', 'light-daltonized', 'light-ansi'].includes(
const isLightTheme = ["light", "light-daltonized", "light-ansi"].includes(theme); theme,
if (isLightTheme) { )
let t1;
if ($[0] !== welcomeMessage) { if (isLightTheme) {
t1 = <Text color="claude">{welcomeMessage} </Text>; return (
$[0] = welcomeMessage; <Box width={WELCOME_V2_WIDTH}>
$[1] = t1; <Text>
} else { <Text>
t1 = $[1]; <Text color="claude">{welcomeMessage} </Text>
} <Text dimColor>v{MACRO.VERSION} </Text>
let t2; </Text>
if ($[2] === Symbol.for("react.memo_cache_sentinel")) { <Text>
t2 = <Text dimColor={true}>v{MACRO.VERSION} </Text>; {'…………………………………………………………………………………………………………………………………………………………'}
$[2] = t2; </Text>
} else { <Text>
t2 = $[2]; {' '}
} </Text>
let t3; <Text>
if ($[3] !== t1) { {' '}
t3 = <Text>{t1}{t2}</Text>; </Text>
$[3] = t1; <Text>
$[4] = t3; {' '}
} else { </Text>
t3 = $[4]; <Text>
} {' ░░░░░░ '}
let t10; </Text>
let t11; <Text>
let t4; {' ░░░ ░░░░░░░░░░ '}
let t5; </Text>
let t6; <Text>
let t7; {' ░░░░░░░░░░░░░░░░░░░ '}
let t8; </Text>
let t9; <Text>
if ($[5] === Symbol.for("react.memo_cache_sentinel")) { {' '}
t4 = <Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026"}</Text>; </Text>
t5 = <Text>{" "}</Text>; <Text>
t6 = <Text>{" "}</Text>; <Text dimColor>{' ░░░░'}</Text>
t7 = <Text>{" "}</Text>; <Text>{' ██ '}</Text>
t8 = <Text>{" \u2591\u2591\u2591\u2591\u2591\u2591 "}</Text>; </Text>
t9 = <Text>{" \u2591\u2591\u2591 \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 "}</Text>; <Text>
t10 = <Text>{" \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 "}</Text>; <Text dimColor>{' ░░░░░░░░░░'}</Text>
t11 = <Text>{" "}</Text>; <Text>{' ██▒▒██ '}</Text>
$[5] = t10; </Text>
$[6] = t11; <Text>
$[7] = t4; {' ▒▒ ██ ▒'}
$[8] = t5; </Text>
$[9] = t6; <Text>
$[10] = t7; {' ▒▒░░▒▒ ▒ ▒▒'}
$[11] = t8; </Text>
$[12] = t9; <Text>
} else { {' '}
t10 = $[5]; <Text color="clawd_body"></Text>
t11 = $[6]; <Text color="clawd_background" backgroundColor="clawd_body">
t4 = $[7]; {' '}
t5 = $[8]; {' '}{' '}
t6 = $[9]; </Text>
t7 = $[10]; <Text color="clawd_body"></Text>
t8 = $[11]; {' ▒▒ ▒▒ '}
t9 = $[12]; </Text>
} <Text>
let t12; {' '}
if ($[13] === Symbol.for("react.memo_cache_sentinel")) { <Text backgroundColor="clawd_body">{' '.repeat(9)}</Text>
t12 = <Text><Text dimColor={true}>{" \u2591\u2591\u2591\u2591"}</Text><Text>{" \u2588\u2588 "}</Text></Text>; {' ░ ▒ '}
$[13] = t12; </Text>
} else { <Text>
t12 = $[13]; {'…………………'}
} <Text backgroundColor="clawd_body"> </Text>
let t13; <Text> </Text>
let t14; <Text backgroundColor="clawd_body"> </Text>
let t15; <Text>{' '}</Text>
if ($[14] === Symbol.for("react.memo_cache_sentinel")) { <Text backgroundColor="clawd_body"> </Text>
t13 = <Text><Text dimColor={true}>{" \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591"}</Text><Text>{" \u2588\u2588\u2592\u2592\u2588\u2588 "}</Text></Text>; <Text> </Text>
t14 = <Text>{" \u2592\u2592 \u2588\u2588 \u2592"}</Text>; <Text backgroundColor="clawd_body"> </Text>
t15 = <Text>{" \u2592\u2592\u2591\u2591\u2592\u2592 \u2592 \u2592\u2592"}</Text>; {'……………………………………………………………………░…………………………▒…………'}
$[14] = t13; </Text>
$[15] = t14; </Text>
$[16] = t15; </Box>
} else { )
t13 = $[14]; }
t14 = $[15];
t15 = $[16]; return (
} <Box width={WELCOME_V2_WIDTH}>
let t16; <Text>
if ($[17] === Symbol.for("react.memo_cache_sentinel")) { <Text>
t16 = <Text>{" "}<Text color="clawd_body"></Text><Text color="clawd_background" backgroundColor="clawd_body">{" "}{" "}{" "}</Text><Text color="clawd_body"></Text>{" \u2592\u2592 \u2592\u2592 "}</Text>; <Text color="claude">{welcomeMessage} </Text>
$[17] = t16; <Text dimColor>v{MACRO.VERSION} </Text>
} else { </Text>
t16 = $[17]; <Text>
} {'…………………………………………………………………………………………………………………………………………………………'}
let t17; </Text>
if ($[18] === Symbol.for("react.memo_cache_sentinel")) { <Text>
t17 = <Text>{" "}<Text backgroundColor="clawd_body">{" ".repeat(9)}</Text>{" \u2591 \u2592 "}</Text>; {' '}
$[18] = t17; </Text>
} else { <Text>
t17 = $[18]; {' * █████▓▓░ '}
} </Text>
let t18; <Text>
if ($[19] === Symbol.for("react.memo_cache_sentinel")) { {' * ███▓░ ░░ '}
t18 = <Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026"}<Text backgroundColor="clawd_body"> </Text><Text> </Text><Text backgroundColor="clawd_body"> </Text><Text>{" "}</Text><Text backgroundColor="clawd_body"> </Text><Text> </Text><Text backgroundColor="clawd_body"> </Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2591\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2592\u2026\u2026\u2026\u2026"}</Text>; </Text>
$[19] = t18; <Text>
} else { {' ░░░░░░ ███▓░ '}
t18 = $[19]; </Text>
} <Text>
let t19; {' ░░░ ░░░░░░░░░░ ███▓░ '}
if ($[20] !== t3) { </Text>
t19 = <Box width={WELCOME_V2_WIDTH}><Text>{t3}{t4}{t5}{t6}{t7}{t8}{t9}{t10}{t11}{t12}{t13}{t14}{t15}{t16}{t17}{t18}</Text></Box>; <Text>
$[20] = t3; <Text>{' ░░░░░░░░░░░░░░░░░░░ '}</Text>
$[21] = t19; <Text bold>*</Text>
} else { <Text>{' ██▓░░ ▓ '}</Text>
t19 = $[21]; </Text>
} <Text>
return t19; {' ░▓▓███▓▓░ '}
} </Text>
let t1; <Text dimColor>
if ($[22] !== welcomeMessage) { {' * ░░░░ '}
t1 = <Text color="claude">{welcomeMessage} </Text>; </Text>
$[22] = welcomeMessage; <Text dimColor>
$[23] = t1; {' ░░░░░░░░ '}
} else { </Text>
t1 = $[23]; <Text dimColor>
} {' ░░░░░░░░░░░░░░░░ '}
let t2; </Text>
if ($[24] === Symbol.for("react.memo_cache_sentinel")) { <Text>
t2 = <Text dimColor={true}>v{MACRO.VERSION} </Text>; {' '}
$[24] = t2; <Text dimColor>*</Text>
} else { <Text> </Text>
t2 = $[24]; </Text>
} <Text>
let t3; {' '}
if ($[25] !== t1) { <Text color="clawd_body"></Text>
t3 = <Text>{t1}{t2}</Text>; <Text color="clawd_background" backgroundColor="clawd_body">
$[25] = t1; {' '}
$[26] = t3; {' '}{' '}
} else { </Text>
t3 = $[26]; <Text color="clawd_body"></Text>
} <Text>{' '}</Text>
let t4; <Text bold>*</Text>
let t5; <Text>{' '}</Text>
let t6; </Text>
let t7; <Text>
let t8; {' '}
let t9; <Text backgroundColor="clawd_body">{' '.repeat(9)}</Text>
if ($[27] === Symbol.for("react.memo_cache_sentinel")) { {' * '}
t4 = <Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026"}</Text>; </Text>
t5 = <Text>{" "}</Text>; <Text>
t6 = <Text>{" * \u2588\u2588\u2588\u2588\u2588\u2593\u2593\u2591 "}</Text>; {'…………………'}
t7 = <Text>{" * \u2588\u2588\u2588\u2593\u2591 \u2591\u2591 "}</Text>; <Text backgroundColor="clawd_body"> </Text>
t8 = <Text>{" \u2591\u2591\u2591\u2591\u2591\u2591 \u2588\u2588\u2588\u2593\u2591 "}</Text>; <Text> </Text>
t9 = <Text>{" \u2591\u2591\u2591 \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 \u2588\u2588\u2588\u2593\u2591 "}</Text>; <Text backgroundColor="clawd_body"> </Text>
$[27] = t4; <Text>{' '}</Text>
$[28] = t5; <Text backgroundColor="clawd_body"> </Text>
$[29] = t6; <Text> </Text>
$[30] = t7; <Text backgroundColor="clawd_body"> </Text>
$[31] = t8; {'………………………………………………………………………………………………………………'}
$[32] = t9; </Text>
} else { </Text>
t4 = $[27]; </Box>
t5 = $[28]; )
t6 = $[29];
t7 = $[30];
t8 = $[31];
t9 = $[32];
}
let t10;
let t11;
let t12;
let t13;
let t14;
if ($[33] === Symbol.for("react.memo_cache_sentinel")) {
t10 = <Text><Text>{" \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 "}</Text><Text bold={true}>*</Text><Text>{" \u2588\u2588\u2593\u2591\u2591 \u2593 "}</Text></Text>;
t11 = <Text>{" \u2591\u2593\u2593\u2588\u2588\u2588\u2593\u2593\u2591 "}</Text>;
t12 = <Text dimColor={true}>{" * \u2591\u2591\u2591\u2591 "}</Text>;
t13 = <Text dimColor={true}>{" \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 "}</Text>;
t14 = <Text dimColor={true}>{" \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 "}</Text>;
$[33] = t10;
$[34] = t11;
$[35] = t12;
$[36] = t13;
$[37] = t14;
} else {
t10 = $[33];
t11 = $[34];
t12 = $[35];
t13 = $[36];
t14 = $[37];
}
let t15;
if ($[38] === Symbol.for("react.memo_cache_sentinel")) {
t15 = <Text>{" "}<Text dimColor={true}>*</Text><Text> </Text></Text>;
$[38] = t15;
} else {
t15 = $[38];
}
let t16;
if ($[39] === Symbol.for("react.memo_cache_sentinel")) {
t16 = <Text>{" "}<Text color="clawd_body"></Text><Text color="clawd_background" backgroundColor="clawd_body">{" "}{" "}{" "}</Text><Text color="clawd_body"></Text><Text>{" "}</Text><Text bold={true}>*</Text><Text>{" "}</Text></Text>;
$[39] = t16;
} else {
t16 = $[39];
}
let t17;
if ($[40] === Symbol.for("react.memo_cache_sentinel")) {
t17 = <Text>{" "}<Text backgroundColor="clawd_body">{" ".repeat(9)}</Text>{" * "}</Text>;
$[40] = t17;
} else {
t17 = $[40];
}
let t18;
if ($[41] === Symbol.for("react.memo_cache_sentinel")) {
t18 = <Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026"}<Text backgroundColor="clawd_body"> </Text><Text> </Text><Text backgroundColor="clawd_body"> </Text><Text>{" "}</Text><Text backgroundColor="clawd_body"> </Text><Text> </Text><Text backgroundColor="clawd_body"> </Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026"}</Text>;
$[41] = t18;
} else {
t18 = $[41];
}
let t19;
if ($[42] !== t3) {
t19 = <Box width={WELCOME_V2_WIDTH}><Text>{t3}{t4}{t5}{t6}{t7}{t8}{t9}{t10}{t11}{t12}{t13}{t14}{t15}{t16}{t17}{t18}</Text></Box>;
$[42] = t3;
$[43] = t19;
} else {
t19 = $[43];
}
return t19;
} }

View File

@@ -1,91 +1,117 @@
import figures from 'figures'; import figures from 'figures'
import { homedir } from 'os'; import { homedir } from 'os'
import * as React from 'react'; import * as React from 'react'
import { Box, Text } from '../../ink.js'; import { Box, Text } from '../../ink.js'
import type { Step } from '../../projectOnboardingState.js'; import type { Step } from '../../projectOnboardingState.js'
import { formatCreditAmount, getCachedReferrerReward } from '../../services/api/referral.js'; import {
import type { LogOption } from '../../types/logs.js'; formatCreditAmount,
import { getCwd } from '../../utils/cwd.js'; getCachedReferrerReward,
import { formatRelativeTimeAgo } from '../../utils/format.js'; } from '../../services/api/referral.js'
import type { FeedConfig, FeedLine } from './Feed.js'; import type { LogOption } from '../../types/logs.js'
import { getCwd } from '../../utils/cwd.js'
import { formatRelativeTimeAgo } from '../../utils/format.js'
import type { FeedConfig, FeedLine } from './Feed.js'
export function createRecentActivityFeed(activities: LogOption[]): FeedConfig { export function createRecentActivityFeed(activities: LogOption[]): FeedConfig {
const lines: FeedLine[] = activities.map(log => { const lines: FeedLine[] = activities.map(log => {
const time = formatRelativeTimeAgo(log.modified); const time = formatRelativeTimeAgo(log.modified)
const description = log.summary && log.summary !== 'No prompt' ? log.summary : log.firstPrompt; const description =
log.summary && log.summary !== 'No prompt' ? log.summary : log.firstPrompt
return { return {
text: description || '', text: description || '',
timestamp: time timestamp: time,
}; }
}); })
return { return {
title: 'Recent activity', title: 'Recent activity',
lines, lines,
footer: lines.length > 0 ? '/resume for more' : undefined, footer: lines.length > 0 ? '/resume for more' : undefined,
emptyMessage: 'No recent activity' emptyMessage: 'No recent activity',
}; }
} }
export function createWhatsNewFeed(releaseNotes: string[]): FeedConfig { export function createWhatsNewFeed(releaseNotes: string[]): FeedConfig {
const lines: FeedLine[] = releaseNotes.map(note => { const lines: FeedLine[] = releaseNotes.map(note => {
if ((process.env.USER_TYPE) === 'ant') { if (process.env.USER_TYPE === 'ant') {
const match = note.match(/^(\d+\s+\w+\s+ago)\s+(.+)$/); const match = note.match(/^(\d+\s+\w+\s+ago)\s+(.+)$/)
if (match) { if (match) {
return { return {
timestamp: match[1], timestamp: match[1],
text: match[2] || '' text: match[2] || '',
}; }
} }
} }
return { return {
text: note text: note,
}; }
}); })
const emptyMessage = (process.env.USER_TYPE) === 'ant' ? 'Unable to fetch latest claude-cli-internal commits' : 'Check the Claude Code changelog for updates';
const emptyMessage =
process.env.USER_TYPE === 'ant'
? 'Unable to fetch latest claude-cli-internal commits'
: 'Check the Claude Code changelog for updates'
return { return {
title: (process.env.USER_TYPE) === 'ant' ? "What's new [ANT-ONLY: Latest CC commits]" : "What's new", title:
process.env.USER_TYPE === 'ant'
? "What's new [ANT-ONLY: Latest CC commits]"
: "What's new",
lines, lines,
footer: lines.length > 0 ? '/release-notes for more' : undefined, footer: lines.length > 0 ? '/release-notes for more' : undefined,
emptyMessage emptyMessage,
}; }
} }
export function createProjectOnboardingFeed(steps: Step[]): FeedConfig { export function createProjectOnboardingFeed(steps: Step[]): FeedConfig {
const enabledSteps = steps.filter(({ const enabledSteps = steps
isEnabled .filter(({ isEnabled }) => isEnabled)
}) => isEnabled).sort((a, b) => Number(a.isComplete) - Number(b.isComplete)); .sort((a, b) => Number(a.isComplete) - Number(b.isComplete))
const lines: FeedLine[] = enabledSteps.map(({
text, const lines: FeedLine[] = enabledSteps.map(({ text, isComplete }) => {
isComplete const checkmark = isComplete ? `${figures.tick} ` : ''
}) => {
const checkmark = isComplete ? `${figures.tick} ` : '';
return { return {
text: `${checkmark}${text}` text: `${checkmark}${text}`,
}; }
}); })
const warningText = getCwd() === homedir() ? 'Note: You have launched claude in your home directory. For the best experience, launch it in a project directory instead.' : undefined;
const warningText =
getCwd() === homedir()
? 'Note: You have launched claude in your home directory. For the best experience, launch it in a project directory instead.'
: undefined
if (warningText) { if (warningText) {
lines.push({ lines.push({
text: warningText text: warningText,
}); })
} }
return { return {
title: 'Tips for getting started', title: 'Tips for getting started',
lines lines,
}; }
} }
export function createGuestPassesFeed(): FeedConfig { export function createGuestPassesFeed(): FeedConfig {
const reward = getCachedReferrerReward(); const reward = getCachedReferrerReward()
const subtitle = reward ? `Share Claude Code and earn ${formatCreditAmount(reward)} of extra usage` : 'Share Claude Code with friends'; const subtitle = reward
? `Share Claude Code and earn ${formatCreditAmount(reward)} of extra usage`
: 'Share Claude Code with friends'
return { return {
title: '3 guest passes', title: '3 guest passes',
lines: [], lines: [],
customContent: { customContent: {
content: <> content: (
<>
<Box marginY={1}> <Box marginY={1}>
<Text color="claude">[] [] []</Text> <Text color="claude">[] [] []</Text>
</Box> </Box>
<Text dimColor>{subtitle}</Text> <Text dimColor>{subtitle}</Text>
</>, </>
width: 48 ),
width: 48,
}, },
footer: '/passes' footer: '/passes',
}; }
} }

View File

@@ -1,63 +1,83 @@
import * as React from 'react'; import * as React from 'react'
import { Box, Text } from '../../ink.js'; import { Box, Text } from '../../ink.js'
import { Select } from '../CustomSelect/select.js'; import { Select } from '../CustomSelect/select.js'
import { PermissionDialog } from '../permissions/PermissionDialog.js'; import { PermissionDialog } from '../permissions/PermissionDialog.js'
type Props = { type Props = {
pluginName: string; pluginName: string
pluginDescription?: string; pluginDescription?: string
fileExtension: string; fileExtension: string
onResponse: (response: 'yes' | 'no' | 'never' | 'disable') => void; onResponse: (response: 'yes' | 'no' | 'never' | 'disable') => void
}; }
const AUTO_DISMISS_MS = 30_000;
const AUTO_DISMISS_MS = 30_000
export function LspRecommendationMenu({ export function LspRecommendationMenu({
pluginName, pluginName,
pluginDescription, pluginDescription,
fileExtension, fileExtension,
onResponse onResponse,
}: Props): React.ReactNode { }: Props): React.ReactNode {
// Use ref to avoid timer reset when onResponse changes // Use ref to avoid timer reset when onResponse changes
const onResponseRef = React.useRef(onResponse); const onResponseRef = React.useRef(onResponse)
onResponseRef.current = onResponse; onResponseRef.current = onResponse
// 30-second auto-dismiss timer - counts as ignored (no) // 30-second auto-dismiss timer - counts as ignored (no)
React.useEffect(() => { React.useEffect(() => {
const timeoutId = setTimeout(ref => ref.current('no'), AUTO_DISMISS_MS, onResponseRef); const timeoutId = setTimeout(
return () => clearTimeout(timeoutId); ref => ref.current('no'),
}, []); AUTO_DISMISS_MS,
onResponseRef,
)
return () => clearTimeout(timeoutId)
}, [])
function onSelect(value: string): void { function onSelect(value: string): void {
switch (value) { switch (value) {
case 'yes': case 'yes':
onResponse('yes'); onResponse('yes')
break; break
case 'no': case 'no':
onResponse('no'); onResponse('no')
break; break
case 'never': case 'never':
onResponse('never'); onResponse('never')
break; break
case 'disable': case 'disable':
onResponse('disable'); onResponse('disable')
break; break
} }
} }
const options = [{
label: <Text> const options = [
{
label: (
<Text>
Yes, install <Text bold>{pluginName}</Text> Yes, install <Text bold>{pluginName}</Text>
</Text>, </Text>
value: 'yes' ),
}, { value: 'yes',
label: 'No, not now', },
value: 'no' {
}, { label: 'No, not now',
label: <Text> value: 'no',
},
{
label: (
<Text>
Never for <Text bold>{pluginName}</Text> Never for <Text bold>{pluginName}</Text>
</Text>, </Text>
value: 'never' ),
}, { value: 'never',
label: 'Disable all LSP recommendations', },
value: 'disable' {
}]; label: 'Disable all LSP recommendations',
return <PermissionDialog title="LSP Plugin Recommendation"> value: 'disable',
},
]
return (
<PermissionDialog title="LSP Plugin Recommendation">
<Box flexDirection="column" paddingX={2} paddingY={1}> <Box flexDirection="column" paddingX={2} paddingY={1}>
<Box marginBottom={1}> <Box marginBottom={1}>
<Text dimColor> <Text dimColor>
@@ -69,9 +89,11 @@ export function LspRecommendationMenu({
<Text dimColor>Plugin:</Text> <Text dimColor>Plugin:</Text>
<Text> {pluginName}</Text> <Text> {pluginName}</Text>
</Box> </Box>
{pluginDescription && <Box> {pluginDescription && (
<Box>
<Text dimColor>{pluginDescription}</Text> <Text dimColor>{pluginDescription}</Text>
</Box>} </Box>
)}
<Box> <Box>
<Text dimColor>Triggered by:</Text> <Text dimColor>Triggered by:</Text>
<Text> {fileExtension} files</Text> <Text> {fileExtension} files</Text>
@@ -80,8 +102,13 @@ export function LspRecommendationMenu({
<Text>Would you like to install this LSP plugin?</Text> <Text>Would you like to install this LSP plugin?</Text>
</Box> </Box>
<Box> <Box>
<Select options={options} onChange={onSelect} onCancel={() => onResponse('no')} /> <Select
options={options}
onChange={onSelect}
onCancel={() => onResponse('no')}
/>
</Box> </Box>
</Box> </Box>
</PermissionDialog>; </PermissionDialog>
)
} }

View File

@@ -1,114 +1,90 @@
import { c as _c } from "react/compiler-runtime"; import React from 'react'
import React from 'react'; import {
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
import { getSettings_DEPRECATED, updateSettingsForSource } from '../utils/settings/settings.js'; logEvent,
import { Select } from './CustomSelect/index.js'; } from 'src/services/analytics/index.js'
import { Dialog } from './design-system/Dialog.js'; import {
import { MCPServerDialogCopy } from './MCPServerDialogCopy.js'; getSettings_DEPRECATED,
updateSettingsForSource,
} from '../utils/settings/settings.js'
import { Select } from './CustomSelect/index.js'
import { Dialog } from './design-system/Dialog.js'
import { MCPServerDialogCopy } from './MCPServerDialogCopy.js'
type Props = { type Props = {
serverName: string; serverName: string
onDone(): void; onDone(): void
}; }
export function MCPServerApprovalDialog(t0) {
const $ = _c(13); export function MCPServerApprovalDialog({
const { serverName,
serverName, onDone,
onDone }: Props): React.ReactNode {
} = t0; function onChange(value: 'yes' | 'yes_all' | 'no') {
let t1; logEvent('tengu_mcp_dialog_choice', {
if ($[0] !== onDone || $[1] !== serverName) { choice:
t1 = function onChange(value) { value as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent("tengu_mcp_dialog_choice", { })
choice: value as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
}); switch (value) {
bb2: switch (value) { case 'yes':
case "yes": case 'yes_all': {
case "yes_all": // Get current enabled servers from settings
{ const currentSettings = getSettings_DEPRECATED() || {}
const currentSettings_0 = getSettings_DEPRECATED() || {}; const enabledServers = currentSettings.enabledMcpjsonServers || []
const enabledServers = currentSettings_0.enabledMcpjsonServers || [];
if (!enabledServers.includes(serverName)) { // Add server if not already enabled
updateSettingsForSource("localSettings", { if (!enabledServers.includes(serverName)) {
enabledMcpjsonServers: [...enabledServers, serverName] updateSettingsForSource('localSettings', {
}); enabledMcpjsonServers: [...enabledServers, serverName],
} })
if (value === "yes_all") { }
updateSettingsForSource("localSettings", {
enableAllProjectMcpServers: true if (value === 'yes_all') {
}); updateSettingsForSource('localSettings', {
} enableAllProjectMcpServers: true,
onDone(); })
break bb2; }
} onDone()
case "no": break
{ }
const currentSettings = getSettings_DEPRECATED() || {}; case 'no': {
const disabledServers = currentSettings.disabledMcpjsonServers || []; // Get current disabled servers from settings
if (!disabledServers.includes(serverName)) { const currentSettings = getSettings_DEPRECATED() || {}
updateSettingsForSource("localSettings", { const disabledServers = currentSettings.disabledMcpjsonServers || []
disabledMcpjsonServers: [...disabledServers, serverName]
}); // Add server if not already disabled
} if (!disabledServers.includes(serverName)) {
onDone(); updateSettingsForSource('localSettings', {
} disabledMcpjsonServers: [...disabledServers, serverName],
} })
}; }
$[0] = onDone; onDone()
$[1] = serverName; break
$[2] = t1; }
} else { }
t1 = $[2]; }
}
const onChange = t1; return (
const t2 = `New MCP server found in .mcp.json: ${serverName}`; <Dialog
let t3; title={`New MCP server found in .mcp.json: ${serverName}`}
if ($[3] !== onChange) { color="warning"
t3 = () => onChange("no"); onCancel={() => onChange('no')}
$[3] = onChange; >
$[4] = t3; <MCPServerDialogCopy />
} else {
t3 = $[4]; <Select
} options={[
let t4; {
if ($[5] === Symbol.for("react.memo_cache_sentinel")) { label: `Use this and all future MCP servers in this project`,
t4 = <MCPServerDialogCopy />; value: 'yes_all',
$[5] = t4; },
} else { { label: `Use this MCP server`, value: 'yes' },
t4 = $[5]; { label: `Continue without using this MCP server`, value: 'no' },
} ]}
let t5; onChange={value => onChange(value as 'yes_all' | 'yes' | 'no')}
if ($[6] === Symbol.for("react.memo_cache_sentinel")) { onCancel={() => onChange('no')}
t5 = [{ />
label: "Use this and all future MCP servers in this project", </Dialog>
value: "yes_all" )
}, {
label: "Use this MCP server",
value: "yes"
}, {
label: "Continue without using this MCP server",
value: "no"
}];
$[6] = t5;
} else {
t5 = $[6];
}
let t6;
if ($[7] !== onChange) {
t6 = <Select options={t5} onChange={value_0 => onChange(value_0 as 'yes_all' | 'yes' | 'no')} onCancel={() => onChange("no")} />;
$[7] = onChange;
$[8] = t6;
} else {
t6 = $[8];
}
let t7;
if ($[9] !== t2 || $[10] !== t3 || $[11] !== t6) {
t7 = <Dialog title={t2} color="warning" onCancel={t3}>{t4}{t6}</Dialog>;
$[9] = t2;
$[10] = t3;
$[11] = t6;
$[12] = t7;
} else {
t7 = $[12];
}
return t7;
} }

View File

@@ -1,202 +1,134 @@
import { c as _c } from "react/compiler-runtime"; import React, { useCallback, useEffect, useState } from 'react'
import React, { useCallback, useEffect, useState } from 'react'; import { gracefulShutdown } from 'src/utils/gracefulShutdown.js'
import { gracefulShutdown } from 'src/utils/gracefulShutdown.js'; import { writeToStdout } from 'src/utils/process.js'
import { writeToStdout } from 'src/utils/process.js'; import { Box, color, Text, useTheme } from '../ink.js'
import { Box, color, Text, useTheme } from '../ink.js'; import { addMcpConfig, getAllMcpConfigs } from '../services/mcp/config.js'
import { addMcpConfig, getAllMcpConfigs } from '../services/mcp/config.js'; import type {
import type { ConfigScope, McpServerConfig, ScopedMcpServerConfig } from '../services/mcp/types.js'; ConfigScope,
import { plural } from '../utils/stringUtils.js'; McpServerConfig,
import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js'; ScopedMcpServerConfig,
import { SelectMulti } from './CustomSelect/SelectMulti.js'; } from '../services/mcp/types.js'
import { Byline } from './design-system/Byline.js'; import { plural } from '../utils/stringUtils.js'
import { Dialog } from './design-system/Dialog.js'; import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js'
import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.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 = { type Props = {
servers: Record<string, McpServerConfig>; servers: Record<string, McpServerConfig>
scope: ConfigScope; scope: ConfigScope
onDone(): void; onDone(): void
}; }
export function MCPServerDesktopImportDialog(t0) {
const $ = _c(36); export function MCPServerDesktopImportDialog({
const { servers,
servers, scope,
scope, onDone,
onDone }: Props): React.ReactNode {
} = t0; const serverNames = Object.keys(servers)
let t1; const [existingServers, setExistingServers] = useState<
if ($[0] !== servers) { Record<string, ScopedMcpServerConfig>
t1 = Object.keys(servers); >({})
$[0] = servers;
$[1] = t1; useEffect(() => {
} else { void getAllMcpConfigs().then(({ servers }) => setExistingServers(servers))
t1 = $[1]; }, [])
}
const serverNames = t1; const collisions = serverNames.filter(
let t2; name => existingServers[name] !== undefined,
if ($[2] === Symbol.for("react.memo_cache_sentinel")) { )
t2 = {};
$[2] = t2; async function onSubmit(selectedServers: string[]) {
} else { let importedCount = 0
t2 = $[2];
}
const [existingServers, setExistingServers] = useState(t2);
let t3;
let t4;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t3 = () => {
getAllMcpConfigs().then(t5 => {
const {
servers: servers_0
} = t5;
return setExistingServers(servers_0);
});
};
t4 = [];
$[3] = t3;
$[4] = t4;
} else {
t3 = $[3];
t4 = $[4];
}
useEffect(t3, t4);
let t5;
if ($[5] !== existingServers || $[6] !== serverNames) {
t5 = serverNames.filter(name => existingServers[name] !== undefined);
$[5] = existingServers;
$[6] = serverNames;
$[7] = t5;
} else {
t5 = $[7];
}
const collisions = t5;
const onSubmit = async function onSubmit(selectedServers) {
let importedCount = 0;
for (const serverName of selectedServers) { for (const serverName of selectedServers) {
const serverConfig = servers[serverName]; const serverConfig = servers[serverName]
if (serverConfig) { if (serverConfig) {
let finalName = serverName; // If the server name already exists, find a new name with _1, _2, etc.
let finalName = serverName
if (existingServers[finalName] !== undefined) { if (existingServers[finalName] !== undefined) {
let counter = 1; let counter = 1
while (existingServers[`${serverName}_${counter}`] !== undefined) { while (existingServers[`${serverName}_${counter}`] !== undefined) {
counter++; counter++
} }
finalName = `${serverName}_${counter}`; finalName = `${serverName}_${counter}`
} }
await addMcpConfig(finalName, serverConfig, scope);
importedCount++; await addMcpConfig(finalName, serverConfig, scope)
importedCount++
} }
} }
done(importedCount);
}; done(importedCount)
const [theme] = useTheme(); }
let t6;
if ($[8] !== onDone || $[9] !== scope || $[10] !== theme) { const [theme] = useTheme()
t6 = importedCount_0 => {
if (importedCount_0 > 0) { // Define done before using in useCallback
writeToStdout(`\n${color("success", theme)(`Successfully imported ${importedCount_0} MCP ${plural(importedCount_0, "server")} to ${scope} config.`)}\n`); const done = useCallback(
(importedCount: number) => {
if (importedCount > 0) {
writeToStdout(
`\n${color('success', theme)(`Successfully imported ${importedCount} MCP ${plural(importedCount, 'server')} to ${scope} config.`)}\n`,
)
} else { } else {
writeToStdout("\nNo servers were imported."); writeToStdout('\nNo servers were imported.')
} }
onDone(); onDone()
gracefulShutdown();
}; void gracefulShutdown()
$[8] = onDone; },
$[9] = scope; [theme, scope, onDone],
$[10] = theme; )
$[11] = t6;
} else { // Handle ESC to cancel (import 0 servers)
t6 = $[11]; const handleEscCancel = useCallback(() => {
} done(0)
const done = t6; }, [done])
let t7;
if ($[12] !== done) { return (
t7 = () => { <>
done(0); <Dialog
}; title="Import MCP Servers from Claude Desktop"
$[12] = done; subtitle={`Found ${serverNames.length} MCP ${plural(serverNames.length, 'server')} in Claude Desktop.`}
$[13] = t7; color="success"
} else { onCancel={handleEscCancel}
t7 = $[13]; hideInputGuide
} >
done; {collisions.length > 0 && (
const handleEscCancel = t7; <Text color="warning">
const t8 = serverNames.length; Note: Some servers already exist with the same name. If selected,
let t9; they will be imported with a numbered suffix.
if ($[14] !== serverNames.length) { </Text>
t9 = plural(serverNames.length, "server"); )}
$[14] = serverNames.length; <Text>Please select the servers you want to import:</Text>
$[15] = t9;
} else { <SelectMulti
t9 = $[15]; options={serverNames.map(server => ({
} label: `${server}${collisions.includes(server) ? ' (already exists)' : ''}`,
const t10 = `Found ${t8} MCP ${t9} in Claude Desktop.`; value: server,
let t11; }))}
if ($[16] !== collisions.length) { defaultValue={serverNames.filter(name => !collisions.includes(name))} // Only preselect non-colliding servers
t11 = collisions.length > 0 && <Text color="warning">Note: Some servers already exist with the same name. If selected, they will be imported with a numbered suffix.</Text>; onSubmit={onSubmit}
$[16] = collisions.length; onCancel={handleEscCancel}
$[17] = t11; hideIndexes
} else { />
t11 = $[17]; </Dialog>
} <Box paddingX={1}>
let t12; <Text dimColor italic>
if ($[18] === Symbol.for("react.memo_cache_sentinel")) { <Byline>
t12 = <Text>Please select the servers you want to import:</Text>; <KeyboardShortcutHint shortcut="Space" action="select" />
$[18] = t12; <KeyboardShortcutHint shortcut="Enter" action="confirm" />
} else { <ConfigurableShortcutHint
t12 = $[18]; action="confirm:no"
} context="Confirmation"
let t13; fallback="Esc"
let t14; description="cancel"
if ($[19] !== collisions || $[20] !== serverNames) { />
t13 = serverNames.map(server => ({ </Byline>
label: `${server}${collisions.includes(server) ? " (already exists)" : ""}`, </Text>
value: server </Box>
})); </>
t14 = serverNames.filter(name_0 => !collisions.includes(name_0)); )
$[19] = collisions;
$[20] = serverNames;
$[21] = t13;
$[22] = t14;
} else {
t13 = $[21];
t14 = $[22];
}
let t15;
if ($[23] !== handleEscCancel || $[24] !== onSubmit || $[25] !== t13 || $[26] !== t14) {
t15 = <SelectMulti options={t13} defaultValue={t14} onSubmit={onSubmit} onCancel={handleEscCancel} hideIndexes={true} />;
$[23] = handleEscCancel;
$[24] = onSubmit;
$[25] = t13;
$[26] = t14;
$[27] = t15;
} else {
t15 = $[27];
}
let t16;
if ($[28] !== handleEscCancel || $[29] !== t10 || $[30] !== t11 || $[31] !== t15) {
t16 = <Dialog title="Import MCP Servers from Claude Desktop" subtitle={t10} color="success" onCancel={handleEscCancel} hideInputGuide={true}>{t11}{t12}{t15}</Dialog>;
$[28] = handleEscCancel;
$[29] = t10;
$[30] = t11;
$[31] = t15;
$[32] = t16;
} else {
t16 = $[32];
}
let t17;
if ($[33] === Symbol.for("react.memo_cache_sentinel")) {
t17 = <Box paddingX={1}><Text dimColor={true} italic={true}><Byline><KeyboardShortcutHint shortcut="Space" action="select" /><KeyboardShortcutHint shortcut="Enter" action="confirm" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="cancel" /></Byline></Text></Box>;
$[33] = t17;
} else {
t17 = $[33];
}
let t18;
if ($[34] !== t16) {
t18 = <>{t16}{t17}</>;
$[34] = t16;
$[35] = t18;
} else {
t18 = $[35];
}
return t18;
} }

View File

@@ -1,14 +1,12 @@
import { c as _c } from "react/compiler-runtime"; import React from 'react'
import React from 'react'; import { Link, Text } from '../ink.js'
import { Link, Text } from '../ink.js';
export function MCPServerDialogCopy() { export function MCPServerDialogCopy(): React.ReactNode {
const $ = _c(1); return (
let t0; <Text>
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { MCP servers may execute code or access system resources. All tool calls
t0 = <Text>MCP servers may execute code or access system resources. All tool calls require approval. Learn more in the{" "}<Link url="https://code.claude.com/docs/en/mcp">MCP documentation</Link>.</Text>; require approval. Learn more in the{' '}
$[0] = t0; <Link url="https://code.claude.com/docs/en/mcp">MCP documentation</Link>.
} else { </Text>
t0 = $[0]; )
}
return t0;
} }

View File

@@ -1,132 +1,117 @@
import { c as _c } from "react/compiler-runtime"; import partition from 'lodash-es/partition.js'
import partition from 'lodash-es/partition.js'; import React, { useCallback } from 'react'
import React, { useCallback } from 'react'; import { logEvent } from 'src/services/analytics/index.js'
import { logEvent } from 'src/services/analytics/index.js'; import { Box, Text } from '../ink.js'
import { Box, Text } from '../ink.js'; import {
import { getSettings_DEPRECATED, updateSettingsForSource } from '../utils/settings/settings.js'; getSettings_DEPRECATED,
import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js'; updateSettingsForSource,
import { SelectMulti } from './CustomSelect/SelectMulti.js'; } from '../utils/settings/settings.js'
import { Byline } from './design-system/Byline.js'; import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js'
import { Dialog } from './design-system/Dialog.js'; import { SelectMulti } from './CustomSelect/SelectMulti.js'
import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js'; import { Byline } from './design-system/Byline.js'
import { MCPServerDialogCopy } from './MCPServerDialogCopy.js'; import { Dialog } from './design-system/Dialog.js'
import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js'
import { MCPServerDialogCopy } from './MCPServerDialogCopy.js'
type Props = { type Props = {
serverNames: string[]; serverNames: string[]
onDone(): void; onDone(): void
};
export function MCPServerMultiselectDialog(t0) {
const $ = _c(21);
const {
serverNames,
onDone
} = t0;
let t1;
if ($[0] !== onDone || $[1] !== serverNames) {
t1 = function onSubmit(selectedServers) {
const currentSettings = getSettings_DEPRECATED() || {};
const enabledServers = currentSettings.enabledMcpjsonServers || [];
const disabledServers = currentSettings.disabledMcpjsonServers || [];
const [approvedServers, rejectedServers] = partition(serverNames, server => selectedServers.includes(server));
logEvent("tengu_mcp_multidialog_choice", {
approved: approvedServers.length,
rejected: rejectedServers.length
});
if (approvedServers.length > 0) {
const newEnabledServers = [...new Set([...enabledServers, ...approvedServers])];
updateSettingsForSource("localSettings", {
enabledMcpjsonServers: newEnabledServers
});
}
if (rejectedServers.length > 0) {
const newDisabledServers = [...new Set([...disabledServers, ...rejectedServers])];
updateSettingsForSource("localSettings", {
disabledMcpjsonServers: newDisabledServers
});
}
onDone();
};
$[0] = onDone;
$[1] = serverNames;
$[2] = t1;
} else {
t1 = $[2];
}
const onSubmit = t1;
let t2;
if ($[3] !== onDone || $[4] !== serverNames) {
t2 = () => {
const currentSettings_0 = getSettings_DEPRECATED() || {};
const disabledServers_0 = currentSettings_0.disabledMcpjsonServers || [];
const newDisabledServers_0 = [...new Set([...disabledServers_0, ...serverNames])];
updateSettingsForSource("localSettings", {
disabledMcpjsonServers: newDisabledServers_0
});
onDone();
};
$[3] = onDone;
$[4] = serverNames;
$[5] = t2;
} else {
t2 = $[5];
}
const handleEscRejectAll = t2;
const t3 = `${serverNames.length} new MCP servers found in .mcp.json`;
let t4;
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
t4 = <MCPServerDialogCopy />;
$[6] = t4;
} else {
t4 = $[6];
}
let t5;
if ($[7] !== serverNames) {
t5 = serverNames.map(_temp);
$[7] = serverNames;
$[8] = t5;
} else {
t5 = $[8];
}
let t6;
if ($[9] !== handleEscRejectAll || $[10] !== onSubmit || $[11] !== serverNames || $[12] !== t5) {
t6 = <SelectMulti options={t5} defaultValue={serverNames} onSubmit={onSubmit} onCancel={handleEscRejectAll} hideIndexes={true} />;
$[9] = handleEscRejectAll;
$[10] = onSubmit;
$[11] = serverNames;
$[12] = t5;
$[13] = t6;
} else {
t6 = $[13];
}
let t7;
if ($[14] !== handleEscRejectAll || $[15] !== t3 || $[16] !== t6) {
t7 = <Dialog title={t3} subtitle="Select any you wish to enable." color="warning" onCancel={handleEscRejectAll} hideInputGuide={true}>{t4}{t6}</Dialog>;
$[14] = handleEscRejectAll;
$[15] = t3;
$[16] = t6;
$[17] = t7;
} else {
t7 = $[17];
}
let t8;
if ($[18] === Symbol.for("react.memo_cache_sentinel")) {
t8 = <Box paddingX={1}><Text dimColor={true} italic={true}><Byline><KeyboardShortcutHint shortcut="Space" action="select" /><KeyboardShortcutHint shortcut="Enter" action="confirm" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="reject all" /></Byline></Text></Box>;
$[18] = t8;
} else {
t8 = $[18];
}
let t9;
if ($[19] !== t7) {
t9 = <>{t7}{t8}</>;
$[19] = t7;
$[20] = t9;
} else {
t9 = $[20];
}
return t9;
} }
function _temp(server_0) {
return { export function MCPServerMultiselectDialog({
label: server_0, serverNames,
value: server_0 onDone,
}; }: Props): React.ReactNode {
function onSubmit(selectedServers: string[]) {
const currentSettings = getSettings_DEPRECATED() || {}
const enabledServers = currentSettings.enabledMcpjsonServers || []
const disabledServers = currentSettings.disabledMcpjsonServers || []
// Use partition to separate approved and rejected servers
const [approvedServers, rejectedServers] = partition(serverNames, server =>
selectedServers.includes(server),
)
logEvent('tengu_mcp_multidialog_choice', {
approved: approvedServers.length,
rejected: rejectedServers.length,
})
// Update settings with approved servers
if (approvedServers.length > 0) {
const newEnabledServers = [
...new Set([...enabledServers, ...approvedServers]),
]
updateSettingsForSource('localSettings', {
enabledMcpjsonServers: newEnabledServers,
})
}
// Update settings with rejected servers
if (rejectedServers.length > 0) {
const newDisabledServers = [
...new Set([...disabledServers, ...rejectedServers]),
]
updateSettingsForSource('localSettings', {
disabledMcpjsonServers: newDisabledServers,
})
}
onDone()
}
// Handle ESC to reject all servers
const handleEscRejectAll = useCallback(() => {
const currentSettings = getSettings_DEPRECATED() || {}
const disabledServers = currentSettings.disabledMcpjsonServers || []
const newDisabledServers = [
...new Set([...disabledServers, ...serverNames]),
]
updateSettingsForSource('localSettings', {
disabledMcpjsonServers: newDisabledServers,
})
onDone()
}, [serverNames, onDone])
return (
<>
<Dialog
title={`${serverNames.length} new MCP servers found in .mcp.json`}
subtitle="Select any you wish to enable."
color="warning"
onCancel={handleEscRejectAll}
hideInputGuide
>
<MCPServerDialogCopy />
<SelectMulti
options={serverNames.map(server => ({
label: server,
value: server,
}))}
defaultValue={serverNames}
onSubmit={onSubmit}
onCancel={handleEscRejectAll}
hideIndexes
/>
</Dialog>
<Box paddingX={1}>
<Text dimColor italic>
<Byline>
<KeyboardShortcutHint shortcut="Space" action="select" />
<KeyboardShortcutHint shortcut="Enter" action="confirm" />
<ConfigurableShortcutHint
action="confirm:no"
context="Confirmation"
fallback="Esc"
description="reject all"
/>
</Byline>
</Text>
</Box>
</>
)
} }

View File

@@ -1,148 +1,88 @@
import { c as _c } from "react/compiler-runtime"; import React from 'react'
import React from 'react'; import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'; import { Box, Text } from '../../ink.js'
import { Box, Text } from '../../ink.js'; import { useKeybinding } from '../../keybindings/useKeybinding.js'
import { useKeybinding } from '../../keybindings/useKeybinding.js'; import type { SettingsJson } from '../../utils/settings/types.js'
import type { SettingsJson } from '../../utils/settings/types.js'; import { Select } from '../CustomSelect/index.js'
import { Select } from '../CustomSelect/index.js'; import { PermissionDialog } from '../permissions/PermissionDialog.js'
import { PermissionDialog } from '../permissions/PermissionDialog.js'; import {
import { extractDangerousSettings, formatDangerousSettingsList } from './utils.js'; extractDangerousSettings,
formatDangerousSettingsList,
} from './utils.js'
type Props = { type Props = {
settings: SettingsJson; settings: SettingsJson
onAccept: () => void; onAccept: () => void
onReject: () => void; onReject: () => void
};
export function ManagedSettingsSecurityDialog(t0) {
const $ = _c(26);
const {
settings,
onAccept,
onReject
} = t0;
const dangerous = extractDangerousSettings(settings);
const settingsList = formatDangerousSettingsList(dangerous);
const exitState = useExitOnCtrlCDWithKeybindings();
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = {
context: "Confirmation"
};
$[0] = t1;
} else {
t1 = $[0];
}
useKeybinding("confirm:no", onReject, t1);
let t2;
if ($[1] !== onAccept || $[2] !== onReject) {
t2 = function onChange(value) {
if (value === "exit") {
onReject();
return;
}
onAccept();
};
$[1] = onAccept;
$[2] = onReject;
$[3] = t2;
} else {
t2 = $[3];
}
const onChange = t2;
const T0 = PermissionDialog;
const t3 = "warning";
const t4 = "warning";
const t5 = "Managed settings require approval";
const T1 = Box;
const t6 = "column";
const t7 = 1;
const t8 = 1;
let t9;
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
t9 = <Text>Your organization has configured managed settings that could allow execution of arbitrary code or interception of your prompts and responses.</Text>;
$[4] = t9;
} else {
t9 = $[4];
}
const T2 = Box;
const t10 = "column";
let t11;
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
t11 = <Text dimColor={true}>Settings requiring approval:</Text>;
$[5] = t11;
} else {
t11 = $[5];
}
const t12 = settingsList.map(_temp);
let t13;
if ($[6] !== T2 || $[7] !== t11 || $[8] !== t12) {
t13 = <T2 flexDirection={t10}>{t11}{t12}</T2>;
$[6] = T2;
$[7] = t11;
$[8] = t12;
$[9] = t13;
} else {
t13 = $[9];
}
let t14;
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
t14 = <Text>Only accept if you trust your organization's IT administration and expect these settings to be configured.</Text>;
$[10] = t14;
} else {
t14 = $[10];
}
let t15;
if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
t15 = [{
label: "Yes, I trust these settings",
value: "accept"
}, {
label: "No, exit Claude Code",
value: "exit"
}];
$[11] = t15;
} else {
t15 = $[11];
}
let t16;
if ($[12] !== onChange) {
t16 = <Select options={t15} onChange={value_0 => onChange(value_0 as 'accept' | 'exit')} onCancel={() => onChange("exit")} />;
$[12] = onChange;
$[13] = t16;
} else {
t16 = $[13];
}
let t17;
if ($[14] !== exitState.keyName || $[15] !== exitState.pending) {
t17 = <Text dimColor={true}>{exitState.pending ? <>Press {exitState.keyName} again to exit</> : <>Enter to confirm · Esc to exit</>}</Text>;
$[14] = exitState.keyName;
$[15] = exitState.pending;
$[16] = t17;
} else {
t17 = $[16];
}
let t18;
if ($[17] !== T1 || $[18] !== t13 || $[19] !== t16 || $[20] !== t17 || $[21] !== t9) {
t18 = <T1 flexDirection={t6} gap={t7} paddingTop={t8}>{t9}{t13}{t14}{t16}{t17}</T1>;
$[17] = T1;
$[18] = t13;
$[19] = t16;
$[20] = t17;
$[21] = t9;
$[22] = t18;
} else {
t18 = $[22];
}
let t19;
if ($[23] !== T0 || $[24] !== t18) {
t19 = <T0 color={t3} titleColor={t4} title={t5}>{t18}</T0>;
$[23] = T0;
$[24] = t18;
$[25] = t19;
} else {
t19 = $[25];
}
return t19;
} }
function _temp(item, index) {
return <Box key={index} paddingLeft={2}><Text><Text dimColor={true}>· </Text><Text>{item}</Text></Text></Box>; export function ManagedSettingsSecurityDialog({
settings,
onAccept,
onReject,
}: Props): React.ReactNode {
const dangerous = extractDangerousSettings(settings)
const settingsList = formatDangerousSettingsList(dangerous)
const exitState = useExitOnCtrlCDWithKeybindings()
useKeybinding('confirm:no', onReject, { context: 'Confirmation' })
function onChange(value: 'accept' | 'exit'): void {
if (value === 'exit') {
onReject()
return
}
onAccept()
}
return (
<PermissionDialog
color="warning"
titleColor="warning"
title="Managed settings require approval"
>
<Box flexDirection="column" gap={1} paddingTop={1}>
<Text>
Your organization has configured managed settings that could allow
execution of arbitrary code or interception of your prompts and
responses.
</Text>
<Box flexDirection="column">
<Text dimColor>Settings requiring approval:</Text>
{settingsList.map((item, index) => (
<Box key={index} paddingLeft={2}>
<Text>
<Text dimColor>· </Text>
<Text>{item}</Text>
</Text>
</Box>
))}
</Box>
<Text>
Only accept if you trust your organization&apos;s IT administration
and expect these settings to be configured.
</Text>
<Select
options={[
{ label: 'Yes, I trust these settings', value: 'accept' },
{ label: 'No, exit Claude Code', value: 'exit' },
]}
onChange={value => onChange(value as 'accept' | 'exit')}
onCancel={() => onChange('exit')}
/>
<Text dimColor>
{exitState.pending ? (
<>Press {exitState.keyName} again to exit</>
) : (
<>Enter to confirm · Esc to exit</>
)}
</Text>
</Box>
</PermissionDialog>
)
} }

View File

@@ -1,26 +1,29 @@
import { c as _c } from "react/compiler-runtime"; import { marked, type Token, type Tokens } from 'marked'
import { marked, type Token, type Tokens } from 'marked'; import React, { Suspense, use, useMemo, useRef } from 'react'
import React, { Suspense, use, useMemo, useRef } from 'react'; import { useSettings } from '../hooks/useSettings.js'
import { useSettings } from '../hooks/useSettings.js'; import { Ansi, Box, useTheme } from '../ink.js'
import { Ansi, Box, useTheme } from '../ink.js'; import {
import { type CliHighlight, getCliHighlightPromise } from '../utils/cliHighlight.js'; type CliHighlight,
import { hashContent } from '../utils/hash.js'; getCliHighlightPromise,
import { configureMarked, formatToken } from '../utils/markdown.js'; } from '../utils/cliHighlight.js'
import { stripPromptXMLTags } from '../utils/messages.js'; import { hashContent } from '../utils/hash.js'
import { MarkdownTable } from './MarkdownTable.js'; import { configureMarked, formatToken } from '../utils/markdown.js'
import { stripPromptXMLTags } from '../utils/messages.js'
import { MarkdownTable } from './MarkdownTable.js'
type Props = { type Props = {
children: string; children: string
/** When true, render all text content as dim */ /** When true, render all text content as dim */
dimColor?: boolean; dimColor?: boolean
}; }
// Module-level token cache — marked.lexer is the hot cost on virtual-scroll // Module-level token cache — marked.lexer is the hot cost on virtual-scroll
// remounts (~3ms per message). useMemo doesn't survive unmount→remount, so // remounts (~3ms per message). useMemo doesn't survive unmount→remount, so
// scrolling back to a previously-visible message re-parses. Messages are // scrolling back to a previously-visible message re-parses. Messages are
// immutable in history; same content → same tokens. Keyed by hash to avoid // immutable in history; same content → same tokens. Keyed by hash to avoid
// retaining full content strings (turn50→turn99 RSS regression, #24180). // retaining full content strings (turn50→turn99 RSS regression, #24180).
const TOKEN_CACHE_MAX = 500; const TOKEN_CACHE_MAX = 500
const tokenCache = new Map<string, Token[]>(); const tokenCache = new Map<string, Token[]>()
// Characters that indicate markdown syntax. If none are present, skip the // Characters that indicate markdown syntax. If none are present, skip the
// ~3ms marked.lexer call entirely — render as a single paragraph. Covers // ~3ms marked.lexer call entirely — render as a single paragraph. Covers
@@ -28,46 +31,45 @@ const tokenCache = new Map<string, Token[]>();
// plain sentences. Checked via indexOf (not regex) for speed. // plain sentences. Checked via indexOf (not regex) for speed.
// Single regex: matches any MD marker or ordered-list start (N. at line start). // Single regex: matches any MD marker or ordered-list start (N. at line start).
// One pass instead of 10× includes scans. // One pass instead of 10× includes scans.
const MD_SYNTAX_RE = /[#*`|[>\-_~]|\n\n|^\d+\. |\n\d+\. /; const MD_SYNTAX_RE = /[#*`|[>\-_~]|\n\n|^\d+\. |\n\d+\. /
function hasMarkdownSyntax(s: string): boolean { function hasMarkdownSyntax(s: string): boolean {
// Sample first 500 chars — if markdown exists it's usually early (headers, // Sample first 500 chars — if markdown exists it's usually early (headers,
// code fence, list). Long tool outputs are mostly plain text tails. // code fence, list). Long tool outputs are mostly plain text tails.
return MD_SYNTAX_RE.test(s.length > 500 ? s.slice(0, 500) : s); return MD_SYNTAX_RE.test(s.length > 500 ? s.slice(0, 500) : s)
} }
function cachedLexer(content: string): Token[] { function cachedLexer(content: string): Token[] {
// Fast path: plain text with no markdown syntax → single paragraph token. // Fast path: plain text with no markdown syntax → single paragraph token.
// Skips marked.lexer's full GFM parse (~3ms on long content). Not cached — // Skips marked.lexer's full GFM parse (~3ms on long content). Not cached —
// reconstruction is a single object allocation, and caching would retain // reconstruction is a single object allocation, and caching would retain
// 4× content in raw/text fields plus the hash key for zero benefit. // 4× content in raw/text fields plus the hash key for zero benefit.
if (!hasMarkdownSyntax(content)) { if (!hasMarkdownSyntax(content)) {
return [{ return [
type: 'paragraph', {
raw: content, type: 'paragraph',
text: content,
tokens: [{
type: 'text',
raw: content, raw: content,
text: content text: content,
}] tokens: [{ type: 'text', raw: content, text: content }],
} as Token]; } as Token,
]
} }
const key = hashContent(content); const key = hashContent(content)
const hit = tokenCache.get(key); const hit = tokenCache.get(key)
if (hit) { if (hit) {
// Promote to MRU — without this the eviction is FIFO (scrolling back to // Promote to MRU — without this the eviction is FIFO (scrolling back to
// an early message evicts the very item you're looking at). // an early message evicts the very item you're looking at).
tokenCache.delete(key); tokenCache.delete(key)
tokenCache.set(key, hit); tokenCache.set(key, hit)
return hit; return hit
} }
const tokens = marked.lexer(content); const tokens = marked.lexer(content)
if (tokenCache.size >= TOKEN_CACHE_MAX) { if (tokenCache.size >= TOKEN_CACHE_MAX) {
// LRU-ish: drop oldest. Map preserves insertion order. // LRU-ish: drop oldest. Map preserves insertion order.
const first = tokenCache.keys().next().value; const first = tokenCache.keys().next().value
if (first !== undefined) tokenCache.delete(first); if (first !== undefined) tokenCache.delete(first)
} }
tokenCache.set(key, tokens); tokenCache.set(key, tokens)
return tokens; return tokens
} }
/** /**
@@ -75,103 +77,78 @@ function cachedLexer(content: string): Token[] {
* - Tables are rendered as React components with proper flexbox layout * - Tables are rendered as React components with proper flexbox layout
* - Other content is rendered as ANSI strings via formatToken * - Other content is rendered as ANSI strings via formatToken
*/ */
export function Markdown(props) { export function Markdown(props: Props): React.ReactNode {
const $ = _c(4); const settings = useSettings()
const settings = useSettings();
if (settings.syntaxHighlightingDisabled) { if (settings.syntaxHighlightingDisabled) {
let t0; return <MarkdownBody {...props} highlight={null} />
if ($[0] !== props) {
t0 = <MarkdownBody {...props} highlight={null} />;
$[0] = props;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
} }
let t0; // Suspense fallback renders with highlight=null — plain markdown shows
if ($[2] !== props) { // for ~50ms on first ever render while cli-highlight loads.
t0 = <Suspense fallback={<MarkdownBody {...props} highlight={null} />}><MarkdownWithHighlight {...props} /></Suspense>; return (
$[2] = props; <Suspense fallback={<MarkdownBody {...props} highlight={null} />}>
$[3] = t0; <MarkdownWithHighlight {...props} />
} else { </Suspense>
t0 = $[3]; )
}
return t0;
} }
function MarkdownWithHighlight(props) {
const $ = _c(4); function MarkdownWithHighlight(props: Props): React.ReactNode {
let t0; const highlight = use(getCliHighlightPromise())
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { return <MarkdownBody {...props} highlight={highlight} />
t0 = getCliHighlightPromise();
$[0] = t0;
} else {
t0 = $[0];
}
const highlight = use(t0);
let t1;
if ($[1] !== highlight || $[2] !== props) {
t1 = <MarkdownBody {...props} highlight={highlight} />;
$[1] = highlight;
$[2] = props;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
} }
function MarkdownBody(t0) {
const $ = _c(7); function MarkdownBody({
const { children,
children, dimColor,
dimColor, highlight,
highlight }: Props & { highlight: CliHighlight | null }): React.ReactNode {
} = t0; const [theme] = useTheme()
const [theme] = useTheme(); configureMarked()
configureMarked();
let elements: React.ReactNode[]; const elements = useMemo(() => {
if ($[0] !== children || $[1] !== dimColor || $[2] !== highlight || $[3] !== theme) { const tokens = cachedLexer(stripPromptXMLTags(children))
const tokens = cachedLexer(stripPromptXMLTags(children)); const elements: React.ReactNode[] = []
elements = []; let nonTableContent = ''
let nonTableContent = "";
const flushNonTableContent = function flushNonTableContent() { function flushNonTableContent(): void {
if (nonTableContent) { if (nonTableContent) {
elements.push(<Ansi key={elements.length} dimColor={dimColor}>{nonTableContent.trim()}</Ansi>); elements.push(
nonTableContent = ""; <Ansi key={elements.length} dimColor={dimColor}>
} {nonTableContent.trim()}
}; </Ansi>,
for (const token of tokens) { )
if (token.type === "table") { nonTableContent = ''
flushNonTableContent();
elements.push(<MarkdownTable key={elements.length} token={token as Tokens.Table} highlight={highlight} />);
} else {
nonTableContent = nonTableContent + formatToken(token, theme, 0, null, null, highlight);
nonTableContent;
} }
} }
flushNonTableContent();
$[0] = children; for (const token of tokens) {
$[1] = dimColor; if (token.type === 'table') {
$[2] = highlight; flushNonTableContent()
$[3] = theme; elements.push(
$[4] = elements; <MarkdownTable
} else { key={elements.length}
elements = $[4] as React.ReactNode[]; token={token as Tokens.Table}
} highlight={highlight}
const elements_0 = elements; />,
let t1; )
if ($[5] !== elements_0) { } else {
t1 = <Box flexDirection="column" gap={1}>{elements_0}</Box>; nonTableContent += formatToken(token, theme, 0, null, null, highlight)
$[5] = elements_0; }
$[6] = t1; }
} else {
t1 = $[6]; flushNonTableContent()
} return elements
return t1; }, [children, dimColor, highlight, theme])
return (
<Box flexDirection="column" gap={1}>
{elements}
</Box>
)
} }
type StreamingProps = { type StreamingProps = {
children: string; children: string
}; }
/** /**
* Renders markdown during streaming by splitting at the last top-level block * Renders markdown during streaming by splitting at the last top-level block
@@ -184,52 +161,55 @@ type StreamingProps = {
* between turns (streamingText → null), resetting the ref. * between turns (streamingText → null), resetting the ref.
*/ */
export function StreamingMarkdown({ export function StreamingMarkdown({
children children,
}: StreamingProps): React.ReactNode { }: StreamingProps): React.ReactNode {
// React Compiler: this component reads and writes stablePrefixRef.current // React Compiler: this component reads and writes stablePrefixRef.current
// during render by design. The boundary only advances (monotonic), so // during render by design. The boundary only advances (monotonic), so
// the ref mutation is idempotent under StrictMode double-render — but the // the ref mutation is idempotent under StrictMode double-render — but the
// compiler can't prove that, and memoizing around the ref reads would // compiler can't prove that, and memoizing around the ref reads would
// break the algorithm (stale boundary). Opt out. // break the algorithm (stale boundary). Opt out.
'use no memo'; 'use no memo'
configureMarked()
configureMarked();
// Strip before boundary tracking so it matches <Markdown>'s stripping // Strip before boundary tracking so it matches <Markdown>'s stripping
// (line 29). When a closing tag arrives, stripped(N+1) is not a prefix // (line 29). When a closing tag arrives, stripped(N+1) is not a prefix
// of stripped(N), but the startsWith reset below handles that with a // of stripped(N), but the startsWith reset below handles that with a
// one-time re-lex on the smaller stripped string. // one-time re-lex on the smaller stripped string.
const stripped = stripPromptXMLTags(children); const stripped = stripPromptXMLTags(children)
const stablePrefixRef = useRef('');
const stablePrefixRef = useRef('')
// Reset if text was replaced (defensive; normally unmount handles this) // Reset if text was replaced (defensive; normally unmount handles this)
if (!stripped.startsWith(stablePrefixRef.current)) { if (!stripped.startsWith(stablePrefixRef.current)) {
stablePrefixRef.current = ''; stablePrefixRef.current = ''
} }
// Lex only from current boundary — O(unstable length), not O(full text) // Lex only from current boundary — O(unstable length), not O(full text)
const boundary = stablePrefixRef.current.length; const boundary = stablePrefixRef.current.length
const tokens = marked.lexer(stripped.substring(boundary)); const tokens = marked.lexer(stripped.substring(boundary))
// Last non-space token is the growing block; everything before is final // Last non-space token is the growing block; everything before is final
let lastContentIdx = tokens.length - 1; let lastContentIdx = tokens.length - 1
while (lastContentIdx >= 0 && tokens[lastContentIdx]!.type === 'space') { while (lastContentIdx >= 0 && tokens[lastContentIdx]!.type === 'space') {
lastContentIdx--; lastContentIdx--
} }
let advance = 0; let advance = 0
for (let i = 0; i < lastContentIdx; i++) { for (let i = 0; i < lastContentIdx; i++) {
advance += tokens[i]!.raw.length; advance += tokens[i]!.raw.length
} }
if (advance > 0) { if (advance > 0) {
stablePrefixRef.current = stripped.substring(0, boundary + advance); stablePrefixRef.current = stripped.substring(0, boundary + advance)
} }
const stablePrefix = stablePrefixRef.current;
const unstableSuffix = stripped.substring(stablePrefix.length); const stablePrefix = stablePrefixRef.current
const unstableSuffix = stripped.substring(stablePrefix.length)
// stablePrefix is memoized inside <Markdown> via useMemo([children, ...]) // stablePrefix is memoized inside <Markdown> via useMemo([children, ...])
// so it never re-parses as the unstable suffix grows // so it never re-parses as the unstable suffix grows
return <Box flexDirection="column" gap={1}> return (
<Box flexDirection="column" gap={1}>
{stablePrefix && <Markdown>{stablePrefix}</Markdown>} {stablePrefix && <Markdown>{stablePrefix}</Markdown>}
{unstableSuffix && <Markdown>{unstableSuffix}</Markdown>} {unstableSuffix && <Markdown>{unstableSuffix}</Markdown>}
</Box>; </Box>
)
} }

View File

@@ -1,38 +1,39 @@
import type { Token, Tokens } from 'marked'; import type { Token, Tokens } from 'marked'
import React from 'react'; import React from 'react'
import stripAnsi from 'strip-ansi'; import stripAnsi from 'strip-ansi'
import { useTerminalSize } from '../hooks/useTerminalSize.js'; import { useTerminalSize } from '../hooks/useTerminalSize.js'
import { stringWidth } from '../ink/stringWidth.js'; import { stringWidth } from '../ink/stringWidth.js'
import { wrapAnsi } from '../ink/wrapAnsi.js'; import { wrapAnsi } from '../ink/wrapAnsi.js'
import { Ansi, useTheme } from '../ink.js'; import { Ansi, useTheme } from '../ink.js'
import type { CliHighlight } from '../utils/cliHighlight.js'; import type { CliHighlight } from '../utils/cliHighlight.js'
import { formatToken, padAligned } from '../utils/markdown.js'; import { formatToken, padAligned } from '../utils/markdown.js'
/** Accounts for parent indentation (e.g. message dot prefix) and terminal /** Accounts for parent indentation (e.g. message dot prefix) and terminal
* resize races. Without enough margin the table overflows its layout box * resize races. Without enough margin the table overflows its layout box
* and Ink's clip truncates differently on alternating frames, causing an * and Ink's clip truncates differently on alternating frames, causing an
* infinite flicker loop in scrollback. */ * infinite flicker loop in scrollback. */
const SAFETY_MARGIN = 4; const SAFETY_MARGIN = 4
/** Minimum column width to prevent degenerate layouts */ /** Minimum column width to prevent degenerate layouts */
const MIN_COLUMN_WIDTH = 3; const MIN_COLUMN_WIDTH = 3
/** /**
* Maximum number of lines per row before switching to vertical format. * Maximum number of lines per row before switching to vertical format.
* When wrapping would make rows taller than this, vertical (key-value) * When wrapping would make rows taller than this, vertical (key-value)
* format provides better readability. * format provides better readability.
*/ */
const MAX_ROW_LINES = 4; const MAX_ROW_LINES = 4
/** ANSI escape codes for text formatting */ /** ANSI escape codes for text formatting */
const ANSI_BOLD_START = '\x1b[1m'; const ANSI_BOLD_START = '\x1b[1m'
const ANSI_BOLD_END = '\x1b[22m'; const ANSI_BOLD_END = '\x1b[22m'
type Props = { type Props = {
token: Tokens.Table; token: Tokens.Table
highlight: CliHighlight | null; highlight: CliHighlight | null
/** Override terminal width (useful for testing) */ /** Override terminal width (useful for testing) */
forceWidth?: number; forceWidth?: number
}; }
/** /**
* Wrap text to fit within a given width, returning array of lines. * Wrap text to fit within a given width, returning array of lines.
@@ -41,24 +42,26 @@ type Props = {
* @param hard - If true, break words that exceed width (needed when columns * @param hard - If true, break words that exceed width (needed when columns
* are narrower than the longest word). Default false. * are narrower than the longest word). Default false.
*/ */
function wrapText(text: string, width: number, options?: { function wrapText(
hard?: boolean; text: string,
}): string[] { width: number,
if (width <= 0) return [text]; options?: { hard?: boolean },
): string[] {
if (width <= 0) return [text]
// Strip trailing whitespace/newlines before wrapping. // Strip trailing whitespace/newlines before wrapping.
// formatToken() adds EOL to paragraphs and other token types, // formatToken() adds EOL to paragraphs and other token types,
// which would otherwise create extra blank lines in table cells. // which would otherwise create extra blank lines in table cells.
const trimmedText = text.trimEnd(); const trimmedText = text.trimEnd()
const wrapped = wrapAnsi(trimmedText, width, { const wrapped = wrapAnsi(trimmedText, width, {
hard: options?.hard ?? false, hard: options?.hard ?? false,
trim: false, trim: false,
wordWrap: true wordWrap: true,
}); })
// Filter out empty lines that result from trailing newlines or // Filter out empty lines that result from trailing newlines or
// multiple consecutive newlines in the source content. // multiple consecutive newlines in the source content.
const lines = wrapped.split('\n').filter(line => line.length > 0); const lines = wrapped.split('\n').filter(line => line.length > 0)
// Ensure we always return at least one line (empty string for empty cells) // Ensure we always return at least one line (empty string for empty cells)
return lines.length > 0 ? lines : ['']; return lines.length > 0 ? lines : ['']
} }
/** /**
@@ -72,154 +75,171 @@ function wrapText(text: string, width: number, options?: {
export function MarkdownTable({ export function MarkdownTable({
token, token,
highlight, highlight,
forceWidth forceWidth,
}: Props): React.ReactNode { }: Props): React.ReactNode {
const [theme] = useTheme(); const [theme] = useTheme()
const { const { columns: actualTerminalWidth } = useTerminalSize()
columns: actualTerminalWidth const terminalWidth = forceWidth ?? actualTerminalWidth
} = useTerminalSize();
const terminalWidth = forceWidth ?? actualTerminalWidth;
// Format cell content to ANSI string // Format cell content to ANSI string
function formatCell(tokens: Token[] | undefined): string { function formatCell(tokens: Token[] | undefined): string {
return tokens?.map(_ => formatToken(_, theme, 0, null, null, highlight)).join('') ?? ''; return (
tokens
?.map(_ => formatToken(_, theme, 0, null, null, highlight))
.join('') ?? ''
)
} }
// Get plain text (stripped of ANSI codes) // Get plain text (stripped of ANSI codes)
function getPlainText(tokens_0: Token[] | undefined): string { function getPlainText(tokens: Token[] | undefined): string {
return stripAnsi(formatCell(tokens_0)); return stripAnsi(formatCell(tokens))
} }
// Get the longest word width in a cell (minimum width to avoid breaking words) // Get the longest word width in a cell (minimum width to avoid breaking words)
function getMinWidth(tokens_1: Token[] | undefined): number { function getMinWidth(tokens: Token[] | undefined): number {
const text = getPlainText(tokens_1); const text = getPlainText(tokens)
const words = text.split(/\s+/).filter(w => w.length > 0); const words = text.split(/\s+/).filter(w => w.length > 0)
if (words.length === 0) return MIN_COLUMN_WIDTH; if (words.length === 0) return MIN_COLUMN_WIDTH
return Math.max(...words.map(w_0 => stringWidth(w_0)), MIN_COLUMN_WIDTH); return Math.max(...words.map(w => stringWidth(w)), MIN_COLUMN_WIDTH)
} }
// Get ideal width (full content without wrapping) // Get ideal width (full content without wrapping)
function getIdealWidth(tokens_2: Token[] | undefined): number { function getIdealWidth(tokens: Token[] | undefined): number {
return Math.max(stringWidth(getPlainText(tokens_2)), MIN_COLUMN_WIDTH); return Math.max(stringWidth(getPlainText(tokens)), MIN_COLUMN_WIDTH)
} }
// Calculate column widths // Calculate column widths
// Step 1: Get minimum (longest word) and ideal (full content) widths // Step 1: Get minimum (longest word) and ideal (full content) widths
const minWidths = token.header.map((header, colIndex) => { const minWidths = token.header.map((header, colIndex) => {
let maxMinWidth = getMinWidth(header.tokens); let maxMinWidth = getMinWidth(header.tokens)
for (const row of token.rows) { for (const row of token.rows) {
maxMinWidth = Math.max(maxMinWidth, getMinWidth(row[colIndex]?.tokens)); maxMinWidth = Math.max(maxMinWidth, getMinWidth(row[colIndex]?.tokens))
} }
return maxMinWidth; return maxMinWidth
}); })
const idealWidths = token.header.map((header_0, colIndex_0) => {
let maxIdeal = getIdealWidth(header_0.tokens); const idealWidths = token.header.map((header, colIndex) => {
for (const row_0 of token.rows) { let maxIdeal = getIdealWidth(header.tokens)
maxIdeal = Math.max(maxIdeal, getIdealWidth(row_0[colIndex_0]?.tokens)); for (const row of token.rows) {
maxIdeal = Math.max(maxIdeal, getIdealWidth(row[colIndex]?.tokens))
} }
return maxIdeal; return maxIdeal
}); })
// Step 2: Calculate available space // Step 2: Calculate available space
// Border overhead: │ content │ content │ = 1 + (width + 3) per column // Border overhead: │ content │ content │ = 1 + (width + 3) per column
const numCols = token.header.length; const numCols = token.header.length
const borderOverhead = 1 + numCols * 3; // │ + (2 padding + 1 border) per col const borderOverhead = 1 + numCols * 3 // │ + (2 padding + 1 border) per col
// Account for SAFETY_MARGIN to avoid triggering the fallback safety check // Account for SAFETY_MARGIN to avoid triggering the fallback safety check
const availableWidth = Math.max(terminalWidth - borderOverhead - SAFETY_MARGIN, numCols * MIN_COLUMN_WIDTH); const availableWidth = Math.max(
terminalWidth - borderOverhead - SAFETY_MARGIN,
numCols * MIN_COLUMN_WIDTH,
)
// Step 3: Calculate column widths that fit available space // Step 3: Calculate column widths that fit available space
const totalMin = minWidths.reduce((sum, w_1) => sum + w_1, 0); const totalMin = minWidths.reduce((sum, w) => sum + w, 0)
const totalIdeal = idealWidths.reduce((sum_0, w_2) => sum_0 + w_2, 0); const totalIdeal = idealWidths.reduce((sum, w) => sum + w, 0)
// Track whether columns are narrower than longest words (needs hard wrap) // Track whether columns are narrower than longest words (needs hard wrap)
let needsHardWrap = false; let needsHardWrap = false
let columnWidths: number[];
let columnWidths: number[]
if (totalIdeal <= availableWidth) { if (totalIdeal <= availableWidth) {
// Everything fits - use ideal widths // Everything fits - use ideal widths
columnWidths = idealWidths; columnWidths = idealWidths
} else if (totalMin <= availableWidth) { } else if (totalMin <= availableWidth) {
// Need to shrink - give each column its min, distribute remaining space // Need to shrink - give each column its min, distribute remaining space
const extraSpace = availableWidth - totalMin; const extraSpace = availableWidth - totalMin
const overflows = idealWidths.map((ideal, i) => ideal - minWidths[i]!); const overflows = idealWidths.map((ideal, i) => ideal - minWidths[i]!)
const totalOverflow = overflows.reduce((sum_1, o) => sum_1 + o, 0); const totalOverflow = overflows.reduce((sum, o) => sum + o, 0)
columnWidths = minWidths.map((min, i_0) => {
if (totalOverflow === 0) return min; columnWidths = minWidths.map((min, i) => {
const extra = Math.floor(overflows[i_0]! / totalOverflow * extraSpace); if (totalOverflow === 0) return min
return min + extra; const extra = Math.floor((overflows[i]! / totalOverflow) * extraSpace)
}); return min + extra
})
} else { } else {
// Table wider than terminal at minimum widths // Table wider than terminal at minimum widths
// Shrink columns proportionally to fit, allowing word breaks // Shrink columns proportionally to fit, allowing word breaks
needsHardWrap = true; needsHardWrap = true
const scaleFactor = availableWidth / totalMin; const scaleFactor = availableWidth / totalMin
columnWidths = minWidths.map(w_3 => Math.max(Math.floor(w_3 * scaleFactor), MIN_COLUMN_WIDTH)); columnWidths = minWidths.map(w =>
Math.max(Math.floor(w * scaleFactor), MIN_COLUMN_WIDTH),
)
} }
// Step 4: Calculate max row lines to determine if vertical format is needed // Step 4: Calculate max row lines to determine if vertical format is needed
function calculateMaxRowLines(): number { function calculateMaxRowLines(): number {
let maxLines = 1; let maxLines = 1
// Check header // Check header
for (let i_1 = 0; i_1 < token.header.length; i_1++) { for (let i = 0; i < token.header.length; i++) {
const content = formatCell(token.header[i_1]!.tokens); const content = formatCell(token.header[i]!.tokens)
const wrapped = wrapText(content, columnWidths[i_1]!, { const wrapped = wrapText(content, columnWidths[i]!, {
hard: needsHardWrap hard: needsHardWrap,
}); })
maxLines = Math.max(maxLines, wrapped.length); maxLines = Math.max(maxLines, wrapped.length)
} }
// Check rows // Check rows
for (const row_1 of token.rows) { for (const row of token.rows) {
for (let i_2 = 0; i_2 < row_1.length; i_2++) { for (let i = 0; i < row.length; i++) {
const content_0 = formatCell(row_1[i_2]?.tokens); const content = formatCell(row[i]?.tokens)
const wrapped_0 = wrapText(content_0, columnWidths[i_2]!, { const wrapped = wrapText(content, columnWidths[i]!, {
hard: needsHardWrap hard: needsHardWrap,
}); })
maxLines = Math.max(maxLines, wrapped_0.length); maxLines = Math.max(maxLines, wrapped.length)
} }
} }
return maxLines; return maxLines
} }
// Use vertical format if wrapping would make rows too tall // Use vertical format if wrapping would make rows too tall
const maxRowLines = calculateMaxRowLines(); const maxRowLines = calculateMaxRowLines()
const useVerticalFormat = maxRowLines > MAX_ROW_LINES; const useVerticalFormat = maxRowLines > MAX_ROW_LINES
// Render a single row with potential multi-line cells // Render a single row with potential multi-line cells
// Returns an array of strings, one per line of the row // Returns an array of strings, one per line of the row
function renderRowLines(cells: Array<{ function renderRowLines(
tokens?: Token[]; cells: Array<{ tokens?: Token[] }>,
}>, isHeader: boolean): string[] { isHeader: boolean,
): string[] {
// Get wrapped lines for each cell (preserving ANSI formatting) // Get wrapped lines for each cell (preserving ANSI formatting)
const cellLines = cells.map((cell, colIndex_1) => { const cellLines = cells.map((cell, colIndex) => {
const formattedText = formatCell(cell.tokens); const formattedText = formatCell(cell.tokens)
const width = columnWidths[colIndex_1]!; const width = columnWidths[colIndex]!
return wrapText(formattedText, width, { return wrapText(formattedText, width, { hard: needsHardWrap })
hard: needsHardWrap })
});
});
// Find max number of lines in this row // Find max number of lines in this row
const maxLines_0 = Math.max(...cellLines.map(lines => lines.length), 1); const maxLines = Math.max(...cellLines.map(lines => lines.length), 1)
// Calculate vertical offset for each cell (to center vertically) // Calculate vertical offset for each cell (to center vertically)
const verticalOffsets = cellLines.map(lines_0 => Math.floor((maxLines_0 - lines_0.length) / 2)); const verticalOffsets = cellLines.map(lines =>
Math.floor((maxLines - lines.length) / 2),
)
// Build each line of the row as a single string // Build each line of the row as a single string
const result: string[] = []; const result: string[] = []
for (let lineIdx = 0; lineIdx < maxLines_0; lineIdx++) { for (let lineIdx = 0; lineIdx < maxLines; lineIdx++) {
let line = '│'; let line = '│'
for (let colIndex_2 = 0; colIndex_2 < cells.length; colIndex_2++) { for (let colIndex = 0; colIndex < cells.length; colIndex++) {
const lines_1 = cellLines[colIndex_2]!; const lines = cellLines[colIndex]!
const offset = verticalOffsets[colIndex_2]!; const offset = verticalOffsets[colIndex]!
const contentLineIdx = lineIdx - offset; const contentLineIdx = lineIdx - offset
const lineText = contentLineIdx >= 0 && contentLineIdx < lines_1.length ? lines_1[contentLineIdx]! : ''; const lineText =
const width_0 = columnWidths[colIndex_2]!; contentLineIdx >= 0 && contentLineIdx < lines.length
? lines[contentLineIdx]!
: ''
const width = columnWidths[colIndex]!
// Headers always centered; data uses table alignment // Headers always centered; data uses table alignment
const align = isHeader ? 'center' : token.align?.[colIndex_2] ?? 'left'; const align = isHeader ? 'center' : (token.align?.[colIndex] ?? 'left')
line += ' ' + padAligned(lineText, stringWidth(lineText), width_0, align) + ' │';
line +=
' ' + padAligned(lineText, stringWidth(lineText), width, align) + ' │'
} }
result.push(line); result.push(line)
} }
return result;
return result
} }
// Render horizontal border as a single string // Render horizontal border as a single string
@@ -227,95 +247,110 @@ export function MarkdownTable({
const [left, mid, cross, right] = { const [left, mid, cross, right] = {
top: ['┌', '─', '┬', '┐'], top: ['┌', '─', '┬', '┐'],
middle: ['├', '─', '┼', '┤'], middle: ['├', '─', '┼', '┤'],
bottom: ['└', '─', '┴', '┘'] bottom: ['└', '─', '┴', '┘'],
}[type] as [string, string, string, string]; }[type] as [string, string, string, string]
let line_0 = left;
columnWidths.forEach((width_1, colIndex_3) => { let line = left
line_0 += mid.repeat(width_1 + 2); columnWidths.forEach((width, colIndex) => {
line_0 += colIndex_3 < columnWidths.length - 1 ? cross : right; line += mid.repeat(width + 2)
}); line += colIndex < columnWidths.length - 1 ? cross : right
return line_0; })
return line
} }
// Render vertical format (key-value pairs) for extra-narrow terminals // Render vertical format (key-value pairs) for extra-narrow terminals
function renderVerticalFormat(): string { function renderVerticalFormat(): string {
const lines_2: string[] = []; const lines: string[] = []
const headers = token.header.map(h => getPlainText(h.tokens)); const headers = token.header.map(h => getPlainText(h.tokens))
const separatorWidth = Math.min(terminalWidth - 1, 40); const separatorWidth = Math.min(terminalWidth - 1, 40)
const separator = '─'.repeat(separatorWidth); const separator = '─'.repeat(separatorWidth)
// Small indent for wrapped lines (just 2 spaces) // Small indent for wrapped lines (just 2 spaces)
const wrapIndent = ' '; const wrapIndent = ' '
token.rows.forEach((row_2, rowIndex) => {
token.rows.forEach((row, rowIndex) => {
if (rowIndex > 0) { if (rowIndex > 0) {
lines_2.push(separator); lines.push(separator)
} }
row_2.forEach((cell_0, colIndex_4) => {
const label = headers[colIndex_4] || `Column ${colIndex_4 + 1}`; row.forEach((cell, colIndex) => {
const label = headers[colIndex] || `Column ${colIndex + 1}`
// Clean value: trim, remove extra internal whitespace/newlines // Clean value: trim, remove extra internal whitespace/newlines
const rawValue = formatCell(cell_0.tokens).trimEnd(); const rawValue = formatCell(cell.tokens).trimEnd()
const value = rawValue.replace(/\n+/g, ' ').replace(/\s+/g, ' ').trim(); const value = rawValue.replace(/\n+/g, ' ').replace(/\s+/g, ' ').trim()
// Wrap value to fit terminal, accounting for label on first line // Wrap value to fit terminal, accounting for label on first line
const firstLineWidth = terminalWidth - stringWidth(label) - 3; const firstLineWidth = terminalWidth - stringWidth(label) - 3
const subsequentLineWidth = terminalWidth - wrapIndent.length - 1; const subsequentLineWidth = terminalWidth - wrapIndent.length - 1
// Two-pass wrap: first line is narrower (label takes space), // Two-pass wrap: first line is narrower (label takes space),
// continuation lines get the full width minus indent. // continuation lines get the full width minus indent.
const firstPassLines = wrapText(value, Math.max(firstLineWidth, 10)); const firstPassLines = wrapText(value, Math.max(firstLineWidth, 10))
const firstLine = firstPassLines[0] || ''; const firstLine = firstPassLines[0] || ''
let wrappedValue: string[];
if (firstPassLines.length <= 1 || subsequentLineWidth <= firstLineWidth) { let wrappedValue: string[]
wrappedValue = firstPassLines; if (
firstPassLines.length <= 1 ||
subsequentLineWidth <= firstLineWidth
) {
wrappedValue = firstPassLines
} else { } else {
// Re-join remaining text and re-wrap to the wider continuation width // Re-join remaining text and re-wrap to the wider continuation width
const remainingText = firstPassLines.slice(1).map(l => l.trim()).join(' '); const remainingText = firstPassLines
const rewrapped = wrapText(remainingText, subsequentLineWidth); .slice(1)
wrappedValue = [firstLine, ...rewrapped]; .map(l => l.trim())
.join(' ')
const rewrapped = wrapText(remainingText, subsequentLineWidth)
wrappedValue = [firstLine, ...rewrapped]
} }
// First line: bold label + value // First line: bold label + value
lines_2.push(`${ANSI_BOLD_START}${label}:${ANSI_BOLD_END} ${wrappedValue[0] || ''}`); lines.push(
`${ANSI_BOLD_START}${label}:${ANSI_BOLD_END} ${wrappedValue[0] || ''}`,
)
// Subsequent lines with small indent (skip empty lines) // Subsequent lines with small indent (skip empty lines)
for (let i_3 = 1; i_3 < wrappedValue.length; i_3++) { for (let i = 1; i < wrappedValue.length; i++) {
const line_1 = wrappedValue[i_3]!; const line = wrappedValue[i]!
if (!line_1.trim()) continue; if (!line.trim()) continue
lines_2.push(`${wrapIndent}${line_1}`); lines.push(`${wrapIndent}${line}`)
} }
}); })
}); })
return lines_2.join('\n');
return lines.join('\n')
} }
// Choose format based on available width // Choose format based on available width
if (useVerticalFormat) { if (useVerticalFormat) {
return <Ansi>{renderVerticalFormat()}</Ansi>; return <Ansi>{renderVerticalFormat()}</Ansi>
} }
// Build the complete horizontal table as an array of strings // Build the complete horizontal table as an array of strings
const tableLines: string[] = []; const tableLines: string[] = []
tableLines.push(renderBorderLine('top')); tableLines.push(renderBorderLine('top'))
tableLines.push(...renderRowLines(token.header, true)); tableLines.push(...renderRowLines(token.header, true))
tableLines.push(renderBorderLine('middle')); tableLines.push(renderBorderLine('middle'))
token.rows.forEach((row_3, rowIndex_0) => { token.rows.forEach((row, rowIndex) => {
tableLines.push(...renderRowLines(row_3, false)); tableLines.push(...renderRowLines(row, false))
if (rowIndex_0 < token.rows.length - 1) { if (rowIndex < token.rows.length - 1) {
tableLines.push(renderBorderLine('middle')); tableLines.push(renderBorderLine('middle'))
} }
}); })
tableLines.push(renderBorderLine('bottom')); tableLines.push(renderBorderLine('bottom'))
// Safety check: verify no line exceeds terminal width. // Safety check: verify no line exceeds terminal width.
// This catches edge cases during terminal resize where calculations // This catches edge cases during terminal resize where calculations
// were based on a different width than the current render target. // were based on a different width than the current render target.
const maxLineWidth = Math.max(...tableLines.map(line_2 => stringWidth(stripAnsi(line_2)))); const maxLineWidth = Math.max(
...tableLines.map(line => stringWidth(stripAnsi(line))),
)
// If we're within SAFETY_MARGIN characters of the edge, use vertical format // If we're within SAFETY_MARGIN characters of the edge, use vertical format
// to account for terminal resize race conditions. // to account for terminal resize race conditions.
if (maxLineWidth > terminalWidth - SAFETY_MARGIN) { if (maxLineWidth > terminalWidth - SAFETY_MARGIN) {
return <Ansi>{renderVerticalFormat()}</Ansi>; return <Ansi>{renderVerticalFormat()}</Ansi>
} }
// Render as a single Ansi block to prevent Ink from wrapping mid-row // Render as a single Ansi block to prevent Ink from wrapping mid-row
return <Ansi>{tableLines.join('\n')}</Ansi>; return <Ansi>{tableLines.join('\n')}</Ansi>
} }

View File

@@ -1,36 +1,40 @@
import * as React from 'react'; import * as React from 'react'
import { useMemoryUsage } from '../hooks/useMemoryUsage.js'; import { useMemoryUsage } from '../hooks/useMemoryUsage.js'
import { Box, Text } from '../ink.js'; import { Box, Text } from '../ink.js'
import { formatFileSize } from '../utils/format.js'; import { formatFileSize } from '../utils/format.js'
export function MemoryUsageIndicator(): React.ReactNode { export function MemoryUsageIndicator(): React.ReactNode {
// Ant-only: the /heapdump link is an internal debugging aid. Gating before // Ant-only: the /heapdump link is an internal debugging aid. Gating before
// the hook means the 10s polling interval is never set up in external builds. // the hook means the 10s polling interval is never set up in external builds.
// USER_TYPE is a build-time constant, so the hook call below is either always // USER_TYPE is a build-time constant, so the hook call below is either always
// reached or dead-code-eliminated — never conditional at runtime. // reached or dead-code-eliminated — never conditional at runtime.
if ((process.env.USER_TYPE) !== 'ant') { if ("external" !== 'ant') {
return null; return null
} }
// eslint-disable-next-line react-hooks/rules-of-hooks // eslint-disable-next-line react-hooks/rules-of-hooks
// biome-ignore lint/correctness/useHookAtTopLevel: USER_TYPE is a build-time constant // biome-ignore lint/correctness/useHookAtTopLevel: USER_TYPE is a build-time constant
const memoryUsage = useMemoryUsage(); const memoryUsage = useMemoryUsage()
if (!memoryUsage) { if (!memoryUsage) {
return null; return null
} }
const {
heapUsed, const { heapUsed, status } = memoryUsage
status
} = memoryUsage;
// Only show indicator when memory usage is high or critical // Only show indicator when memory usage is high or critical
if (status === 'normal') { if (status === 'normal') {
return null; return null
} }
const formattedSize = formatFileSize(heapUsed);
const color = status === 'critical' ? 'error' : 'warning'; const formattedSize = formatFileSize(heapUsed)
return <Box> const color = status === 'critical' ? 'error' : 'warning'
return (
<Box>
<Text color={color} wrap="truncate"> <Text color={color} wrap="truncate">
High memory usage ({formattedSize}) · /heapdump High memory usage ({formattedSize}) · /heapdump
</Text> </Text>
</Box>; </Box>
)
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,42 +1,30 @@
import { c as _c } from "react/compiler-runtime"; import React from 'react'
import React from 'react'; import { stringWidth } from '../ink/stringWidth.js'
import { stringWidth } from '../ink/stringWidth.js'; import { Box, Text } from '../ink.js'
import { Box, Text } from '../ink.js'; import type { NormalizedMessage } from '../types/message.js'
import type { NormalizedMessage } from '../types/message.js';
type Props = { type Props = {
message: NormalizedMessage; message: NormalizedMessage
isTranscriptMode: boolean; isTranscriptMode: boolean
}; }
export function MessageModel(t0) {
const $ = _c(5); export function MessageModel({
const { message,
message, isTranscriptMode,
isTranscriptMode }: Props): React.ReactNode {
} = t0; const shouldShowModel =
const shouldShowModel = isTranscriptMode && message.type === "assistant" && message.message.model && message.message.content.some(_temp); isTranscriptMode &&
message.type === 'assistant' &&
message.message.model &&
message.message.content.some(c => c.type === 'text')
if (!shouldShowModel) { if (!shouldShowModel) {
return null; return null
} }
const t1 = stringWidth(message.message.model) + 8;
let t2; return (
if ($[0] !== message.message.model) { <Box minWidth={stringWidth(message.message.model) + 8}>
t2 = <Text dimColor={true}>{message.message.model}</Text>; <Text dimColor>{message.message.model}</Text>
$[0] = message.message.model; </Box>
$[1] = t2; )
} else {
t2 = $[1];
}
let t3;
if ($[2] !== t1 || $[3] !== t2) {
t3 = <Box minWidth={t1}>{t2}</Box>;
$[2] = t1;
$[3] = t2;
$[4] = t3;
} else {
t3 = $[4];
}
return t3;
}
function _temp(c) {
return c.type === "text";
} }

View File

@@ -1,77 +1,49 @@
import { c as _c } from "react/compiler-runtime"; import * as React from 'react'
import * as React from 'react'; import { useContext } from 'react'
import { useContext } from 'react'; import { Box, NoSelect, Text } from '../ink.js'
import { Box, NoSelect, Text } from '../ink.js'; import { Ratchet } from './design-system/Ratchet.js'
import { Ratchet } from './design-system/Ratchet.js';
type Props = { type Props = {
children: React.ReactNode; children: React.ReactNode
height?: number; height?: number
}; }
export function MessageResponse(t0) {
const $ = _c(8); export function MessageResponse({ children, height }: Props): React.ReactNode {
const { const isMessageResponse = useContext(MessageResponseContext)
children,
height
} = t0;
const isMessageResponse = useContext(MessageResponseContext);
if (isMessageResponse) { if (isMessageResponse) {
return children; return children
} }
let t1; const content = (
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { <MessageResponseProvider>
t1 = <NoSelect fromLeftEdge={true} flexShrink={0}><Text dimColor={true}>{" "}  </Text></NoSelect>; <Box flexDirection="row" height={height} overflowY="hidden">
$[0] = t1; <NoSelect fromLeftEdge flexShrink={0}>
} else { <Text dimColor>{' '} &nbsp;</Text>
t1 = $[0]; </NoSelect>
} <Box flexShrink={1} flexGrow={1}>
let t2; {children}
if ($[1] !== children) { </Box>
t2 = <Box flexShrink={1} flexGrow={1}>{children}</Box>; </Box>
$[1] = children; </MessageResponseProvider>
$[2] = t2; )
} else {
t2 = $[2];
}
let t3;
if ($[3] !== height || $[4] !== t2) {
t3 = <MessageResponseProvider><Box flexDirection="row" height={height} overflowY="hidden">{t1}{t2}</Box></MessageResponseProvider>;
$[3] = height;
$[4] = t2;
$[5] = t3;
} else {
t3 = $[5];
}
const content = t3;
if (height !== undefined) { if (height !== undefined) {
return content; return content
} }
let t4; return <Ratchet lock="offscreen">{content}</Ratchet>
if ($[6] !== content) {
t4 = <Ratchet lock="offscreen">{content}</Ratchet>;
$[6] = content;
$[7] = t4;
} else {
t4 = $[7];
}
return t4;
} }
// This is a context that is used to determine if the message response // This is a context that is used to determine if the message response
// is rendered as a descendant of another MessageResponse. We use it // is rendered as a descendant of another MessageResponse. We use it
// to avoid rendering nested ⎿ characters. // to avoid rendering nested ⎿ characters.
const MessageResponseContext = React.createContext(false); const MessageResponseContext = React.createContext(false)
function MessageResponseProvider(t0) {
const $ = _c(2); function MessageResponseProvider({
const { children,
children }: {
} = t0; children: React.ReactNode
let t1; }): React.ReactNode {
if ($[0] !== children) { return (
t1 = <MessageResponseContext.Provider value={true}>{children}</MessageResponseContext.Provider>; <MessageResponseContext.Provider value={true}>
$[0] = children; {children}
$[1] = t1; </MessageResponseContext.Provider>
} else { )
t1 = $[1];
}
return t1;
} }

View File

@@ -1,44 +1,52 @@
import { c as _c } from "react/compiler-runtime"; import * as React from 'react'
import * as React from 'react'; import type { Command } from '../commands.js'
import type { Command } from '../commands.js'; import { Box } from '../ink.js'
import { Box } from '../ink.js'; import type { Screen } from '../screens/REPL.js'
import type { Screen } from '../screens/REPL.js'; import type { Tools } from '../Tool.js'
import type { Tools } from '../Tool.js'; import type { RenderableMessage } from '../types/message.js'
import type { RenderableMessage } from '../types/message.js'; import {
import { getDisplayMessageFromCollapsed, getToolSearchOrReadInfo, getToolUseIdsFromCollapsedGroup, hasAnyToolInProgress } from '../utils/collapseReadSearch.js'; getDisplayMessageFromCollapsed,
import { type buildMessageLookups, EMPTY_STRING_SET, getProgressMessagesFromLookup, getSiblingToolUseIDsFromLookup, getToolUseID } from '../utils/messages.js'; getToolSearchOrReadInfo,
import { hasThinkingContent, Message } from './Message.js'; getToolUseIdsFromCollapsedGroup,
import { MessageModel } from './MessageModel.js'; hasAnyToolInProgress,
import { shouldRenderStatically } from './Messages.js'; } from '../utils/collapseReadSearch.js'
import { MessageTimestamp } from './MessageTimestamp.js'; import {
type buildMessageLookups,
EMPTY_STRING_SET,
getProgressMessagesFromLookup,
getSiblingToolUseIDsFromLookup,
getToolUseID,
} from '../utils/messages.js'
import { hasThinkingContent, Message } from './Message.js'
import { MessageModel } from './MessageModel.js'
import { shouldRenderStatically } from './Messages.js'
import { MessageTimestamp } from './MessageTimestamp.js'
import { OffscreenFreeze } from './OffscreenFreeze.js'
/** Narrowed content block shape used for type assertions on MessageContent arrays. */
type ContentBlockLike = { type: string; name?: string; input?: unknown; id?: string; text?: string };
import { OffscreenFreeze } from './OffscreenFreeze.js';
export type Props = { export type Props = {
message: RenderableMessage; message: RenderableMessage
/** Whether the previous message in renderableMessages is also a user message. */ /** Whether the previous message in renderableMessages is also a user message. */
isUserContinuation: boolean; isUserContinuation: boolean
/** /**
* Whether there is non-skippable content after this message in renderableMessages. * Whether there is non-skippable content after this message in renderableMessages.
* Only needs to be accurate for `collapsed_read_search` messages — used to decide * Only needs to be accurate for `collapsed_read_search` messages — used to decide
* if the collapsed group spinner should stay active. Pass `false` otherwise. * if the collapsed group spinner should stay active. Pass `false` otherwise.
*/ */
hasContentAfter: boolean; hasContentAfter: boolean
tools: Tools; tools: Tools
commands: Command[]; commands: Command[]
verbose: boolean; verbose: boolean
inProgressToolUseIDs: Set<string>; inProgressToolUseIDs: Set<string>
streamingToolUseIDs: Set<string>; streamingToolUseIDs: Set<string>
screen: Screen; screen: Screen
canAnimate: boolean; canAnimate: boolean
onOpenRateLimitOptions?: () => void; onOpenRateLimitOptions?: () => void
lastThinkingBlockId: string | null; lastThinkingBlockId: string | null
latestBashOutputUUID: string | null; latestBashOutputUUID: string | null
columns: number; columns: number
isLoading: boolean; isLoading: boolean
lookups: ReturnType<typeof buildMessageLookups>; lookups: ReturnType<typeof buildMessageLookups>
}; }
/** /**
* Scans forward from `index+1` to check if any "real" content follows. Used to * Scans forward from `index+1` to check if any "real" content follows. Used to
@@ -50,290 +58,244 @@ export type Props = {
* to each MessageRow (which React Compiler would pin in the fiber's memoCache, * to each MessageRow (which React Compiler would pin in the fiber's memoCache,
* accumulating every historical version of the array ≈ 1-2MB over a 7-turn session). * accumulating every historical version of the array ≈ 1-2MB over a 7-turn session).
*/ */
export function hasContentAfterIndex(messages: RenderableMessage[], index: number, tools: Tools, streamingToolUseIDs: Set<string>): boolean { export function hasContentAfterIndex(
messages: RenderableMessage[],
index: number,
tools: Tools,
streamingToolUseIDs: Set<string>,
): boolean {
for (let i = index + 1; i < messages.length; i++) { for (let i = index + 1; i < messages.length; i++) {
const msg = messages[i]; const msg = messages[i]
if (msg?.type === 'assistant') { if (msg?.type === 'assistant') {
const content = (msg.message.content as ContentBlockLike[])[0]; const content = msg.message.content[0]
if (content?.type === 'thinking' || content?.type === 'redacted_thinking') { if (
continue; content?.type === 'thinking' ||
content?.type === 'redacted_thinking'
) {
continue
} }
if (content?.type === 'tool_use') { if (content?.type === 'tool_use') {
if (getToolSearchOrReadInfo(content.name!, content.input, tools).isCollapsible) { if (
continue; getToolSearchOrReadInfo(content.name, content.input, tools)
.isCollapsible
) {
continue
} }
// Non-collapsible tool uses appear in syntheticStreamingToolUseMessages // Non-collapsible tool uses appear in syntheticStreamingToolUseMessages
// before their ID is added to inProgressToolUseIDs. Skip while streaming // before their ID is added to inProgressToolUseIDs. Skip while streaming
// to avoid briefly finalizing the read group. // to avoid briefly finalizing the read group.
if (streamingToolUseIDs.has(content.id!)) { if (streamingToolUseIDs.has(content.id)) {
continue; continue
} }
} }
return true; return true
} }
if (msg?.type === 'system' || msg?.type === 'attachment') { if (msg?.type === 'system' || msg?.type === 'attachment') {
continue; continue
} }
// Tool results arrive while the collapsed group is still being built // Tool results arrive while the collapsed group is still being built
if (msg?.type === 'user') { if (msg?.type === 'user') {
const content = (msg.message.content as ContentBlockLike[])[0]; const content = msg.message.content[0]
if (content?.type === 'tool_result') { if (content?.type === 'tool_result') {
continue; continue
} }
} }
// Collapsible grouped_tool_use messages arrive transiently before being // Collapsible grouped_tool_use messages arrive transiently before being
// merged into the current collapsed group on the next render cycle // merged into the current collapsed group on the next render cycle
if (msg?.type === 'grouped_tool_use') { if (msg?.type === 'grouped_tool_use') {
const firstInput = (msg.messages[0]?.message.content as ContentBlockLike[])?.[0]?.input; const firstInput = msg.messages[0]?.message.content[0]?.input
if (getToolSearchOrReadInfo(msg.toolName, firstInput, tools).isCollapsible) { if (
continue; getToolSearchOrReadInfo(msg.toolName, firstInput, tools).isCollapsible
) {
continue
} }
} }
return true; return true
} }
return false; return false
} }
function MessageRowImpl(t0) {
const $ = _c(64); function MessageRowImpl({
const { message: msg,
message: msg, isUserContinuation,
isUserContinuation, hasContentAfter,
hasContentAfter, tools,
tools, commands,
commands, verbose,
verbose, inProgressToolUseIDs,
inProgressToolUseIDs, streamingToolUseIDs,
screen,
canAnimate,
onOpenRateLimitOptions,
lastThinkingBlockId,
latestBashOutputUUID,
columns,
isLoading,
lookups,
}: Props): React.ReactNode {
const isTranscriptMode = screen === 'transcript'
const isGrouped = msg.type === 'grouped_tool_use'
const isCollapsed = msg.type === 'collapsed_read_search'
// A collapsed group is "active" (grey dot, present tense "Reading…") when its tools
// are still executing OR when the overall query is still running with nothing after it.
// hasAnyToolInProgress takes priority: if tools are running, always show active regardless
// of what else is in the message list (avoids false finalization during parallel execution).
const isActiveCollapsedGroup =
isCollapsed &&
(hasAnyToolInProgress(msg, inProgressToolUseIDs) ||
(isLoading && !hasContentAfter))
const displayMsg = isGrouped
? msg.displayMessage
: isCollapsed
? getDisplayMessageFromCollapsed(msg)
: msg
const progressMessagesForMessage =
isGrouped || isCollapsed ? [] : getProgressMessagesFromLookup(msg, lookups)
const siblingToolUseIDs =
isGrouped || isCollapsed
? EMPTY_STRING_SET
: getSiblingToolUseIDsFromLookup(msg, lookups)
const isStatic = shouldRenderStatically(
msg,
streamingToolUseIDs, streamingToolUseIDs,
inProgressToolUseIDs,
siblingToolUseIDs,
screen, screen,
canAnimate, lookups,
onOpenRateLimitOptions, )
lastThinkingBlockId,
latestBashOutputUUID, let shouldAnimate = false
columns,
isLoading,
lookups
} = t0;
const isTranscriptMode = screen === "transcript";
const isGrouped = msg.type === "grouped_tool_use";
const isCollapsed = msg.type === "collapsed_read_search";
let t1;
if ($[0] !== hasContentAfter || $[1] !== inProgressToolUseIDs || $[2] !== isCollapsed || $[3] !== isLoading || $[4] !== msg) {
t1 = isCollapsed && (hasAnyToolInProgress(msg, inProgressToolUseIDs) || isLoading && !hasContentAfter);
$[0] = hasContentAfter;
$[1] = inProgressToolUseIDs;
$[2] = isCollapsed;
$[3] = isLoading;
$[4] = msg;
$[5] = t1;
} else {
t1 = $[5];
}
const isActiveCollapsedGroup = t1;
let t2;
if ($[6] !== isCollapsed || $[7] !== isGrouped || $[8] !== msg) {
t2 = isGrouped ? msg.displayMessage : isCollapsed ? getDisplayMessageFromCollapsed(msg) : msg;
$[6] = isCollapsed;
$[7] = isGrouped;
$[8] = msg;
$[9] = t2;
} else {
t2 = $[9];
}
const displayMsg = t2;
let t3;
if ($[10] !== isCollapsed || $[11] !== isGrouped || $[12] !== lookups || $[13] !== msg) {
t3 = isGrouped || isCollapsed ? [] : getProgressMessagesFromLookup(msg, lookups);
$[10] = isCollapsed;
$[11] = isGrouped;
$[12] = lookups;
$[13] = msg;
$[14] = t3;
} else {
t3 = $[14];
}
const progressMessagesForMessage = t3;
let t4;
if ($[15] !== inProgressToolUseIDs || $[16] !== isCollapsed || $[17] !== isGrouped || $[18] !== lookups || $[19] !== msg || $[20] !== screen || $[21] !== streamingToolUseIDs) {
const siblingToolUseIDs = isGrouped || isCollapsed ? EMPTY_STRING_SET : getSiblingToolUseIDsFromLookup(msg, lookups);
t4 = shouldRenderStatically(msg, streamingToolUseIDs, inProgressToolUseIDs, siblingToolUseIDs, screen, lookups);
$[15] = inProgressToolUseIDs;
$[16] = isCollapsed;
$[17] = isGrouped;
$[18] = lookups;
$[19] = msg;
$[20] = screen;
$[21] = streamingToolUseIDs;
$[22] = t4;
} else {
t4 = $[22];
}
const isStatic = t4;
let shouldAnimate = false;
if (canAnimate) { if (canAnimate) {
if (isGrouped) { if (isGrouped) {
let t5; shouldAnimate = msg.messages.some(m => {
if ($[23] !== inProgressToolUseIDs || $[24] !== msg.messages) { const content = m.message.content[0]
let t6; return (
if ($[26] !== inProgressToolUseIDs) { content?.type === 'tool_use' && inProgressToolUseIDs.has(content.id)
t6 = m => { )
const content = m.message.content[0]; })
return content?.type === "tool_use" && inProgressToolUseIDs.has(content.id); } else if (isCollapsed) {
}; shouldAnimate = hasAnyToolInProgress(msg, inProgressToolUseIDs)
$[26] = inProgressToolUseIDs;
$[27] = t6;
} else {
t6 = $[27];
}
t5 = msg.messages.some(t6);
$[23] = inProgressToolUseIDs;
$[24] = msg.messages;
$[25] = t5;
} else {
t5 = $[25];
}
shouldAnimate = t5;
} else { } else {
if (isCollapsed) { const toolUseID = getToolUseID(msg)
let t5; shouldAnimate = !toolUseID || inProgressToolUseIDs.has(toolUseID)
if ($[28] !== inProgressToolUseIDs || $[29] !== msg) {
t5 = hasAnyToolInProgress(msg, inProgressToolUseIDs);
$[28] = inProgressToolUseIDs;
$[29] = msg;
$[30] = t5;
} else {
t5 = $[30];
}
shouldAnimate = t5;
} else {
let t5;
if ($[31] !== inProgressToolUseIDs || $[32] !== msg) {
const toolUseID = getToolUseID(msg);
t5 = !toolUseID || inProgressToolUseIDs.has(toolUseID);
$[31] = inProgressToolUseIDs;
$[32] = msg;
$[33] = t5;
} else {
t5 = $[33];
}
shouldAnimate = t5;
}
} }
} }
let t5;
if ($[34] !== displayMsg || $[35] !== isTranscriptMode) { const hasMetadata =
t5 = isTranscriptMode && displayMsg.type === "assistant" && displayMsg.message.content.some(_temp) && (displayMsg.timestamp || displayMsg.message.model); isTranscriptMode &&
$[34] = displayMsg; displayMsg.type === 'assistant' &&
$[35] = isTranscriptMode; displayMsg.message.content.some(c => c.type === 'text') &&
$[36] = t5; (displayMsg.timestamp || displayMsg.message.model)
} else {
t5 = $[36]; const messageEl = (
} <Message
const hasMetadata = t5; message={msg}
const t6 = !hasMetadata; lookups={lookups}
const t7 = hasMetadata ? undefined : columns; addMargin={!hasMetadata}
let t8; containerWidth={hasMetadata ? undefined : columns}
if ($[37] !== commands || $[38] !== inProgressToolUseIDs || $[39] !== isActiveCollapsedGroup || $[40] !== isStatic || $[41] !== isTranscriptMode || $[42] !== isUserContinuation || $[43] !== lastThinkingBlockId || $[44] !== latestBashOutputUUID || $[45] !== lookups || $[46] !== msg || $[47] !== onOpenRateLimitOptions || $[48] !== progressMessagesForMessage || $[49] !== shouldAnimate || $[50] !== t6 || $[51] !== t7 || $[52] !== tools || $[53] !== verbose) { tools={tools}
t8 = <Message message={msg} lookups={lookups} addMargin={t6} containerWidth={t7} tools={tools} commands={commands} verbose={verbose} inProgressToolUseIDs={inProgressToolUseIDs} progressMessagesForMessage={progressMessagesForMessage} shouldAnimate={shouldAnimate} shouldShowDot={true} isTranscriptMode={isTranscriptMode} isStatic={isStatic} onOpenRateLimitOptions={onOpenRateLimitOptions} isActiveCollapsedGroup={isActiveCollapsedGroup} isUserContinuation={isUserContinuation} lastThinkingBlockId={lastThinkingBlockId} latestBashOutputUUID={latestBashOutputUUID} />; commands={commands}
$[37] = commands; verbose={verbose}
$[38] = inProgressToolUseIDs; inProgressToolUseIDs={inProgressToolUseIDs}
$[39] = isActiveCollapsedGroup; progressMessagesForMessage={progressMessagesForMessage}
$[40] = isStatic; shouldAnimate={shouldAnimate}
$[41] = isTranscriptMode; shouldShowDot={true}
$[42] = isUserContinuation; isTranscriptMode={isTranscriptMode}
$[43] = lastThinkingBlockId; isStatic={isStatic}
$[44] = latestBashOutputUUID; onOpenRateLimitOptions={onOpenRateLimitOptions}
$[45] = lookups; isActiveCollapsedGroup={isActiveCollapsedGroup}
$[46] = msg; isUserContinuation={isUserContinuation}
$[47] = onOpenRateLimitOptions; lastThinkingBlockId={lastThinkingBlockId}
$[48] = progressMessagesForMessage; latestBashOutputUUID={latestBashOutputUUID}
$[49] = shouldAnimate; />
$[50] = t6; )
$[51] = t7; // OffscreenFreeze: the outer React.memo already bails for static messages,
$[52] = tools; // so this only wraps rows that DO re-render — in-progress tools, collapsed
$[53] = verbose; // read/search spinners, bash elapsed timers. When those rows have scrolled
$[54] = t8; // into terminal scrollback (non-fullscreen external builds), any content
} else { // change forces log-update.ts into a full terminal reset per tick. Freezing
t8 = $[54]; // returns the cached element ref so React bails and produces zero diff.
}
const messageEl = t8;
if (!hasMetadata) { if (!hasMetadata) {
let t9; return <OffscreenFreeze>{messageEl}</OffscreenFreeze>
if ($[55] !== messageEl) {
t9 = <OffscreenFreeze>{messageEl}</OffscreenFreeze>;
$[55] = messageEl;
$[56] = t9;
} else {
t9 = $[56];
}
return t9;
} }
let t9; // Margin on children, not here — else null items (hook_success etc.) get phantom 1-row spacing.
if ($[57] !== displayMsg || $[58] !== isTranscriptMode) { return (
t9 = <Box flexDirection="row" justifyContent="flex-end" gap={1} marginTop={1}><MessageTimestamp message={displayMsg} isTranscriptMode={isTranscriptMode} /><MessageModel message={displayMsg} isTranscriptMode={isTranscriptMode} /></Box>; <OffscreenFreeze>
$[57] = displayMsg; <Box width={columns} flexDirection="column">
$[58] = isTranscriptMode; <Box
$[59] = t9; flexDirection="row"
} else { justifyContent="flex-end"
t9 = $[59]; gap={1}
} marginTop={1}
let t10; >
if ($[60] !== columns || $[61] !== messageEl || $[62] !== t9) { <MessageTimestamp
t10 = <OffscreenFreeze><Box width={columns} flexDirection="column">{t9}{messageEl}</Box></OffscreenFreeze>; message={displayMsg}
$[60] = columns; isTranscriptMode={isTranscriptMode}
$[61] = messageEl; />
$[62] = t9; <MessageModel
$[63] = t10; message={displayMsg}
} else { isTranscriptMode={isTranscriptMode}
t10 = $[63]; />
} </Box>
return t10; {messageEl}
</Box>
</OffscreenFreeze>
)
} }
/** /**
* Checks if a message is "streaming" - i.e., its content may still be changing. * Checks if a message is "streaming" - i.e., its content may still be changing.
* Exported for testing. * Exported for testing.
*/ */
function _temp(c) { export function isMessageStreaming(
return c.type === "text"; msg: RenderableMessage,
} streamingToolUseIDs: Set<string>,
export function isMessageStreaming(msg: RenderableMessage, streamingToolUseIDs: Set<string>): boolean { ): boolean {
if (msg.type === 'grouped_tool_use') { if (msg.type === 'grouped_tool_use') {
return msg.messages.some(m => { return msg.messages.some(m => {
const content = (m.message.content as ContentBlockLike[])[0]; const content = m.message.content[0]
return content?.type === 'tool_use' && streamingToolUseIDs.has(content.id!); return content?.type === 'tool_use' && streamingToolUseIDs.has(content.id)
}); })
} }
if (msg.type === 'collapsed_read_search') { if (msg.type === 'collapsed_read_search') {
const toolIds = getToolUseIdsFromCollapsedGroup(msg); const toolIds = getToolUseIdsFromCollapsedGroup(msg)
return toolIds.some(id => streamingToolUseIDs.has(id)); return toolIds.some(id => streamingToolUseIDs.has(id))
} }
const toolUseID = getToolUseID(msg); const toolUseID = getToolUseID(msg)
return !!toolUseID && streamingToolUseIDs.has(toolUseID); return !!toolUseID && streamingToolUseIDs.has(toolUseID)
} }
/** /**
* Checks if all tools in a message are resolved. * Checks if all tools in a message are resolved.
* Exported for testing. * Exported for testing.
*/ */
export function allToolsResolved(msg: RenderableMessage, resolvedToolUseIDs: Set<string>): boolean { export function allToolsResolved(
msg: RenderableMessage,
resolvedToolUseIDs: Set<string>,
): boolean {
if (msg.type === 'grouped_tool_use') { if (msg.type === 'grouped_tool_use') {
return msg.messages.every(m => { return msg.messages.every(m => {
const content = (m.message.content as ContentBlockLike[])[0]; const content = m.message.content[0]
return content?.type === 'tool_use' && resolvedToolUseIDs.has(content.id!); return content?.type === 'tool_use' && resolvedToolUseIDs.has(content.id)
}); })
} }
if (msg.type === 'collapsed_read_search') { if (msg.type === 'collapsed_read_search') {
const toolIds = getToolUseIdsFromCollapsedGroup(msg); const toolIds = getToolUseIdsFromCollapsedGroup(msg)
return toolIds.every(id => resolvedToolUseIDs.has(id)); return toolIds.every(id => resolvedToolUseIDs.has(id))
} }
if (msg.type === 'assistant') { if (msg.type === 'assistant') {
const block = (msg.message.content as ContentBlockLike[])[0]; const block = msg.message.content[0]
if (block?.type === 'server_tool_use') { if (block?.type === 'server_tool_use') {
return resolvedToolUseIDs.has(block.id!); return resolvedToolUseIDs.has(block.id)
} }
} }
const toolUseID = getToolUseID(msg); const toolUseID = getToolUseID(msg)
return !toolUseID || resolvedToolUseIDs.has(toolUseID); return !toolUseID || resolvedToolUseIDs.has(toolUseID)
} }
/** /**
@@ -344,42 +306,52 @@ export function allToolsResolved(msg: RenderableMessage, resolvedToolUseIDs: Set
*/ */
export function areMessageRowPropsEqual(prev: Props, next: Props): boolean { export function areMessageRowPropsEqual(prev: Props, next: Props): boolean {
// Different message reference = content may have changed, must re-render // Different message reference = content may have changed, must re-render
if (prev.message !== next.message) return false; if (prev.message !== next.message) return false
// Screen mode change = re-render // Screen mode change = re-render
if (prev.screen !== next.screen) return false; if (prev.screen !== next.screen) return false
// Verbose toggle changes thinking block visibility // Verbose toggle changes thinking block visibility
if (prev.verbose !== next.verbose) return false; if (prev.verbose !== next.verbose) return false
// collapsed_read_search is never static in prompt mode (matches shouldRenderStatically) // collapsed_read_search is never static in prompt mode (matches shouldRenderStatically)
if (prev.message.type === 'collapsed_read_search' && next.screen !== 'transcript') { if (
return false; prev.message.type === 'collapsed_read_search' &&
next.screen !== 'transcript'
) {
return false
} }
// Width change affects Box layout // Width change affects Box layout
if (prev.columns !== next.columns) return false; if (prev.columns !== next.columns) return false
// latestBashOutputUUID affects rendering (full vs truncated output) // latestBashOutputUUID affects rendering (full vs truncated output)
const prevIsLatestBash = prev.latestBashOutputUUID === prev.message.uuid; const prevIsLatestBash = prev.latestBashOutputUUID === prev.message.uuid
const nextIsLatestBash = next.latestBashOutputUUID === next.message.uuid; const nextIsLatestBash = next.latestBashOutputUUID === next.message.uuid
if (prevIsLatestBash !== nextIsLatestBash) return false; if (prevIsLatestBash !== nextIsLatestBash) return false
// lastThinkingBlockId affects thinking block visibility — but only for // lastThinkingBlockId affects thinking block visibility — but only for
// messages that HAVE thinking content. Checking unconditionally busts the // messages that HAVE thinking content. Checking unconditionally busts the
// memo for every scrollback message whenever thinking starts/stops (CC-941). // memo for every scrollback message whenever thinking starts/stops (CC-941).
if (prev.lastThinkingBlockId !== next.lastThinkingBlockId && hasThinkingContent(next.message as Parameters<typeof hasThinkingContent>[0])) { if (
return false; prev.lastThinkingBlockId !== next.lastThinkingBlockId &&
hasThinkingContent(next.message)
) {
return false
} }
// Check if this message is still "in flight" // Check if this message is still "in flight"
const isStreaming = isMessageStreaming(prev.message, prev.streamingToolUseIDs); const isStreaming = isMessageStreaming(prev.message, prev.streamingToolUseIDs)
const isResolved = allToolsResolved(prev.message, prev.lookups.resolvedToolUseIDs); const isResolved = allToolsResolved(
prev.message,
prev.lookups.resolvedToolUseIDs,
)
// Only bail out for truly static messages // Only bail out for truly static messages
if (isStreaming || !isResolved) return false; if (isStreaming || !isResolved) return false
// Static message - safe to skip re-render // Static message - safe to skip re-render
return true; return true
} }
export const MessageRow = React.memo(MessageRowImpl, areMessageRowPropsEqual);
export const MessageRow = React.memo(MessageRowImpl, areMessageRowPropsEqual)

File diff suppressed because it is too large Load Diff

View File

@@ -1,62 +1,39 @@
import { c as _c } from "react/compiler-runtime"; import React from 'react'
import React from 'react'; import { stringWidth } from '../ink/stringWidth.js'
import { stringWidth } from '../ink/stringWidth.js'; import { Box, Text } from '../ink.js'
import { Box, Text } from '../ink.js'; import type { NormalizedMessage } from '../types/message.js'
import type { NormalizedMessage } from '../types/message.js';
type Props = { type Props = {
message: NormalizedMessage; message: NormalizedMessage
isTranscriptMode: boolean; isTranscriptMode: boolean
}; }
export function MessageTimestamp(t0) {
const $ = _c(10); export function MessageTimestamp({
const { message,
message, isTranscriptMode,
isTranscriptMode }: Props): React.ReactNode {
} = t0; const shouldShowTimestamp =
const shouldShowTimestamp = isTranscriptMode && message.timestamp && message.type === "assistant" && message.message.content.some(_temp); isTranscriptMode &&
message.timestamp &&
message.type === 'assistant' &&
message.message.content.some(c => c.type === 'text')
if (!shouldShowTimestamp) { if (!shouldShowTimestamp) {
return null; return null
} }
let T0;
let formattedTimestamp; const formattedTimestamp = new Date(message.timestamp).toLocaleTimeString(
let t1; 'en-US',
if ($[0] !== message.timestamp) { {
formattedTimestamp = new Date(message.timestamp).toLocaleTimeString("en-US", { hour: '2-digit',
hour: "2-digit", minute: '2-digit',
minute: "2-digit", hour12: true,
hour12: true },
}); )
T0 = Box;
t1 = stringWidth(formattedTimestamp); return (
$[0] = message.timestamp; <Box minWidth={stringWidth(formattedTimestamp)}>
$[1] = T0; <Text dimColor>{formattedTimestamp}</Text>
$[2] = formattedTimestamp; </Box>
$[3] = t1; )
} else {
T0 = $[1];
formattedTimestamp = $[2];
t1 = $[3];
}
let t2;
if ($[4] !== formattedTimestamp) {
t2 = <Text dimColor={true}>{formattedTimestamp}</Text>;
$[4] = formattedTimestamp;
$[5] = t2;
} else {
t2 = $[5];
}
let t3;
if ($[6] !== T0 || $[7] !== t1 || $[8] !== t2) {
t3 = <T0 minWidth={t1}>{t2}</T0>;
$[6] = T0;
$[7] = t1;
$[8] = t2;
$[9] = t3;
} else {
t3 = $[9];
}
return t3;
}
function _temp(c) {
return c.type === "text";
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,447 +1,368 @@
import { c as _c } from "react/compiler-runtime"; import capitalize from 'lodash-es/capitalize.js'
import capitalize from 'lodash-es/capitalize.js'; import * as React from 'react'
import * as React from 'react'; import { useCallback, useMemo, useState } from 'react'
import { useCallback, useMemo, useState } from 'react'; import { useExitOnCtrlCDWithKeybindings } from 'src/hooks/useExitOnCtrlCDWithKeybindings.js'
import { useExitOnCtrlCDWithKeybindings } from 'src/hooks/useExitOnCtrlCDWithKeybindings.js'; import {
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
import { FAST_MODE_MODEL_DISPLAY, isFastModeAvailable, isFastModeCooldown, isFastModeEnabled } from 'src/utils/fastMode.js'; logEvent,
import { Box, Text } from '../ink.js'; } from 'src/services/analytics/index.js'
import { useKeybindings } from '../keybindings/useKeybinding.js'; import {
import { useAppState, useSetAppState } from '../state/AppState.js'; FAST_MODE_MODEL_DISPLAY,
import { convertEffortValueToLevel, type EffortLevel, getDefaultEffortForModel, modelSupportsEffort, modelSupportsMaxEffort, resolvePickerEffortPersistence, toPersistableEffort } from '../utils/effort.js'; isFastModeAvailable,
import { getDefaultMainLoopModel, type ModelSetting, modelDisplayString, parseUserSpecifiedModel } from '../utils/model/model.js'; isFastModeCooldown,
import { getModelOptions } from '../utils/model/modelOptions.js'; isFastModeEnabled,
import { getSettingsForSource, updateSettingsForSource } from '../utils/settings/settings.js'; } from 'src/utils/fastMode.js'
import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js'; import { Box, Text } from '../ink.js'
import { Select } from './CustomSelect/index.js'; import { useKeybindings } from '../keybindings/useKeybinding.js'
import { Byline } from './design-system/Byline.js'; import { useAppState, useSetAppState } from '../state/AppState.js'
import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js'; import {
import { Pane } from './design-system/Pane.js'; convertEffortValueToLevel,
import { effortLevelToSymbol } from './EffortIndicator.js'; type EffortLevel,
getDefaultEffortForModel,
modelSupportsEffort,
modelSupportsMaxEffort,
resolvePickerEffortPersistence,
toPersistableEffort,
} from '../utils/effort.js'
import {
getDefaultMainLoopModel,
type ModelSetting,
modelDisplayString,
parseUserSpecifiedModel,
} from '../utils/model/model.js'
import { getModelOptions } from '../utils/model/modelOptions.js'
import {
getSettingsForSource,
updateSettingsForSource,
} 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 { effortLevelToSymbol } from './EffortIndicator.js'
export type Props = { export type Props = {
initial: string | null; initial: string | null
sessionModel?: ModelSetting; sessionModel?: ModelSetting
onSelect: (model: string | null, effort: EffortLevel | undefined) => void; onSelect: (model: string | null, effort: EffortLevel | undefined) => void
onCancel?: () => void; onCancel?: () => void
isStandaloneCommand?: boolean; isStandaloneCommand?: boolean
showFastModeNotice?: boolean; showFastModeNotice?: boolean
/** Overrides the dim header line below "Select model". */ /** Overrides the dim header line below "Select model". */
headerText?: string; headerText?: string
/** /**
* When true, skip writing effortLevel to userSettings on selection. * When true, skip writing effortLevel to userSettings on selection.
* Used by the assistant installer wizard where the model choice is * Used by the assistant installer wizard where the model choice is
* project-scoped (written to the assistant's .claude/settings.json via * project-scoped (written to the assistant's .claude/settings.json via
* install.ts) and should not leak to the user's global ~/.claude/settings. * install.ts) and should not leak to the user's global ~/.claude/settings.
*/ */
skipSettingsWrite?: boolean; skipSettingsWrite?: boolean
}; }
const NO_PREFERENCE = '__NO_PREFERENCE__';
export function ModelPicker(t0) { const NO_PREFERENCE = '__NO_PREFERENCE__'
const $ = _c(82);
const { export function ModelPicker({
initial, initial,
sessionModel, sessionModel,
onSelect, onSelect,
onCancel, onCancel,
isStandaloneCommand, isStandaloneCommand,
showFastModeNotice, showFastModeNotice,
headerText, headerText,
skipSettingsWrite skipSettingsWrite,
} = t0; }: Props): React.ReactNode {
const setAppState = useSetAppState(); const setAppState = useSetAppState()
const exitState = useExitOnCtrlCDWithKeybindings(); const exitState = useExitOnCtrlCDWithKeybindings()
const initialValue = initial === null ? NO_PREFERENCE : initial; const maxVisible = 10
const [focusedValue, setFocusedValue] = useState(initialValue);
const isFastMode = useAppState(_temp); const initialValue = initial === null ? NO_PREFERENCE : initial
const [hasToggledEffort, setHasToggledEffort] = useState(false); const [focusedValue, setFocusedValue] = useState<string | undefined>(
const effortValue = useAppState(_temp2); initialValue,
let t1; )
if ($[0] !== effortValue) {
t1 = effortValue !== undefined ? convertEffortValueToLevel(effortValue) : undefined; const isFastMode = useAppState(s =>
$[0] = effortValue; isFastModeEnabled() ? s.fastMode : false,
$[1] = t1; )
} else {
t1 = $[1]; const [hasToggledEffort, setHasToggledEffort] = useState(false)
} const effortValue = useAppState(s => s.effortValue)
const [effort, setEffort] = useState(t1); const [effort, setEffort] = useState<EffortLevel | undefined>(
const t2 = isFastMode ?? false; effortValue !== undefined
let t3; ? convertEffortValueToLevel(effortValue)
if ($[2] !== t2) { : undefined,
t3 = getModelOptions(t2); )
$[2] = t2;
$[3] = t3; // Memoize all derived values to prevent re-renders
} else { const modelOptions = useMemo(
t3 = $[3]; () => getModelOptions(isFastMode ?? false),
} [isFastMode],
const modelOptions = t3; )
let t4;
bb0: { // Ensure the initial value is in the options list
// This handles edge cases where the user's current model (e.g., 'haiku' for 3P users)
// is not in the base options but should still be selectable and shown as selected
const optionsWithInitial = useMemo(() => {
if (initial !== null && !modelOptions.some(opt => opt.value === initial)) { if (initial !== null && !modelOptions.some(opt => opt.value === initial)) {
let t5; return [
if ($[4] !== initial) { ...modelOptions,
t5 = modelDisplayString(initial); {
$[4] = initial;
$[5] = t5;
} else {
t5 = $[5];
}
let t6;
if ($[6] !== initial || $[7] !== t5) {
t6 = {
value: initial, value: initial,
label: t5, label: modelDisplayString(initial),
description: "Current model" description: 'Current model',
}; },
$[6] = initial; ]
$[7] = t5;
$[8] = t6;
} else {
t6 = $[8];
}
let t7;
if ($[9] !== modelOptions || $[10] !== t6) {
t7 = [...modelOptions, t6];
$[9] = modelOptions;
$[10] = t6;
$[11] = t7;
} else {
t7 = $[11];
}
t4 = t7;
break bb0;
} }
t4 = modelOptions; return modelOptions
} }, [modelOptions, initial])
const optionsWithInitial = t4;
let t5; const selectOptions = useMemo(
if ($[12] !== optionsWithInitial) { () =>
t5 = optionsWithInitial.map(_temp3); optionsWithInitial.map(opt => ({
$[12] = optionsWithInitial; ...opt,
$[13] = t5; value: opt.value === null ? NO_PREFERENCE : opt.value,
} else { })),
t5 = $[13]; [optionsWithInitial],
} )
const selectOptions = t5; const initialFocusValue = useMemo(
let t6; () =>
if ($[14] !== initialValue || $[15] !== selectOptions) { selectOptions.some(_ => _.value === initialValue)
t6 = selectOptions.some(_ => _.value === initialValue) ? initialValue : selectOptions[0]?.value ?? undefined; ? initialValue
$[14] = initialValue; : (selectOptions[0]?.value ?? undefined),
$[15] = selectOptions; [selectOptions, initialValue],
$[16] = t6; )
} else { const visibleCount = Math.min(maxVisible, selectOptions.length)
t6 = $[16]; const hiddenCount = Math.max(0, selectOptions.length - visibleCount)
}
const initialFocusValue = t6; const focusedModelName = selectOptions.find(
const visibleCount = Math.min(10, selectOptions.length); opt => opt.value === focusedValue,
const hiddenCount = Math.max(0, selectOptions.length - visibleCount); )?.label
let t7; const focusedModel = resolveOptionModel(focusedValue)
if ($[17] !== focusedValue || $[18] !== selectOptions) { const focusedSupportsEffort = focusedModel
t7 = selectOptions.find(opt_1 => opt_1.value === focusedValue)?.label; ? modelSupportsEffort(focusedModel)
$[17] = focusedValue; : false
$[18] = selectOptions; const focusedSupportsMax = focusedModel
$[19] = t7; ? modelSupportsMaxEffort(focusedModel)
} else { : false
t7 = $[19]; const focusedDefaultEffort = getDefaultEffortLevelForOption(focusedValue)
} // Clamp display when 'max' is selected but the focused model doesn't support it.
const focusedModelName = t7; // resolveAppliedEffort() does the same downgrade at API-send time.
let focusedSupportsEffort; const displayEffort =
let t8; effort === 'max' && !focusedSupportsMax ? 'high' : effort
if ($[20] !== focusedValue) {
const focusedModel = resolveOptionModel(focusedValue); const handleFocus = useCallback(
focusedSupportsEffort = focusedModel ? modelSupportsEffort(focusedModel) : false; (value: string) => {
t8 = focusedModel ? modelSupportsMaxEffort(focusedModel) : false; setFocusedValue(value)
$[20] = focusedValue;
$[21] = focusedSupportsEffort;
$[22] = t8;
} else {
focusedSupportsEffort = $[21];
t8 = $[22];
}
const focusedSupportsMax = t8;
let t9;
if ($[23] !== focusedValue) {
t9 = getDefaultEffortLevelForOption(focusedValue);
$[23] = focusedValue;
$[24] = t9;
} else {
t9 = $[24];
}
const focusedDefaultEffort = t9;
const displayEffort = effort === "max" && !focusedSupportsMax ? "high" : effort;
let t10;
if ($[25] !== effortValue || $[26] !== hasToggledEffort) {
t10 = value => {
setFocusedValue(value);
if (!hasToggledEffort && effortValue === undefined) { if (!hasToggledEffort && effortValue === undefined) {
setEffort(getDefaultEffortLevelForOption(value)); setEffort(getDefaultEffortLevelForOption(value))
} }
}; },
$[25] = effortValue; [hasToggledEffort, effortValue],
$[26] = hasToggledEffort; )
$[27] = t10;
} else { // Effort level cycling keybindings
t10 = $[27]; const handleCycleEffort = useCallback(
} (direction: 'left' | 'right') => {
const handleFocus = t10; if (!focusedSupportsEffort) return
let t11; setEffort(prev =>
if ($[28] !== focusedDefaultEffort || $[29] !== focusedSupportsEffort || $[30] !== focusedSupportsMax) { cycleEffortLevel(
t11 = direction => { prev ?? focusedDefaultEffort,
if (!focusedSupportsEffort) { direction,
return; focusedSupportsMax,
),
)
setHasToggledEffort(true)
},
[focusedSupportsEffort, focusedSupportsMax, focusedDefaultEffort],
)
useKeybindings(
{
'modelPicker:decreaseEffort': () => handleCycleEffort('left'),
'modelPicker:increaseEffort': () => handleCycleEffort('right'),
},
{ context: 'ModelPicker' },
)
function handleSelect(value: string): void {
logEvent('tengu_model_command_menu_effort', {
effort:
effort as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
if (!skipSettingsWrite) {
// Prior comes from userSettings on disk — NOT merged settings (which
// includes project/policy layers that must not leak into the user's
// global ~/.claude/settings.json), and NOT AppState.effortValue (which
// includes session-ephemeral sources like --effort CLI flag).
// See resolvePickerEffortPersistence JSDoc.
const effortLevel = resolvePickerEffortPersistence(
effort,
getDefaultEffortLevelForOption(value),
getSettingsForSource('userSettings')?.effortLevel,
hasToggledEffort,
)
const persistable = toPersistableEffort(effortLevel)
if (persistable !== undefined) {
updateSettingsForSource('userSettings', { effortLevel: persistable })
} }
setEffort(prev => cycleEffortLevel(prev ?? focusedDefaultEffort, direction, focusedSupportsMax)); setAppState(prev => ({ ...prev, effortValue: effortLevel }))
setHasToggledEffort(true); }
};
$[28] = focusedDefaultEffort; const selectedModel = resolveOptionModel(value)
$[29] = focusedSupportsEffort; const selectedEffort =
$[30] = focusedSupportsMax; hasToggledEffort && selectedModel && modelSupportsEffort(selectedModel)
$[31] = t11; ? effort
} else { : undefined
t11 = $[31]; if (value === NO_PREFERENCE) {
onSelect(null, selectedEffort)
return
}
onSelect(value, selectedEffort)
} }
const handleCycleEffort = t11;
let t12; const content = (
if ($[32] !== handleCycleEffort) { <Box flexDirection="column">
t12 = { <Box flexDirection="column">
"modelPicker:decreaseEffort": () => handleCycleEffort("left"), <Box marginBottom={1} flexDirection="column">
"modelPicker:increaseEffort": () => handleCycleEffort("right") <Text color="remember" bold>
}; Select model
$[32] = handleCycleEffort; </Text>
$[33] = t12; <Text dimColor>
} else { {headerText ??
t12 = $[33]; 'Switch between Claude models. Applies to this session and future Claude Code sessions. For other/previous model names, specify with --model.'}
} </Text>
let t13; {sessionModel && (
if ($[34] === Symbol.for("react.memo_cache_sentinel")) { <Text dimColor>
t13 = { Currently using {modelDisplayString(sessionModel)} for this
context: "ModelPicker" session (set by plan mode). Selecting a model will undo this.
}; </Text>
$[34] = t13; )}
} else { </Box>
t13 = $[34];
} <Box flexDirection="column" marginBottom={1}>
useKeybindings(t12, t13); <Box flexDirection="column">
let t14; <Select
if ($[35] !== effort || $[36] !== hasToggledEffort || $[37] !== onSelect || $[38] !== setAppState || $[39] !== skipSettingsWrite) { defaultValue={initialValue}
t14 = function handleSelect(value_0) { defaultFocusValue={initialFocusValue}
logEvent("tengu_model_command_menu_effort", { options={selectOptions}
effort: effort as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS onChange={handleSelect}
}); onFocus={handleFocus}
if (!skipSettingsWrite) { onCancel={onCancel ?? (() => {})}
const effortLevel = resolvePickerEffortPersistence(effort, getDefaultEffortLevelForOption(value_0), getSettingsForSource("userSettings")?.effortLevel, hasToggledEffort); visibleOptionCount={visibleCount}
const persistable = toPersistableEffort(effortLevel); />
if (persistable !== undefined) { </Box>
updateSettingsForSource("userSettings", { {hiddenCount > 0 && (
effortLevel: persistable <Box paddingLeft={3}>
}); <Text dimColor>and {hiddenCount} more</Text>
} </Box>
setAppState(prev_0 => ({ )}
...prev_0, </Box>
effortValue: effortLevel
})); <Box marginBottom={1} flexDirection="column">
} {focusedSupportsEffort ? (
const selectedModel = resolveOptionModel(value_0); <Text dimColor>
const selectedEffort = hasToggledEffort && selectedModel && modelSupportsEffort(selectedModel) ? effort : undefined; <EffortLevelIndicator effort={displayEffort} />{' '}
if (value_0 === NO_PREFERENCE) { {capitalize(displayEffort)} effort
onSelect(null, selectedEffort); {displayEffort === focusedDefaultEffort ? ` (default)` : ``}{' '}
return; <Text color="subtle"> to adjust</Text>
} </Text>
onSelect(value_0, selectedEffort); ) : (
}; <Text color="subtle">
$[35] = effort; <EffortLevelIndicator effort={undefined} /> Effort not supported
$[36] = hasToggledEffort; {focusedModelName ? ` for ${focusedModelName}` : ''}
$[37] = onSelect; </Text>
$[38] = setAppState; )}
$[39] = skipSettingsWrite; </Box>
$[40] = t14;
} else { {isFastModeEnabled() ? (
t14 = $[40]; showFastModeNotice ? (
} <Box marginBottom={1}>
const handleSelect = t14; <Text dimColor>
let t15; Fast mode is <Text bold>ON</Text> and available with{' '}
if ($[41] === Symbol.for("react.memo_cache_sentinel")) { {FAST_MODE_MODEL_DISPLAY} only (/fast). Switching to other
t15 = <Text color="remember" bold={true}>Select model</Text>; models turn off fast mode.
$[41] = t15; </Text>
} else { </Box>
t15 = $[41]; ) : isFastModeAvailable() && !isFastModeCooldown() ? (
} <Box marginBottom={1}>
const t16 = headerText ?? "Switch between Claude models. Applies to this session and future Claude Code sessions. For other/previous model names, specify with --model."; <Text dimColor>
let t17; Use <Text bold>/fast</Text> to turn on Fast mode (
if ($[42] !== t16) { {FAST_MODE_MODEL_DISPLAY} only).
t17 = <Text dimColor={true}>{t16}</Text>; </Text>
$[42] = t16; </Box>
$[43] = t17; ) : null
} else { ) : null}
t17 = $[43]; </Box>
}
let t18; {isStandaloneCommand && (
if ($[44] !== sessionModel) { <Text dimColor italic>
t18 = sessionModel && <Text dimColor={true}>Currently using {modelDisplayString(sessionModel)} for this session (set by plan mode). Selecting a model will undo this.</Text>; {exitState.pending ? (
$[44] = sessionModel; <>Press {exitState.keyName} again to exit</>
$[45] = t18; ) : (
} else { <Byline>
t18 = $[45]; <KeyboardShortcutHint shortcut="Enter" action="confirm" />
} <ConfigurableShortcutHint
let t19; action="select:cancel"
if ($[46] !== t17 || $[47] !== t18) { context="Select"
t19 = <Box marginBottom={1} flexDirection="column">{t15}{t17}{t18}</Box>; fallback="Esc"
$[46] = t17; description="exit"
$[47] = t18; />
$[48] = t19; </Byline>
} else { )}
t19 = $[48]; </Text>
} )}
const t20 = onCancel ?? _temp4; </Box>
let t21; )
if ($[49] !== handleFocus || $[50] !== handleSelect || $[51] !== initialFocusValue || $[52] !== initialValue || $[53] !== selectOptions || $[54] !== t20 || $[55] !== visibleCount) {
t21 = <Box flexDirection="column"><Select defaultValue={initialValue} defaultFocusValue={initialFocusValue} options={selectOptions} onChange={handleSelect} onFocus={handleFocus} onCancel={t20} visibleOptionCount={visibleCount} /></Box>;
$[49] = handleFocus;
$[50] = handleSelect;
$[51] = initialFocusValue;
$[52] = initialValue;
$[53] = selectOptions;
$[54] = t20;
$[55] = visibleCount;
$[56] = t21;
} else {
t21 = $[56];
}
let t22;
if ($[57] !== hiddenCount) {
t22 = hiddenCount > 0 && <Box paddingLeft={3}><Text dimColor={true}>and {hiddenCount} more</Text></Box>;
$[57] = hiddenCount;
$[58] = t22;
} else {
t22 = $[58];
}
let t23;
if ($[59] !== t21 || $[60] !== t22) {
t23 = <Box flexDirection="column" marginBottom={1}>{t21}{t22}</Box>;
$[59] = t21;
$[60] = t22;
$[61] = t23;
} else {
t23 = $[61];
}
let t24;
if ($[62] !== displayEffort || $[63] !== focusedDefaultEffort || $[64] !== focusedModelName || $[65] !== focusedSupportsEffort) {
t24 = <Box marginBottom={1} flexDirection="column">{focusedSupportsEffort ? <Text dimColor={true}><EffortLevelIndicator effort={displayEffort} />{" "}{capitalize(displayEffort)} effort{displayEffort === focusedDefaultEffort ? " (default)" : ""}{" "}<Text color="subtle"> to adjust</Text></Text> : <Text color="subtle"><EffortLevelIndicator effort={undefined} /> Effort not supported{focusedModelName ? ` for ${focusedModelName}` : ""}</Text>}</Box>;
$[62] = displayEffort;
$[63] = focusedDefaultEffort;
$[64] = focusedModelName;
$[65] = focusedSupportsEffort;
$[66] = t24;
} else {
t24 = $[66];
}
let t25;
if ($[67] !== showFastModeNotice) {
t25 = isFastModeEnabled() ? showFastModeNotice ? <Box marginBottom={1}><Text dimColor={true}>Fast mode is <Text bold={true}>ON</Text> and available with{" "}{FAST_MODE_MODEL_DISPLAY} only (/fast). Switching to other models turn off fast mode.</Text></Box> : isFastModeAvailable() && !isFastModeCooldown() ? <Box marginBottom={1}><Text dimColor={true}>Use <Text bold={true}>/fast</Text> to turn on Fast mode ({FAST_MODE_MODEL_DISPLAY} only).</Text></Box> : null : null;
$[67] = showFastModeNotice;
$[68] = t25;
} else {
t25 = $[68];
}
let t26;
if ($[69] !== t19 || $[70] !== t23 || $[71] !== t24 || $[72] !== t25) {
t26 = <Box flexDirection="column">{t19}{t23}{t24}{t25}</Box>;
$[69] = t19;
$[70] = t23;
$[71] = t24;
$[72] = t25;
$[73] = t26;
} else {
t26 = $[73];
}
let t27;
if ($[74] !== exitState || $[75] !== isStandaloneCommand) {
t27 = isStandaloneCommand && <Text dimColor={true} italic={true}>{exitState.pending ? <>Press {exitState.keyName} again to exit</> : <Byline><KeyboardShortcutHint shortcut="Enter" action="confirm" /><ConfigurableShortcutHint action="select:cancel" context="Select" fallback="Esc" description="exit" /></Byline>}</Text>;
$[74] = exitState;
$[75] = isStandaloneCommand;
$[76] = t27;
} else {
t27 = $[76];
}
let t28;
if ($[77] !== t26 || $[78] !== t27) {
t28 = <Box flexDirection="column">{t26}{t27}</Box>;
$[77] = t26;
$[78] = t27;
$[79] = t28;
} else {
t28 = $[79];
}
const content = t28;
if (!isStandaloneCommand) { if (!isStandaloneCommand) {
return content; return content
} }
let t29;
if ($[80] !== content) { return <Pane color="permission">{content}</Pane>
t29 = <Pane color="permission">{content}</Pane>;
$[80] = content;
$[81] = t29;
} else {
t29 = $[81];
}
return t29;
}
function _temp4() {}
function _temp3(opt_0) {
return {
...opt_0,
value: opt_0.value === null ? NO_PREFERENCE : opt_0.value
};
}
function _temp2(s_0) {
return s_0.effortValue;
}
function _temp(s) {
return isFastModeEnabled() ? s.fastMode : false;
} }
function resolveOptionModel(value?: string): string | undefined { function resolveOptionModel(value?: string): string | undefined {
if (!value) return undefined; if (!value) return undefined
return value === NO_PREFERENCE ? getDefaultMainLoopModel() : parseUserSpecifiedModel(value); return value === NO_PREFERENCE
? getDefaultMainLoopModel()
: parseUserSpecifiedModel(value)
} }
function EffortLevelIndicator(t0) {
const $ = _c(5); function EffortLevelIndicator({
const { effort,
effort }: {
} = t0; effort?: EffortLevel
const t1 = effort ? "claude" : "subtle"; }): React.ReactNode {
const t2 = effort ?? "low"; return (
let t3; <Text color={effort ? 'claude' : 'subtle'}>
if ($[0] !== t2) { {effortLevelToSymbol(effort ?? 'low')}
t3 = effortLevelToSymbol(t2); </Text>
$[0] = t2; )
$[1] = t3;
} else {
t3 = $[1];
}
let t4;
if ($[2] !== t1 || $[3] !== t3) {
t4 = <Text color={t1}>{t3}</Text>;
$[2] = t1;
$[3] = t3;
$[4] = t4;
} else {
t4 = $[4];
}
return t4;
} }
function cycleEffortLevel(current: EffortLevel, direction: 'left' | 'right', includeMax: boolean): EffortLevel {
const levels: EffortLevel[] = includeMax ? ['low', 'medium', 'high', 'max'] : ['low', 'medium', 'high']; function cycleEffortLevel(
current: EffortLevel,
direction: 'left' | 'right',
includeMax: boolean,
): EffortLevel {
const levels: EffortLevel[] = includeMax
? ['low', 'medium', 'high', 'max']
: ['low', 'medium', 'high']
// If the current level isn't in the cycle (e.g. 'max' after switching to a // If the current level isn't in the cycle (e.g. 'max' after switching to a
// non-Opus model), clamp to 'high'. // non-Opus model), clamp to 'high'.
const idx = levels.indexOf(current); const idx = levels.indexOf(current)
const currentIndex = idx !== -1 ? idx : levels.indexOf('high'); const currentIndex = idx !== -1 ? idx : levels.indexOf('high')
if (direction === 'right') { if (direction === 'right') {
return levels[(currentIndex + 1) % levels.length]!; return levels[(currentIndex + 1) % levels.length]!
} else { } else {
return levels[(currentIndex - 1 + levels.length) % levels.length]!; return levels[(currentIndex - 1 + levels.length) % levels.length]!
} }
} }
function getDefaultEffortLevelForOption(value?: string): EffortLevel { function getDefaultEffortLevelForOption(value?: string): EffortLevel {
const resolved = resolveOptionModel(value) ?? getDefaultMainLoopModel(); const resolved = resolveOptionModel(value) ?? getDefaultMainLoopModel()
const defaultValue = getDefaultEffortForModel(resolved); const defaultValue = getDefaultEffortForModel(resolved)
return defaultValue !== undefined ? convertEffortValueToLevel(defaultValue) : 'high'; return defaultValue !== undefined
? convertEffortValueToLevel(defaultValue)
: 'high'
} }

View File

@@ -1,134 +1,152 @@
import * as React from 'react'; import * as React from 'react'
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react'
import { logEvent } from 'src/services/analytics/index.js'; import { logEvent } from 'src/services/analytics/index.js'
import { logForDebugging } from 'src/utils/debug.js'; import { logForDebugging } from 'src/utils/debug.js'
import { logError } from 'src/utils/log.js'; import { logError } from 'src/utils/log.js'
import { useInterval } from 'usehooks-ts'; import { useInterval } from 'usehooks-ts'
import { useUpdateNotification } from '../hooks/useUpdateNotification.js'; import { useUpdateNotification } from '../hooks/useUpdateNotification.js'
import { Box, Text } from '../ink.js'; import { Box, Text } from '../ink.js'
import type { AutoUpdaterResult } from '../utils/autoUpdater.js'; import type { AutoUpdaterResult } from '../utils/autoUpdater.js'
import { getMaxVersion, getMaxVersionMessage } from '../utils/autoUpdater.js'; import { getMaxVersion, getMaxVersionMessage } from '../utils/autoUpdater.js'
import { isAutoUpdaterDisabled } from '../utils/config.js'; import { isAutoUpdaterDisabled } from '../utils/config.js'
import { installLatest } from '../utils/nativeInstaller/index.js'; import { installLatest } from '../utils/nativeInstaller/index.js'
import { gt } from '../utils/semver.js'; import { gt } from '../utils/semver.js'
import { getInitialSettings } from '../utils/settings/settings.js'; import { getInitialSettings } from '../utils/settings/settings.js'
/** /**
* Categorize error messages for analytics * Categorize error messages for analytics
*/ */
function getErrorType(errorMessage: string): string { function getErrorType(errorMessage: string): string {
if (errorMessage.includes('timeout')) { if (errorMessage.includes('timeout')) {
return 'timeout'; return 'timeout'
} }
if (errorMessage.includes('Checksum mismatch')) { if (errorMessage.includes('Checksum mismatch')) {
return 'checksum_mismatch'; return 'checksum_mismatch'
} }
if (errorMessage.includes('ENOENT') || errorMessage.includes('not found')) { if (errorMessage.includes('ENOENT') || errorMessage.includes('not found')) {
return 'not_found'; return 'not_found'
} }
if (errorMessage.includes('EACCES') || errorMessage.includes('permission')) { if (errorMessage.includes('EACCES') || errorMessage.includes('permission')) {
return 'permission_denied'; return 'permission_denied'
} }
if (errorMessage.includes('ENOSPC')) { if (errorMessage.includes('ENOSPC')) {
return 'disk_full'; return 'disk_full'
} }
if (errorMessage.includes('npm')) { if (errorMessage.includes('npm')) {
return 'npm_error'; return 'npm_error'
} }
if (errorMessage.includes('network') || errorMessage.includes('ECONNREFUSED') || errorMessage.includes('ENOTFOUND')) { if (
return 'network_error'; errorMessage.includes('network') ||
errorMessage.includes('ECONNREFUSED') ||
errorMessage.includes('ENOTFOUND')
) {
return 'network_error'
} }
return 'unknown'; return 'unknown'
} }
type Props = { type Props = {
isUpdating: boolean; isUpdating: boolean
onChangeIsUpdating: (isUpdating: boolean) => void; onChangeIsUpdating: (isUpdating: boolean) => void
onAutoUpdaterResult: (autoUpdaterResult: AutoUpdaterResult) => void; onAutoUpdaterResult: (autoUpdaterResult: AutoUpdaterResult) => void
autoUpdaterResult: AutoUpdaterResult | null; autoUpdaterResult: AutoUpdaterResult | null
showSuccessMessage: boolean; showSuccessMessage: boolean
verbose: boolean; verbose: boolean
}; }
export function NativeAutoUpdater({ export function NativeAutoUpdater({
isUpdating, isUpdating,
onChangeIsUpdating, onChangeIsUpdating,
onAutoUpdaterResult, onAutoUpdaterResult,
autoUpdaterResult, autoUpdaterResult,
showSuccessMessage, showSuccessMessage,
verbose verbose,
}: Props): React.ReactNode { }: Props): React.ReactNode {
const [versions, setVersions] = useState<{ const [versions, setVersions] = useState<{
current?: string | null; current?: string | null
latest?: string | null; latest?: string | null
}>({}); }>({})
const [maxVersionIssue, setMaxVersionIssue] = useState<string | null>(null); const [maxVersionIssue, setMaxVersionIssue] = useState<string | null>(null)
const updateSemver = useUpdateNotification(autoUpdaterResult?.version); const updateSemver = useUpdateNotification(autoUpdaterResult?.version)
const channel = getInitialSettings()?.autoUpdatesChannel ?? 'latest'; const channel = getInitialSettings()?.autoUpdatesChannel ?? 'latest'
// Track latest isUpdating value in a ref so the memoized checkForUpdates // Track latest isUpdating value in a ref so the memoized checkForUpdates
// callback always sees the current value without changing callback identity // callback always sees the current value without changing callback identity
// (which would re-trigger the initial-check useEffect below and cause // (which would re-trigger the initial-check useEffect below and cause
// repeated downloads on remount — the upstream trigger for #22413). // repeated downloads on remount — the upstream trigger for #22413).
const isUpdatingRef = useRef(isUpdating); const isUpdatingRef = useRef(isUpdating)
isUpdatingRef.current = isUpdating; isUpdatingRef.current = isUpdating
const checkForUpdates = React.useCallback(async () => { const checkForUpdates = React.useCallback(async () => {
if (isUpdatingRef.current) { if (isUpdatingRef.current) {
return; return
} }
if (("production" as string) === 'test' || ("production" as string) === 'development') {
logForDebugging('NativeAutoUpdater: Skipping update check in test/dev environment'); if (
return; "production" === 'test' ||
"production" === 'development'
) {
logForDebugging(
'NativeAutoUpdater: Skipping update check in test/dev environment',
)
return
} }
if (isAutoUpdaterDisabled()) { if (isAutoUpdaterDisabled()) {
return; return
} }
onChangeIsUpdating(true);
const startTime = Date.now(); onChangeIsUpdating(true)
const startTime = Date.now()
// Log the start of an auto-update check for funnel analysis // Log the start of an auto-update check for funnel analysis
logEvent('tengu_native_auto_updater_start', {}); logEvent('tengu_native_auto_updater_start', {})
try { try {
// Check if current version is above the max allowed version // Check if current version is above the max allowed version
const maxVersion = await getMaxVersion(); const maxVersion = await getMaxVersion()
if (maxVersion && gt(MACRO.VERSION, maxVersion)) { if (maxVersion && gt(MACRO.VERSION, maxVersion)) {
const msg = await getMaxVersionMessage(); const msg = await getMaxVersionMessage()
setMaxVersionIssue(msg ?? 'affects your version'); setMaxVersionIssue(msg ?? 'affects your version')
} }
const result = await installLatest(channel);
const currentVersion = MACRO.VERSION; const result = await installLatest(channel)
const latencyMs = Date.now() - startTime; const currentVersion = MACRO.VERSION
const latencyMs = Date.now() - startTime
// Handle lock contention gracefully - just return without treating as error // Handle lock contention gracefully - just return without treating as error
if (result.lockFailed) { if (result.lockFailed) {
logEvent('tengu_native_auto_updater_lock_contention', { logEvent('tengu_native_auto_updater_lock_contention', {
latency_ms: latencyMs latency_ms: latencyMs,
}); })
return; // Silently skip this update check, will try again later return // Silently skip this update check, will try again later
} }
// Update versions for display // Update versions for display
setVersions({ setVersions({ current: currentVersion, latest: result.latestVersion })
current: currentVersion,
latest: result.latestVersion
});
if (result.wasUpdated) { if (result.wasUpdated) {
logEvent('tengu_native_auto_updater_success', { logEvent('tengu_native_auto_updater_success', {
latency_ms: latencyMs latency_ms: latencyMs,
}); })
onAutoUpdaterResult({ onAutoUpdaterResult({
version: result.latestVersion, version: result.latestVersion,
status: 'success' status: 'success',
}); })
} else { } else {
// Already up to date // Already up to date
logEvent('tengu_native_auto_updater_up_to_date', { logEvent('tengu_native_auto_updater_up_to_date', {
latency_ms: latencyMs latency_ms: latencyMs,
}); })
} }
} catch (error) { } catch (error) {
const latencyMs = Date.now() - startTime; const latencyMs = Date.now() - startTime
const errorMessage = error instanceof Error ? error.message : String(error); const errorMessage =
logError(error); error instanceof Error ? error.message : String(error)
const errorType = getErrorType(errorMessage); logError(error)
const errorType = getErrorType(errorMessage)
logEvent('tengu_native_auto_updater_fail', { logEvent('tengu_native_auto_updater_fail', {
latency_ms: latencyMs, latency_ms: latencyMs,
error_timeout: errorType === 'timeout', error_timeout: errorType === 'timeout',
@@ -137,56 +155,77 @@ export function NativeAutoUpdater({
error_permission: errorType === 'permission_denied', error_permission: errorType === 'permission_denied',
error_disk_full: errorType === 'disk_full', error_disk_full: errorType === 'disk_full',
error_npm: errorType === 'npm_error', error_npm: errorType === 'npm_error',
error_network: errorType === 'network_error' error_network: errorType === 'network_error',
}); })
onAutoUpdaterResult({ onAutoUpdaterResult({
version: null, version: null,
status: 'install_failed' status: 'install_failed',
}); })
} finally { } finally {
onChangeIsUpdating(false); onChangeIsUpdating(false)
} }
// isUpdating intentionally omitted from deps; we read isUpdatingRef // isUpdating intentionally omitted from deps; we read isUpdatingRef
// instead so the guard is always current without changing callback // instead so the guard is always current without changing callback
// identity (which would re-trigger the initial-check useEffect below). // identity (which would re-trigger the initial-check useEffect below).
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
// biome-ignore lint/correctness/useExhaustiveDependencies: isUpdating read via ref // biome-ignore lint/correctness/useExhaustiveDependencies: isUpdating read via ref
}, [onAutoUpdaterResult, channel]); }, [onAutoUpdaterResult, channel])
// Initial check // Initial check
useEffect(() => { useEffect(() => {
void checkForUpdates(); void checkForUpdates()
}, [checkForUpdates]); }, [checkForUpdates])
// Check every 30 minutes // Check every 30 minutes
useInterval(checkForUpdates, 30 * 60 * 1000); useInterval(checkForUpdates, 30 * 60 * 1000)
const hasUpdateResult = !!autoUpdaterResult?.version;
const hasVersionInfo = !!versions.current && !!versions.latest; const hasUpdateResult = !!autoUpdaterResult?.version
const hasVersionInfo = !!versions.current && !!versions.latest
// Show the component when: // Show the component when:
// - warning banner needed (above max version), or // - warning banner needed (above max version), or
// - there's an update result to display (success/error), or // - there's an update result to display (success/error), or
// - actively checking and we have version info to show // - actively checking and we have version info to show
const shouldRender = !!maxVersionIssue || hasUpdateResult || isUpdating && hasVersionInfo; const shouldRender =
!!maxVersionIssue || hasUpdateResult || (isUpdating && hasVersionInfo)
if (!shouldRender) { if (!shouldRender) {
return null; return null
} }
return <Box flexDirection="row" gap={1}>
{verbose && <Text dimColor wrap="truncate"> return (
<Box flexDirection="row" gap={1}>
{verbose && (
<Text dimColor wrap="truncate">
current: {versions.current} &middot; {channel}: {versions.latest} current: {versions.current} &middot; {channel}: {versions.latest}
</Text>} </Text>
{isUpdating ? <Box> )}
{isUpdating ? (
<Box>
<Text dimColor wrap="truncate"> <Text dimColor wrap="truncate">
Checking for updates Checking for updates
</Text> </Text>
</Box> : autoUpdaterResult?.status === 'success' && showSuccessMessage && updateSemver && <Text color="success" wrap="truncate"> </Box>
) : (
autoUpdaterResult?.status === 'success' &&
showSuccessMessage &&
updateSemver && (
<Text color="success" wrap="truncate">
Update installed · Restart to update Update installed · Restart to update
</Text>} </Text>
{autoUpdaterResult?.status === 'install_failed' && <Text color="error" wrap="truncate"> )
)}
{autoUpdaterResult?.status === 'install_failed' && (
<Text color="error" wrap="truncate">
Auto-update failed &middot; Try <Text bold>/status</Text> Auto-update failed &middot; Try <Text bold>/status</Text>
</Text>} </Text>
{maxVersionIssue && (process.env.USER_TYPE) === 'ant' && <Text color="warning"> )}
{maxVersionIssue && process.env.USER_TYPE === 'ant' && (
<Text color="warning">
Known issue: {maxVersionIssue} &middot; Run{' '} Known issue: {maxVersionIssue} &middot; Run{' '}
<Text bold>claude rollback --safe</Text> to downgrade <Text bold>claude rollback --safe</Text> to downgrade
</Text>} </Text>
</Box>; )}
</Box>
)
} }

View File

@@ -1,91 +1,49 @@
import { c as _c } from "react/compiler-runtime"; import { relative } from 'path'
import { relative } from 'path'; import * as React from 'react'
import * as React from 'react'; import { getCwd } from 'src/utils/cwd.js'
import { getCwd } from 'src/utils/cwd.js'; import { Box, Text } from '../ink.js'
import { Box, Text } from '../ink.js'; import { HighlightedCode } from './HighlightedCode.js'
import { HighlightedCode } from './HighlightedCode.js'; import { MessageResponse } from './MessageResponse.js'
import { MessageResponse } from './MessageResponse.js';
type Props = { type Props = {
notebook_path: string; notebook_path: string
cell_id: string | undefined; cell_id: string | undefined
new_source: string; new_source: string
cell_type?: 'code' | 'markdown'; cell_type?: 'code' | 'markdown'
edit_mode?: 'replace' | 'insert' | 'delete'; edit_mode?: 'replace' | 'insert' | 'delete'
verbose: boolean; verbose: boolean
}; }
export function NotebookEditToolUseRejectedMessage(t0) {
const $ = _c(20); export function NotebookEditToolUseRejectedMessage({
const { notebook_path,
notebook_path, cell_id,
cell_id, new_source,
new_source, cell_type,
cell_type, edit_mode = 'replace',
edit_mode: t1, verbose,
verbose }: Props): React.ReactNode {
} = t0; const operation = edit_mode === 'delete' ? 'delete' : `${edit_mode} cell in`
const edit_mode = t1 === undefined ? "replace" : t1;
const operation = edit_mode === "delete" ? "delete" : `${edit_mode} cell in`; return (
let t2; <MessageResponse>
if ($[0] !== operation) { <Box flexDirection="column">
t2 = <Text color="subtle">User rejected {operation} </Text>; <Box flexDirection="row">
$[0] = operation; <Text color="subtle">User rejected {operation} </Text>
$[1] = t2; <Text bold color="subtle">
} else { {verbose ? notebook_path : relative(getCwd(), notebook_path)}
t2 = $[1]; </Text>
} <Text color="subtle"> at cell {cell_id}</Text>
let t3; </Box>
if ($[2] !== notebook_path || $[3] !== verbose) { {edit_mode !== 'delete' && (
t3 = verbose ? notebook_path : relative(getCwd(), notebook_path); <Box marginTop={1} flexDirection="column">
$[2] = notebook_path; <HighlightedCode
$[3] = verbose; code={new_source}
$[4] = t3; filePath={cell_type === 'markdown' ? 'file.md' : 'file.py'}
} else { dim
t3 = $[4]; />
} </Box>
let t4; )}
if ($[5] !== t3) { </Box>
t4 = <Text bold={true} color="subtle">{t3}</Text>; </MessageResponse>
$[5] = t3; )
$[6] = t4;
} else {
t4 = $[6];
}
let t5;
if ($[7] !== cell_id) {
t5 = <Text color="subtle"> at cell {cell_id}</Text>;
$[7] = cell_id;
$[8] = t5;
} else {
t5 = $[8];
}
let t6;
if ($[9] !== t2 || $[10] !== t4 || $[11] !== t5) {
t6 = <Box flexDirection="row">{t2}{t4}{t5}</Box>;
$[9] = t2;
$[10] = t4;
$[11] = t5;
$[12] = t6;
} else {
t6 = $[12];
}
let t7;
if ($[13] !== cell_type || $[14] !== edit_mode || $[15] !== new_source) {
t7 = edit_mode !== "delete" && <Box marginTop={1} flexDirection="column"><HighlightedCode code={new_source} filePath={cell_type === "markdown" ? "file.md" : "file.py"} dim={true} /></Box>;
$[13] = cell_type;
$[14] = edit_mode;
$[15] = new_source;
$[16] = t7;
} else {
t7 = $[16];
}
let t8;
if ($[17] !== t6 || $[18] !== t7) {
t8 = <MessageResponse><Box flexDirection="column">{t6}{t7}</Box></MessageResponse>;
$[17] = t6;
$[18] = t7;
$[19] = t8;
} else {
t8 = $[19];
}
return t8;
} }

View File

@@ -1,10 +1,11 @@
import React, { useContext, useRef } from 'react'; import React, { useContext, useRef } from 'react'
import { useTerminalViewport } from '../ink/hooks/use-terminal-viewport.js'; import { useTerminalViewport } from '../ink/hooks/use-terminal-viewport.js'
import { Box } from '../ink.js'; import { Box } from '../ink.js'
import { InVirtualListContext } from './messageActions.js'; import { InVirtualListContext } from './messageActions.js'
type Props = { type Props = {
children: React.ReactNode; children: React.ReactNode
}; }
/** /**
* Freezes children when they scroll above the terminal viewport (into scrollback). * Freezes children when they scroll above the terminal viewport (into scrollback).
@@ -20,24 +21,19 @@ type Props = {
* The cache is one slot deep: the first re-render after scrolling back into view * The cache is one slot deep: the first re-render after scrolling back into view
* picks up the live children. Content still updates normally while visible. * picks up the live children. Content still updates normally while visible.
*/ */
export function OffscreenFreeze({ export function OffscreenFreeze({ children }: Props): React.ReactNode {
children
}: Props): React.ReactNode {
// React Compiler: reading cached.current in the return is the entire // React Compiler: reading cached.current in the return is the entire
// freeze mechanism — memoizing this component would defeat it. Opt out. // freeze mechanism — memoizing this component would defeat it. Opt out.
'use no memo'; 'use no memo'
const inVirtualList = useContext(InVirtualListContext)
const inVirtualList = useContext(InVirtualListContext); const [ref, { isVisible }] = useTerminalViewport()
const [ref, { const cached = useRef(children)
isVisible
}] = useTerminalViewport();
const cached = useRef(children);
// Virtual list has no terminal scrollback — the ScrollBox clips inside the // Virtual list has no terminal scrollback — the ScrollBox clips inside the
// viewport, so there's nothing to freeze. Freezing there also blocks // viewport, so there's nothing to freeze. Freezing there also blocks
// click-to-expand since useTerminalViewport's visibility calc can disagree // click-to-expand since useTerminalViewport's visibility calc can disagree
// with the ScrollBox's virtual scroll position. // with the ScrollBox's virtual scroll position.
if (isVisible || inVirtualList) { if (isVisible || inVirtualList) {
cached.current = children; cached.current = children
} }
return <Box ref={ref}>{cached.current}</Box>; return <Box ref={ref}>{cached.current}</Box>
} }

View File

@@ -1,68 +1,96 @@
import { c as _c } from "react/compiler-runtime"; import React, { useCallback, useEffect, useMemo, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import {
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
import { setupTerminal, shouldOfferTerminalSetup } from '../commands/terminalSetup/terminalSetup.js'; logEvent,
import { useExitOnCtrlCDWithKeybindings } from '../hooks/useExitOnCtrlCDWithKeybindings.js'; } from 'src/services/analytics/index.js'
import { Box, Link, Newline, Text, useTheme } from '../ink.js'; import {
import { useKeybindings } from '../keybindings/useKeybinding.js'; setupTerminal,
import { isAnthropicAuthEnabled } from '../utils/auth.js'; shouldOfferTerminalSetup,
import { normalizeApiKeyForConfig } from '../utils/authPortable.js'; } from '../commands/terminalSetup/terminalSetup.js'
import { getCustomApiKeyStatus } from '../utils/config.js'; import { useExitOnCtrlCDWithKeybindings } from '../hooks/useExitOnCtrlCDWithKeybindings.js'
import { env } from '../utils/env.js'; import { Box, Link, Newline, Text, useTheme } from '../ink.js'
import { isRunningOnHomespace } from '../utils/envUtils.js'; import { useKeybindings } from '../keybindings/useKeybinding.js'
import { PreflightStep } from '../utils/preflightChecks.js'; import { isAnthropicAuthEnabled } from '../utils/auth.js'
import type { ThemeSetting } from '../utils/theme.js'; import { normalizeApiKeyForConfig } from '../utils/authPortable.js'
import { ApproveApiKey } from './ApproveApiKey.js'; import { getCustomApiKeyStatus } from '../utils/config.js'
import { ConsoleOAuthFlow } from './ConsoleOAuthFlow.js'; import { env } from '../utils/env.js'
import { Select } from './CustomSelect/select.js'; import { isRunningOnHomespace } from '../utils/envUtils.js'
import { WelcomeV2 } from './LogoV2/WelcomeV2.js'; import { PreflightStep } from '../utils/preflightChecks.js'
import { PressEnterToContinue } from './PressEnterToContinue.js'; import type { ThemeSetting } from '../utils/theme.js'
import { ThemePicker } from './ThemePicker.js'; import { ApproveApiKey } from './ApproveApiKey.js'
import { OrderedList } from './ui/OrderedList.js'; import { ConsoleOAuthFlow } from './ConsoleOAuthFlow.js'
type StepId = 'preflight' | 'theme' | 'oauth' | 'api-key' | 'security' | 'terminal-setup'; import { Select } from './CustomSelect/select.js'
import { WelcomeV2 } from './LogoV2/WelcomeV2.js'
import { PressEnterToContinue } from './PressEnterToContinue.js'
import { ThemePicker } from './ThemePicker.js'
import { OrderedList } from './ui/OrderedList.js'
type StepId =
| 'preflight'
| 'theme'
| 'oauth'
| 'api-key'
| 'security'
| 'terminal-setup'
interface OnboardingStep { interface OnboardingStep {
id: StepId; id: StepId
component: React.ReactNode; component: React.ReactNode
} }
type Props = { type Props = {
onDone(): void; onDone(): void
}; }
export function Onboarding({
onDone export function Onboarding({ onDone }: Props): React.ReactNode {
}: Props): React.ReactNode { const [currentStepIndex, setCurrentStepIndex] = useState(0)
const [currentStepIndex, setCurrentStepIndex] = useState(0); const [skipOAuth, setSkipOAuth] = useState(false)
const [skipOAuth, setSkipOAuth] = useState(false); const [oauthEnabled] = useState(() => isAnthropicAuthEnabled())
const [oauthEnabled] = useState(() => isAnthropicAuthEnabled()); const [theme, setTheme] = useTheme()
const [theme, setTheme] = useTheme();
useEffect(() => { useEffect(() => {
logEvent('tengu_began_setup', { logEvent('tengu_began_setup', {
oauthEnabled oauthEnabled,
}); })
}, [oauthEnabled]); }, [oauthEnabled])
function goToNextStep() { function goToNextStep() {
if (currentStepIndex < steps.length - 1) { if (currentStepIndex < steps.length - 1) {
const nextIndex = currentStepIndex + 1; const nextIndex = currentStepIndex + 1
setCurrentStepIndex(nextIndex); setCurrentStepIndex(nextIndex)
logEvent('tengu_onboarding_step', { logEvent('tengu_onboarding_step', {
oauthEnabled, oauthEnabled,
stepId: steps[nextIndex]?.id as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS stepId: steps[nextIndex]
}); ?.id as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
} else { } else {
onDone(); onDone()
} }
} }
function handleThemeSelection(newTheme: ThemeSetting) { function handleThemeSelection(newTheme: ThemeSetting) {
setTheme(newTheme); setTheme(newTheme)
goToNextStep(); goToNextStep()
} }
const exitState = useExitOnCtrlCDWithKeybindings();
const exitState = useExitOnCtrlCDWithKeybindings()
// Define all onboarding steps // Define all onboarding steps
const themeStep = <Box marginX={1}> const themeStep = (
<ThemePicker onThemeSelect={handleThemeSelection} showIntroText={true} helpText="To change this later, run /theme" hideEscToCancel={true} skipExitHandling={true} // Skip exit handling as Onboarding already handles it <Box marginX={1}>
/> <ThemePicker
</Box>; onThemeSelect={handleThemeSelection}
const securityStep = <Box flexDirection="column" gap={1} paddingLeft={1}> showIntroText={true}
helpText="To change this later, run /theme"
hideEscToCancel={true}
skipExitHandling={true} // Skip exit handling as Onboarding already handles it
/>
</Box>
)
const securityStep = (
<Box flexDirection="column" gap={1} paddingLeft={1}>
<Text bold>Security notes:</Text> <Text bold>Security notes:</Text>
<Box flexDirection="column" width={70}> <Box flexDirection="column" width={70}>
{/** {/**
@@ -92,152 +120,182 @@ export function Onboarding({
</OrderedList> </OrderedList>
</Box> </Box>
<PressEnterToContinue /> <PressEnterToContinue />
</Box>; </Box>
const preflightStep = <PreflightStep onSuccess={goToNextStep} />; )
const preflightStep = <PreflightStep onSuccess={goToNextStep} />
// Create the steps array - determine which steps to include based on reAuth and oauthEnabled // Create the steps array - determine which steps to include based on reAuth and oauthEnabled
const apiKeyNeedingApproval = useMemo(() => { const apiKeyNeedingApproval = useMemo(() => {
// Add API key step if needed // Add API key step if needed
// On homespace, ANTHROPIC_API_KEY is preserved in process.env for child // On homespace, ANTHROPIC_API_KEY is preserved in process.env for child
// processes but ignored by Claude Code itself (see auth.ts). // processes but ignored by Claude Code itself (see auth.ts).
if (!process.env.ANTHROPIC_API_KEY || isRunningOnHomespace()) { if (!process.env.ANTHROPIC_API_KEY || isRunningOnHomespace()) {
return ''; return ''
} }
const customApiKeyTruncated = normalizeApiKeyForConfig(process.env.ANTHROPIC_API_KEY); const customApiKeyTruncated = normalizeApiKeyForConfig(
process.env.ANTHROPIC_API_KEY,
)
if (getCustomApiKeyStatus(customApiKeyTruncated) === 'new') { if (getCustomApiKeyStatus(customApiKeyTruncated) === 'new') {
return customApiKeyTruncated; return customApiKeyTruncated
} }
}, []); }, [])
function handleApiKeyDone(approved: boolean) { function handleApiKeyDone(approved: boolean) {
if (approved) { if (approved) {
setSkipOAuth(true); setSkipOAuth(true)
} }
goToNextStep(); goToNextStep()
} }
const steps: OnboardingStep[] = [];
const steps: OnboardingStep[] = []
if (oauthEnabled) { if (oauthEnabled) {
steps.push({ steps.push({ id: 'preflight', component: preflightStep })
id: 'preflight',
component: preflightStep
});
} }
steps.push({ steps.push({ id: 'theme', component: themeStep })
id: 'theme',
component: themeStep
});
if (apiKeyNeedingApproval) { if (apiKeyNeedingApproval) {
steps.push({ steps.push({
id: 'api-key', id: 'api-key',
component: <ApproveApiKey customApiKeyTruncated={apiKeyNeedingApproval} onDone={handleApiKeyDone} /> component: (
}); <ApproveApiKey
customApiKeyTruncated={apiKeyNeedingApproval}
onDone={handleApiKeyDone}
/>
),
})
} }
if (oauthEnabled) { if (oauthEnabled) {
steps.push({ steps.push({
id: 'oauth', id: 'oauth',
component: <SkippableStep skip={skipOAuth} onSkip={goToNextStep}> component: (
<SkippableStep skip={skipOAuth} onSkip={goToNextStep}>
<ConsoleOAuthFlow onDone={goToNextStep} /> <ConsoleOAuthFlow onDone={goToNextStep} />
</SkippableStep> </SkippableStep>
}); ),
})
} }
steps.push({
id: 'security', steps.push({ id: 'security', component: securityStep })
component: securityStep
});
if (shouldOfferTerminalSetup()) { if (shouldOfferTerminalSetup()) {
steps.push({ steps.push({
id: 'terminal-setup', id: 'terminal-setup',
component: <Box flexDirection="column" gap={1} paddingLeft={1}> component: (
<Box flexDirection="column" gap={1} paddingLeft={1}>
<Text bold>Use Claude Code&apos;s terminal setup?</Text> <Text bold>Use Claude Code&apos;s terminal setup?</Text>
<Box flexDirection="column" width={70} gap={1}> <Box flexDirection="column" width={70} gap={1}>
<Text> <Text>
For the optimal coding experience, enable the recommended settings For the optimal coding experience, enable the recommended settings
<Newline /> <Newline />
for your terminal:{' '} for your terminal:{' '}
{env.terminal === 'Apple_Terminal' ? 'Option+Enter for newlines and visual bell' : 'Shift+Enter for newlines'} {env.terminal === 'Apple_Terminal'
? 'Option+Enter for newlines and visual bell'
: 'Shift+Enter for newlines'}
</Text> </Text>
<Select options={[{ <Select
label: 'Yes, use recommended settings', options={[
value: 'install' {
}, { label: 'Yes, use recommended settings',
label: 'No, maybe later with /terminal-setup', value: 'install',
value: 'no' },
}]} onChange={value => { {
if (value === 'install') { label: 'No, maybe later with /terminal-setup',
// Errors already logged in setupTerminal, just swallow and proceed value: 'no',
void setupTerminal(theme).catch(() => {}).finally(goToNextStep); },
} else { ]}
goToNextStep(); onChange={value => {
} if (value === 'install') {
}} onCancel={() => goToNextStep()} /> // Errors already logged in setupTerminal, just swallow and proceed
void setupTerminal(theme)
.catch(() => {})
.finally(goToNextStep)
} else {
goToNextStep()
}
}}
onCancel={() => goToNextStep()}
/>
<Text dimColor> <Text dimColor>
{exitState.pending ? <>Press {exitState.keyName} again to exit</> : <>Enter to confirm · Esc to skip</>} {exitState.pending ? (
<>Press {exitState.keyName} again to exit</>
) : (
<>Enter to confirm · Esc to skip</>
)}
</Text> </Text>
</Box> </Box>
</Box> </Box>
}); ),
})
} }
const currentStep = steps[currentStepIndex];
const currentStep = steps[currentStepIndex]
// Handle Enter on security step and Escape on terminal-setup step // Handle Enter on security step and Escape on terminal-setup step
// Dependencies match what goToNextStep uses internally // Dependencies match what goToNextStep uses internally
const handleSecurityContinue = useCallback(() => { const handleSecurityContinue = useCallback(() => {
if (currentStepIndex === steps.length - 1) { if (currentStepIndex === steps.length - 1) {
onDone(); onDone()
} else { } else {
goToNextStep(); goToNextStep()
} }
}, [currentStepIndex, steps.length, oauthEnabled, onDone]); }, [currentStepIndex, steps.length, oauthEnabled, onDone])
const handleTerminalSetupSkip = useCallback(() => { const handleTerminalSetupSkip = useCallback(() => {
goToNextStep(); goToNextStep()
}, [currentStepIndex, steps.length, oauthEnabled, onDone]); }, [currentStepIndex, steps.length, oauthEnabled, onDone])
useKeybindings({
'confirm:yes': handleSecurityContinue useKeybindings(
}, { {
context: 'Confirmation', 'confirm:yes': handleSecurityContinue,
isActive: currentStep?.id === 'security' },
}); {
useKeybindings({ context: 'Confirmation',
'confirm:no': handleTerminalSetupSkip isActive: currentStep?.id === 'security',
}, { },
context: 'Confirmation', )
isActive: currentStep?.id === 'terminal-setup'
}); useKeybindings(
return <Box flexDirection="column"> {
'confirm:no': handleTerminalSetupSkip,
},
{
context: 'Confirmation',
isActive: currentStep?.id === 'terminal-setup',
},
)
return (
<Box flexDirection="column">
<WelcomeV2 /> <WelcomeV2 />
<Box flexDirection="column" marginTop={1}> <Box flexDirection="column" marginTop={1}>
{currentStep?.component} {currentStep?.component}
{exitState.pending && <Box padding={1}> {exitState.pending && (
<Box padding={1}>
<Text dimColor>Press {exitState.keyName} again to exit</Text> <Text dimColor>Press {exitState.keyName} again to exit</Text>
</Box>} </Box>
)}
</Box> </Box>
</Box>; </Box>
)
} }
export function SkippableStep(t0) {
const $ = _c(4); export function SkippableStep({
const { skip,
skip, onSkip,
onSkip, children,
children }: {
} = t0; skip: boolean
let t1; onSkip(): void
let t2; children: React.ReactNode
if ($[0] !== onSkip || $[1] !== skip) { }): React.ReactNode {
t1 = () => { useEffect(() => {
if (skip) { if (skip) {
onSkip(); onSkip()
} }
}; }, [skip, onSkip])
t2 = [skip, onSkip];
$[0] = onSkip;
$[1] = skip;
$[2] = t1;
$[3] = t2;
} else {
t1 = $[2];
t2 = $[3];
}
useEffect(t1, t2);
if (skip) { if (skip) {
return null; return null
} }
return children; return children
} }

View File

@@ -1,111 +1,95 @@
import { c as _c } from "react/compiler-runtime"; import * as React from 'react'
import * as React from 'react'; import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'; import {
import { getAllOutputStyles, OUTPUT_STYLE_CONFIG, type OutputStyleConfig } from '../constants/outputStyles.js'; getAllOutputStyles,
import { Box, Text } from '../ink.js'; OUTPUT_STYLE_CONFIG,
import type { OutputStyle } from '../utils/config.js'; type OutputStyleConfig,
import { getCwd } from '../utils/cwd.js'; } from '../constants/outputStyles.js'
import type { OptionWithDescription } from './CustomSelect/select.js'; import { Box, Text } from '../ink.js'
import { Select } from './CustomSelect/select.js'; import type { OutputStyle } from '../utils/config.js'
import { Dialog } from './design-system/Dialog.js'; import { getCwd } from '../utils/cwd.js'
const DEFAULT_OUTPUT_STYLE_LABEL = 'Default'; import type { OptionWithDescription } from './CustomSelect/select.js'
const DEFAULT_OUTPUT_STYLE_DESCRIPTION = 'Claude completes coding tasks efficiently and provides concise responses'; import { Select } from './CustomSelect/select.js'
import { Dialog } from './design-system/Dialog.js'
const DEFAULT_OUTPUT_STYLE_LABEL = 'Default'
const DEFAULT_OUTPUT_STYLE_DESCRIPTION =
'Claude completes coding tasks efficiently and provides concise responses'
function mapConfigsToOptions(styles: { function mapConfigsToOptions(styles: {
[styleName: string]: OutputStyleConfig | null; [styleName: string]: OutputStyleConfig | null
}): OptionWithDescription[] { }): OptionWithDescription[] {
return Object.entries(styles).map(([style, config]) => ({ return Object.entries(styles).map(([style, config]) => ({
label: config?.name ?? DEFAULT_OUTPUT_STYLE_LABEL, label: config?.name ?? DEFAULT_OUTPUT_STYLE_LABEL,
value: style, value: style,
description: config?.description ?? DEFAULT_OUTPUT_STYLE_DESCRIPTION description: config?.description ?? DEFAULT_OUTPUT_STYLE_DESCRIPTION,
})); }))
} }
export type OutputStylePickerProps = { export type OutputStylePickerProps = {
initialStyle: OutputStyle; initialStyle: OutputStyle
onComplete: (style: OutputStyle) => void; onComplete: (style: OutputStyle) => void
onCancel: () => void; onCancel: () => void
isStandaloneCommand?: boolean; isStandaloneCommand?: boolean
}; }
export function OutputStylePicker(t0) {
const $ = _c(16); export function OutputStylePicker({
const { initialStyle,
initialStyle, onComplete,
onComplete, onCancel,
onCancel, isStandaloneCommand,
isStandaloneCommand }: OutputStylePickerProps): React.ReactNode {
} = t0; const [styleOptions, setStyleOptions] = useState<OptionWithDescription[]>([])
let t1; const [isLoading, setIsLoading] = useState(true)
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = []; useEffect(() => {
$[0] = t1; // Load all output styles including custom ones
} else { getAllOutputStyles(getCwd())
t1 = $[0]; .then(allStyles => {
} const options = mapConfigsToOptions(allStyles)
const [styleOptions, setStyleOptions] = useState(t1); setStyleOptions(options)
const [isLoading, setIsLoading] = useState(true); setIsLoading(false)
let t2; })
let t3; .catch(() => {
if ($[1] === Symbol.for("react.memo_cache_sentinel")) { // On error, fall back to built-in styles only
t2 = () => { const builtInOptions = mapConfigsToOptions(OUTPUT_STYLE_CONFIG)
getAllOutputStyles(getCwd()).then(allStyles => { setStyleOptions(builtInOptions)
const options = mapConfigsToOptions(allStyles); setIsLoading(false)
setStyleOptions(options); })
setIsLoading(false); }, [])
}).catch(() => {
const builtInOptions = mapConfigsToOptions(OUTPUT_STYLE_CONFIG); const handleStyleSelect = useCallback(
setStyleOptions(builtInOptions); (style: string) => {
setIsLoading(false); const outputStyle = style as OutputStyle
}); onComplete(outputStyle)
}; },
t3 = []; [onComplete],
$[1] = t2; )
$[2] = t3;
} else { return (
t2 = $[1]; <Dialog
t3 = $[2]; title="Preferred output style"
} onCancel={onCancel}
useEffect(t2, t3); hideInputGuide={!isStandaloneCommand}
let t4; hideBorder={!isStandaloneCommand}
if ($[3] !== onComplete) { >
t4 = style => { <Box flexDirection="column" gap={1}>
const outputStyle = style as OutputStyle; <Box marginTop={1}>
onComplete(outputStyle); <Text dimColor>
}; This changes how Claude Code communicates with you
$[3] = onComplete; </Text>
$[4] = t4; </Box>
} else { {isLoading ? (
t4 = $[4]; <Text dimColor>Loading output styles</Text>
} ) : (
const handleStyleSelect = t4; <Select
const t5 = !isStandaloneCommand; options={styleOptions}
const t6 = !isStandaloneCommand; onChange={handleStyleSelect}
let t7; visibleOptionCount={10}
if ($[5] === Symbol.for("react.memo_cache_sentinel")) { defaultValue={initialStyle}
t7 = <Box marginTop={1}><Text dimColor={true}>This changes how Claude Code communicates with you</Text></Box>; />
$[5] = t7; )}
} else { </Box>
t7 = $[5]; </Dialog>
} )
let t8;
if ($[6] !== handleStyleSelect || $[7] !== initialStyle || $[8] !== isLoading || $[9] !== styleOptions) {
t8 = <Box flexDirection="column" gap={1}>{t7}{isLoading ? <Text dimColor={true}>Loading output styles</Text> : <Select options={styleOptions} onChange={handleStyleSelect} visibleOptionCount={10} defaultValue={initialStyle} />}</Box>;
$[6] = handleStyleSelect;
$[7] = initialStyle;
$[8] = isLoading;
$[9] = styleOptions;
$[10] = t8;
} else {
t8 = $[10];
}
let t9;
if ($[11] !== onCancel || $[12] !== t5 || $[13] !== t6 || $[14] !== t8) {
t9 = <Dialog title="Preferred output style" onCancel={onCancel} hideInputGuide={t5} hideBorder={t6}>{t8}</Dialog>;
$[11] = onCancel;
$[12] = t5;
$[13] = t6;
$[14] = t8;
$[15] = t9;
} else {
t9 = $[15];
}
return t9;
} }

View File

@@ -1,103 +1,119 @@
import { c as _c } from "react/compiler-runtime"; import * as React from 'react'
import * as React from 'react'; import { useState } from 'react'
import { useState } from 'react'; import { useInterval } from 'usehooks-ts'
import { useInterval } from 'usehooks-ts'; import { Text } from '../ink.js'
import { Text } from '../ink.js'; import {
import { type AutoUpdaterResult, getLatestVersionFromGcs, getMaxVersion, shouldSkipVersion } from '../utils/autoUpdater.js'; type AutoUpdaterResult,
import { isAutoUpdaterDisabled } from '../utils/config.js'; getLatestVersionFromGcs,
import { logForDebugging } from '../utils/debug.js'; getMaxVersion,
import { getPackageManager, type PackageManager } from '../utils/nativeInstaller/packageManagers.js'; shouldSkipVersion,
import { gt, gte } from '../utils/semver.js'; } from '../utils/autoUpdater.js'
import { getInitialSettings } from '../utils/settings/settings.js'; import { isAutoUpdaterDisabled } from '../utils/config.js'
import { logForDebugging } from '../utils/debug.js'
import {
getPackageManager,
type PackageManager,
} from '../utils/nativeInstaller/packageManagers.js'
import { gt, gte } from '../utils/semver.js'
import { getInitialSettings } from '../utils/settings/settings.js'
type Props = { type Props = {
isUpdating: boolean; isUpdating: boolean
onChangeIsUpdating: (isUpdating: boolean) => void; onChangeIsUpdating: (isUpdating: boolean) => void
onAutoUpdaterResult: (autoUpdaterResult: AutoUpdaterResult) => void; onAutoUpdaterResult: (autoUpdaterResult: AutoUpdaterResult) => void
autoUpdaterResult: AutoUpdaterResult | null; autoUpdaterResult: AutoUpdaterResult | null
showSuccessMessage: boolean; showSuccessMessage: boolean
verbose: boolean; verbose: boolean
}; }
export function PackageManagerAutoUpdater(t0) {
const $ = _c(10); export function PackageManagerAutoUpdater({ verbose }: Props): React.ReactNode {
const { const [updateAvailable, setUpdateAvailable] = useState(false)
verbose const [packageManager, setPackageManager] =
} = t0; useState<PackageManager>('unknown')
const [updateAvailable, setUpdateAvailable] = useState(false);
const [packageManager, setPackageManager] = useState("unknown"); const checkForUpdates = React.useCallback(async () => {
let t1; if (
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { "production" === 'test' ||
t1 = async () => { "production" === 'development'
false || false; ) {
if (isAutoUpdaterDisabled()) { return
return; }
}
const [channel, pm] = await Promise.all([Promise.resolve(getInitialSettings()?.autoUpdatesChannel ?? "latest"), getPackageManager()]); if (isAutoUpdaterDisabled()) {
setPackageManager(pm); return
let latest = await getLatestVersionFromGcs(channel); }
const maxVersion = await getMaxVersion();
if (maxVersion && latest && gt(latest, maxVersion)) { const [channel, pm] = await Promise.all([
logForDebugging(`PackageManagerAutoUpdater: maxVersion ${maxVersion} is set, capping update from ${latest} to ${maxVersion}`); Promise.resolve(getInitialSettings()?.autoUpdatesChannel ?? 'latest'),
if (gte(MACRO.VERSION, maxVersion)) { getPackageManager(),
logForDebugging(`PackageManagerAutoUpdater: current version ${MACRO.VERSION} is already at or above maxVersion ${maxVersion}, skipping update`); ])
setUpdateAvailable(false); setPackageManager(pm)
return;
} let latest = await getLatestVersionFromGcs(channel)
latest = maxVersion;
} // Check if max version is set (server-side kill switch for auto-updates)
const hasUpdate = latest && !gte(MACRO.VERSION, latest) && !shouldSkipVersion(latest); const maxVersion = await getMaxVersion()
setUpdateAvailable(!!hasUpdate);
if (hasUpdate) { if (maxVersion && latest && gt(latest, maxVersion)) {
logForDebugging(`PackageManagerAutoUpdater: Update available ${MACRO.VERSION} -> ${latest}`); logForDebugging(
} `PackageManagerAutoUpdater: maxVersion ${maxVersion} is set, capping update from ${latest} to ${maxVersion}`,
}; )
$[0] = t1; if (gte(MACRO.VERSION, maxVersion)) {
} else { logForDebugging(
t1 = $[0]; `PackageManagerAutoUpdater: current version ${MACRO.VERSION} is already at or above maxVersion ${maxVersion}, skipping update`,
} )
const checkForUpdates = t1; setUpdateAvailable(false)
let t2; return
let t3; }
if ($[1] === Symbol.for("react.memo_cache_sentinel")) { latest = maxVersion
t2 = () => { }
checkForUpdates();
}; const hasUpdate =
t3 = [checkForUpdates]; latest && !gte(MACRO.VERSION, latest) && !shouldSkipVersion(latest)
$[1] = t2;
$[2] = t3; setUpdateAvailable(!!hasUpdate)
} else {
t2 = $[1]; if (hasUpdate) {
t3 = $[2]; logForDebugging(
} `PackageManagerAutoUpdater: Update available ${MACRO.VERSION} -> ${latest}`,
React.useEffect(t2, t3); )
useInterval(checkForUpdates, 1800000); }
if (!updateAvailable) { }, [])
return null;
} // Initial check
const updateCommand = packageManager === "homebrew" ? "brew upgrade claude-code" : packageManager === "winget" ? "winget upgrade Anthropic.ClaudeCode" : packageManager === "apk" ? "apk upgrade claude-code" : "your package manager update command"; React.useEffect(() => {
let t4; void checkForUpdates()
if ($[3] !== verbose) { }, [checkForUpdates])
t4 = verbose && <Text dimColor={true} wrap="truncate">currentVersion: {MACRO.VERSION}</Text>;
$[3] = verbose; // Check every 30 minutes
$[4] = t4; useInterval(checkForUpdates, 30 * 60 * 1000)
} else {
t4 = $[4]; if (!updateAvailable) {
} return null
let t5; }
if ($[5] !== updateCommand) {
t5 = <Text color="warning" wrap="truncate">Update available! Run: <Text bold={true}>{updateCommand}</Text></Text>; // pacman, deb, and rpm don't get specific commands because they each have
$[5] = updateCommand; // multiple frontends (pacman: yay/paru/makepkg, deb: apt/apt-get/aptitude/nala,
$[6] = t5; // rpm: dnf/yum/zypper)
} else { const updateCommand =
t5 = $[6]; packageManager === 'homebrew'
} ? 'brew upgrade claude-code'
let t6; : packageManager === 'winget'
if ($[7] !== t4 || $[8] !== t5) { ? 'winget upgrade Anthropic.ClaudeCode'
t6 = <>{t4}{t5}</>; : packageManager === 'apk'
$[7] = t4; ? 'apk upgrade claude-code'
$[8] = t5; : 'your package manager update command'
$[9] = t6;
} else { return (
t6 = $[9]; <>
} {verbose && (
return t6; <Text dimColor wrap="truncate">
currentVersion: {MACRO.VERSION}
</Text>
)}
<Text color="warning" wrap="truncate">
Update available! Run: <Text bold>{updateCommand}</Text>
</Text>
</>
)
} }

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