style(B1-4): 格式化 components/PromptInput,FeedbackSurvey,tasks,agents,skills,design-system,wizard (73 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:05 +08:00
parent ae7b92f673
commit 9ba95d209e
73 changed files with 12315 additions and 13264 deletions

View File

@@ -1,173 +1,167 @@
import { c as _c } from "react/compiler-runtime";
import React from 'react';
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
import { Box, Text } from '../../ink.js';
import { FeedbackSurveyView, isValidResponseInput } from './FeedbackSurveyView.js';
import type { TranscriptShareResponse } from './TranscriptSharePrompt.js';
import { TranscriptSharePrompt } from './TranscriptSharePrompt.js';
import { useDebouncedDigitInput } from './useDebouncedDigitInput.js';
import type { FeedbackSurveyResponse } from './utils.js';
import React from 'react'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from 'src/services/analytics/index.js'
import { Box, Text } from '../../ink.js'
import {
FeedbackSurveyView,
isValidResponseInput,
} from './FeedbackSurveyView.js'
import type { TranscriptShareResponse } from './TranscriptSharePrompt.js'
import { TranscriptSharePrompt } from './TranscriptSharePrompt.js'
import { useDebouncedDigitInput } from './useDebouncedDigitInput.js'
import type { FeedbackSurveyResponse } from './utils.js'
type Props = {
state: 'closed' | 'open' | 'thanks' | 'transcript_prompt' | 'submitting' | 'submitted';
lastResponse: FeedbackSurveyResponse | null;
handleSelect: (selected: FeedbackSurveyResponse) => void;
handleTranscriptSelect?: (selected: TranscriptShareResponse) => void;
inputValue: string;
setInputValue: (value: string) => void;
onRequestFeedback?: () => void;
message?: string;
};
export function FeedbackSurvey(t0) {
const $ = _c(16);
const {
state,
lastResponse,
handleSelect,
handleTranscriptSelect,
inputValue,
setInputValue,
onRequestFeedback,
message
} = t0;
if (state === "closed") {
return null;
state:
| 'closed'
| 'open'
| 'thanks'
| 'transcript_prompt'
| 'submitting'
| 'submitted'
lastResponse: FeedbackSurveyResponse | null
handleSelect: (selected: FeedbackSurveyResponse) => void
handleTranscriptSelect?: (selected: TranscriptShareResponse) => void
inputValue: string
setInputValue: (value: string) => void
onRequestFeedback?: () => void
message?: string
}
export function FeedbackSurvey({
state,
lastResponse,
handleSelect,
handleTranscriptSelect,
inputValue,
setInputValue,
onRequestFeedback,
message,
}: Props): React.ReactNode {
if (state === 'closed') {
return null
}
if (state === "thanks") {
let t1;
if ($[0] !== inputValue || $[1] !== lastResponse || $[2] !== onRequestFeedback || $[3] !== setInputValue) {
t1 = <FeedbackSurveyThanks lastResponse={lastResponse} inputValue={inputValue} setInputValue={setInputValue} onRequestFeedback={onRequestFeedback} />;
$[0] = inputValue;
$[1] = lastResponse;
$[2] = onRequestFeedback;
$[3] = setInputValue;
$[4] = t1;
} else {
t1 = $[4];
}
return t1;
if (state === 'thanks') {
return (
<FeedbackSurveyThanks
lastResponse={lastResponse}
inputValue={inputValue}
setInputValue={setInputValue}
onRequestFeedback={onRequestFeedback}
/>
)
}
if (state === "submitted") {
let t1;
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <Box marginTop={1}><Text color="success">{"\u2713"} Thanks for sharing your transcript!</Text></Box>;
$[5] = t1;
} else {
t1 = $[5];
}
return t1;
if (state === 'submitted') {
return (
<Box marginTop={1}>
<Text color="success">
{'\u2713'} Thanks for sharing your transcript!
</Text>
</Box>
)
}
if (state === "submitting") {
let t1;
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <Box marginTop={1}><Text dimColor={true}>Sharing transcript{"\u2026"}</Text></Box>;
$[6] = t1;
} else {
t1 = $[6];
}
return t1;
if (state === 'submitting') {
return (
<Box marginTop={1}>
<Text dimColor>Sharing transcript{'\u2026'}</Text>
</Box>
)
}
if (state === "transcript_prompt") {
if (state === 'transcript_prompt') {
if (!handleTranscriptSelect) {
return null;
return null
}
if (inputValue && !["1", "2", "3"].includes(inputValue)) {
return null;
// Hide prompt if user is typing non-response characters
if (inputValue && !['1', '2', '3'].includes(inputValue)) {
return null
}
let t1;
if ($[7] !== handleTranscriptSelect || $[8] !== inputValue || $[9] !== setInputValue) {
t1 = <TranscriptSharePrompt onSelect={handleTranscriptSelect} inputValue={inputValue} setInputValue={setInputValue} />;
$[7] = handleTranscriptSelect;
$[8] = inputValue;
$[9] = setInputValue;
$[10] = t1;
} else {
t1 = $[10];
}
return t1;
return (
<TranscriptSharePrompt
onSelect={handleTranscriptSelect}
inputValue={inputValue}
setInputValue={setInputValue}
/>
)
}
// state === 'open'
// Hide the survey if the user is typing anything other than a survey response.
// This prevents the survey from showing up when the user is typing a message,
// which can result in accidental survey submissions (e.g. "s3cmd").
if (inputValue && !isValidResponseInput(inputValue)) {
return null;
return null
}
let t1;
if ($[11] !== handleSelect || $[12] !== inputValue || $[13] !== message || $[14] !== setInputValue) {
t1 = <FeedbackSurveyView onSelect={handleSelect} inputValue={inputValue} setInputValue={setInputValue} message={message} />;
$[11] = handleSelect;
$[12] = inputValue;
$[13] = message;
$[14] = setInputValue;
$[15] = t1;
} else {
t1 = $[15];
}
return t1;
return (
<FeedbackSurveyView
onSelect={handleSelect}
inputValue={inputValue}
setInputValue={setInputValue}
message={message}
/>
)
}
type ThanksProps = {
lastResponse: FeedbackSurveyResponse | null;
inputValue: string;
setInputValue: (value: string) => void;
onRequestFeedback?: () => void;
};
const isFollowUpDigit = (char: string): char is '1' => char === '1';
function FeedbackSurveyThanks(t0) {
const $ = _c(12);
const {
lastResponse,
lastResponse: FeedbackSurveyResponse | null
inputValue: string
setInputValue: (value: string) => void
onRequestFeedback?: () => void
}
const isFollowUpDigit = (char: string): char is '1' => char === '1'
function FeedbackSurveyThanks({
lastResponse,
inputValue,
setInputValue,
onRequestFeedback,
}: ThanksProps): React.ReactNode {
const showFollowUp = onRequestFeedback && lastResponse === 'good'
// Listen for "1" keypress to launch /feedback
useDebouncedDigitInput({
inputValue,
setInputValue,
onRequestFeedback
} = t0;
const showFollowUp = onRequestFeedback && lastResponse === "good";
const t1 = Boolean(showFollowUp);
let t2;
if ($[0] !== lastResponse || $[1] !== onRequestFeedback) {
t2 = () => {
logEvent("tengu_feedback_survey_event", {
event_type: "followup_accepted" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
response: lastResponse as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
});
onRequestFeedback?.();
};
$[0] = lastResponse;
$[1] = onRequestFeedback;
$[2] = t2;
} else {
t2 = $[2];
}
let t3;
if ($[3] !== inputValue || $[4] !== setInputValue || $[5] !== t1 || $[6] !== t2) {
t3 = {
inputValue,
setInputValue,
isValidDigit: isFollowUpDigit,
enabled: t1,
once: true,
onDigit: t2
};
$[3] = inputValue;
$[4] = setInputValue;
$[5] = t1;
$[6] = t2;
$[7] = t3;
} else {
t3 = $[7];
}
useDebouncedDigitInput(t3);
const feedbackCommand = false ? "/issue" : "/feedback";
let t4;
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
t4 = <Text color="success">Thanks for the feedback!</Text>;
$[8] = t4;
} else {
t4 = $[8];
}
let t5;
if ($[9] !== lastResponse || $[10] !== showFollowUp) {
t5 = <Box marginTop={1} flexDirection="column">{t4}{showFollowUp ? <Text dimColor={true}>(Optional) Press [<Text color="ansi:cyan">1</Text>] to tell us what went well {" \xB7 "}{feedbackCommand}</Text> : lastResponse === "bad" ? <Text dimColor={true}>Use /issue to report model behavior issues.</Text> : <Text dimColor={true}>Use {feedbackCommand} to share detailed feedback anytime.</Text>}</Box>;
$[9] = lastResponse;
$[10] = showFollowUp;
$[11] = t5;
} else {
t5 = $[11];
}
return t5;
isValidDigit: isFollowUpDigit,
enabled: Boolean(showFollowUp),
once: true,
onDigit: () => {
logEvent('tengu_feedback_survey_event', {
event_type:
'followup_accepted' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
response:
lastResponse as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
onRequestFeedback?.()
},
})
const feedbackCommand =
process.env.USER_TYPE === 'ant' ? '/issue' : '/feedback'
return (
<Box marginTop={1} flexDirection="column">
<Text color="success">Thanks for the feedback!</Text>
{showFollowUp ? (
<Text dimColor>
(Optional) Press [<Text color="ansi:cyan">1</Text>] to tell us what
went well {' \u00b7 '}
{feedbackCommand}
</Text>
) : lastResponse === 'bad' ? (
<Text dimColor>Use /issue to report model behavior issues.</Text>
) : (
<Text dimColor>
Use {feedbackCommand} to share detailed feedback anytime.
</Text>
)}
</Box>
)
}

View File

@@ -1,107 +1,72 @@
import { c as _c } from "react/compiler-runtime";
import React from 'react';
import { Box, Text } from '../../ink.js';
import { useDebouncedDigitInput } from './useDebouncedDigitInput.js';
import type { FeedbackSurveyResponse } from './utils.js';
import React from 'react'
import { Box, Text } from '../../ink.js'
import { useDebouncedDigitInput } from './useDebouncedDigitInput.js'
import type { FeedbackSurveyResponse } from './utils.js'
type Props = {
onSelect: (option: FeedbackSurveyResponse) => void;
inputValue: string;
setInputValue: (value: string) => void;
message?: string;
};
const RESPONSE_INPUTS = ['0', '1', '2', '3'] as const;
type ResponseInput = (typeof RESPONSE_INPUTS)[number];
onSelect: (option: FeedbackSurveyResponse) => void
inputValue: string
setInputValue: (value: string) => void
message?: string
}
const RESPONSE_INPUTS = ['0', '1', '2', '3'] as const
type ResponseInput = (typeof RESPONSE_INPUTS)[number]
const inputToResponse: Record<ResponseInput, FeedbackSurveyResponse> = {
'0': 'dismissed',
'1': 'bad',
'2': 'fine',
'3': 'good'
} as const;
export const isValidResponseInput = (input: string): input is ResponseInput => (RESPONSE_INPUTS as readonly string[]).includes(input);
const DEFAULT_MESSAGE = 'How is Claude doing this session? (optional)';
export function FeedbackSurveyView(t0) {
const $ = _c(15);
const {
onSelect,
'3': 'good',
} as const
export const isValidResponseInput = (input: string): input is ResponseInput =>
(RESPONSE_INPUTS as readonly string[]).includes(input)
const DEFAULT_MESSAGE = 'How is Claude doing this session? (optional)'
export function FeedbackSurveyView({
onSelect,
inputValue,
setInputValue,
message = DEFAULT_MESSAGE,
}: Props): React.ReactNode {
useDebouncedDigitInput({
inputValue,
setInputValue,
message: t1
} = t0;
const message = t1 === undefined ? DEFAULT_MESSAGE : t1;
let t2;
if ($[0] !== onSelect) {
t2 = digit => onSelect(inputToResponse[digit]);
$[0] = onSelect;
$[1] = t2;
} else {
t2 = $[1];
}
let t3;
if ($[2] !== inputValue || $[3] !== setInputValue || $[4] !== t2) {
t3 = {
inputValue,
setInputValue,
isValidDigit: isValidResponseInput,
onDigit: t2
};
$[2] = inputValue;
$[3] = setInputValue;
$[4] = t2;
$[5] = t3;
} else {
t3 = $[5];
}
useDebouncedDigitInput(t3);
let t4;
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
t4 = <Text color="ansi:cyan"> </Text>;
$[6] = t4;
} else {
t4 = $[6];
}
let t5;
if ($[7] !== message) {
t5 = <Box>{t4}<Text bold={true}>{message}</Text></Box>;
$[7] = message;
$[8] = t5;
} else {
t5 = $[8];
}
let t6;
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
t6 = <Box width={10}><Text><Text color="ansi:cyan">1</Text>: Bad</Text></Box>;
$[9] = t6;
} else {
t6 = $[9];
}
let t7;
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
t7 = <Box width={10}><Text><Text color="ansi:cyan">2</Text>: Fine</Text></Box>;
$[10] = t7;
} else {
t7 = $[10];
}
let t8;
if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
t8 = <Box width={10}><Text><Text color="ansi:cyan">3</Text>: Good</Text></Box>;
$[11] = t8;
} else {
t8 = $[11];
}
let t9;
if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
t9 = <Box marginLeft={2}>{t6}{t7}{t8}<Box><Text><Text color="ansi:cyan">0</Text>: Dismiss</Text></Box></Box>;
$[12] = t9;
} else {
t9 = $[12];
}
let t10;
if ($[13] !== t5) {
t10 = <Box flexDirection="column" marginTop={1}>{t5}{t9}</Box>;
$[13] = t5;
$[14] = t10;
} else {
t10 = $[14];
}
return t10;
isValidDigit: isValidResponseInput,
onDigit: digit => onSelect(inputToResponse[digit]),
})
return (
<Box flexDirection="column" marginTop={1}>
<Box>
<Text color="ansi:cyan"> </Text>
<Text bold>{message}</Text>
</Box>
<Box marginLeft={2}>
<Box width={10}>
<Text>
<Text color="ansi:cyan">1</Text>: Bad
</Text>
</Box>
<Box width={10}>
<Text>
<Text color="ansi:cyan">2</Text>: Fine
</Text>
</Box>
<Box width={10}>
<Text>
<Text color="ansi:cyan">3</Text>: Good
</Text>
</Box>
<Box>
<Text>
<Text color="ansi:cyan">0</Text>: Dismiss
</Text>
</Box>
</Box>
</Box>
)
}

View File

@@ -1,87 +1,74 @@
import { c as _c } from "react/compiler-runtime";
import React from 'react';
import { BLACK_CIRCLE } from '../../constants/figures.js';
import { Box, Text } from '../../ink.js';
import { useDebouncedDigitInput } from './useDebouncedDigitInput.js';
export type TranscriptShareResponse = 'yes' | 'no' | 'dont_ask_again';
import React from 'react'
import { BLACK_CIRCLE } from '../../constants/figures.js'
import { Box, Text } from '../../ink.js'
import { useDebouncedDigitInput } from './useDebouncedDigitInput.js'
export type TranscriptShareResponse = 'yes' | 'no' | 'dont_ask_again'
type Props = {
onSelect: (option: TranscriptShareResponse) => void;
inputValue: string;
setInputValue: (value: string) => void;
};
const RESPONSE_INPUTS = ['1', '2', '3'] as const;
type ResponseInput = (typeof RESPONSE_INPUTS)[number];
onSelect: (option: TranscriptShareResponse) => void
inputValue: string
setInputValue: (value: string) => void
}
const RESPONSE_INPUTS = ['1', '2', '3'] as const
type ResponseInput = (typeof RESPONSE_INPUTS)[number]
const inputToResponse: Record<ResponseInput, TranscriptShareResponse> = {
'1': 'yes',
'2': 'no',
'3': 'dont_ask_again'
} as const;
const isValidResponseInput = (input: string): input is ResponseInput => (RESPONSE_INPUTS as readonly string[]).includes(input);
export function TranscriptSharePrompt(t0) {
const $ = _c(11);
const {
onSelect,
'3': 'dont_ask_again',
} as const
const isValidResponseInput = (input: string): input is ResponseInput =>
(RESPONSE_INPUTS as readonly string[]).includes(input)
export function TranscriptSharePrompt({
onSelect,
inputValue,
setInputValue,
}: Props): React.ReactNode {
useDebouncedDigitInput({
inputValue,
setInputValue
} = t0;
let t1;
if ($[0] !== onSelect) {
t1 = digit => onSelect(inputToResponse[digit]);
$[0] = onSelect;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] !== inputValue || $[3] !== setInputValue || $[4] !== t1) {
t2 = {
inputValue,
setInputValue,
isValidDigit: isValidResponseInput,
onDigit: t1
};
$[2] = inputValue;
$[3] = setInputValue;
$[4] = t1;
$[5] = t2;
} else {
t2 = $[5];
}
useDebouncedDigitInput(t2);
let t3;
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
t3 = <Box><Text color="ansi:cyan">{BLACK_CIRCLE} </Text><Text bold={true}>Can Anthropic look at your session transcript to help us improve Claude Code?</Text></Box>;
$[6] = t3;
} else {
t3 = $[6];
}
let t4;
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
t4 = <Box marginLeft={2}><Text dimColor={true}>Learn more: https://code.claude.com/docs/en/data-usage#session-quality-surveys</Text></Box>;
$[7] = t4;
} else {
t4 = $[7];
}
let t5;
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
t5 = <Box width={10}><Text><Text color="ansi:cyan">1</Text>: Yes</Text></Box>;
$[8] = t5;
} else {
t5 = $[8];
}
let t6;
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
t6 = <Box width={10}><Text><Text color="ansi:cyan">2</Text>: No</Text></Box>;
$[9] = t6;
} else {
t6 = $[9];
}
let t7;
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
t7 = <Box flexDirection="column" marginTop={1}>{t3}{t4}<Box marginLeft={2}>{t5}{t6}<Box><Text><Text color="ansi:cyan">3</Text>: Don't ask again</Text></Box></Box></Box>;
$[10] = t7;
} else {
t7 = $[10];
}
return t7;
setInputValue,
isValidDigit: isValidResponseInput,
onDigit: digit => onSelect(inputToResponse[digit]),
})
return (
<Box flexDirection="column" marginTop={1}>
<Box>
<Text color="ansi:cyan">{BLACK_CIRCLE} </Text>
<Text bold>
Can Anthropic look at your session transcript to help us improve
Claude Code?
</Text>
</Box>
<Box marginLeft={2}>
<Text dimColor>
Learn more:
https://code.claude.com/docs/en/data-usage#session-quality-surveys
</Text>
</Box>
<Box marginLeft={2}>
<Box width={10}>
<Text>
<Text color="ansi:cyan">1</Text>: Yes
</Text>
</Box>
<Box width={10}>
<Text>
<Text color="ansi:cyan">2</Text>: No
</Text>
</Box>
<Box>
<Text>
<Text color="ansi:cyan">3</Text>: Don&apos;t ask again
</Text>
</Box>
</Box>
</Box>
)
}

View File

@@ -1,32 +1,41 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useDynamicConfig } from 'src/hooks/useDynamicConfig.js';
import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js';
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
import { isPolicyAllowed } from '../../services/policyLimits/index.js';
import type { Message } from '../../types/message.js';
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
import { isEnvTruthy } from '../../utils/envUtils.js';
import { getLastAssistantMessage } from '../../utils/messages.js';
import { getMainLoopModel } from '../../utils/model/model.js';
import { getInitialSettings } from '../../utils/settings/settings.js';
import { logOTelEvent } from '../../utils/telemetry/events.js';
import { submitTranscriptShare, type TranscriptShareTrigger } from './submitTranscriptShare.js';
import type { TranscriptShareResponse } from './TranscriptSharePrompt.js';
import { useSurveyState } from './useSurveyState.js';
import type { FeedbackSurveyResponse, FeedbackSurveyType } from './utils.js';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useDynamicConfig } from 'src/hooks/useDynamicConfig.js'
import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from 'src/services/analytics/index.js'
import { isPolicyAllowed } from '../../services/policyLimits/index.js'
import type { Message } from '../../types/message.js'
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
import { isEnvTruthy } from '../../utils/envUtils.js'
import { getLastAssistantMessage } from '../../utils/messages.js'
import { getMainLoopModel } from '../../utils/model/model.js'
import { getInitialSettings } from '../../utils/settings/settings.js'
import { logOTelEvent } from '../../utils/telemetry/events.js'
import {
submitTranscriptShare,
type TranscriptShareTrigger,
} from './submitTranscriptShare.js'
import type { TranscriptShareResponse } from './TranscriptSharePrompt.js'
import { useSurveyState } from './useSurveyState.js'
import type { FeedbackSurveyResponse, FeedbackSurveyType } from './utils.js'
type FeedbackSurveyConfig = {
minTimeBeforeFeedbackMs: number;
minTimeBetweenFeedbackMs: number;
minTimeBetweenGlobalFeedbackMs: number;
minUserTurnsBeforeFeedback: number;
minUserTurnsBetweenFeedback: number;
hideThanksAfterMs: number;
onForModels: string[];
probability: number;
};
minTimeBeforeFeedbackMs: number
minTimeBetweenFeedbackMs: number
minTimeBetweenGlobalFeedbackMs: number
minUserTurnsBeforeFeedback: number
minUserTurnsBetweenFeedback: number
hideThanksAfterMs: number
onForModels: string[]
probability: number
}
type TranscriptAskConfig = {
probability: number;
};
probability: number
}
const DEFAULT_FEEDBACK_SURVEY_CONFIG: FeedbackSurveyConfig = {
minTimeBeforeFeedbackMs: 600000,
minTimeBetweenFeedbackMs: 3600000,
@@ -35,261 +44,381 @@ const DEFAULT_FEEDBACK_SURVEY_CONFIG: FeedbackSurveyConfig = {
minUserTurnsBetweenFeedback: 10,
hideThanksAfterMs: 3000,
onForModels: ['*'],
probability: 0.005
};
probability: 0.005,
}
const DEFAULT_TRANSCRIPT_ASK_CONFIG: TranscriptAskConfig = {
probability: 0
};
export function useFeedbackSurvey(messages: Message[], isLoading: boolean, submitCount: number, surveyType: FeedbackSurveyType = 'session', hasActivePrompt: boolean = false): {
state: 'closed' | 'open' | 'thanks' | 'transcript_prompt' | 'submitting' | 'submitted';
lastResponse: FeedbackSurveyResponse | null;
handleSelect: (selected: FeedbackSurveyResponse) => boolean;
handleTranscriptSelect: (selected: TranscriptShareResponse) => void;
probability: 0,
}
export function useFeedbackSurvey(
messages: Message[],
isLoading: boolean,
submitCount: number,
surveyType: FeedbackSurveyType = 'session',
hasActivePrompt: boolean = false,
): {
state:
| 'closed'
| 'open'
| 'thanks'
| 'transcript_prompt'
| 'submitting'
| 'submitted'
lastResponse: FeedbackSurveyResponse | null
handleSelect: (selected: FeedbackSurveyResponse) => boolean
handleTranscriptSelect: (selected: TranscriptShareResponse) => void
} {
const lastAssistantMessageIdRef = useRef('unknown');
lastAssistantMessageIdRef.current = getLastAssistantMessage(messages)?.message?.id || 'unknown';
const lastAssistantMessageIdRef = useRef('unknown')
lastAssistantMessageIdRef.current =
getLastAssistantMessage(messages)?.message?.id || 'unknown'
const [feedbackSurvey, setFeedbackSurvey] = useState<{
timeLastShown: number | null;
submitCountAtLastAppearance: number | null;
}>(() => ({
timeLastShown: null,
submitCountAtLastAppearance: null
}));
const config = useDynamicConfig<FeedbackSurveyConfig>('tengu_feedback_survey_config', DEFAULT_FEEDBACK_SURVEY_CONFIG);
const badTranscriptAskConfig = useDynamicConfig<TranscriptAskConfig>('tengu_bad_survey_transcript_ask_config', DEFAULT_TRANSCRIPT_ASK_CONFIG);
const goodTranscriptAskConfig = useDynamicConfig<TranscriptAskConfig>('tengu_good_survey_transcript_ask_config', DEFAULT_TRANSCRIPT_ASK_CONFIG);
const settingsRate = getInitialSettings().feedbackSurveyRate;
const sessionStartTime = useRef(Date.now());
const submitCountAtSessionStart = useRef(submitCount);
const submitCountRef = useRef(submitCount);
submitCountRef.current = submitCount;
const messagesRef = useRef(messages);
messagesRef.current = messages;
timeLastShown: number | null
submitCountAtLastAppearance: number | null
}>(() => ({ timeLastShown: null, submitCountAtLastAppearance: null }))
const config = useDynamicConfig<FeedbackSurveyConfig>(
'tengu_feedback_survey_config',
DEFAULT_FEEDBACK_SURVEY_CONFIG,
)
const badTranscriptAskConfig = useDynamicConfig<TranscriptAskConfig>(
'tengu_bad_survey_transcript_ask_config',
DEFAULT_TRANSCRIPT_ASK_CONFIG,
)
const goodTranscriptAskConfig = useDynamicConfig<TranscriptAskConfig>(
'tengu_good_survey_transcript_ask_config',
DEFAULT_TRANSCRIPT_ASK_CONFIG,
)
const settingsRate = getInitialSettings().feedbackSurveyRate
const sessionStartTime = useRef(Date.now())
const submitCountAtSessionStart = useRef(submitCount)
const submitCountRef = useRef(submitCount)
submitCountRef.current = submitCount
const messagesRef = useRef(messages)
messagesRef.current = messages
// Probability gate: roll once when eligibility conditions are met, not on every
// useMemo re-evaluation. Without this, each dependency change (submitCount,
// isLoading toggle, etc.) re-rolls Math.random(), making the survey almost
// certain to appear after enough renders.
const probabilityPassedRef = useRef(false);
const lastEligibleSubmitCountRef = useRef<number | null>(null);
const updateLastShownTime = useCallback((timestamp: number, submitCountValue: number) => {
setFeedbackSurvey(prev => {
if (prev.timeLastShown === timestamp && prev.submitCountAtLastAppearance === submitCountValue) {
return prev;
}
return {
timeLastShown: timestamp,
submitCountAtLastAppearance: submitCountValue
};
});
// Persist cross-session pacing state (previously done by onChangeAppState observer)
if (getGlobalConfig().feedbackSurveyState?.lastShownTime !== timestamp) {
saveGlobalConfig(current => ({
...current,
feedbackSurveyState: {
lastShownTime: timestamp
const probabilityPassedRef = useRef(false)
const lastEligibleSubmitCountRef = useRef<number | null>(null)
const updateLastShownTime = useCallback(
(timestamp: number, submitCountValue: number) => {
setFeedbackSurvey(prev => {
if (
prev.timeLastShown === timestamp &&
prev.submitCountAtLastAppearance === submitCountValue
) {
return prev
}
}));
}
}, []);
const onOpen = useCallback((appearanceId: string) => {
updateLastShownTime(Date.now(), submitCountRef.current);
logEvent('tengu_feedback_survey_event', {
event_type: 'appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
appearance_id: appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
last_assistant_message_id: lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
survey_type: surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
});
void logOTelEvent('feedback_survey', {
event_type: 'appeared',
appearance_id: appearanceId,
survey_type: surveyType
});
}, [updateLastShownTime, surveyType]);
const onSelect = useCallback((appearanceId_0: string, selected: FeedbackSurveyResponse) => {
updateLastShownTime(Date.now(), submitCountRef.current);
logEvent('tengu_feedback_survey_event', {
event_type: 'responded' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
appearance_id: appearanceId_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
response: selected as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
last_assistant_message_id: lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
survey_type: surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
});
void logOTelEvent('feedback_survey', {
event_type: 'responded',
appearance_id: appearanceId_0,
response: selected,
survey_type: surveyType
});
}, [updateLastShownTime, surveyType]);
const shouldShowTranscriptPrompt = useCallback((selected_0: FeedbackSurveyResponse) => {
// Only bad and good ratings trigger the transcript ask
if (selected_0 !== 'bad' && selected_0 !== 'good') {
return false;
}
return {
timeLastShown: timestamp,
submitCountAtLastAppearance: submitCountValue,
}
})
// Persist cross-session pacing state (previously done by onChangeAppState observer)
if (getGlobalConfig().feedbackSurveyState?.lastShownTime !== timestamp) {
saveGlobalConfig(current => ({
...current,
feedbackSurveyState: {
lastShownTime: timestamp,
},
}))
}
},
[],
)
// Don't show if user previously chose "Don't ask again"
if (getGlobalConfig().transcriptShareDismissed) {
return false;
}
// Don't show if product feedback is blocked by org policy (ZDR)
if (!isPolicyAllowed('allow_product_feedback')) {
return false;
}
// Probability gate from GrowthBook config (separate per rating)
const probability = selected_0 === 'bad' ? badTranscriptAskConfig.probability : goodTranscriptAskConfig.probability;
return Math.random() <= probability;
}, [badTranscriptAskConfig.probability, goodTranscriptAskConfig.probability]);
const onTranscriptPromptShown = useCallback((appearanceId_1: string, surveyResponse: FeedbackSurveyResponse) => {
const trigger: TranscriptShareTrigger = surveyResponse === 'good' ? 'good_feedback_survey' : 'bad_feedback_survey';
logEvent('tengu_feedback_survey_event', {
event_type: 'transcript_prompt_appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
appearance_id: appearanceId_1 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
last_assistant_message_id: lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
survey_type: surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
trigger: trigger as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
});
void logOTelEvent('feedback_survey', {
event_type: 'transcript_prompt_appeared',
appearance_id: appearanceId_1,
survey_type: surveyType
});
}, [surveyType]);
const onTranscriptSelect = useCallback(async (appearanceId_2: string, selected_1: TranscriptShareResponse, surveyResponse_0: FeedbackSurveyResponse | null): Promise<boolean> => {
const trigger_0: TranscriptShareTrigger = surveyResponse_0 === 'good' ? 'good_feedback_survey' : 'bad_feedback_survey';
logEvent('tengu_feedback_survey_event', {
event_type: `transcript_share_${selected_1}` as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
appearance_id: appearanceId_2 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
last_assistant_message_id: lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
survey_type: surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
trigger: trigger_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
});
if (selected_1 === 'dont_ask_again') {
saveGlobalConfig(current_0 => ({
...current_0,
transcriptShareDismissed: true
}));
}
if (selected_1 === 'yes') {
const result = await submitTranscriptShare(messagesRef.current, trigger_0, appearanceId_2);
const onOpen = useCallback(
(appearanceId: string) => {
updateLastShownTime(Date.now(), submitCountRef.current)
logEvent('tengu_feedback_survey_event', {
event_type: (result.success ? 'transcript_share_submitted' : 'transcript_share_failed') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
appearance_id: appearanceId_2 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
trigger: trigger_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
});
return result.success;
}
return false;
}, [surveyType]);
const {
state,
lastResponse,
open,
handleSelect,
handleTranscriptSelect
} = useSurveyState({
hideThanksAfterMs: config.hideThanksAfterMs,
onOpen,
onSelect,
shouldShowTranscriptPrompt,
onTranscriptPromptShown,
onTranscriptSelect
});
const currentModel = getMainLoopModel();
event_type:
'appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
appearance_id:
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
last_assistant_message_id:
lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
survey_type:
surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
void logOTelEvent('feedback_survey', {
event_type: 'appeared',
appearance_id: appearanceId,
survey_type: surveyType,
})
},
[updateLastShownTime, surveyType],
)
const onSelect = useCallback(
(appearanceId: string, selected: FeedbackSurveyResponse) => {
updateLastShownTime(Date.now(), submitCountRef.current)
logEvent('tengu_feedback_survey_event', {
event_type:
'responded' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
appearance_id:
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
response:
selected as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
last_assistant_message_id:
lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
survey_type:
surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
void logOTelEvent('feedback_survey', {
event_type: 'responded',
appearance_id: appearanceId,
response: selected,
survey_type: surveyType,
})
},
[updateLastShownTime, surveyType],
)
const shouldShowTranscriptPrompt = useCallback(
(selected: FeedbackSurveyResponse) => {
// Only bad and good ratings trigger the transcript ask
if (selected !== 'bad' && selected !== 'good') {
return false
}
// Don't show if user previously chose "Don't ask again"
if (getGlobalConfig().transcriptShareDismissed) {
return false
}
// Don't show if product feedback is blocked by org policy (ZDR)
if (!isPolicyAllowed('allow_product_feedback')) {
return false
}
// Probability gate from GrowthBook config (separate per rating)
const probability =
selected === 'bad'
? badTranscriptAskConfig.probability
: goodTranscriptAskConfig.probability
return Math.random() <= probability
},
[badTranscriptAskConfig.probability, goodTranscriptAskConfig.probability],
)
const onTranscriptPromptShown = useCallback(
(appearanceId: string, surveyResponse: FeedbackSurveyResponse) => {
const trigger: TranscriptShareTrigger =
surveyResponse === 'good'
? 'good_feedback_survey'
: 'bad_feedback_survey'
logEvent('tengu_feedback_survey_event', {
event_type:
'transcript_prompt_appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
appearance_id:
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
last_assistant_message_id:
lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
survey_type:
surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
trigger:
trigger as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
void logOTelEvent('feedback_survey', {
event_type: 'transcript_prompt_appeared',
appearance_id: appearanceId,
survey_type: surveyType,
})
},
[surveyType],
)
const onTranscriptSelect = useCallback(
async (
appearanceId: string,
selected: TranscriptShareResponse,
surveyResponse: FeedbackSurveyResponse | null,
): Promise<boolean> => {
const trigger: TranscriptShareTrigger =
surveyResponse === 'good'
? 'good_feedback_survey'
: 'bad_feedback_survey'
logEvent('tengu_feedback_survey_event', {
event_type:
`transcript_share_${selected}` as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
appearance_id:
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
last_assistant_message_id:
lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
survey_type:
surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
trigger:
trigger as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
if (selected === 'dont_ask_again') {
saveGlobalConfig(current => ({
...current,
transcriptShareDismissed: true,
}))
}
if (selected === 'yes') {
const result = await submitTranscriptShare(
messagesRef.current,
trigger,
appearanceId,
)
logEvent('tengu_feedback_survey_event', {
event_type: (result.success
? 'transcript_share_submitted'
: 'transcript_share_failed') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
appearance_id:
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
trigger:
trigger as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
return result.success
}
return false
},
[surveyType],
)
const { state, lastResponse, open, handleSelect, handleTranscriptSelect } =
useSurveyState({
hideThanksAfterMs: config.hideThanksAfterMs,
onOpen,
onSelect,
shouldShowTranscriptPrompt,
onTranscriptPromptShown,
onTranscriptSelect,
})
const currentModel = getMainLoopModel()
const isModelAllowed = useMemo(() => {
if (config.onForModels.length === 0) {
return false;
return false
}
if (config.onForModels.includes('*')) {
return true;
return true
}
return config.onForModels.includes(currentModel);
}, [config.onForModels, currentModel]);
return config.onForModels.includes(currentModel)
}, [config.onForModels, currentModel])
const shouldOpen = useMemo(() => {
if (state !== 'closed') {
return false;
return false
}
if (isLoading) {
return false;
return false
}
// Don't show survey when permission or ask question prompts are visible
if (hasActivePrompt) {
return false;
return false
}
// Force display for testing
if (process.env.CLAUDE_FORCE_DISPLAY_SURVEY && !feedbackSurvey.timeLastShown) {
return true;
if (
process.env.CLAUDE_FORCE_DISPLAY_SURVEY &&
!feedbackSurvey.timeLastShown
) {
return true
}
if (!isModelAllowed) {
return false;
return false
}
if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY)) {
return false;
return false
}
if (isFeedbackSurveyDisabled()) {
return false;
return false
}
// Check if product feedback is allowed by org policy
if (!isPolicyAllowed('allow_product_feedback')) {
return false;
return false
}
// Check session-local pacing
if (feedbackSurvey.timeLastShown) {
// Check time elapsed since last appearance in this session
const timeSinceLastShown = Date.now() - feedbackSurvey.timeLastShown;
const timeSinceLastShown = Date.now() - feedbackSurvey.timeLastShown
if (timeSinceLastShown < config.minTimeBetweenFeedbackMs) {
return false;
return false
}
// Check user turn requirement for subsequent appearances
if (feedbackSurvey.submitCountAtLastAppearance !== null && submitCount < feedbackSurvey.submitCountAtLastAppearance + config.minUserTurnsBetweenFeedback) {
return false;
if (
feedbackSurvey.submitCountAtLastAppearance !== null &&
submitCount <
feedbackSurvey.submitCountAtLastAppearance +
config.minUserTurnsBetweenFeedback
) {
return false
}
} else {
// First appearance in this session
const timeSinceSessionStart = Date.now() - sessionStartTime.current;
const timeSinceSessionStart = Date.now() - sessionStartTime.current
if (timeSinceSessionStart < config.minTimeBeforeFeedbackMs) {
return false;
return false
}
if (submitCount < submitCountAtSessionStart.current + config.minUserTurnsBeforeFeedback) {
return false;
if (
submitCount <
submitCountAtSessionStart.current + config.minUserTurnsBeforeFeedback
) {
return false
}
}
// Probability check: roll once per eligibility window to avoid re-rolling
// on every useMemo re-evaluation (which would make triggering near-certain).
if (lastEligibleSubmitCountRef.current !== submitCount) {
lastEligibleSubmitCountRef.current = submitCount;
probabilityPassedRef.current = Math.random() <= (settingsRate ?? config.probability);
lastEligibleSubmitCountRef.current = submitCount
probabilityPassedRef.current =
Math.random() <= (settingsRate ?? config.probability)
}
if (!probabilityPassedRef.current) {
return false;
return false
}
// Check global pacing (across all sessions)
// Leave this till last because it reads from the filesystem which is expensive.
const globalFeedbackState = getGlobalConfig().feedbackSurveyState;
const globalFeedbackState = getGlobalConfig().feedbackSurveyState
if (globalFeedbackState?.lastShownTime) {
const timeSinceGlobalLastShown = Date.now() - globalFeedbackState.lastShownTime;
const timeSinceGlobalLastShown =
Date.now() - globalFeedbackState.lastShownTime
if (timeSinceGlobalLastShown < config.minTimeBetweenGlobalFeedbackMs) {
return false;
return false
}
}
return true;
}, [state, isLoading, hasActivePrompt, isModelAllowed, feedbackSurvey.timeLastShown, feedbackSurvey.submitCountAtLastAppearance, submitCount, config.minTimeBetweenFeedbackMs, config.minTimeBetweenGlobalFeedbackMs, config.minUserTurnsBetweenFeedback, config.minTimeBeforeFeedbackMs, config.minUserTurnsBeforeFeedback, config.probability, settingsRate]);
return true
}, [
state,
isLoading,
hasActivePrompt,
isModelAllowed,
feedbackSurvey.timeLastShown,
feedbackSurvey.submitCountAtLastAppearance,
submitCount,
config.minTimeBetweenFeedbackMs,
config.minTimeBetweenGlobalFeedbackMs,
config.minUserTurnsBetweenFeedback,
config.minTimeBeforeFeedbackMs,
config.minUserTurnsBeforeFeedback,
config.probability,
settingsRate,
])
useEffect(() => {
if (shouldOpen) {
open();
open()
}
}, [shouldOpen, open]);
return {
state,
lastResponse,
handleSelect,
handleTranscriptSelect
};
}, [shouldOpen, open])
return { state, lastResponse, handleSelect, handleTranscriptSelect }
}

View File

@@ -1,212 +1,283 @@
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js';
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js';
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
import { isAutoMemoryEnabled } from '../../memdir/paths.js';
import { isPolicyAllowed } from '../../services/policyLimits/index.js';
import { FILE_READ_TOOL_NAME } from '../../tools/FileReadTool/prompt.js';
import type { Message } from '../../types/message.js';
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
import { isEnvTruthy } from '../../utils/envUtils.js';
import { isAutoManagedMemoryFile } from '../../utils/memoryFileDetection.js';
import { extractTextContent, getLastAssistantMessage } from '../../utils/messages.js';
import { logOTelEvent } from '../../utils/telemetry/events.js';
import { submitTranscriptShare } from './submitTranscriptShare.js';
import type { TranscriptShareResponse } from './TranscriptSharePrompt.js';
import { useSurveyState } from './useSurveyState.js';
import type { FeedbackSurveyResponse } from './utils.js';
const HIDE_THANKS_AFTER_MS = 3000;
const MEMORY_SURVEY_GATE = 'tengu_dunwich_bell';
const MEMORY_SURVEY_EVENT = 'tengu_memory_survey_event';
const SURVEY_PROBABILITY = 0.2;
const TRANSCRIPT_SHARE_TRIGGER = 'memory_survey';
const MEMORY_WORD_RE = /\bmemor(?:y|ies)\b/i;
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js'
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from 'src/services/analytics/index.js'
import { isAutoMemoryEnabled } from '../../memdir/paths.js'
import { isPolicyAllowed } from '../../services/policyLimits/index.js'
import { FILE_READ_TOOL_NAME } from '../../tools/FileReadTool/prompt.js'
import type { Message } from '../../types/message.js'
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
import { isEnvTruthy } from '../../utils/envUtils.js'
import { isAutoManagedMemoryFile } from '../../utils/memoryFileDetection.js'
import {
extractTextContent,
getLastAssistantMessage,
} from '../../utils/messages.js'
import { logOTelEvent } from '../../utils/telemetry/events.js'
import { submitTranscriptShare } from './submitTranscriptShare.js'
import type { TranscriptShareResponse } from './TranscriptSharePrompt.js'
import { useSurveyState } from './useSurveyState.js'
import type { FeedbackSurveyResponse } from './utils.js'
const HIDE_THANKS_AFTER_MS = 3000
const MEMORY_SURVEY_GATE = 'tengu_dunwich_bell'
const MEMORY_SURVEY_EVENT = 'tengu_memory_survey_event'
const SURVEY_PROBABILITY = 0.2
const TRANSCRIPT_SHARE_TRIGGER = 'memory_survey'
const MEMORY_WORD_RE = /\bmemor(?:y|ies)\b/i
function hasMemoryFileRead(messages: Message[]): boolean {
for (const message of messages) {
if (message.type !== 'assistant') {
continue;
continue
}
const content = message.message.content;
const content = message.message.content
if (!Array.isArray(content)) {
continue;
continue
}
for (const block of content) {
if (block.type !== 'tool_use' || block.name !== FILE_READ_TOOL_NAME) {
continue;
continue
}
const input = block.input as {
file_path?: unknown;
};
if (typeof input.file_path === 'string' && isAutoManagedMemoryFile(input.file_path)) {
return true;
const input = block.input as { file_path?: unknown }
if (
typeof input.file_path === 'string' &&
isAutoManagedMemoryFile(input.file_path)
) {
return true
}
}
}
return false;
return false
}
export function useMemorySurvey(messages: Message[], isLoading: boolean, hasActivePrompt = false, {
enabled = true
}: {
enabled?: boolean;
} = {}): {
state: 'closed' | 'open' | 'thanks' | 'transcript_prompt' | 'submitting' | 'submitted';
lastResponse: FeedbackSurveyResponse | null;
handleSelect: (selected: FeedbackSurveyResponse) => void;
handleTranscriptSelect: (selected: TranscriptShareResponse) => void;
export function useMemorySurvey(
messages: Message[],
isLoading: boolean,
hasActivePrompt = false,
{ enabled = true }: { enabled?: boolean } = {},
): {
state:
| 'closed'
| 'open'
| 'thanks'
| 'transcript_prompt'
| 'submitting'
| 'submitted'
lastResponse: FeedbackSurveyResponse | null
handleSelect: (selected: FeedbackSurveyResponse) => void
handleTranscriptSelect: (selected: TranscriptShareResponse) => void
} {
// Track assistant message UUIDs that were already evaluated so we don't
// re-roll probability on re-renders or re-scan messages for the same turn.
const seenAssistantUuids = useRef<Set<string>>(new Set());
const seenAssistantUuids = useRef<Set<string>>(new Set())
// Once a memory file read is observed it stays true for the session —
// skip the O(n) scan on subsequent turns.
const memoryReadSeen = useRef(false);
const messagesRef = useRef(messages);
messagesRef.current = messages;
const memoryReadSeen = useRef(false)
const messagesRef = useRef(messages)
messagesRef.current = messages
const onOpen = useCallback((appearanceId: string) => {
logEvent(MEMORY_SURVEY_EVENT, {
event_type: 'appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
appearance_id: appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
});
event_type:
'appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
appearance_id:
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
void logOTelEvent('feedback_survey', {
event_type: 'appeared',
appearance_id: appearanceId,
survey_type: 'memory'
});
}, []);
const onSelect = useCallback((appearanceId_0: string, selected: FeedbackSurveyResponse) => {
survey_type: 'memory',
})
}, [])
const onSelect = useCallback(
(appearanceId: string, selected: FeedbackSurveyResponse) => {
logEvent(MEMORY_SURVEY_EVENT, {
event_type:
'responded' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
appearance_id:
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
response:
selected as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
void logOTelEvent('feedback_survey', {
event_type: 'responded',
appearance_id: appearanceId,
response: selected,
survey_type: 'memory',
})
},
[],
)
const shouldShowTranscriptPrompt = useCallback(
(selected: FeedbackSurveyResponse) => {
if (process.env.USER_TYPE !== 'ant') {
return false
}
if (selected !== 'bad' && selected !== 'good') {
return false
}
if (getGlobalConfig().transcriptShareDismissed) {
return false
}
if (!isPolicyAllowed('allow_product_feedback')) {
return false
}
return true
},
[],
)
const onTranscriptPromptShown = useCallback((appearanceId: string) => {
logEvent(MEMORY_SURVEY_EVENT, {
event_type: 'responded' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
appearance_id: appearanceId_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
response: selected as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
});
void logOTelEvent('feedback_survey', {
event_type: 'responded',
appearance_id: appearanceId_0,
response: selected,
survey_type: 'memory'
});
}, []);
const shouldShowTranscriptPrompt = useCallback((selected_0: FeedbackSurveyResponse) => {
if ((process.env.USER_TYPE) !== 'ant') {
return false;
}
if (selected_0 !== 'bad' && selected_0 !== 'good') {
return false;
}
if (getGlobalConfig().transcriptShareDismissed) {
return false;
}
if (!isPolicyAllowed('allow_product_feedback')) {
return false;
}
return true;
}, []);
const onTranscriptPromptShown = useCallback((appearanceId_1: string) => {
logEvent(MEMORY_SURVEY_EVENT, {
event_type: 'transcript_prompt_appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
appearance_id: appearanceId_1 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
trigger: TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
});
event_type:
'transcript_prompt_appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
appearance_id:
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
trigger:
TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
void logOTelEvent('feedback_survey', {
event_type: 'transcript_prompt_appeared',
appearance_id: appearanceId_1,
survey_type: 'memory'
});
}, []);
const onTranscriptSelect = useCallback(async (appearanceId_2: string, selected_1: TranscriptShareResponse): Promise<boolean> => {
logEvent(MEMORY_SURVEY_EVENT, {
event_type: `transcript_share_${selected_1}` as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
appearance_id: appearanceId_2 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
trigger: TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
});
if (selected_1 === 'dont_ask_again') {
saveGlobalConfig(current => ({
...current,
transcriptShareDismissed: true
}));
}
if (selected_1 === 'yes') {
const result = await submitTranscriptShare(messagesRef.current, TRANSCRIPT_SHARE_TRIGGER, appearanceId_2);
appearance_id: appearanceId,
survey_type: 'memory',
})
}, [])
const onTranscriptSelect = useCallback(
async (
appearanceId: string,
selected: TranscriptShareResponse,
): Promise<boolean> => {
logEvent(MEMORY_SURVEY_EVENT, {
event_type: (result.success ? 'transcript_share_submitted' : 'transcript_share_failed') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
appearance_id: appearanceId_2 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
trigger: TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
});
return result.success;
}
return false;
}, []);
const {
state,
lastResponse,
open,
handleSelect,
handleTranscriptSelect
} = useSurveyState({
hideThanksAfterMs: HIDE_THANKS_AFTER_MS,
onOpen,
onSelect,
shouldShowTranscriptPrompt,
onTranscriptPromptShown,
onTranscriptSelect
});
const lastAssistant = useMemo(() => getLastAssistantMessage(messages), [messages]);
event_type:
`transcript_share_${selected}` as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
appearance_id:
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
trigger:
TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
if (selected === 'dont_ask_again') {
saveGlobalConfig(current => ({
...current,
transcriptShareDismissed: true,
}))
}
if (selected === 'yes') {
const result = await submitTranscriptShare(
messagesRef.current,
TRANSCRIPT_SHARE_TRIGGER,
appearanceId,
)
logEvent(MEMORY_SURVEY_EVENT, {
event_type: (result.success
? 'transcript_share_submitted'
: 'transcript_share_failed') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
appearance_id:
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
trigger:
TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
return result.success
}
return false
},
[],
)
const { state, lastResponse, open, handleSelect, handleTranscriptSelect } =
useSurveyState({
hideThanksAfterMs: HIDE_THANKS_AFTER_MS,
onOpen,
onSelect,
shouldShowTranscriptPrompt,
onTranscriptPromptShown,
onTranscriptSelect,
})
const lastAssistant = useMemo(
() => getLastAssistantMessage(messages),
[messages],
)
useEffect(() => {
if (!enabled) return;
if (!enabled) return
// /clear resets messages but REPL stays mounted — reset refs so a memory
// read from the previous conversation doesn't leak into the new one.
if (messages.length === 0) {
memoryReadSeen.current = false;
seenAssistantUuids.current.clear();
return;
memoryReadSeen.current = false
seenAssistantUuids.current.clear()
return
}
if (state !== 'closed' || isLoading || hasActivePrompt) {
return;
return
}
// 3P default: survey off (no GrowthBook on Bedrock/Vertex/Foundry).
if (!getFeatureValue_CACHED_MAY_BE_STALE(MEMORY_SURVEY_GATE, false)) {
return;
return
}
if (!isAutoMemoryEnabled()) {
return;
return
}
if (isFeedbackSurveyDisabled()) {
return;
return
}
if (!isPolicyAllowed('allow_product_feedback')) {
return;
return
}
if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY)) {
return;
return
}
if (!lastAssistant || seenAssistantUuids.current.has(lastAssistant.uuid)) {
return;
return
}
const text = extractTextContent(Array.isArray(lastAssistant.message.content) ? lastAssistant.message.content : [], ' ');
const text = extractTextContent(lastAssistant.message.content, ' ')
if (!MEMORY_WORD_RE.test(text)) {
return;
return
}
// Mark as evaluated before the memory-read scan so a turn that mentions
// "memory" but has no memory read doesn't trigger repeated O(n) scans
// on subsequent renders with the same last assistant message.
seenAssistantUuids.current.add(lastAssistant.uuid);
seenAssistantUuids.current.add(lastAssistant.uuid)
if (!memoryReadSeen.current) {
memoryReadSeen.current = hasMemoryFileRead(messages);
memoryReadSeen.current = hasMemoryFileRead(messages)
}
if (!memoryReadSeen.current) {
return;
return
}
if (Math.random() < SURVEY_PROBABILITY) {
open();
open()
}
}, [enabled, state, isLoading, hasActivePrompt, lastAssistant, messages, open]);
return {
}, [
enabled,
state,
lastResponse,
handleSelect,
handleTranscriptSelect
};
isLoading,
hasActivePrompt,
lastAssistant,
messages,
open,
])
return { state, lastResponse, handleSelect, handleTranscriptSelect }
}

View File

@@ -1,205 +1,195 @@
import { c as _c } from "react/compiler-runtime";
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js';
import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js';
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
import { shouldUseSessionMemoryCompaction } from '../../services/compact/sessionMemoryCompact.js';
import type { Message } from '../../types/message.js';
import { isEnvTruthy } from '../../utils/envUtils.js';
import { isCompactBoundaryMessage } from '../../utils/messages.js';
import { logOTelEvent } from '../../utils/telemetry/events.js';
import { useSurveyState } from './useSurveyState.js';
import type { FeedbackSurveyResponse } from './utils.js';
const HIDE_THANKS_AFTER_MS = 3000;
const POST_COMPACT_SURVEY_GATE = 'tengu_post_compact_survey';
const SURVEY_PROBABILITY = 0.2; // Show survey 20% of the time after compaction
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js'
import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from 'src/services/analytics/index.js'
import { shouldUseSessionMemoryCompaction } from '../../services/compact/sessionMemoryCompact.js'
import type { Message } from '../../types/message.js'
import { isEnvTruthy } from '../../utils/envUtils.js'
import { isCompactBoundaryMessage } from '../../utils/messages.js'
import { logOTelEvent } from '../../utils/telemetry/events.js'
import { useSurveyState } from './useSurveyState.js'
import type { FeedbackSurveyResponse } from './utils.js'
function hasMessageAfterBoundary(messages: Message[], boundaryUuid: string): boolean {
const boundaryIndex = messages.findIndex(msg => msg.uuid === boundaryUuid);
const HIDE_THANKS_AFTER_MS = 3000
const POST_COMPACT_SURVEY_GATE = 'tengu_post_compact_survey'
const SURVEY_PROBABILITY = 0.2 // Show survey 20% of the time after compaction
function hasMessageAfterBoundary(
messages: Message[],
boundaryUuid: string,
): boolean {
const boundaryIndex = messages.findIndex(msg => msg.uuid === boundaryUuid)
if (boundaryIndex === -1) {
return false;
return false
}
// Check if there's a user or assistant message after the boundary
for (let i = boundaryIndex + 1; i < messages.length; i++) {
const msg = messages[i];
const msg = messages[i]
if (msg && (msg.type === 'user' || msg.type === 'assistant')) {
return true;
return true
}
}
return false;
return false
}
export function usePostCompactSurvey(messages, isLoading, t0, t1) {
const $ = _c(23);
const hasActivePrompt = t0 === undefined ? false : t0;
let t2;
if ($[0] !== t1) {
t2 = t1 === undefined ? {} : t1;
$[0] = t1;
$[1] = t2;
} else {
t2 = $[1];
}
const {
enabled: t3
} = t2;
const enabled = t3 === undefined ? true : t3;
const [gateEnabled, setGateEnabled] = useState(null);
let t4;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t4 = new Set();
$[2] = t4;
} else {
t4 = $[2];
}
const seenCompactBoundaries = useRef(t4);
const pendingCompactBoundaryUuid = useRef(null);
const onOpen = _temp;
const onSelect = _temp2;
let t5;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t5 = {
hideThanksAfterMs: HIDE_THANKS_AFTER_MS,
onOpen,
onSelect
};
$[3] = t5;
} else {
t5 = $[3];
}
const {
state,
lastResponse,
open,
handleSelect
} = useSurveyState(t5);
let t6;
let t7;
if ($[4] !== enabled) {
t6 = () => {
if (!enabled) {
return;
}
setGateEnabled(checkStatsigFeatureGate_CACHED_MAY_BE_STALE(POST_COMPACT_SURVEY_GATE));
};
t7 = [enabled];
$[4] = enabled;
$[5] = t6;
$[6] = t7;
} else {
t6 = $[5];
t7 = $[6];
}
useEffect(t6, t7);
let t8;
if ($[7] !== messages) {
t8 = new Set(messages.filter(_temp3).map(_temp4));
$[7] = messages;
$[8] = t8;
} else {
t8 = $[8];
}
const currentCompactBoundaries = t8;
let t10;
let t9;
if ($[9] !== currentCompactBoundaries || $[10] !== enabled || $[11] !== gateEnabled || $[12] !== hasActivePrompt || $[13] !== isLoading || $[14] !== messages || $[15] !== open || $[16] !== state) {
t9 = () => {
if (!enabled) {
return;
}
if (state !== "closed" || isLoading) {
return;
}
if (hasActivePrompt) {
return;
}
if (gateEnabled !== true) {
return;
}
if (isFeedbackSurveyDisabled()) {
return;
}
if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY)) {
return;
}
if (pendingCompactBoundaryUuid.current !== null) {
if (hasMessageAfterBoundary(messages, pendingCompactBoundaryUuid.current)) {
pendingCompactBoundaryUuid.current = null;
if (Math.random() < SURVEY_PROBABILITY) {
open();
}
return;
export function usePostCompactSurvey(
messages: Message[],
isLoading: boolean,
hasActivePrompt = false,
{ enabled = true }: { enabled?: boolean } = {},
): {
state:
| 'closed'
| 'open'
| 'thanks'
| 'transcript_prompt'
| 'submitting'
| 'submitted'
lastResponse: FeedbackSurveyResponse | null
handleSelect: (selected: FeedbackSurveyResponse) => void
} {
const [gateEnabled, setGateEnabled] = useState<boolean | null>(null)
const seenCompactBoundaries = useRef<Set<string>>(new Set())
// Track the compact boundary we're waiting on (to show survey after next message)
const pendingCompactBoundaryUuid = useRef<string | null>(null)
const onOpen = useCallback((appearanceId: string) => {
const smCompactionEnabled = shouldUseSessionMemoryCompaction()
logEvent('tengu_post_compact_survey_event', {
event_type:
'appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
appearance_id:
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
session_memory_compaction_enabled:
smCompactionEnabled as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
void logOTelEvent('feedback_survey', {
event_type: 'appeared',
appearance_id: appearanceId,
survey_type: 'post_compact',
})
}, [])
const onSelect = useCallback(
(appearanceId: string, selected: FeedbackSurveyResponse) => {
const smCompactionEnabled = shouldUseSessionMemoryCompaction()
logEvent('tengu_post_compact_survey_event', {
event_type:
'responded' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
appearance_id:
appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
response:
selected as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
session_memory_compaction_enabled:
smCompactionEnabled as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
void logOTelEvent('feedback_survey', {
event_type: 'responded',
appearance_id: appearanceId,
response: selected,
survey_type: 'post_compact',
})
},
[],
)
const { state, lastResponse, open, handleSelect } = useSurveyState({
hideThanksAfterMs: HIDE_THANKS_AFTER_MS,
onOpen,
onSelect,
})
// Check the feature gate on mount
useEffect(() => {
if (!enabled) return
setGateEnabled(
checkStatsigFeatureGate_CACHED_MAY_BE_STALE(POST_COMPACT_SURVEY_GATE),
)
}, [enabled])
// Find compact boundary messages
const currentCompactBoundaries = useMemo(
() =>
new Set(
messages
.filter(msg => isCompactBoundaryMessage(msg))
.map(msg => msg.uuid),
),
[messages],
)
// Detect new compact boundaries and defer showing survey until next message
useEffect(() => {
if (!enabled) return
// Don't process if already showing
if (state !== 'closed' || isLoading) {
return
}
// Don't show survey when permission or ask question prompts are visible
if (hasActivePrompt) {
return
}
// Check if the gate is enabled
if (gateEnabled !== true) {
return
}
if (isFeedbackSurveyDisabled()) {
return
}
// Check if survey is explicitly disabled
if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY)) {
return
}
// First, check if we have a pending compact and a new message has arrived
if (pendingCompactBoundaryUuid.current !== null) {
if (
hasMessageAfterBoundary(messages, pendingCompactBoundaryUuid.current)
) {
// A new message arrived after the compact - decide whether to show survey
pendingCompactBoundaryUuid.current = null
// Only show survey 20% of the time
if (Math.random() < SURVEY_PROBABILITY) {
open()
}
return
}
const newBoundaries = Array.from(currentCompactBoundaries).filter(uuid => !seenCompactBoundaries.current.has(uuid));
if (newBoundaries.length > 0) {
seenCompactBoundaries.current = new Set(currentCompactBoundaries);
pendingCompactBoundaryUuid.current = newBoundaries[newBoundaries.length - 1];
}
};
t10 = [enabled, currentCompactBoundaries, state, isLoading, hasActivePrompt, gateEnabled, messages, open];
$[9] = currentCompactBoundaries;
$[10] = enabled;
$[11] = gateEnabled;
$[12] = hasActivePrompt;
$[13] = isLoading;
$[14] = messages;
$[15] = open;
$[16] = state;
$[17] = t10;
$[18] = t9;
} else {
t10 = $[17];
t9 = $[18];
}
useEffect(t9, t10);
let t11;
if ($[19] !== handleSelect || $[20] !== lastResponse || $[21] !== state) {
t11 = {
state,
lastResponse,
handleSelect
};
$[19] = handleSelect;
$[20] = lastResponse;
$[21] = state;
$[22] = t11;
} else {
t11 = $[22];
}
return t11;
}
function _temp4(msg_0) {
return msg_0.uuid;
}
function _temp3(msg) {
return isCompactBoundaryMessage(msg);
}
function _temp2(appearanceId_0, selected) {
const smCompactionEnabled_0 = shouldUseSessionMemoryCompaction();
logEvent("tengu_post_compact_survey_event", {
event_type: "responded" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
appearance_id: appearanceId_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
response: selected as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
session_memory_compaction_enabled: smCompactionEnabled_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
});
logOTelEvent("feedback_survey", {
event_type: "responded",
appearance_id: appearanceId_0,
response: selected,
survey_type: "post_compact"
});
}
function _temp(appearanceId) {
const smCompactionEnabled = shouldUseSessionMemoryCompaction();
logEvent("tengu_post_compact_survey_event", {
event_type: "appeared" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
appearance_id: appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
session_memory_compaction_enabled: smCompactionEnabled as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
});
logOTelEvent("feedback_survey", {
event_type: "appeared",
appearance_id: appearanceId,
survey_type: "post_compact"
});
}
// Find new compact boundaries that we haven't seen yet
const newBoundaries = Array.from(currentCompactBoundaries).filter(
uuid => !seenCompactBoundaries.current.has(uuid),
)
if (newBoundaries.length > 0) {
// Mark these boundaries as seen
seenCompactBoundaries.current = new Set(currentCompactBoundaries)
// Don't show survey immediately - wait for next message
// Store the most recent new boundary UUID
pendingCompactBoundaryUuid.current =
newBoundaries[newBoundaries.length - 1]!
}
}, [
enabled,
currentCompactBoundaries,
state,
isLoading,
hasActivePrompt,
gateEnabled,
messages,
open,
])
return { state, lastResponse, handleSelect }
}

View File

@@ -1,99 +1,144 @@
import { randomUUID } from 'crypto';
import { useCallback, useRef, useState } from 'react';
import type { TranscriptShareResponse } from './TranscriptSharePrompt.js';
import type { FeedbackSurveyResponse } from './utils.js';
type SurveyState = 'closed' | 'open' | 'thanks' | 'transcript_prompt' | 'submitting' | 'submitted';
import { randomUUID } from 'crypto'
import { useCallback, useRef, useState } from 'react'
import type { TranscriptShareResponse } from './TranscriptSharePrompt.js'
import type { FeedbackSurveyResponse } from './utils.js'
type SurveyState =
| 'closed'
| 'open'
| 'thanks'
| 'transcript_prompt'
| 'submitting'
| 'submitted'
type UseSurveyStateOptions = {
hideThanksAfterMs: number;
onOpen: (appearanceId: string) => void | Promise<void>;
onSelect: (appearanceId: string, selected: FeedbackSurveyResponse) => void | Promise<void>;
shouldShowTranscriptPrompt?: (selected: FeedbackSurveyResponse) => boolean;
onTranscriptPromptShown?: (appearanceId: string, surveyResponse: FeedbackSurveyResponse) => void;
onTranscriptSelect?: (appearanceId: string, selected: TranscriptShareResponse, surveyResponse: FeedbackSurveyResponse | null) => boolean | Promise<boolean>;
};
hideThanksAfterMs: number
onOpen: (appearanceId: string) => void | Promise<void>
onSelect: (
appearanceId: string,
selected: FeedbackSurveyResponse,
) => void | Promise<void>
shouldShowTranscriptPrompt?: (selected: FeedbackSurveyResponse) => boolean
onTranscriptPromptShown?: (
appearanceId: string,
surveyResponse: FeedbackSurveyResponse,
) => void
onTranscriptSelect?: (
appearanceId: string,
selected: TranscriptShareResponse,
surveyResponse: FeedbackSurveyResponse | null,
) => boolean | Promise<boolean>
}
export function useSurveyState({
hideThanksAfterMs,
onOpen,
onSelect,
shouldShowTranscriptPrompt,
onTranscriptPromptShown,
onTranscriptSelect
onTranscriptSelect,
}: UseSurveyStateOptions): {
state: SurveyState;
lastResponse: FeedbackSurveyResponse | null;
open: () => void;
handleSelect: (selected: FeedbackSurveyResponse) => boolean;
handleTranscriptSelect: (selected: TranscriptShareResponse) => void;
state: SurveyState
lastResponse: FeedbackSurveyResponse | null
open: () => void
handleSelect: (selected: FeedbackSurveyResponse) => boolean
handleTranscriptSelect: (selected: TranscriptShareResponse) => void
} {
const [state, setState] = useState<SurveyState>('closed');
const [lastResponse, setLastResponse] = useState<FeedbackSurveyResponse | null>(null);
const appearanceId = useRef(randomUUID());
const lastResponseRef = useRef<FeedbackSurveyResponse | null>(null);
const [state, setState] = useState<SurveyState>('closed')
const [lastResponse, setLastResponse] =
useState<FeedbackSurveyResponse | null>(null)
const appearanceId = useRef(randomUUID())
const lastResponseRef = useRef<FeedbackSurveyResponse | null>(null)
const showThanksThenClose = useCallback(() => {
setState('thanks');
setTimeout((setState_0, setLastResponse_0) => {
setState_0('closed');
setLastResponse_0(null);
}, hideThanksAfterMs, setState, setLastResponse);
}, [hideThanksAfterMs]);
setState('thanks')
setTimeout(
(setState, setLastResponse) => {
setState('closed')
setLastResponse(null)
},
hideThanksAfterMs,
setState,
setLastResponse,
)
}, [hideThanksAfterMs])
const showSubmittedThenClose = useCallback(() => {
setState('submitted');
setTimeout(setState, hideThanksAfterMs, 'closed');
}, [hideThanksAfterMs]);
setState('submitted')
setTimeout(setState, hideThanksAfterMs, 'closed')
}, [hideThanksAfterMs])
const open = useCallback(() => {
if (state !== 'closed') {
return;
return
}
setState('open');
appearanceId.current = randomUUID();
void onOpen(appearanceId.current);
}, [state, onOpen]);
const handleSelect = useCallback((selected: FeedbackSurveyResponse): boolean => {
setLastResponse(selected);
lastResponseRef.current = selected;
// Always fire the survey response event first
void onSelect(appearanceId.current, selected);
if (selected === 'dismissed') {
setState('closed');
setLastResponse(null);
} else if (shouldShowTranscriptPrompt?.(selected)) {
setState('transcript_prompt');
onTranscriptPromptShown?.(appearanceId.current, selected);
return true;
} else {
showThanksThenClose();
}
return false;
}, [showThanksThenClose, onSelect, shouldShowTranscriptPrompt, onTranscriptPromptShown]);
const handleTranscriptSelect = useCallback((selected_0: TranscriptShareResponse) => {
switch (selected_0) {
case 'yes':
setState('submitting');
void (async () => {
try {
const success = await onTranscriptSelect?.(appearanceId.current, selected_0, lastResponseRef.current);
if (success) {
showSubmittedThenClose();
} else {
showThanksThenClose();
setState('open')
appearanceId.current = randomUUID()
void onOpen(appearanceId.current)
}, [state, onOpen])
const handleSelect = useCallback(
(selected: FeedbackSurveyResponse): boolean => {
setLastResponse(selected)
lastResponseRef.current = selected
// Always fire the survey response event first
void onSelect(appearanceId.current, selected)
if (selected === 'dismissed') {
setState('closed')
setLastResponse(null)
} else if (shouldShowTranscriptPrompt?.(selected)) {
setState('transcript_prompt')
onTranscriptPromptShown?.(appearanceId.current, selected)
return true
} else {
showThanksThenClose()
}
return false
},
[
showThanksThenClose,
onSelect,
shouldShowTranscriptPrompt,
onTranscriptPromptShown,
],
)
const handleTranscriptSelect = useCallback(
(selected: TranscriptShareResponse) => {
switch (selected) {
case 'yes':
setState('submitting')
void (async () => {
try {
const success = await onTranscriptSelect?.(
appearanceId.current,
selected,
lastResponseRef.current,
)
if (success) {
showSubmittedThenClose()
} else {
showThanksThenClose()
}
} catch {
showThanksThenClose()
}
} catch {
showThanksThenClose();
}
})();
break;
case 'no':
case 'dont_ask_again':
void onTranscriptSelect?.(appearanceId.current, selected_0, lastResponseRef.current);
showThanksThenClose();
break;
}
}, [showThanksThenClose, showSubmittedThenClose, onTranscriptSelect]);
return {
state,
lastResponse,
open,
handleSelect,
handleTranscriptSelect
};
})()
break
case 'no':
case 'dont_ask_again':
void onTranscriptSelect?.(
appearanceId.current,
selected,
lastResponseRef.current,
)
showThanksThenClose()
break
}
},
[showThanksThenClose, showSubmittedThenClose, onTranscriptSelect],
)
return { state, lastResponse, open, handleSelect, handleTranscriptSelect }
}

View File

@@ -1,50 +1,38 @@
import { c as _c } from "react/compiler-runtime";
import * as React from 'react';
import { stringWidth } from '../../ink/stringWidth.js';
import { Box, Text } from '../../ink.js';
import TextInput from '../TextInput.js';
import * as React from 'react'
import { stringWidth } from '../../ink/stringWidth.js'
import { Box, Text } from '../../ink.js'
import TextInput from '../TextInput.js'
type Props = {
value: string;
onChange: (value: string) => void;
historyFailedMatch: boolean;
};
function HistorySearchInput(t0) {
const $ = _c(9);
const {
value,
onChange,
historyFailedMatch
} = t0;
const t1 = historyFailedMatch ? "no matching prompt:" : "search prompts:";
let t2;
if ($[0] !== t1) {
t2 = <Text dimColor={true}>{t1}</Text>;
$[0] = t1;
$[1] = t2;
} else {
t2 = $[1];
}
const t3 = stringWidth(value) + 1;
let t4;
if ($[2] !== onChange || $[3] !== t3 || $[4] !== value) {
t4 = <TextInput value={value} onChange={onChange} cursorOffset={value.length} onChangeCursorOffset={_temp} columns={t3} focus={true} showCursor={true} multiline={false} dimColor={true} />;
$[2] = onChange;
$[3] = t3;
$[4] = value;
$[5] = t4;
} else {
t4 = $[5];
}
let t5;
if ($[6] !== t2 || $[7] !== t4) {
t5 = <Box gap={1}>{t2}{t4}</Box>;
$[6] = t2;
$[7] = t4;
$[8] = t5;
} else {
t5 = $[8];
}
return t5;
value: string
onChange: (value: string) => void
historyFailedMatch: boolean
}
function _temp() {}
export default HistorySearchInput;
function HistorySearchInput({
value,
onChange,
historyFailedMatch,
}: Props): React.ReactNode {
return (
<Box gap={1}>
<Text dimColor>
{historyFailedMatch ? 'no matching prompt:' : 'search prompts:'}
</Text>
<TextInput
value={value}
onChange={onChange}
// Force cursor to end of search input since navigation should cancel search
cursorOffset={value.length}
onChangeCursorOffset={() => {}}
columns={stringWidth(value) + 1}
focus={true}
showCursor={true}
multiline={false}
dimColor={true}
/>
</Box>
)
}
export default HistorySearchInput

View File

@@ -1,11 +1,28 @@
import * as React from 'react';
import { FLAG_ICON } from '../../constants/figures.js';
import { Box, Text } from '../../ink.js';
import * as React from 'react'
import { FLAG_ICON } from '../../constants/figures.js'
import { Box, Text } from '../../ink.js'
/**
* ANT-ONLY: Banner shown in the transcript that prompts users to report
* issues via /issue. Appears when friction is detected in the conversation.
*/
export function IssueFlagBanner() {
return null;
export function IssueFlagBanner(): React.ReactNode {
if (process.env.USER_TYPE !== 'ant') {
return null
}
return (
<Box flexDirection="row" marginTop={1} width="100%">
<Box minWidth={2}>
<Text color="warning">{FLAG_ICON}</Text>
</Box>
<Text>
<Text dimColor>[ANT-ONLY] </Text>
<Text color="warning" bold>
Something off with Claude?
</Text>
<Text dimColor> /issue to report it</Text>
</Text>
</Box>
)
}

View File

@@ -1,218 +1,201 @@
import { c as _c } from "react/compiler-runtime";
import { feature } from 'bun:bundle';
import * as React from 'react';
import { type ReactNode, useEffect, useMemo, useState } from 'react';
import { type Notification, useNotifications } from 'src/context/notifications.js';
import { logEvent } from 'src/services/analytics/index.js';
import { useAppState } from 'src/state/AppState.js';
import { useVoiceState } from '../../context/voice.js';
import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js';
import { useIdeConnectionStatus } from '../../hooks/useIdeConnectionStatus.js';
import type { IDESelection } from '../../hooks/useIdeSelection.js';
import { useMainLoopModel } from '../../hooks/useMainLoopModel.js';
import { useVoiceEnabled } from '../../hooks/useVoiceEnabled.js';
import { Box, Text } from '../../ink.js';
import { useClaudeAiLimits } from '../../services/claudeAiLimitsHook.js';
import { calculateTokenWarningState } from '../../services/compact/autoCompact.js';
import type { MCPServerConnection } from '../../services/mcp/types.js';
import type { Message } from '../../types/message.js';
import { getApiKeyHelperElapsedMs, getConfiguredApiKeyHelper, getSubscriptionType } from '../../utils/auth.js';
import type { AutoUpdaterResult } from '../../utils/autoUpdater.js';
import { getExternalEditor } from '../../utils/editor.js';
import { isEnvTruthy } from '../../utils/envUtils.js';
import { formatDuration } from '../../utils/format.js';
import { setEnvHookNotifier } from '../../utils/hooks/fileChangedWatcher.js';
import { toIDEDisplayName } from '../../utils/ide.js';
import { getMessagesAfterCompactBoundary } from '../../utils/messages.js';
import { tokenCountFromLastAPIResponse } from '../../utils/tokens.js';
import { AutoUpdaterWrapper } from '../AutoUpdaterWrapper.js';
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
import { IdeStatusIndicator } from '../IdeStatusIndicator.js';
import { MemoryUsageIndicator } from '../MemoryUsageIndicator.js';
import { SentryErrorBoundary } from '../SentryErrorBoundary.js';
import { TokenWarning } from '../TokenWarning.js';
import { SandboxPromptFooterHint } from './SandboxPromptFooterHint.js';
import { feature } from 'bun:bundle'
import * as React from 'react'
import { type ReactNode, useEffect, useMemo, useState } from 'react'
import {
type Notification,
useNotifications,
} from 'src/context/notifications.js'
import { logEvent } from 'src/services/analytics/index.js'
import { useAppState } from 'src/state/AppState.js'
import { useVoiceState } from '../../context/voice.js'
import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js'
import { useIdeConnectionStatus } from '../../hooks/useIdeConnectionStatus.js'
import type { IDESelection } from '../../hooks/useIdeSelection.js'
import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'
import { useVoiceEnabled } from '../../hooks/useVoiceEnabled.js'
import { Box, Text } from '../../ink.js'
import { useClaudeAiLimits } from '../../services/claudeAiLimitsHook.js'
import { calculateTokenWarningState } from '../../services/compact/autoCompact.js'
import type { MCPServerConnection } from '../../services/mcp/types.js'
import type { Message } from '../../types/message.js'
import {
getApiKeyHelperElapsedMs,
getConfiguredApiKeyHelper,
getSubscriptionType,
} from '../../utils/auth.js'
import type { AutoUpdaterResult } from '../../utils/autoUpdater.js'
import { getExternalEditor } from '../../utils/editor.js'
import { isEnvTruthy } from '../../utils/envUtils.js'
import { formatDuration } from '../../utils/format.js'
import { setEnvHookNotifier } from '../../utils/hooks/fileChangedWatcher.js'
import { toIDEDisplayName } from '../../utils/ide.js'
import { getMessagesAfterCompactBoundary } from '../../utils/messages.js'
import { tokenCountFromLastAPIResponse } from '../../utils/tokens.js'
import { AutoUpdaterWrapper } from '../AutoUpdaterWrapper.js'
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
import { IdeStatusIndicator } from '../IdeStatusIndicator.js'
import { MemoryUsageIndicator } from '../MemoryUsageIndicator.js'
import { SentryErrorBoundary } from '../SentryErrorBoundary.js'
import { TokenWarning } from '../TokenWarning.js'
import { SandboxPromptFooterHint } from './SandboxPromptFooterHint.js'
/* eslint-disable @typescript-eslint/no-require-imports */
const VoiceIndicator: typeof import('./VoiceIndicator.js').VoiceIndicator = feature('VOICE_MODE') ? require('./VoiceIndicator.js').VoiceIndicator : () => null;
const VoiceIndicator: typeof import('./VoiceIndicator.js').VoiceIndicator =
feature('VOICE_MODE')
? require('./VoiceIndicator.js').VoiceIndicator
: () => null
/* eslint-enable @typescript-eslint/no-require-imports */
export const FOOTER_TEMPORARY_STATUS_TIMEOUT = 5000;
export const FOOTER_TEMPORARY_STATUS_TIMEOUT = 5000
type Props = {
apiKeyStatus: VerificationStatus;
autoUpdaterResult: AutoUpdaterResult | null;
isAutoUpdating: boolean;
debug: boolean;
verbose: boolean;
messages: Message[];
onAutoUpdaterResult: (result: AutoUpdaterResult) => void;
onChangeIsUpdating: (isUpdating: boolean) => void;
ideSelection: IDESelection | undefined;
mcpClients?: MCPServerConnection[];
isInputWrapped?: boolean;
isNarrow?: boolean;
};
export function Notifications(t0) {
const $ = _c(34);
const {
apiKeyStatus,
autoUpdaterResult,
debug,
isAutoUpdating,
verbose,
messages,
onAutoUpdaterResult,
onChangeIsUpdating,
ideSelection,
mcpClients,
isInputWrapped: t1,
isNarrow: t2
} = t0;
const isInputWrapped = t1 === undefined ? false : t1;
const isNarrow = t2 === undefined ? false : t2;
let t3;
if ($[0] !== messages) {
const messagesForTokenCount = getMessagesAfterCompactBoundary(messages);
t3 = tokenCountFromLastAPIResponse(messagesForTokenCount);
$[0] = messages;
$[1] = t3;
} else {
t3 = $[1];
}
const tokenUsage = t3;
const mainLoopModel = useMainLoopModel();
let t4;
if ($[2] !== mainLoopModel || $[3] !== tokenUsage) {
t4 = calculateTokenWarningState(tokenUsage, mainLoopModel);
$[2] = mainLoopModel;
$[3] = tokenUsage;
$[4] = t4;
} else {
t4 = $[4];
}
const isShowingCompactMessage = t4.isAboveWarningThreshold;
const {
status: ideStatus
} = useIdeConnectionStatus(mcpClients);
const notifications = useAppState(_temp);
const {
apiKeyStatus: VerificationStatus
autoUpdaterResult: AutoUpdaterResult | null
isAutoUpdating: boolean
debug: boolean
verbose: boolean
messages: Message[]
onAutoUpdaterResult: (result: AutoUpdaterResult) => void
onChangeIsUpdating: (isUpdating: boolean) => void
ideSelection: IDESelection | undefined
mcpClients?: MCPServerConnection[]
isInputWrapped?: boolean
isNarrow?: boolean
}
export function Notifications({
apiKeyStatus,
autoUpdaterResult,
debug,
isAutoUpdating,
verbose,
messages,
onAutoUpdaterResult,
onChangeIsUpdating,
ideSelection,
mcpClients,
isInputWrapped = false,
isNarrow = false,
}: Props): ReactNode {
const tokenUsage = useMemo(() => {
const messagesForTokenCount = getMessagesAfterCompactBoundary(messages)
return tokenCountFromLastAPIResponse(messagesForTokenCount)
}, [messages])
// AppState-sourced model — same source as API requests. getMainLoopModel()
// re-reads settings.json on every call, so another session's /model write
// would leak into this session's display (anthropics/claude-code#37596).
const mainLoopModel = useMainLoopModel()
const isShowingCompactMessage = calculateTokenWarningState(
tokenUsage,
mainLoopModel,
).isAboveWarningThreshold
const { status: ideStatus } = useIdeConnectionStatus(mcpClients)
const notifications = useAppState(s => s.notifications)
const { addNotification, removeNotification } = useNotifications()
const claudeAiLimits = useClaudeAiLimits()
// Register env hook notifier for CwdChanged/FileChanged feedback
useEffect(() => {
setEnvHookNotifier((text, isError) => {
addNotification({
key: 'env-hook',
text,
color: isError ? 'error' : undefined,
priority: isError ? 'medium' : 'low',
timeoutMs: isError ? 8000 : 5000,
})
})
return () => setEnvHookNotifier(null)
}, [addNotification])
// Check if we should show the IDE selection indicator
const shouldShowIdeSelection =
ideStatus === 'connected' &&
(ideSelection?.filePath ||
(ideSelection?.text && ideSelection.lineCount > 0))
// Hide update installed message when showing IDE selection
const shouldShowAutoUpdater =
!shouldShowIdeSelection ||
isAutoUpdating ||
autoUpdaterResult?.status !== 'success'
// Check if we're in overage mode for UI indicators
const isInOverageMode = claudeAiLimits.isUsingOverage
const subscriptionType = getSubscriptionType()
const isTeamOrEnterprise =
subscriptionType === 'team' || subscriptionType === 'enterprise'
// Check if the external editor hint should be shown
const editor = getExternalEditor()
const shouldShowExternalEditorHint =
isInputWrapped &&
!isShowingCompactMessage &&
apiKeyStatus !== 'invalid' &&
apiKeyStatus !== 'missing' &&
editor !== undefined
// Show external editor hint as notification when input is wrapped
useEffect(() => {
if (shouldShowExternalEditorHint && editor) {
logEvent('tengu_external_editor_hint_shown', {})
addNotification({
key: 'external-editor-hint',
jsx: (
<Text dimColor>
<ConfigurableShortcutHint
action="chat:externalEditor"
context="Chat"
fallback="ctrl+g"
description={`edit in ${toIDEDisplayName(editor)}`}
/>
</Text>
),
priority: 'immediate',
timeoutMs: 5000,
})
} else {
removeNotification('external-editor-hint')
}
}, [
shouldShowExternalEditorHint,
editor,
addNotification,
removeNotification
} = useNotifications();
const claudeAiLimits = useClaudeAiLimits();
let t5;
let t6;
if ($[5] !== addNotification) {
t5 = () => {
setEnvHookNotifier((text, isError) => {
addNotification({
key: "env-hook",
text,
color: isError ? "error" : undefined,
priority: isError ? "medium" : "low",
timeoutMs: isError ? 8000 : 5000
});
});
return _temp2;
};
t6 = [addNotification];
$[5] = addNotification;
$[6] = t5;
$[7] = t6;
} else {
t5 = $[6];
t6 = $[7];
}
useEffect(t5, t6);
const shouldShowIdeSelection = ideStatus === "connected" && (ideSelection?.filePath || ideSelection?.text && ideSelection.lineCount > 0);
const shouldShowAutoUpdater = !shouldShowIdeSelection || isAutoUpdating || autoUpdaterResult?.status !== "success";
const isInOverageMode = claudeAiLimits.isUsingOverage;
let t7;
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
t7 = getSubscriptionType();
$[8] = t7;
} else {
t7 = $[8];
}
const subscriptionType = t7;
const isTeamOrEnterprise = subscriptionType === "team" || subscriptionType === "enterprise";
let t8;
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
t8 = getExternalEditor();
$[9] = t8;
} else {
t8 = $[9];
}
const editor = t8;
const shouldShowExternalEditorHint = isInputWrapped && !isShowingCompactMessage && apiKeyStatus !== "invalid" && apiKeyStatus !== "missing" && editor !== undefined;
let t10;
let t9;
if ($[10] !== addNotification || $[11] !== removeNotification || $[12] !== shouldShowExternalEditorHint) {
t9 = () => {
if (shouldShowExternalEditorHint && editor) {
logEvent("tengu_external_editor_hint_shown", {});
addNotification({
key: "external-editor-hint",
jsx: <Text dimColor={true}><ConfigurableShortcutHint action="chat:externalEditor" context="Chat" fallback="ctrl+g" description={`edit in ${toIDEDisplayName(editor)}`} /></Text>,
priority: "immediate",
timeoutMs: 5000
});
} else {
removeNotification("external-editor-hint");
}
};
t10 = [shouldShowExternalEditorHint, editor, addNotification, removeNotification];
$[10] = addNotification;
$[11] = removeNotification;
$[12] = shouldShowExternalEditorHint;
$[13] = t10;
$[14] = t9;
} else {
t10 = $[13];
t9 = $[14];
}
useEffect(t9, t10);
const t11 = isNarrow ? "flex-start" : "flex-end";
const t12 = isInOverageMode ?? false;
let t13;
if ($[15] !== apiKeyStatus || $[16] !== autoUpdaterResult || $[17] !== debug || $[18] !== ideSelection || $[19] !== isAutoUpdating || $[20] !== isShowingCompactMessage || $[21] !== mainLoopModel || $[22] !== mcpClients || $[23] !== notifications || $[24] !== onAutoUpdaterResult || $[25] !== onChangeIsUpdating || $[26] !== shouldShowAutoUpdater || $[27] !== t12 || $[28] !== tokenUsage || $[29] !== verbose) {
t13 = <NotificationContent ideSelection={ideSelection} mcpClients={mcpClients} notifications={notifications} isInOverageMode={t12} isTeamOrEnterprise={isTeamOrEnterprise} apiKeyStatus={apiKeyStatus} debug={debug} verbose={verbose} tokenUsage={tokenUsage} mainLoopModel={mainLoopModel} shouldShowAutoUpdater={shouldShowAutoUpdater} autoUpdaterResult={autoUpdaterResult} isAutoUpdating={isAutoUpdating} isShowingCompactMessage={isShowingCompactMessage} onAutoUpdaterResult={onAutoUpdaterResult} onChangeIsUpdating={onChangeIsUpdating} />;
$[15] = apiKeyStatus;
$[16] = autoUpdaterResult;
$[17] = debug;
$[18] = ideSelection;
$[19] = isAutoUpdating;
$[20] = isShowingCompactMessage;
$[21] = mainLoopModel;
$[22] = mcpClients;
$[23] = notifications;
$[24] = onAutoUpdaterResult;
$[25] = onChangeIsUpdating;
$[26] = shouldShowAutoUpdater;
$[27] = t12;
$[28] = tokenUsage;
$[29] = verbose;
$[30] = t13;
} else {
t13 = $[30];
}
let t14;
if ($[31] !== t11 || $[32] !== t13) {
t14 = <SentryErrorBoundary><Box flexDirection="column" alignItems={t11} flexShrink={0} overflowX="hidden">{t13}</Box></SentryErrorBoundary>;
$[31] = t11;
$[32] = t13;
$[33] = t14;
} else {
t14 = $[33];
}
return t14;
}
function _temp2() {
return setEnvHookNotifier(null);
}
function _temp(s) {
return s.notifications;
removeNotification,
])
return (
<SentryErrorBoundary>
<Box
flexDirection="column"
alignItems={isNarrow ? 'flex-start' : 'flex-end'}
flexShrink={0}
overflowX="hidden"
>
<NotificationContent
ideSelection={ideSelection}
mcpClients={mcpClients}
notifications={notifications}
isInOverageMode={isInOverageMode ?? false}
isTeamOrEnterprise={isTeamOrEnterprise}
apiKeyStatus={apiKeyStatus}
debug={debug}
verbose={verbose}
tokenUsage={tokenUsage}
mainLoopModel={mainLoopModel}
shouldShowAutoUpdater={shouldShowAutoUpdater}
autoUpdaterResult={autoUpdaterResult}
isAutoUpdating={isAutoUpdating}
isShowingCompactMessage={isShowingCompactMessage}
onAutoUpdaterResult={onAutoUpdaterResult}
onChangeIsUpdating={onChangeIsUpdating}
/>
</Box>
</SentryErrorBoundary>
)
}
function NotificationContent({
ideSelection,
mcpClients,
@@ -229,103 +212,155 @@ function NotificationContent({
isAutoUpdating,
isShowingCompactMessage,
onAutoUpdaterResult,
onChangeIsUpdating
onChangeIsUpdating,
}: {
ideSelection: IDESelection | undefined;
mcpClients?: MCPServerConnection[];
ideSelection: IDESelection | undefined
mcpClients?: MCPServerConnection[]
notifications: {
current: Notification | null;
queue: Notification[];
};
isInOverageMode: boolean;
isTeamOrEnterprise: boolean;
apiKeyStatus: VerificationStatus;
debug: boolean;
verbose: boolean;
tokenUsage: number;
mainLoopModel: string;
shouldShowAutoUpdater: boolean;
autoUpdaterResult: AutoUpdaterResult | null;
isAutoUpdating: boolean;
isShowingCompactMessage: boolean;
onAutoUpdaterResult: (result: AutoUpdaterResult) => void;
onChangeIsUpdating: (isUpdating: boolean) => void;
current: Notification | null
queue: Notification[]
}
isInOverageMode: boolean
isTeamOrEnterprise: boolean
apiKeyStatus: VerificationStatus
debug: boolean
verbose: boolean
tokenUsage: number
mainLoopModel: string
shouldShowAutoUpdater: boolean
autoUpdaterResult: AutoUpdaterResult | null
isAutoUpdating: boolean
isShowingCompactMessage: boolean
onAutoUpdaterResult: (result: AutoUpdaterResult) => void
onChangeIsUpdating: (isUpdating: boolean) => void
}): ReactNode {
// Poll apiKeyHelper inflight state to show slow-helper notice.
// Gated on configuration — most users never set apiKeyHelper, so the
// effect is a no-op for them (no interval allocated).
const [apiKeyHelperSlow, setApiKeyHelperSlow] = useState<string | null>(null);
const [apiKeyHelperSlow, setApiKeyHelperSlow] = useState<string | null>(null)
useEffect(() => {
if (!getConfiguredApiKeyHelper()) return;
const interval = setInterval((setSlow: React.Dispatch<React.SetStateAction<string | null>>) => {
const ms = getApiKeyHelperElapsedMs();
const next = ms >= 10_000 ? formatDuration(ms) : null;
setSlow(prev => next === prev ? prev : next);
}, 1000, setApiKeyHelperSlow);
return () => clearInterval(interval);
}, []);
if (!getConfiguredApiKeyHelper()) return
const interval = setInterval(
(setSlow: React.Dispatch<React.SetStateAction<string | null>>) => {
const ms = getApiKeyHelperElapsedMs()
const next = ms >= 10_000 ? formatDuration(ms) : null
setSlow(prev => (next === prev ? prev : next))
},
1000,
setApiKeyHelperSlow,
)
return () => clearInterval(interval)
}, [])
// Voice state (VOICE_MODE builds only, runtime-gated by GrowthBook)
const voiceState = feature('VOICE_MODE') ?
const voiceState = feature('VOICE_MODE')
? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
useVoiceState(s => s.voiceState)
: ('idle' as const)
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
useVoiceState(s => s.voiceState) : 'idle' as const;
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
const voiceEnabled = feature('VOICE_MODE') ? useVoiceEnabled() : false;
const voiceError = feature('VOICE_MODE') ?
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
useVoiceState(s_0 => s_0.voiceError) : null;
const isBriefOnly = feature('KAIROS') || feature('KAIROS_BRIEF') ?
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
useAppState(s_1 => s_1.isBriefOnly) : false;
const voiceEnabled = feature('VOICE_MODE') ? useVoiceEnabled() : false
const voiceError = feature('VOICE_MODE')
? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
useVoiceState(s => s.voiceError)
: null
const isBriefOnly =
feature('KAIROS') || feature('KAIROS_BRIEF')
? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
useAppState(s => s.isBriefOnly)
: false
// When voice is actively recording or processing, replace all
// notifications with just the voice indicator.
if (feature('VOICE_MODE') && voiceEnabled && (voiceState === 'recording' || voiceState === 'processing')) {
return <VoiceIndicator voiceState={voiceState} />;
if (
feature('VOICE_MODE') &&
voiceEnabled &&
(voiceState === 'recording' || voiceState === 'processing')
) {
return <VoiceIndicator voiceState={voiceState} />
}
return <>
return (
<>
<IdeStatusIndicator ideSelection={ideSelection} mcpClients={mcpClients} />
{notifications.current && ('jsx' in notifications.current ? <Text wrap="truncate" key={notifications.current.key}>
{notifications.current &&
('jsx' in notifications.current ? (
<Text wrap="truncate" key={notifications.current.key}>
{notifications.current.jsx}
</Text> : <Text color={notifications.current.color} dimColor={!notifications.current.color} wrap="truncate">
</Text>
) : (
<Text
color={notifications.current.color}
dimColor={!notifications.current.color}
wrap="truncate"
>
{notifications.current.text}
</Text>)}
{isInOverageMode && !isTeamOrEnterprise && <Box>
</Text>
))}
{isInOverageMode && !isTeamOrEnterprise && (
<Box>
<Text dimColor wrap="truncate">
Now using extra usage
</Text>
</Box>}
{apiKeyHelperSlow && <Box>
</Box>
)}
{apiKeyHelperSlow && (
<Box>
<Text color="warning" wrap="truncate">
apiKeyHelper is taking a while{' '}
</Text>
<Text dimColor wrap="truncate">
({apiKeyHelperSlow})
</Text>
</Box>}
{(apiKeyStatus === 'invalid' || apiKeyStatus === 'missing') && <Box>
</Box>
)}
{(apiKeyStatus === 'invalid' || apiKeyStatus === 'missing') && (
<Box>
<Text color="error" wrap="truncate">
{isEnvTruthy(process.env.CLAUDE_CODE_REMOTE) ? 'Authentication error · Try again' : 'Not logged in · Run /login'}
{isEnvTruthy(process.env.CLAUDE_CODE_REMOTE)
? 'Authentication error · Try again'
: 'Not logged in · Run /login'}
</Text>
</Box>}
{debug && <Box>
</Box>
)}
{debug && (
<Box>
<Text color="warning" wrap="truncate">
Debug mode
</Text>
</Box>}
{apiKeyStatus !== 'invalid' && apiKeyStatus !== 'missing' && verbose && <Box>
</Box>
)}
{apiKeyStatus !== 'invalid' && apiKeyStatus !== 'missing' && verbose && (
<Box>
<Text dimColor wrap="truncate">
{tokenUsage} tokens
</Text>
</Box>}
{!isBriefOnly && <TokenWarning tokenUsage={tokenUsage} model={mainLoopModel} />}
{shouldShowAutoUpdater && <AutoUpdaterWrapper verbose={verbose} onAutoUpdaterResult={onAutoUpdaterResult} autoUpdaterResult={autoUpdaterResult} isUpdating={isAutoUpdating} onChangeIsUpdating={onChangeIsUpdating} showSuccessMessage={!isShowingCompactMessage} />}
{feature('VOICE_MODE') ? voiceEnabled && voiceError && <Box>
</Box>
)}
{!isBriefOnly && (
<TokenWarning tokenUsage={tokenUsage} model={mainLoopModel} />
)}
{shouldShowAutoUpdater && (
<AutoUpdaterWrapper
verbose={verbose}
onAutoUpdaterResult={onAutoUpdaterResult}
autoUpdaterResult={autoUpdaterResult}
isUpdating={isAutoUpdating}
onChangeIsUpdating={onChangeIsUpdating}
showSuccessMessage={!isShowingCompactMessage}
/>
)}
{feature('VOICE_MODE')
? voiceEnabled &&
voiceError && (
<Box>
<Text color="error" wrap="truncate">
{voiceError}
</Text>
</Box> : null}
</Box>
)
: null}
<MemoryUsageIndicator />
<SandboxPromptFooterHint />
</>;
</>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,65 +1,77 @@
import { feature } from 'bun:bundle';
import * as React from 'react';
import { memo, type ReactNode, useMemo, useRef } from 'react';
import { isBridgeEnabled } from '../../bridge/bridgeEnabled.js';
import { getBridgeStatus } from '../../bridge/bridgeStatusUtil.js';
import { useSetPromptOverlay } from '../../context/promptOverlayContext.js';
import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js';
import type { IDESelection } from '../../hooks/useIdeSelection.js';
import { useSettings } from '../../hooks/useSettings.js';
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
import { Box, Text } from '../../ink.js';
import type { MCPServerConnection } from '../../services/mcp/types.js';
import { useAppState } from '../../state/AppState.js';
import type { ToolPermissionContext } from '../../Tool.js';
import type { Message } from '../../types/message.js';
import type { PromptInputMode, VimMode } from '../../types/textInputTypes.js';
import type { AutoUpdaterResult } from '../../utils/autoUpdater.js';
import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js';
import { isUndercover } from '../../utils/undercover.js';
import { CoordinatorTaskPanel, useCoordinatorTaskCount } from '../CoordinatorAgentStatus.js';
import { getLastAssistantMessageId, StatusLine, statusLineShouldDisplay } from '../StatusLine.js';
import { Notifications } from './Notifications.js';
import { PromptInputFooterLeftSide } from './PromptInputFooterLeftSide.js';
import { PromptInputFooterSuggestions, type SuggestionItem } from './PromptInputFooterSuggestions.js';
import { PromptInputHelpMenu } from './PromptInputHelpMenu.js';
import { feature } from 'bun:bundle'
import * as React from 'react'
import { memo, type ReactNode, useMemo, useRef } from 'react'
import { isBridgeEnabled } from '../../bridge/bridgeEnabled.js'
import { getBridgeStatus } from '../../bridge/bridgeStatusUtil.js'
import { useSetPromptOverlay } from '../../context/promptOverlayContext.js'
import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js'
import type { IDESelection } from '../../hooks/useIdeSelection.js'
import { useSettings } from '../../hooks/useSettings.js'
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
import { Box, Text } from '../../ink.js'
import type { MCPServerConnection } from '../../services/mcp/types.js'
import { useAppState } from '../../state/AppState.js'
import type { ToolPermissionContext } from '../../Tool.js'
import type { Message } from '../../types/message.js'
import type { PromptInputMode, VimMode } from '../../types/textInputTypes.js'
import type { AutoUpdaterResult } from '../../utils/autoUpdater.js'
import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'
import { isUndercover } from '../../utils/undercover.js'
import {
CoordinatorTaskPanel,
useCoordinatorTaskCount,
} from '../CoordinatorAgentStatus.js'
import {
getLastAssistantMessageId,
StatusLine,
statusLineShouldDisplay,
} from '../StatusLine.js'
import { Notifications } from './Notifications.js'
import { PromptInputFooterLeftSide } from './PromptInputFooterLeftSide.js'
import {
PromptInputFooterSuggestions,
type SuggestionItem,
} from './PromptInputFooterSuggestions.js'
import { PromptInputHelpMenu } from './PromptInputHelpMenu.js'
type Props = {
apiKeyStatus: VerificationStatus;
debug: boolean;
apiKeyStatus: VerificationStatus
debug: boolean
exitMessage: {
show: boolean;
key?: string;
};
vimMode: VimMode | undefined;
mode: PromptInputMode;
autoUpdaterResult: AutoUpdaterResult | null;
isAutoUpdating: boolean;
verbose: boolean;
onAutoUpdaterResult: (result: AutoUpdaterResult) => void;
onChangeIsUpdating: (isUpdating: boolean) => void;
suggestions: SuggestionItem[];
selectedSuggestion: number;
maxColumnWidth?: number;
toolPermissionContext: ToolPermissionContext;
helpOpen: boolean;
suppressHint: boolean;
isLoading: boolean;
tasksSelected: boolean;
teamsSelected: boolean;
bridgeSelected: boolean;
tmuxSelected: boolean;
teammateFooterIndex?: number;
ideSelection: IDESelection | undefined;
mcpClients?: MCPServerConnection[];
isPasting?: boolean;
isInputWrapped?: boolean;
messages: Message[];
isSearching: boolean;
historyQuery: string;
setHistoryQuery: (query: string) => void;
historyFailedMatch: boolean;
onOpenTasksDialog?: (taskId?: string) => void;
};
show: boolean
key?: string
}
vimMode: VimMode | undefined
mode: PromptInputMode
autoUpdaterResult: AutoUpdaterResult | null
isAutoUpdating: boolean
verbose: boolean
onAutoUpdaterResult: (result: AutoUpdaterResult) => void
onChangeIsUpdating: (isUpdating: boolean) => void
suggestions: SuggestionItem[]
selectedSuggestion: number
maxColumnWidth?: number
toolPermissionContext: ToolPermissionContext
helpOpen: boolean
suppressHint: boolean
isLoading: boolean
tasksSelected: boolean
teamsSelected: boolean
bridgeSelected: boolean
tmuxSelected: boolean
teammateFooterIndex?: number
ideSelection: IDESelection | undefined
mcpClients?: MCPServerConnection[]
isPasting?: boolean
isInputWrapped?: boolean
messages: Message[]
isSearching: boolean
historyQuery: string
setHistoryQuery: (query: string) => void
historyFailedMatch: boolean
onOpenTasksDialog?: (taskId?: string) => void
}
function PromptInputFooter({
apiKeyStatus,
debug,
@@ -92,99 +104,176 @@ function PromptInputFooter({
historyQuery,
setHistoryQuery,
historyFailedMatch,
onOpenTasksDialog
onOpenTasksDialog,
}: Props): ReactNode {
const settings = useSettings();
const {
columns,
rows
} = useTerminalSize();
const messagesRef = useRef(messages);
messagesRef.current = messages;
const lastAssistantMessageId = useMemo(() => getLastAssistantMessageId(messages), [messages]);
const isNarrow = columns < 80;
const settings = useSettings()
const { columns, rows } = useTerminalSize()
const messagesRef = useRef(messages)
messagesRef.current = messages
const lastAssistantMessageId = useMemo(
() => getLastAssistantMessageId(messages),
[messages],
)
const isNarrow = columns < 80
// In fullscreen the bottom slot is flexShrink:0, so every row here is a row
// stolen from the ScrollBox. Drop the optional StatusLine first. Non-fullscreen
// has terminal scrollback to absorb overflow, so we never hide StatusLine there.
const isFullscreen = isFullscreenEnvEnabled();
const isShort = isFullscreen && rows < 24;
const isFullscreen = isFullscreenEnvEnabled()
const isShort = isFullscreen && rows < 24
// Pill highlights when tasks is the active footer item AND no specific
// agent row is selected. When coordinatorTaskIndex >= 0 the pointer has
// moved into CoordinatorTaskPanel, so the pill should un-highlight.
// coordinatorTaskCount === 0 covers the bash-only case (no agent rows
// exist, pill is the only selectable item).
const coordinatorTaskCount = useCoordinatorTaskCount();
const coordinatorTaskIndex = useAppState(s => s.coordinatorTaskIndex);
const pillSelected = tasksSelected && (coordinatorTaskCount === 0 || coordinatorTaskIndex < 0);
const coordinatorTaskCount = useCoordinatorTaskCount()
const coordinatorTaskIndex = useAppState(s => s.coordinatorTaskIndex)
const pillSelected =
tasksSelected && (coordinatorTaskCount === 0 || coordinatorTaskIndex < 0)
// Hide `? for shortcuts` if the user has a custom status line, or during ctrl-r
const suppressHint = suppressHintFromProps || statusLineShouldDisplay(settings) || isSearching;
const suppressHint =
suppressHintFromProps || statusLineShouldDisplay(settings) || isSearching
// Fullscreen: portal data to FullscreenLayout — see promptOverlayContext.tsx
const overlayData = useMemo(() => isFullscreen && suggestions.length ? {
suggestions,
selectedSuggestion,
maxColumnWidth
} : null, [isFullscreen, suggestions, selectedSuggestion, maxColumnWidth]);
useSetPromptOverlay(overlayData);
const overlayData = useMemo(
() =>
isFullscreen && suggestions.length
? { suggestions, selectedSuggestion, maxColumnWidth }
: null,
[isFullscreen, suggestions, selectedSuggestion, maxColumnWidth],
)
useSetPromptOverlay(overlayData)
if (suggestions.length && !isFullscreen) {
return <Box paddingX={2} paddingY={0}>
<PromptInputFooterSuggestions suggestions={suggestions} selectedSuggestion={selectedSuggestion} maxColumnWidth={maxColumnWidth} />
</Box>;
return (
<Box paddingX={2} paddingY={0}>
<PromptInputFooterSuggestions
suggestions={suggestions}
selectedSuggestion={selectedSuggestion}
maxColumnWidth={maxColumnWidth}
/>
</Box>
)
}
if (helpOpen) {
return <PromptInputHelpMenu dimColor={true} fixedWidth={true} paddingX={2} />;
return (
<PromptInputHelpMenu dimColor={true} fixedWidth={true} paddingX={2} />
)
}
return <>
<Box flexDirection={isNarrow ? 'column' : 'row'} justifyContent={isNarrow ? 'flex-start' : 'space-between'} paddingX={2} gap={isNarrow ? 0 : 1}>
return (
<>
<Box
flexDirection={isNarrow ? 'column' : 'row'}
justifyContent={isNarrow ? 'flex-start' : 'space-between'}
paddingX={2}
gap={isNarrow ? 0 : 1}
>
<Box flexDirection="column" flexShrink={isNarrow ? 0 : 1}>
{mode === 'prompt' && !isShort && !exitMessage.show && !isPasting && statusLineShouldDisplay(settings) && <StatusLine messagesRef={messagesRef} lastAssistantMessageId={lastAssistantMessageId} vimMode={vimMode} />}
<PromptInputFooterLeftSide exitMessage={exitMessage} vimMode={vimMode} mode={mode} toolPermissionContext={toolPermissionContext} suppressHint={suppressHint} isLoading={isLoading} tasksSelected={pillSelected} teamsSelected={teamsSelected} teammateFooterIndex={teammateFooterIndex} tmuxSelected={tmuxSelected} isPasting={isPasting} isSearching={isSearching} historyQuery={historyQuery} setHistoryQuery={setHistoryQuery} historyFailedMatch={historyFailedMatch} onOpenTasksDialog={onOpenTasksDialog} />
{mode === 'prompt' &&
!isShort &&
!exitMessage.show &&
!isPasting &&
statusLineShouldDisplay(settings) && (
<StatusLine
messagesRef={messagesRef}
lastAssistantMessageId={lastAssistantMessageId}
vimMode={vimMode}
/>
)}
<PromptInputFooterLeftSide
exitMessage={exitMessage}
vimMode={vimMode}
mode={mode}
toolPermissionContext={toolPermissionContext}
suppressHint={suppressHint}
isLoading={isLoading}
tasksSelected={pillSelected}
teamsSelected={teamsSelected}
teammateFooterIndex={teammateFooterIndex}
tmuxSelected={tmuxSelected}
isPasting={isPasting}
isSearching={isSearching}
historyQuery={historyQuery}
setHistoryQuery={setHistoryQuery}
historyFailedMatch={historyFailedMatch}
onOpenTasksDialog={onOpenTasksDialog}
/>
</Box>
<Box flexShrink={1} gap={1}>
{isFullscreen ? null : <Notifications apiKeyStatus={apiKeyStatus} autoUpdaterResult={autoUpdaterResult} debug={debug} isAutoUpdating={isAutoUpdating} verbose={verbose} messages={messages} onAutoUpdaterResult={onAutoUpdaterResult} onChangeIsUpdating={onChangeIsUpdating} ideSelection={ideSelection} mcpClients={mcpClients} isInputWrapped={isInputWrapped} isNarrow={isNarrow} />}
{(process.env.USER_TYPE) === 'ant' && isUndercover() && <Text dimColor>undercover</Text>}
{isFullscreen ? null : (
<Notifications
apiKeyStatus={apiKeyStatus}
autoUpdaterResult={autoUpdaterResult}
debug={debug}
isAutoUpdating={isAutoUpdating}
verbose={verbose}
messages={messages}
onAutoUpdaterResult={onAutoUpdaterResult}
onChangeIsUpdating={onChangeIsUpdating}
ideSelection={ideSelection}
mcpClients={mcpClients}
isInputWrapped={isInputWrapped}
isNarrow={isNarrow}
/>
)}
{process.env.USER_TYPE === 'ant' && isUndercover() && (
<Text dimColor>undercover</Text>
)}
<BridgeStatusIndicator bridgeSelected={bridgeSelected} />
</Box>
</Box>
{(process.env.USER_TYPE) === 'ant' && <CoordinatorTaskPanel />}
</>;
{process.env.USER_TYPE === 'ant' && <CoordinatorTaskPanel />}
</>
)
}
export default memo(PromptInputFooter);
export default memo(PromptInputFooter)
type BridgeStatusProps = {
bridgeSelected: boolean;
};
bridgeSelected: boolean
}
function BridgeStatusIndicator({
bridgeSelected
bridgeSelected,
}: BridgeStatusProps): React.ReactNode {
if (!feature('BRIDGE_MODE')) return null;
if (!feature('BRIDGE_MODE')) return null
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
const enabled = useAppState(s => s.replBridgeEnabled);
const enabled = useAppState(s => s.replBridgeEnabled)
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
const connected = useAppState(s_0 => s_0.replBridgeConnected);
const connected = useAppState(s => s.replBridgeConnected)
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
const sessionActive = useAppState(s_1 => s_1.replBridgeSessionActive);
const sessionActive = useAppState(s => s.replBridgeSessionActive)
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
const reconnecting = useAppState(s_2 => s_2.replBridgeReconnecting);
const reconnecting = useAppState(s => s.replBridgeReconnecting)
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
const explicit = useAppState(s_3 => s_3.replBridgeExplicit);
const explicit = useAppState(s => s.replBridgeExplicit)
// Failed state is surfaced via notification (useReplBridge), not a footer pill.
if (!isBridgeEnabled() || !enabled) return null;
if (!isBridgeEnabled() || !enabled) return null
const status = getBridgeStatus({
error: undefined,
connected,
sessionActive,
reconnecting
});
reconnecting,
})
// For implicit (config-driven) remote, only show the reconnecting state
if (!explicit && status.label !== 'Remote Control reconnecting') {
return null;
return null
}
return <Text color={bridgeSelected ? 'background' : status.color} inverse={bridgeSelected} wrap="truncate">
return (
<Text
color={bridgeSelected ? 'background' : status.color}
inverse={bridgeSelected}
wrap="truncate"
>
{status.label}
{bridgeSelected && <Text dimColor> · Enter to view</Text>}
</Text>;
</Text>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,292 +1,248 @@
import { c as _c } from "react/compiler-runtime";
import * as React from 'react';
import { memo, type ReactNode } from 'react';
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
import { stringWidth } from '../../ink/stringWidth.js';
import { Box, Text } from '../../ink.js';
import { truncatePathMiddle, truncateToWidth } from '../../utils/format.js';
import type { Theme } from '../../utils/theme.js';
import * as React from 'react'
import { memo, type ReactNode } from 'react'
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
import { stringWidth } from '../../ink/stringWidth.js'
import { Box, Text } from '../../ink.js'
import { truncatePathMiddle, truncateToWidth } from '../../utils/format.js'
import type { Theme } from '../../utils/theme.js'
export type SuggestionItem = {
id: string;
displayText: string;
tag?: string;
description?: string;
metadata?: unknown;
color?: keyof Theme;
};
export type SuggestionType = 'command' | 'file' | 'directory' | 'agent' | 'shell' | 'custom-title' | 'slack-channel' | 'none';
export const OVERLAY_MAX_ITEMS = 5;
id: string
displayText: string
tag?: string
description?: string
metadata?: unknown
color?: keyof Theme
}
export type SuggestionType =
| 'command'
| 'file'
| 'directory'
| 'agent'
| 'shell'
| 'custom-title'
| 'slack-channel'
| 'none'
export const OVERLAY_MAX_ITEMS = 5
/**
* Get the icon for a suggestion based on its type
* Icons: + for files, ◇ for MCP resources, * for agents
*/
function getIcon(itemId: string): string {
if (itemId.startsWith('file-')) return '+';
if (itemId.startsWith('mcp-resource-')) return '◇';
if (itemId.startsWith('agent-')) return '*';
return '+';
if (itemId.startsWith('file-')) return '+'
if (itemId.startsWith('mcp-resource-')) return '◇'
if (itemId.startsWith('agent-')) return '*'
return '+'
}
/**
* Check if an item is a unified suggestion type (file, mcp-resource, or agent)
*/
function isUnifiedSuggestion(itemId: string): boolean {
return itemId.startsWith('file-') || itemId.startsWith('mcp-resource-') || itemId.startsWith('agent-');
return (
itemId.startsWith('file-') ||
itemId.startsWith('mcp-resource-') ||
itemId.startsWith('agent-')
)
}
const SuggestionItemRow = memo(function SuggestionItemRow(t0: { item: SuggestionItem; maxColumnWidth: number; isSelected: boolean }) {
const $ = _c(36);
const {
item,
maxColumnWidth,
isSelected
} = t0;
const columns = useTerminalSize().columns;
const isUnified = isUnifiedSuggestion(item.id);
const SuggestionItemRow = memo(function SuggestionItemRow({
item,
maxColumnWidth,
isSelected,
}: {
item: SuggestionItem
maxColumnWidth?: number
isSelected: boolean
}): ReactNode {
const columns = useTerminalSize().columns
const isUnified = isUnifiedSuggestion(item.id)
// For unified suggestions (file, mcp-resource, agent), use single-line layout with icon
if (isUnified) {
let t1;
if ($[0] !== item.id) {
t1 = getIcon(item.id);
$[0] = item.id;
$[1] = t1;
} else {
t1 = $[1];
}
const icon = t1;
const textColor = isSelected ? "suggestion" : undefined;
const dimColor = !isSelected;
const isFile = item.id.startsWith("file-");
const isMcpResource = item.id.startsWith("mcp-resource-");
const separatorWidth = item.description ? 3 : 0;
let displayText;
const icon = getIcon(item.id)
const textColor: keyof Theme | undefined = isSelected
? 'suggestion'
: undefined
const dimColor = !isSelected
const isFile = item.id.startsWith('file-')
const isMcpResource = item.id.startsWith('mcp-resource-')
// Calculate layout widths
// Layout: "X " (2) + displayText + " " (3) + description + padding (4)
const iconWidth = 2 // icon + space (fixed)
const paddingWidth = 4
const separatorWidth = item.description ? 3 : 0 // ' ' separator
// For files, truncate middle of path to show both directory context and filename
// For MCP resources, limit displayText to 30 chars (truncate from end)
// For agents, no truncation
let displayText: string
if (isFile) {
let t2;
if ($[2] !== item.description) {
t2 = item.description ? Math.min(20, stringWidth(item.description)) : 0;
$[2] = item.description;
$[3] = t2;
} else {
t2 = $[3];
}
const descReserve = t2;
const maxPathLength = columns - 2 - 4 - separatorWidth - descReserve;
let t3;
if ($[4] !== item.displayText || $[5] !== maxPathLength) {
t3 = truncatePathMiddle(item.displayText, maxPathLength);
$[4] = item.displayText;
$[5] = maxPathLength;
$[6] = t3;
} else {
t3 = $[6];
}
displayText = t3;
// Reserve space for description if present, otherwise use all available space
const descReserve = item.description
? Math.min(20, stringWidth(item.description))
: 0
const maxPathLength =
columns - iconWidth - paddingWidth - separatorWidth - descReserve
displayText = truncatePathMiddle(item.displayText, maxPathLength)
} else if (isMcpResource) {
const maxDisplayTextLength = 30
displayText = truncateToWidth(item.displayText, maxDisplayTextLength)
} else {
if (isMcpResource) {
let t2;
if ($[7] !== item.displayText) {
t2 = truncateToWidth(item.displayText, 30);
$[7] = item.displayText;
$[8] = t2;
} else {
t2 = $[8];
}
displayText = t2;
} else {
displayText = item.displayText;
}
displayText = item.displayText
}
const availableWidth = columns - 2 - stringWidth(displayText) - separatorWidth - 4;
let lineContent;
const availableWidth =
columns -
iconWidth -
stringWidth(displayText) -
separatorWidth -
paddingWidth
// Build the full line as a single string to prevent wrapping
let lineContent: string
if (item.description) {
const maxDescLength = Math.max(0, availableWidth);
let t2;
if ($[9] !== item.description || $[10] !== maxDescLength) {
t2 = truncateToWidth(item.description.replace(/\s+/g, " "), maxDescLength);
$[9] = item.description;
$[10] = maxDescLength;
$[11] = t2;
} else {
t2 = $[11];
}
const truncatedDesc = t2;
lineContent = `${icon} ${displayText} ${truncatedDesc}`;
const maxDescLength = Math.max(0, availableWidth)
const truncatedDesc = truncateToWidth(
item.description.replace(/\s+/g, ' '),
maxDescLength,
)
lineContent = `${icon} ${displayText} ${truncatedDesc}`
} else {
lineContent = `${icon} ${displayText}`;
lineContent = `${icon} ${displayText}`
}
let t2;
if ($[12] !== dimColor || $[13] !== lineContent || $[14] !== textColor) {
t2 = <Text color={textColor} dimColor={dimColor} wrap="truncate">{lineContent}</Text>;
$[12] = dimColor;
$[13] = lineContent;
$[14] = textColor;
$[15] = t2;
} else {
t2 = $[15];
}
return t2;
return (
<Text color={textColor} dimColor={dimColor} wrap="truncate">
{lineContent}
</Text>
)
}
const maxNameWidth = Math.floor(columns * 0.4);
const displayTextWidth = Math.min(maxColumnWidth ?? stringWidth(item.displayText) + 5, maxNameWidth);
const textColor_0 = item.color || (isSelected ? "suggestion" : undefined);
const shouldDim = !isSelected;
let displayText_0 = item.displayText;
if (stringWidth(displayText_0) > displayTextWidth - 2) {
const t1 = displayTextWidth - 2;
let t2;
if ($[16] !== displayText_0 || $[17] !== t1) {
t2 = truncateToWidth(displayText_0, t1);
$[16] = displayText_0;
$[17] = t1;
$[18] = t2;
} else {
t2 = $[18];
}
displayText_0 = t2;
// For non-unified suggestions (commands, shell, etc.), use improved layout from main
// Cap the command name column at 40% of terminal width to ensure description has space
const maxNameWidth = Math.floor(columns * 0.4)
const displayTextWidth = Math.min(
maxColumnWidth ?? stringWidth(item.displayText) + 5,
maxNameWidth,
)
const textColor = item.color || (isSelected ? 'suggestion' : undefined)
const shouldDim = !isSelected
// Truncate and pad the display text to fixed width
let displayText = item.displayText
if (stringWidth(displayText) > displayTextWidth - 2) {
displayText = truncateToWidth(displayText, displayTextWidth - 2)
}
const paddedDisplayText = displayText_0 + " ".repeat(Math.max(0, displayTextWidth - stringWidth(displayText_0)));
const tagText = item.tag ? `[${item.tag}] ` : "";
const tagWidth = stringWidth(tagText);
const descriptionWidth = Math.max(0, columns - displayTextWidth - tagWidth - 4);
let t1;
if ($[19] !== descriptionWidth || $[20] !== item.description) {
t1 = item.description ? truncateToWidth(item.description.replace(/\s+/g, " "), descriptionWidth) : "";
$[19] = descriptionWidth;
$[20] = item.description;
$[21] = t1;
} else {
t1 = $[21];
}
const truncatedDescription = t1;
let t2;
if ($[22] !== paddedDisplayText || $[23] !== shouldDim || $[24] !== textColor_0) {
t2 = <Text color={textColor_0} dimColor={shouldDim}>{paddedDisplayText}</Text>;
$[22] = paddedDisplayText;
$[23] = shouldDim;
$[24] = textColor_0;
$[25] = t2;
} else {
t2 = $[25];
}
let t3;
if ($[26] !== tagText) {
t3 = tagText ? <Text dimColor={true}>{tagText}</Text> : null;
$[26] = tagText;
$[27] = t3;
} else {
t3 = $[27];
}
const t4 = isSelected ? "suggestion" : undefined;
const t5 = !isSelected;
let t6;
if ($[28] !== t4 || $[29] !== t5 || $[30] !== truncatedDescription) {
t6 = <Text color={t4} dimColor={t5}>{truncatedDescription}</Text>;
$[28] = t4;
$[29] = t5;
$[30] = truncatedDescription;
$[31] = t6;
} else {
t6 = $[31];
}
let t7;
if ($[32] !== t2 || $[33] !== t3 || $[34] !== t6) {
t7 = <Text wrap="truncate">{t2}{t3}{t6}</Text>;
$[32] = t2;
$[33] = t3;
$[34] = t6;
$[35] = t7;
} else {
t7 = $[35];
}
return t7;
});
const paddedDisplayText =
displayText +
' '.repeat(Math.max(0, displayTextWidth - stringWidth(displayText)))
const tagText = item.tag ? `[${item.tag}] ` : ''
const tagWidth = stringWidth(tagText)
const descriptionWidth = Math.max(
0,
columns - displayTextWidth - tagWidth - 4,
)
// Skill descriptions can contain newlines (e.g. /claude-api's "TRIGGER
// when:" block). A multi-line row grows the overlay past minHeight; when
// the filter narrows past that skill, the overlay shrinks and leaves
// ghost rows. Flatten to one line before truncating.
const truncatedDescription = item.description
? truncateToWidth(item.description.replace(/\s+/g, ' '), descriptionWidth)
: ''
return (
<Text wrap="truncate">
<Text color={textColor} dimColor={shouldDim}>
{paddedDisplayText}
</Text>
{tagText ? <Text dimColor>{tagText}</Text> : null}
<Text
color={isSelected ? 'suggestion' : undefined}
dimColor={!isSelected}
>
{truncatedDescription}
</Text>
</Text>
)
})
type Props = {
suggestions: SuggestionItem[];
selectedSuggestion: number;
maxColumnWidth?: number;
suggestions: SuggestionItem[]
selectedSuggestion: number
maxColumnWidth?: number
/**
* When true, the suggestions are rendered inside a position=absolute
* overlay. We omit minHeight and flex-end so the y-clamp in the
* renderer doesn't push fewer items down into the prompt area.
*/
overlay?: boolean;
};
export function PromptInputFooterSuggestions(t0) {
const $ = _c(22);
const {
suggestions,
selectedSuggestion,
maxColumnWidth: maxColumnWidthProp,
overlay
} = t0;
const {
rows
} = useTerminalSize();
const maxVisibleItems = overlay ? OVERLAY_MAX_ITEMS : Math.min(6, Math.max(1, rows - 3));
overlay?: boolean
}
export function PromptInputFooterSuggestions({
suggestions,
selectedSuggestion,
maxColumnWidth: maxColumnWidthProp,
overlay,
}: Props): ReactNode {
const { rows } = useTerminalSize()
// Maximum number of suggestions to show at once (leaving space for prompt).
// Overlay mode (fullscreen) uses a fixed 5 — the floating box sits over
// the ScrollBox, so terminal height isn't the constraint.
const maxVisibleItems = overlay
? OVERLAY_MAX_ITEMS
: Math.min(6, Math.max(1, rows - 3))
// No suggestions to display
if (suggestions.length === 0) {
return null;
return null
}
let t1;
if ($[0] !== maxColumnWidthProp || $[1] !== suggestions) {
t1 = maxColumnWidthProp ?? Math.max(...suggestions.map(_temp)) + 5;
$[0] = maxColumnWidthProp;
$[1] = suggestions;
$[2] = t1;
} else {
t1 = $[2];
}
const maxColumnWidth = t1;
const startIndex = Math.max(0, Math.min(selectedSuggestion - Math.floor(maxVisibleItems / 2), suggestions.length - maxVisibleItems));
const endIndex = Math.min(startIndex + maxVisibleItems, suggestions.length);
let T0;
let t2;
let t3;
let t4;
if ($[3] !== endIndex || $[4] !== maxColumnWidth || $[5] !== overlay || $[6] !== selectedSuggestion || $[7] !== startIndex || $[8] !== suggestions) {
const visibleItems = suggestions.slice(startIndex, endIndex);
T0 = Box;
t2 = "column";
t3 = overlay ? undefined : "flex-end";
let t5;
if ($[13] !== maxColumnWidth || $[14] !== selectedSuggestion || $[15] !== suggestions) {
t5 = item_0 => <SuggestionItemRow key={item_0.id} item={item_0} maxColumnWidth={maxColumnWidth} isSelected={item_0.id === suggestions[selectedSuggestion]?.id} />;
$[13] = maxColumnWidth;
$[14] = selectedSuggestion;
$[15] = suggestions;
$[16] = t5;
} else {
t5 = $[16];
}
t4 = visibleItems.map(t5);
$[3] = endIndex;
$[4] = maxColumnWidth;
$[5] = overlay;
$[6] = selectedSuggestion;
$[7] = startIndex;
$[8] = suggestions;
$[9] = T0;
$[10] = t2;
$[11] = t3;
$[12] = t4;
} else {
T0 = $[9];
t2 = $[10];
t3 = $[11];
t4 = $[12];
}
let t5;
if ($[17] !== T0 || $[18] !== t2 || $[19] !== t3 || $[20] !== t4) {
t5 = <T0 flexDirection={t2} justifyContent={t3}>{t4}</T0>;
$[17] = T0;
$[18] = t2;
$[19] = t3;
$[20] = t4;
$[21] = t5;
} else {
t5 = $[21];
}
return t5;
// Use prop if provided (stable width from all commands), otherwise calculate from visible
const maxColumnWidth =
maxColumnWidthProp ??
Math.max(...suggestions.map(item => stringWidth(item.displayText))) + 5
// Calculate visible items range based on selected index
const startIndex = Math.max(
0,
Math.min(
selectedSuggestion - Math.floor(maxVisibleItems / 2),
suggestions.length - maxVisibleItems,
),
)
const endIndex = Math.min(startIndex + maxVisibleItems, suggestions.length)
const visibleItems = suggestions.slice(startIndex, endIndex)
// In non-overlay (inline) mode, justifyContent keeps suggestions
// anchored to the bottom (near the prompt). In overlay mode we omit
// both minHeight and flex-end: the parent is position=absolute with
// bottom='100%', so its y is clamped to 0 by the renderer when it
// would go negative. Adding minHeight + flex-end would create empty
// padding rows that shift the visible items down into the prompt area
// when the list has fewer items than maxVisibleItems.
return (
<Box
flexDirection="column"
justifyContent={overlay ? undefined : 'flex-end'}
>
{visibleItems.map(item => (
<SuggestionItemRow
key={item.id}
item={item}
maxColumnWidth={maxColumnWidth}
isSelected={item.id === suggestions[selectedSuggestion]?.id}
/>
))}
</Box>
)
}
function _temp(item) {
return stringWidth(item.displayText);
}
export default memo(PromptInputFooterSuggestions);
export default memo(PromptInputFooterSuggestions)

View File

@@ -1,357 +1,149 @@
import { c as _c } from "react/compiler-runtime";
import { feature } from 'bun:bundle';
import * as React from 'react';
import { Box, Text } from 'src/ink.js';
import { getPlatform } from 'src/utils/platform.js';
import { isKeybindingCustomizationEnabled } from '../../keybindings/loadUserBindings.js';
import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js';
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js';
import { isFastModeAvailable, isFastModeEnabled } from '../../utils/fastMode.js';
import { getNewlineInstructions } from './utils.js';
import { feature } from 'bun:bundle'
import * as React from 'react'
import { Box, Text } from 'src/ink.js'
import { getPlatform } from 'src/utils/platform.js'
import { isKeybindingCustomizationEnabled } from '../../keybindings/loadUserBindings.js'
import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
import { isFastModeAvailable, isFastModeEnabled } from '../../utils/fastMode.js'
import { getNewlineInstructions } from './utils.js'
/** Format a shortcut for display in the help menu (e.g., "ctrl+o" → "ctrl + o") */
function formatShortcut(shortcut: string): string {
return shortcut.replace(/\+/g, ' + ');
return shortcut.replace(/\+/g, ' + ')
}
type Props = {
dimColor?: boolean;
fixedWidth?: boolean;
gap?: number;
paddingX?: number;
};
export function PromptInputHelpMenu(props) {
const $ = _c(99);
const {
dimColor,
fixedWidth,
gap,
paddingX
} = props;
const t0 = useShortcutDisplay("app:toggleTranscript", "Global", "ctrl+o");
let t1;
if ($[0] !== t0) {
t1 = formatShortcut(t0);
$[0] = t0;
$[1] = t1;
} else {
t1 = $[1];
}
const transcriptShortcut = t1;
const t2 = useShortcutDisplay("app:toggleTodos", "Global", "ctrl+t");
let t3;
if ($[2] !== t2) {
t3 = formatShortcut(t2);
$[2] = t2;
$[3] = t3;
} else {
t3 = $[3];
}
const todosShortcut = t3;
const t4 = useShortcutDisplay("chat:undo", "Chat", "ctrl+_");
let t5;
if ($[4] !== t4) {
t5 = formatShortcut(t4);
$[4] = t4;
$[5] = t5;
} else {
t5 = $[5];
}
const undoShortcut = t5;
const t6 = useShortcutDisplay("chat:stash", "Chat", "ctrl+s");
let t7;
if ($[6] !== t6) {
t7 = formatShortcut(t6);
$[6] = t6;
$[7] = t7;
} else {
t7 = $[7];
}
const stashShortcut = t7;
const t8 = useShortcutDisplay("chat:cycleMode", "Chat", "shift+tab");
let t9;
if ($[8] !== t8) {
t9 = formatShortcut(t8);
$[8] = t8;
$[9] = t9;
} else {
t9 = $[9];
}
const cycleModeShortcut = t9;
const t10 = useShortcutDisplay("chat:modelPicker", "Chat", "alt+p");
let t11;
if ($[10] !== t10) {
t11 = formatShortcut(t10);
$[10] = t10;
$[11] = t11;
} else {
t11 = $[11];
}
const modelPickerShortcut = t11;
const t12 = useShortcutDisplay("chat:fastMode", "Chat", "alt+o");
let t13;
if ($[12] !== t12) {
t13 = formatShortcut(t12);
$[12] = t12;
$[13] = t13;
} else {
t13 = $[13];
}
const fastModeShortcut = t13;
const t14 = useShortcutDisplay("chat:externalEditor", "Chat", "ctrl+g");
let t15;
if ($[14] !== t14) {
t15 = formatShortcut(t14);
$[14] = t14;
$[15] = t15;
} else {
t15 = $[15];
}
const externalEditorShortcut = t15;
const t16 = useShortcutDisplay("app:toggleTerminal", "Global", "meta+j");
let t17;
if ($[16] !== t16) {
t17 = formatShortcut(t16);
$[16] = t16;
$[17] = t17;
} else {
t17 = $[17];
}
const terminalShortcut = t17;
const t18 = useShortcutDisplay("chat:imagePaste", "Chat", "ctrl+v");
let t19;
if ($[18] !== t18) {
t19 = formatShortcut(t18);
$[18] = t18;
$[19] = t19;
} else {
t19 = $[19];
}
const imagePasteShortcut = t19;
let t20;
if ($[20] !== dimColor || $[21] !== terminalShortcut) {
t20 = feature("TERMINAL_PANEL") ? getFeatureValue_CACHED_MAY_BE_STALE("tengu_terminal_panel", false) ? <Box><Text dimColor={dimColor}>{terminalShortcut} for terminal</Text></Box> : null : null;
$[20] = dimColor;
$[21] = terminalShortcut;
$[22] = t20;
} else {
t20 = $[22];
}
const terminalShortcutElement = t20;
const t21 = fixedWidth ? 24 : undefined;
let t22;
if ($[23] !== dimColor) {
t22 = <Box><Text dimColor={dimColor}>! for bash mode</Text></Box>;
$[23] = dimColor;
$[24] = t22;
} else {
t22 = $[24];
}
let t23;
if ($[25] !== dimColor) {
t23 = <Box><Text dimColor={dimColor}>/ for commands</Text></Box>;
$[25] = dimColor;
$[26] = t23;
} else {
t23 = $[26];
}
let t24;
if ($[27] !== dimColor) {
t24 = <Box><Text dimColor={dimColor}>@ for file paths</Text></Box>;
$[27] = dimColor;
$[28] = t24;
} else {
t24 = $[28];
}
let t25;
if ($[29] !== dimColor) {
t25 = <Box><Text dimColor={dimColor}>{"& for background"}</Text></Box>;
$[29] = dimColor;
$[30] = t25;
} else {
t25 = $[30];
}
let t26;
if ($[31] !== dimColor) {
t26 = <Box><Text dimColor={dimColor}>/btw for side question</Text></Box>;
$[31] = dimColor;
$[32] = t26;
} else {
t26 = $[32];
}
let t27;
if ($[33] !== t21 || $[34] !== t22 || $[35] !== t23 || $[36] !== t24 || $[37] !== t25 || $[38] !== t26) {
t27 = <Box flexDirection="column" width={t21}>{t22}{t23}{t24}{t25}{t26}</Box>;
$[33] = t21;
$[34] = t22;
$[35] = t23;
$[36] = t24;
$[37] = t25;
$[38] = t26;
$[39] = t27;
} else {
t27 = $[39];
}
const t28 = fixedWidth ? 35 : undefined;
let t29;
if ($[40] !== dimColor) {
t29 = <Box><Text dimColor={dimColor}>double tap esc to clear input</Text></Box>;
$[40] = dimColor;
$[41] = t29;
} else {
t29 = $[41];
}
let t30;
if ($[42] !== cycleModeShortcut || $[43] !== dimColor) {
t30 = <Box><Text dimColor={dimColor}>{cycleModeShortcut}{" "}{false ? "to cycle modes" : "to auto-accept edits"}</Text></Box>;
$[42] = cycleModeShortcut;
$[43] = dimColor;
$[44] = t30;
} else {
t30 = $[44];
}
let t31;
if ($[45] !== dimColor || $[46] !== transcriptShortcut) {
t31 = <Box><Text dimColor={dimColor}>{transcriptShortcut} for verbose output</Text></Box>;
$[45] = dimColor;
$[46] = transcriptShortcut;
$[47] = t31;
} else {
t31 = $[47];
}
let t32;
if ($[48] !== dimColor || $[49] !== todosShortcut) {
t32 = <Box><Text dimColor={dimColor}>{todosShortcut} to toggle tasks</Text></Box>;
$[48] = dimColor;
$[49] = todosShortcut;
$[50] = t32;
} else {
t32 = $[50];
}
let t33;
if ($[51] === Symbol.for("react.memo_cache_sentinel")) {
t33 = getNewlineInstructions();
$[51] = t33;
} else {
t33 = $[51];
}
let t34;
if ($[52] !== dimColor) {
t34 = <Box><Text dimColor={dimColor}>{t33}</Text></Box>;
$[52] = dimColor;
$[53] = t34;
} else {
t34 = $[53];
}
let t35;
if ($[54] !== t28 || $[55] !== t29 || $[56] !== t30 || $[57] !== t31 || $[58] !== t32 || $[59] !== t34 || $[60] !== terminalShortcutElement) {
t35 = <Box flexDirection="column" width={t28}>{t29}{t30}{t31}{t32}{terminalShortcutElement}{t34}</Box>;
$[54] = t28;
$[55] = t29;
$[56] = t30;
$[57] = t31;
$[58] = t32;
$[59] = t34;
$[60] = terminalShortcutElement;
$[61] = t35;
} else {
t35 = $[61];
}
let t36;
if ($[62] !== dimColor || $[63] !== undoShortcut) {
t36 = <Box><Text dimColor={dimColor}>{undoShortcut} to undo</Text></Box>;
$[62] = dimColor;
$[63] = undoShortcut;
$[64] = t36;
} else {
t36 = $[64];
}
let t37;
if ($[65] !== dimColor) {
t37 = getPlatform() !== "windows" && <Box><Text dimColor={dimColor}>ctrl + z to suspend</Text></Box>;
$[65] = dimColor;
$[66] = t37;
} else {
t37 = $[66];
}
let t38;
if ($[67] !== dimColor || $[68] !== imagePasteShortcut) {
t38 = <Box><Text dimColor={dimColor}>{imagePasteShortcut} to paste images</Text></Box>;
$[67] = dimColor;
$[68] = imagePasteShortcut;
$[69] = t38;
} else {
t38 = $[69];
}
let t39;
if ($[70] !== dimColor || $[71] !== modelPickerShortcut) {
t39 = <Box><Text dimColor={dimColor}>{modelPickerShortcut} to switch model</Text></Box>;
$[70] = dimColor;
$[71] = modelPickerShortcut;
$[72] = t39;
} else {
t39 = $[72];
}
let t40;
if ($[73] !== dimColor || $[74] !== fastModeShortcut) {
t40 = isFastModeEnabled() && isFastModeAvailable() && <Box><Text dimColor={dimColor}>{fastModeShortcut} to toggle fast mode</Text></Box>;
$[73] = dimColor;
$[74] = fastModeShortcut;
$[75] = t40;
} else {
t40 = $[75];
}
let t41;
if ($[76] !== dimColor || $[77] !== stashShortcut) {
t41 = <Box><Text dimColor={dimColor}>{stashShortcut} to stash prompt</Text></Box>;
$[76] = dimColor;
$[77] = stashShortcut;
$[78] = t41;
} else {
t41 = $[78];
}
let t42;
if ($[79] !== dimColor || $[80] !== externalEditorShortcut) {
t42 = <Box><Text dimColor={dimColor}>{externalEditorShortcut} to edit in $EDITOR</Text></Box>;
$[79] = dimColor;
$[80] = externalEditorShortcut;
$[81] = t42;
} else {
t42 = $[81];
}
let t43;
if ($[82] !== dimColor) {
t43 = isKeybindingCustomizationEnabled() && <Box><Text dimColor={dimColor}>/keybindings to customize</Text></Box>;
$[82] = dimColor;
$[83] = t43;
} else {
t43 = $[83];
}
let t44;
if ($[84] !== t36 || $[85] !== t37 || $[86] !== t38 || $[87] !== t39 || $[88] !== t40 || $[89] !== t41 || $[90] !== t42 || $[91] !== t43) {
t44 = <Box flexDirection="column">{t36}{t37}{t38}{t39}{t40}{t41}{t42}{t43}</Box>;
$[84] = t36;
$[85] = t37;
$[86] = t38;
$[87] = t39;
$[88] = t40;
$[89] = t41;
$[90] = t42;
$[91] = t43;
$[92] = t44;
} else {
t44 = $[92];
}
let t45;
if ($[93] !== gap || $[94] !== paddingX || $[95] !== t27 || $[96] !== t35 || $[97] !== t44) {
t45 = <Box paddingX={paddingX} flexDirection="row" gap={gap}>{t27}{t35}{t44}</Box>;
$[93] = gap;
$[94] = paddingX;
$[95] = t27;
$[96] = t35;
$[97] = t44;
$[98] = t45;
} else {
t45 = $[98];
}
return t45;
dimColor?: boolean
fixedWidth?: boolean
gap?: number
paddingX?: number
}
export function PromptInputHelpMenu(props: Props): React.ReactNode {
const { dimColor, fixedWidth, gap, paddingX } = props
// Get configured shortcuts from keybinding system
const transcriptShortcut = formatShortcut(
useShortcutDisplay('app:toggleTranscript', 'Global', 'ctrl+o'),
)
const todosShortcut = formatShortcut(
useShortcutDisplay('app:toggleTodos', 'Global', 'ctrl+t'),
)
const undoShortcut = formatShortcut(
useShortcutDisplay('chat:undo', 'Chat', 'ctrl+_'),
)
const stashShortcut = formatShortcut(
useShortcutDisplay('chat:stash', 'Chat', 'ctrl+s'),
)
const cycleModeShortcut = formatShortcut(
useShortcutDisplay('chat:cycleMode', 'Chat', 'shift+tab'),
)
const modelPickerShortcut = formatShortcut(
useShortcutDisplay('chat:modelPicker', 'Chat', 'alt+p'),
)
const fastModeShortcut = formatShortcut(
useShortcutDisplay('chat:fastMode', 'Chat', 'alt+o'),
)
const externalEditorShortcut = formatShortcut(
useShortcutDisplay('chat:externalEditor', 'Chat', 'ctrl+g'),
)
const terminalShortcut = formatShortcut(
useShortcutDisplay('app:toggleTerminal', 'Global', 'meta+j'),
)
const imagePasteShortcut = formatShortcut(
useShortcutDisplay('chat:imagePaste', 'Chat', 'ctrl+v'),
)
// Compute terminal shortcut element outside JSX to satisfy feature() constraint
const terminalShortcutElement = feature('TERMINAL_PANEL') ? (
getFeatureValue_CACHED_MAY_BE_STALE('tengu_terminal_panel', false) ? (
<Box>
<Text dimColor={dimColor}>{terminalShortcut} for terminal</Text>
</Box>
) : null
) : null
return (
<Box paddingX={paddingX} flexDirection="row" gap={gap}>
<Box flexDirection="column" width={fixedWidth ? 24 : undefined}>
<Box>
<Text dimColor={dimColor}>! for bash mode</Text>
</Box>
<Box>
<Text dimColor={dimColor}>/ for commands</Text>
</Box>
<Box>
<Text dimColor={dimColor}>@ for file paths</Text>
</Box>
<Box>
<Text dimColor={dimColor}>& for background</Text>
</Box>
<Box>
<Text dimColor={dimColor}>/btw for side question</Text>
</Box>
</Box>
<Box flexDirection="column" width={fixedWidth ? 35 : undefined}>
<Box>
<Text dimColor={dimColor}>double tap esc to clear input</Text>
</Box>
<Box>
<Text dimColor={dimColor}>
{cycleModeShortcut}{' '}
{process.env.USER_TYPE === 'ant'
? 'to cycle modes'
: 'to auto-accept edits'}
</Text>
</Box>
<Box>
<Text dimColor={dimColor}>
{transcriptShortcut} for verbose output
</Text>
</Box>
<Box>
<Text dimColor={dimColor}>{todosShortcut} to toggle tasks</Text>
</Box>
{terminalShortcutElement}
<Box>
<Text dimColor={dimColor}>{getNewlineInstructions()}</Text>
</Box>
</Box>
<Box flexDirection="column">
<Box>
<Text dimColor={dimColor}>{undoShortcut} to undo</Text>
</Box>
{getPlatform() !== 'windows' && (
<Box>
<Text dimColor={dimColor}>ctrl + z to suspend</Text>
</Box>
)}
<Box>
<Text dimColor={dimColor}>{imagePasteShortcut} to paste images</Text>
</Box>
<Box>
<Text dimColor={dimColor}>{modelPickerShortcut} to switch model</Text>
</Box>
{isFastModeEnabled() && isFastModeAvailable() && (
<Box>
<Text dimColor={dimColor}>
{fastModeShortcut} to toggle fast mode
</Text>
</Box>
)}
<Box>
<Text dimColor={dimColor}>{stashShortcut} to stash prompt</Text>
</Box>
<Box>
<Text dimColor={dimColor}>
{externalEditorShortcut} to edit in $EDITOR
</Text>
</Box>
{isKeybindingCustomizationEnabled() && (
<Box>
<Text dimColor={dimColor}>/keybindings to customize</Text>
</Box>
)}
</Box>
</Box>
)
}

View File

@@ -1,18 +1,22 @@
import { c as _c } from "react/compiler-runtime";
import figures from 'figures';
import * as React from 'react';
import { Box, Text } from 'src/ink.js';
import { AGENT_COLOR_TO_THEME_COLOR, AGENT_COLORS, type AgentColorName } from 'src/tools/AgentTool/agentColorManager.js';
import type { PromptInputMode } from 'src/types/textInputTypes.js';
import { getTeammateColor } from 'src/utils/teammate.js';
import type { Theme } from 'src/utils/theme.js';
import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js';
import figures from 'figures'
import * as React from 'react'
import { Box, Text } from 'src/ink.js'
import {
AGENT_COLOR_TO_THEME_COLOR,
AGENT_COLORS,
type AgentColorName,
} from 'src/tools/AgentTool/agentColorManager.js'
import type { PromptInputMode } from 'src/types/textInputTypes.js'
import { getTeammateColor } from 'src/utils/teammate.js'
import type { Theme } from 'src/utils/theme.js'
import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'
type Props = {
mode: PromptInputMode;
isLoading: boolean;
viewingAgentName?: string;
viewingAgentColor?: AgentColorName;
};
mode: PromptInputMode
isLoading: boolean
viewingAgentName?: string
viewingAgentColor?: AgentColorName
}
/**
* Gets the theme color key for the teammate's assigned color.
@@ -20,73 +24,81 @@ type Props = {
*/
function getTeammateThemeColor(): keyof Theme | undefined {
if (!isAgentSwarmsEnabled()) {
return undefined;
return undefined
}
const colorName = getTeammateColor();
const colorName = getTeammateColor()
if (!colorName) {
return undefined;
return undefined
}
if (AGENT_COLORS.includes(colorName as AgentColorName)) {
return AGENT_COLOR_TO_THEME_COLOR[colorName as AgentColorName];
return AGENT_COLOR_TO_THEME_COLOR[colorName as AgentColorName]
}
return undefined;
return undefined
}
type PromptCharProps = {
isLoading: boolean;
isLoading: boolean
// Dead code elimination: parameter named themeColor to avoid "teammate" string in external builds
themeColor?: keyof Theme;
};
themeColor?: keyof Theme
}
/**
* Renders the prompt character ().
* Teammate color overrides the default color when set.
*/
function PromptChar(t0) {
const $ = _c(3);
const {
isLoading,
themeColor
} = t0;
const teammateColor = themeColor;
const color = teammateColor ?? (false ? "subtle" : undefined);
let t1;
if ($[0] !== color || $[1] !== isLoading) {
t1 = <Text color={color} dimColor={isLoading}>{figures.pointer} </Text>;
$[0] = color;
$[1] = isLoading;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
function PromptChar({
isLoading,
themeColor,
}: PromptCharProps): React.ReactNode {
// Assign to original name for clarity within the function
const teammateColor = themeColor
const isAnt = process.env.USER_TYPE === 'ant'
const color = teammateColor ?? (isAnt ? 'subtle' : undefined)
return (
<Text color={color} dimColor={isLoading}>
{figures.pointer}&nbsp;
</Text>
)
}
export function PromptInputModeIndicator(t0) {
const $ = _c(6);
const {
mode,
isLoading,
viewingAgentName,
viewingAgentColor
} = t0;
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = getTeammateThemeColor();
$[0] = t1;
} else {
t1 = $[0];
}
const teammateColor = t1;
const viewedTeammateThemeColor = viewingAgentColor ? AGENT_COLOR_TO_THEME_COLOR[viewingAgentColor] : undefined;
let t2;
if ($[1] !== isLoading || $[2] !== mode || $[3] !== viewedTeammateThemeColor || $[4] !== viewingAgentName) {
t2 = <Box alignItems="flex-start" alignSelf="flex-start" flexWrap="nowrap" justifyContent="flex-start">{viewingAgentName ? <PromptChar isLoading={isLoading} themeColor={viewedTeammateThemeColor} /> : mode === "bash" ? <Text color="bashBorder" dimColor={isLoading}>! </Text> : <PromptChar isLoading={isLoading} themeColor={isAgentSwarmsEnabled() ? teammateColor : undefined} />}</Box>;
$[1] = isLoading;
$[2] = mode;
$[3] = viewedTeammateThemeColor;
$[4] = viewingAgentName;
$[5] = t2;
} else {
t2 = $[5];
}
return t2;
export function PromptInputModeIndicator({
mode,
isLoading,
viewingAgentName,
viewingAgentColor,
}: Props): React.ReactNode {
const teammateColor = getTeammateThemeColor()
// Convert viewed teammate's color to theme color
// Falls back to PromptChar's default (subtle for ants, undefined for external)
const viewedTeammateThemeColor = viewingAgentColor
? AGENT_COLOR_TO_THEME_COLOR[viewingAgentColor]
: undefined
return (
<Box
alignItems="flex-start"
alignSelf="flex-start"
flexWrap="nowrap"
justifyContent="flex-start"
>
{viewingAgentName ? (
// Use teammate's color on the standard prompt character, matching established style
<PromptChar
isLoading={isLoading}
themeColor={viewedTeammateThemeColor}
/>
) : mode === 'bash' ? (
<Text color="bashBorder" dimColor={isLoading}>
!&nbsp;
</Text>
) : (
<PromptChar
isLoading={isLoading}
themeColor={isAgentSwarmsEnabled() ? teammateColor : undefined}
/>
)}
</Box>
)
}

View File

@@ -1,17 +1,26 @@
import { feature } from 'bun:bundle';
import * as React from 'react';
import { useMemo } from 'react';
import { Box } from 'src/ink.js';
import { useAppState } from 'src/state/AppState.js';
import { STATUS_TAG, SUMMARY_TAG, TASK_NOTIFICATION_TAG } from '../../constants/xml.js';
import { QueuedMessageProvider } from '../../context/QueuedMessageContext.js';
import { useCommandQueue } from '../../hooks/useCommandQueue.js';
import type { QueuedCommand } from '../../types/textInputTypes.js';
import { isQueuedCommandVisible } from '../../utils/messageQueueManager.js';
import { createUserMessage, EMPTY_LOOKUPS, normalizeMessages } from '../../utils/messages.js';
import { jsonParse } from '../../utils/slowOperations.js';
import { Message } from '../Message.js';
const EMPTY_SET = new Set<string>();
import { feature } from 'bun:bundle'
import * as React from 'react'
import { useMemo } from 'react'
import { Box } from 'src/ink.js'
import { useAppState } from 'src/state/AppState.js'
import {
STATUS_TAG,
SUMMARY_TAG,
TASK_NOTIFICATION_TAG,
} from '../../constants/xml.js'
import { QueuedMessageProvider } from '../../context/QueuedMessageContext.js'
import { useCommandQueue } from '../../hooks/useCommandQueue.js'
import type { QueuedCommand } from '../../types/textInputTypes.js'
import { isQueuedCommandVisible } from '../../utils/messageQueueManager.js'
import {
createUserMessage,
EMPTY_LOOKUPS,
normalizeMessages,
} from '../../utils/messages.js'
import { jsonParse } from '../../utils/slowOperations.js'
import { Message } from '../Message.js'
const EMPTY_SET = new Set<string>()
/**
* Check if a command value is an idle notification that should be hidden.
@@ -19,15 +28,15 @@ const EMPTY_SET = new Set<string>();
*/
function isIdleNotification(value: string): boolean {
try {
const parsed = jsonParse(value);
return parsed?.type === 'idle_notification';
const parsed = jsonParse(value)
return parsed?.type === 'idle_notification'
} catch {
return false;
return false
}
}
// Maximum number of task notification lines to show
const MAX_VISIBLE_NOTIFICATIONS = 3;
const MAX_VISIBLE_NOTIFICATIONS = 3
/**
* Create a synthetic overflow notification message for capped task notifications.
@@ -36,7 +45,7 @@ function createOverflowNotificationMessage(count: number): string {
return `<${TASK_NOTIFICATION_TAG}>
<${SUMMARY_TAG}>+${count} more tasks completed</${SUMMARY_TAG}>
<${STATUS_TAG}>completed</${STATUS_TAG}>
</${TASK_NOTIFICATION_TAG}>`;
</${TASK_NOTIFICATION_TAG}>`
}
/**
@@ -44,73 +53,114 @@ function createOverflowNotificationMessage(count: number): string {
* Other command types are always shown in full.
* Idle notifications are filtered out entirely.
*/
function processQueuedCommands(queuedCommands: QueuedCommand[]): QueuedCommand[] {
function processQueuedCommands(
queuedCommands: QueuedCommand[],
): QueuedCommand[] {
// Filter out idle notifications - they are processed silently
const filteredCommands = queuedCommands.filter(cmd => typeof cmd.value !== 'string' || !isIdleNotification(cmd.value));
const filteredCommands = queuedCommands.filter(
cmd => typeof cmd.value !== 'string' || !isIdleNotification(cmd.value),
)
// Separate task notifications from other commands
const taskNotifications = filteredCommands.filter(cmd => cmd.mode === 'task-notification');
const otherCommands = filteredCommands.filter(cmd => cmd.mode !== 'task-notification');
const taskNotifications = filteredCommands.filter(
cmd => cmd.mode === 'task-notification',
)
const otherCommands = filteredCommands.filter(
cmd => cmd.mode !== 'task-notification',
)
// If notifications fit within limit, return all commands as-is
if (taskNotifications.length <= MAX_VISIBLE_NOTIFICATIONS) {
return [...otherCommands, ...taskNotifications];
return [...otherCommands, ...taskNotifications]
}
// Show first (MAX_VISIBLE_NOTIFICATIONS - 1) notifications, then a summary
const visibleNotifications = taskNotifications.slice(0, MAX_VISIBLE_NOTIFICATIONS - 1);
const overflowCount = taskNotifications.length - (MAX_VISIBLE_NOTIFICATIONS - 1);
const visibleNotifications = taskNotifications.slice(
0,
MAX_VISIBLE_NOTIFICATIONS - 1,
)
const overflowCount =
taskNotifications.length - (MAX_VISIBLE_NOTIFICATIONS - 1)
// Create synthetic overflow message
const overflowCommand: QueuedCommand = {
value: createOverflowNotificationMessage(overflowCount),
mode: 'task-notification'
};
return [...otherCommands, ...visibleNotifications, overflowCommand];
mode: 'task-notification',
}
return [...otherCommands, ...visibleNotifications, overflowCommand]
}
function PromptInputQueuedCommandsImpl(): React.ReactNode {
const queuedCommands = useCommandQueue();
const viewingAgent = useAppState(s => !!s.viewingAgentTaskId);
const queuedCommands = useCommandQueue()
const viewingAgent = useAppState(s => !!s.viewingAgentTaskId)
// Brief layout: dim queue items + skip the paddingX (brief messages
// already indent themselves). Gate mirrors the brief-spinner/message
// check elsewhere — no teammate-view override needed since this
// component early-returns when viewing a teammate.
const useBriefLayout = feature('KAIROS') || feature('KAIROS_BRIEF') ?
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
useAppState(s_0 => s_0.isBriefOnly) : false;
const useBriefLayout =
feature('KAIROS') || feature('KAIROS_BRIEF')
? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
useAppState(s => s.isBriefOnly)
: false
// createUserMessage mints a fresh UUID per call; without memoization, streaming
// re-renders defeat Message's areMessagePropsEqual (compares uuid) → flicker.
const messages = useMemo(() => {
if (queuedCommands.length === 0) return null;
if (queuedCommands.length === 0) return null
// task-notification is shown via useInboxNotification; most isMeta commands
// (scheduled tasks, proactive ticks) are system-generated and hidden.
// Channel messages are the exception — isMeta but shown so the keyboard
// user sees what arrived.
const visibleCommands = queuedCommands.filter(isQueuedCommandVisible);
if (visibleCommands.length === 0) return null;
const processedCommands = processQueuedCommands(visibleCommands);
return normalizeMessages(processedCommands.map(cmd => {
let content = cmd.value;
if (cmd.mode === 'bash' && typeof content === 'string') {
content = `<bash-input>${content}</bash-input>`;
}
// [Image #N] placeholders are inline in the text value (inserted at
// paste time), so the queue preview shows them without stub blocks.
return createUserMessage({
content
});
}));
}, [queuedCommands]);
const visibleCommands = queuedCommands.filter(isQueuedCommandVisible)
if (visibleCommands.length === 0) return null
const processedCommands = processQueuedCommands(visibleCommands)
return normalizeMessages(
processedCommands.map(cmd => {
let content = cmd.value
if (cmd.mode === 'bash' && typeof content === 'string') {
content = `<bash-input>${content}</bash-input>`
}
// [Image #N] placeholders are inline in the text value (inserted at
// paste time), so the queue preview shows them without stub blocks.
return createUserMessage({ content })
}),
)
}, [queuedCommands])
// Don't show leader's queued commands when viewing any agent's transcript
if (viewingAgent || messages === null) {
return null;
return null
}
return <Box marginTop={1} flexDirection="column">
{messages.map((message, i) => <QueuedMessageProvider key={i} isFirst={i === 0} useBriefLayout={useBriefLayout}>
<Message message={message} lookups={EMPTY_LOOKUPS} addMargin={false} tools={[]} commands={[]} verbose={false} inProgressToolUseIDs={EMPTY_SET} progressMessagesForMessage={[]} shouldAnimate={false} shouldShowDot={false} isTranscriptMode={false} isStatic={true} />
</QueuedMessageProvider>)}
</Box>;
return (
<Box marginTop={1} flexDirection="column">
{messages.map((message, i) => (
<QueuedMessageProvider
key={i}
isFirst={i === 0}
useBriefLayout={useBriefLayout}
>
<Message
message={message}
lookups={EMPTY_LOOKUPS}
addMargin={false}
tools={[]}
commands={[]}
verbose={false}
inProgressToolUseIDs={EMPTY_SET}
progressMessagesForMessage={[]}
shouldAnimate={false}
shouldShowDot={false}
isTranscriptMode={false}
isStatic={true}
/>
</QueuedMessageProvider>
))}
</Box>
)
}
export const PromptInputQueuedCommands = React.memo(PromptInputQueuedCommandsImpl);
export const PromptInputQueuedCommands = React.memo(
PromptInputQueuedCommandsImpl,
)

View File

@@ -1,24 +1,21 @@
import { c as _c } from "react/compiler-runtime";
import figures from 'figures';
import * as React from 'react';
import { Box, Text } from 'src/ink.js';
import figures from 'figures'
import * as React from 'react'
import { Box, Text } from 'src/ink.js'
type Props = {
hasStash: boolean;
};
export function PromptInputStashNotice(t0) {
const $ = _c(1);
const {
hasStash
} = t0;
if (!hasStash) {
return null;
}
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <Box paddingLeft={2}><Text dimColor={true}>{figures.pointerSmall} Stashed (auto-restores after submit)</Text></Box>;
$[0] = t1;
} else {
t1 = $[0];
}
return t1;
hasStash: boolean
}
export function PromptInputStashNotice({ hasStash }: Props): React.ReactNode {
if (!hasStash) {
return null
}
return (
<Box paddingLeft={2}>
<Text dimColor>
{figures.pointerSmall} Stashed (auto-restores after submit)
</Text>
</Box>
)
}

View File

@@ -1,63 +1,61 @@
import { c as _c } from "react/compiler-runtime";
import * as React from 'react';
import { type ReactNode, useEffect, useRef, useState } from 'react';
import { Box, Text } from '../../ink.js';
import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js';
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js';
export function SandboxPromptFooterHint() {
const $ = _c(6);
const [recentViolationCount, setRecentViolationCount] = useState(0);
const timerRef = useRef(null);
const detailsShortcut = useShortcutDisplay("app:toggleTranscript", "Global", "ctrl+o");
let t0;
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = () => {
if (!SandboxManager.isSandboxingEnabled()) {
return;
}
const store = SandboxManager.getSandboxViolationStore();
let lastCount = store.getTotalCount();
const unsubscribe = store.subscribe(() => {
const currentCount = store.getTotalCount();
const newViolations = currentCount - lastCount;
if (newViolations > 0) {
setRecentViolationCount(newViolations);
lastCount = currentCount;
if (timerRef.current) {
clearTimeout(timerRef.current);
}
timerRef.current = setTimeout(setRecentViolationCount, 5000, 0);
}
});
return () => {
unsubscribe();
import * as React from 'react'
import { type ReactNode, useEffect, useRef, useState } from 'react'
import { Box, Text } from '../../ink.js'
import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'
export function SandboxPromptFooterHint(): ReactNode {
const [recentViolationCount, setRecentViolationCount] = useState(0)
const timerRef = useRef<NodeJS.Timeout | null>(null)
const detailsShortcut = useShortcutDisplay(
'app:toggleTranscript',
'Global',
'ctrl+o',
)
useEffect(() => {
if (!SandboxManager.isSandboxingEnabled()) {
return
}
const store = SandboxManager.getSandboxViolationStore()
let lastCount = store.getTotalCount()
const unsubscribe = store.subscribe(() => {
const currentCount = store.getTotalCount()
const newViolations = currentCount - lastCount
if (newViolations > 0) {
setRecentViolationCount(newViolations)
lastCount = currentCount
if (timerRef.current) {
clearTimeout(timerRef.current);
clearTimeout(timerRef.current)
}
};
};
t1 = [];
$[0] = t0;
$[1] = t1;
} else {
t0 = $[0];
t1 = $[1];
}
useEffect(t0, t1);
timerRef.current = setTimeout(setRecentViolationCount, 5000, 0)
}
})
return () => {
unsubscribe()
if (timerRef.current) {
clearTimeout(timerRef.current)
}
}
}, [])
if (!SandboxManager.isSandboxingEnabled() || recentViolationCount === 0) {
return null;
return null
}
const t2 = recentViolationCount === 1 ? "operation" : "operations";
let t3;
if ($[2] !== detailsShortcut || $[3] !== recentViolationCount || $[4] !== t2) {
t3 = <Box paddingX={0} paddingY={0}><Text color="inactive" wrap="truncate"> Sandbox blocked {recentViolationCount}{" "}{t2} ·{" "}{detailsShortcut} for details · /sandbox to disable</Text></Box>;
$[2] = detailsShortcut;
$[3] = recentViolationCount;
$[4] = t2;
$[5] = t3;
} else {
t3 = $[5];
}
return t3;
return (
<Box paddingX={0} paddingY={0}>
<Text color="inactive" wrap="truncate">
Sandbox blocked {recentViolationCount}{' '}
{recentViolationCount === 1 ? 'operation' : 'operations'} ·{' '}
{detailsShortcut} for details · /sandbox to disable
</Text>
</Box>
)
}

View File

@@ -1,142 +1,121 @@
import { c as _c } from "react/compiler-runtime";
import * as React from 'react';
import { Ansi, Box, Text, useAnimationFrame } from '../../ink.js';
import { segmentTextByHighlights, type TextHighlight } from '../../utils/textHighlighting.js';
import { ShimmerChar } from '../Spinner/ShimmerChar.js';
import * as React from 'react'
import { Ansi, Box, Text, useAnimationFrame } from '../../ink.js'
import {
segmentTextByHighlights,
type TextHighlight,
} from '../../utils/textHighlighting.js'
import { ShimmerChar } from '../Spinner/ShimmerChar.js'
type Props = {
text: string;
highlights: TextHighlight[];
};
text: string
highlights: TextHighlight[]
}
type LinePart = {
text: string;
highlight: TextHighlight | undefined;
start: number;
};
export function HighlightedInput(t0) {
const $ = _c(23);
const {
text,
highlights
} = t0;
let lines;
if ($[0] !== highlights || $[1] !== text) {
const segments = segmentTextByHighlights(text, highlights);
lines = [[]];
let pos = 0;
text: string
highlight: TextHighlight | undefined
start: number
}
export function HighlightedInput({ text, highlights }: Props): React.ReactNode {
// The shimmer animation (below) re-renders this component at 20fps while the
// ultrathink keyword is present. text/highlights are referentially stable
// across animation ticks (parent doesn't re-render), so memoize everything
// that derives from them: segmentTextByHighlights alone is ~85µs/call
// (tokenize + sort + O(n²) overlap), which adds up fast at 20fps.
const { lines, hasShimmer, sweepStart, cycleLength } = React.useMemo(() => {
const segments = segmentTextByHighlights(text, highlights)
// Split segments by newlines into per-line groups. Ink's row-direction Box
// indents continuation lines of a multi-line child to that child's X offset.
// By splitting at newlines, each line renders as its own row, avoiding the
// incorrect indentation when highlighted text is followed by wrapped content.
const lines: LinePart[][] = [[]]
let pos = 0
for (const segment of segments) {
const parts = segment.text.split("\n");
const parts = segment.text.split('\n')
for (let i = 0; i < parts.length; i++) {
if (i > 0) {
lines.push([]);
pos = pos + 1;
lines.push([])
pos += 1
}
const part = parts[i];
const part = parts[i]!
if (part.length > 0) {
lines[lines.length - 1].push({
lines[lines.length - 1]!.push({
text: part,
highlight: segment.highlight,
start: pos
});
start: pos,
})
}
pos = pos + part.length;
pos += part.length
}
}
$[0] = highlights;
$[1] = text;
$[2] = lines;
} else {
lines = $[2];
}
let t1;
if ($[3] !== highlights) {
t1 = highlights.some(_temp);
$[3] = highlights;
$[4] = t1;
} else {
t1 = $[4];
}
const hasShimmer = t1;
let sweepStart = 0;
let cycleLength = 1;
if (hasShimmer) {
let lo = Infinity;
let hi = -Infinity;
if ($[5] !== hi || $[6] !== highlights || $[7] !== lo) {
for (const h_0 of highlights) {
if (h_0.shimmerColor) {
lo = Math.min(lo, h_0.start);
hi = Math.max(hi, h_0.end);
// Scope the sweep to shimmer-highlighted ranges so cycle time doesn't grow
// with input length. Padding creates an offscreen pause between sweeps.
const hasShimmer = highlights.some(h => h.shimmerColor)
let sweepStart = 0
let cycleLength = 1
if (hasShimmer) {
const padding = 10
let lo = Infinity
let hi = -Infinity
for (const h of highlights) {
if (h.shimmerColor) {
lo = Math.min(lo, h.start)
hi = Math.max(hi, h.end)
}
}
$[5] = hi;
$[6] = highlights;
$[7] = lo;
$[8] = lo;
$[9] = hi;
} else {
lo = $[8] as number;
hi = $[9] as number;
sweepStart = lo - padding
cycleLength = hi - lo + padding * 2
}
sweepStart = lo - 10;
cycleLength = hi - lo + 20;
}
let t2;
if ($[10] !== cycleLength || $[11] !== hasShimmer || $[12] !== lines || $[13] !== sweepStart) {
t2 = {
lines,
hasShimmer,
sweepStart,
cycleLength
};
$[10] = cycleLength;
$[11] = hasShimmer;
$[12] = lines;
$[13] = sweepStart;
$[14] = t2;
} else {
t2 = $[14];
}
const {
lines: lines_0,
hasShimmer: hasShimmer_0,
sweepStart: sweepStart_0,
cycleLength: cycleLength_0
} = t2;
const [ref, time] = useAnimationFrame(hasShimmer_0 ? 50 : null);
const glimmerIndex = hasShimmer_0 ? sweepStart_0 + Math.floor(time / 50) % cycleLength_0 : -100;
let t3;
if ($[15] !== glimmerIndex || $[16] !== lines_0) {
let t4;
if ($[18] !== glimmerIndex) {
t4 = (lineParts, lineIndex) => <Box key={lineIndex}>{lineParts.length === 0 ? <Text> </Text> : lineParts.map((part_0, partIndex) => {
if (part_0.highlight?.shimmerColor && part_0.highlight.color) {
return <Text key={partIndex}>{part_0.text.split("").map((char, charIndex) => <ShimmerChar key={charIndex} char={char} index={part_0.start + charIndex} glimmerIndex={glimmerIndex} messageColor={part_0.highlight.color} shimmerColor={part_0.highlight.shimmerColor} />)}</Text>;
}
return <Text key={partIndex} color={part_0.highlight?.color} dimColor={part_0.highlight?.dimColor} inverse={part_0.highlight?.inverse}><Ansi>{part_0.text}</Ansi></Text>;
})}</Box>;
$[18] = glimmerIndex;
$[19] = t4;
} else {
t4 = $[19];
}
t3 = lines_0.map(t4);
$[15] = glimmerIndex;
$[16] = lines_0;
$[17] = t3;
} else {
t3 = $[17];
}
let t4;
if ($[20] !== ref || $[21] !== t3) {
t4 = <Box ref={ref} flexDirection="column">{t3}</Box>;
$[20] = ref;
$[21] = t3;
$[22] = t4;
} else {
t4 = $[22];
}
return t4;
}
function _temp(h) {
return h.shimmerColor;
return { lines, hasShimmer, sweepStart, cycleLength }
}, [text, highlights])
const [ref, time] = useAnimationFrame(hasShimmer ? 50 : null)
const glimmerIndex = hasShimmer
? sweepStart + (Math.floor(time / 50) % cycleLength)
: -100
return (
<Box ref={ref} flexDirection="column">
{lines.map((lineParts, lineIndex) => (
<Box key={lineIndex}>
{lineParts.length === 0 ? (
<Text> </Text>
) : (
lineParts.map((part, partIndex) => {
if (part.highlight?.shimmerColor && part.highlight.color) {
return (
<Text key={partIndex}>
{part.text.split('').map((char, charIndex) => (
<ShimmerChar
key={charIndex}
char={char}
index={part.start + charIndex}
glimmerIndex={glimmerIndex}
messageColor={part.highlight!.color!}
shimmerColor={part.highlight!.shimmerColor!}
/>
))}
</Text>
)
}
return (
<Text
key={partIndex}
color={part.highlight?.color}
dimColor={part.highlight?.dimColor}
inverse={part.highlight?.inverse}
>
<Ansi>{part.text}</Ansi>
</Text>
)
})
)}
</Box>
))}
</Box>
)
}

View File

@@ -1,73 +1,32 @@
import { c as _c } from "react/compiler-runtime";
import { feature } from 'bun:bundle';
import * as React from 'react';
import { useSettings } from '../../hooks/useSettings.js';
import { Box, Text, useAnimationFrame } from '../../ink.js';
import { interpolateColor, toRGBColor } from '../Spinner/utils.js';
import { feature } from 'bun:bundle'
import * as React from 'react'
import { useSettings } from '../../hooks/useSettings.js'
import { Box, Text, useAnimationFrame } from '../../ink.js'
import { interpolateColor, toRGBColor } from '../Spinner/utils.js'
type Props = {
voiceState: 'idle' | 'recording' | 'processing';
};
voiceState: 'idle' | 'recording' | 'processing'
}
// Processing shimmer colors: dim gray to lighter gray (matches ThinkingShimmerText)
const PROCESSING_DIM = {
r: 153,
g: 153,
b: 153
};
const PROCESSING_BRIGHT = {
r: 185,
g: 185,
b: 185
};
const PULSE_PERIOD_S = 2; // 2 second period for all pulsing animations
const PROCESSING_DIM = { r: 153, g: 153, b: 153 }
const PROCESSING_BRIGHT = { r: 185, g: 185, b: 185 }
export function VoiceIndicator(props) {
const $ = _c(2);
if (!feature("VOICE_MODE")) {
return null;
}
let t0;
if ($[0] !== props) {
t0 = <VoiceIndicatorImpl {...props} />;
$[0] = props;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
const PULSE_PERIOD_S = 2 // 2 second period for all pulsing animations
export function VoiceIndicator(props: Props): React.ReactNode {
if (!feature('VOICE_MODE')) return null
return <VoiceIndicatorImpl {...props} />
}
function VoiceIndicatorImpl(t0) {
const $ = _c(2);
const {
voiceState
} = t0;
function VoiceIndicatorImpl({ voiceState }: Props): React.ReactNode {
switch (voiceState) {
case "recording":
{
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <Text dimColor={true}>listening</Text>;
$[0] = t1;
} else {
t1 = $[0];
}
return t1;
}
case "processing":
{
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <ProcessingShimmer />;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}
case "idle":
{
return null;
}
case 'recording':
return <Text dimColor>listening</Text>
case 'processing':
return <ProcessingShimmer />
case 'idle':
return null
}
}
@@ -75,62 +34,30 @@ function VoiceIndicatorImpl(t0) {
// is too brief for a 1s-period shimmer to register, and a 50ms animation
// timer here runs concurrently with auto-repeat spaces arriving every
// 30-80ms, compounding re-renders during an already-busy window.
export function VoiceWarmupHint() {
const $ = _c(1);
if (!feature("VOICE_MODE")) {
return null;
}
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <Text dimColor={true}>keep holding</Text>;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
export function VoiceWarmupHint(): React.ReactNode {
if (!feature('VOICE_MODE')) return null
return <Text dimColor>keep holding</Text>
}
function ProcessingShimmer() {
const $ = _c(8);
const settings = useSettings();
const reducedMotion = settings.prefersReducedMotion ?? false;
const [ref, time] = useAnimationFrame(reducedMotion ? null : 50);
function ProcessingShimmer(): React.ReactNode {
const settings = useSettings()
const reducedMotion = settings.prefersReducedMotion ?? false
const [ref, time] = useAnimationFrame(reducedMotion ? null : 50)
if (reducedMotion) {
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <Text color="warning">Voice: processing</Text>;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
return <Text color="warning">Voice: processing</Text>
}
const elapsedSec = time / 1000;
const opacity = (Math.sin(elapsedSec * Math.PI * 2 / PULSE_PERIOD_S) + 1) / 2;
let t0;
if ($[1] !== opacity) {
t0 = toRGBColor(interpolateColor(PROCESSING_DIM, PROCESSING_BRIGHT, opacity));
$[1] = opacity;
$[2] = t0;
} else {
t0 = $[2];
}
const color = t0;
let t1;
if ($[3] !== color) {
t1 = <Text color={color}>Voice: processing</Text>;
$[3] = color;
$[4] = t1;
} else {
t1 = $[4];
}
let t2;
if ($[5] !== ref || $[6] !== t1) {
t2 = <Box ref={ref}>{t1}</Box>;
$[5] = ref;
$[6] = t1;
$[7] = t2;
} else {
t2 = $[7];
}
return t2;
const elapsedSec = time / 1000
const opacity =
(Math.sin((elapsedSec * Math.PI * 2) / PULSE_PERIOD_S) + 1) / 2
const color = toRGBColor(
interpolateColor(PROCESSING_DIM, PROCESSING_BRIGHT, opacity),
)
return (
<Box ref={ref}>
<Text color={color}>Voice: processing</Text>
</Box>
)
}

View File

@@ -1,219 +1,148 @@
import { c as _c } from "react/compiler-runtime";
import figures from 'figures';
import * as React from 'react';
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
import { Box, Text } from '../../ink.js';
import { useKeybinding } from '../../keybindings/useKeybinding.js';
import type { Tools } from '../../Tool.js';
import { getAgentColor } from '../../tools/AgentTool/agentColorManager.js';
import { getMemoryScopeDisplay } from '../../tools/AgentTool/agentMemory.js';
import { resolveAgentTools } from '../../tools/AgentTool/agentToolUtils.js';
import { type AgentDefinition, isBuiltInAgent } from '../../tools/AgentTool/loadAgentsDir.js';
import { getAgentModelDisplay } from '../../utils/model/agent.js';
import { Markdown } from '../Markdown.js';
import { getActualRelativeAgentFilePath } from './agentFileUtils.js';
import figures from 'figures'
import * as React from 'react'
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
import { Box, Text } from '../../ink.js'
import { useKeybinding } from '../../keybindings/useKeybinding.js'
import type { Tools } from '../../Tool.js'
import { getAgentColor } from '../../tools/AgentTool/agentColorManager.js'
import { getMemoryScopeDisplay } from '../../tools/AgentTool/agentMemory.js'
import { resolveAgentTools } from '../../tools/AgentTool/agentToolUtils.js'
import {
type AgentDefinition,
isBuiltInAgent,
} from '../../tools/AgentTool/loadAgentsDir.js'
import { getAgentModelDisplay } from '../../utils/model/agent.js'
import { Markdown } from '../Markdown.js'
import { getActualRelativeAgentFilePath } from './agentFileUtils.js'
type Props = {
agent: AgentDefinition;
tools: Tools;
allAgents?: AgentDefinition[];
onBack: () => void;
};
export function AgentDetail(t0) {
const $ = _c(48);
const {
agent,
tools,
onBack
} = t0;
const resolvedTools = resolveAgentTools(agent, tools, false);
let t1;
if ($[0] !== agent) {
t1 = getActualRelativeAgentFilePath(agent);
$[0] = agent;
$[1] = t1;
} else {
t1 = $[1];
}
const filePath = t1;
let t2;
if ($[2] !== agent.agentType) {
t2 = getAgentColor(agent.agentType);
$[2] = agent.agentType;
$[3] = t2;
} else {
t2 = $[3];
}
const backgroundColor = t2;
let t3;
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
t3 = {
context: "Confirmation"
};
$[4] = t3;
} else {
t3 = $[4];
}
useKeybinding("confirm:no", onBack, t3);
let t4;
if ($[5] !== onBack) {
t4 = e => {
if (e.key === "return") {
e.preventDefault();
onBack();
}
};
$[5] = onBack;
$[6] = t4;
} else {
t4 = $[6];
}
const handleKeyDown = t4;
const renderToolsList = function renderToolsList() {
if (resolvedTools.hasWildcard) {
return <Text>All tools</Text>;
}
if (!agent.tools || agent.tools.length === 0) {
return <Text>None</Text>;
}
return <>{resolvedTools.validTools.length > 0 && <Text>{resolvedTools.validTools.join(", ")}</Text>}{resolvedTools.invalidTools.length > 0 && <Text color="warning">{figures.warning} Unrecognized:{" "}{resolvedTools.invalidTools.join(", ")}</Text>}</>;
};
const T0 = Box;
const t5 = "column";
const t6 = 1;
const t7 = 0;
const t8 = true;
let t9;
if ($[7] !== filePath) {
t9 = <Text dimColor={true}>{filePath}</Text>;
$[7] = filePath;
$[8] = t9;
} else {
t9 = $[8];
}
let t10;
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
t10 = <Text><Text bold={true}>Description</Text> (tells Claude when to use this agent):</Text>;
$[9] = t10;
} else {
t10 = $[9];
}
let t11;
if ($[10] !== agent.whenToUse) {
t11 = <Box flexDirection="column">{t10}<Box marginLeft={2}><Text>{agent.whenToUse}</Text></Box></Box>;
$[10] = agent.whenToUse;
$[11] = t11;
} else {
t11 = $[11];
}
const T1 = Box;
let t12;
if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
t12 = <Text><Text bold={true}>Tools</Text>:{" "}</Text>;
$[12] = t12;
} else {
t12 = $[12];
}
const t13 = renderToolsList();
let t14;
if ($[13] !== T1 || $[14] !== t12 || $[15] !== t13) {
t14 = <T1>{t12}{t13}</T1>;
$[13] = T1;
$[14] = t12;
$[15] = t13;
$[16] = t14;
} else {
t14 = $[16];
}
let t15;
if ($[17] === Symbol.for("react.memo_cache_sentinel")) {
t15 = <Text bold={true}>Model</Text>;
$[17] = t15;
} else {
t15 = $[17];
}
let t16;
if ($[18] !== agent.model) {
t16 = getAgentModelDisplay(agent.model);
$[18] = agent.model;
$[19] = t16;
} else {
t16 = $[19];
}
let t17;
if ($[20] !== t16) {
t17 = <Text>{t15}: {t16}</Text>;
$[20] = t16;
$[21] = t17;
} else {
t17 = $[21];
}
let t18;
if ($[22] !== agent.permissionMode) {
t18 = agent.permissionMode && <Text><Text bold={true}>Permission mode</Text>: {agent.permissionMode}</Text>;
$[22] = agent.permissionMode;
$[23] = t18;
} else {
t18 = $[23];
}
let t19;
if ($[24] !== agent.memory) {
t19 = agent.memory && <Text><Text bold={true}>Memory</Text>: {getMemoryScopeDisplay(agent.memory)}</Text>;
$[24] = agent.memory;
$[25] = t19;
} else {
t19 = $[25];
}
let t20;
if ($[26] !== agent.hooks) {
t20 = agent.hooks && Object.keys(agent.hooks).length > 0 && <Text><Text bold={true}>Hooks</Text>: {Object.keys(agent.hooks).join(", ")}</Text>;
$[26] = agent.hooks;
$[27] = t20;
} else {
t20 = $[27];
}
let t21;
if ($[28] !== agent.skills) {
t21 = agent.skills && agent.skills.length > 0 && <Text><Text bold={true}>Skills</Text>:{" "}{agent.skills.length > 10 ? `${agent.skills.length} skills` : agent.skills.join(", ")}</Text>;
$[28] = agent.skills;
$[29] = t21;
} else {
t21 = $[29];
}
let t22;
if ($[30] !== agent.agentType || $[31] !== backgroundColor) {
t22 = backgroundColor && <Box><Text><Text bold={true}>Color</Text>:{" "}<Text backgroundColor={backgroundColor} color="inverseText">{" "}{agent.agentType}{" "}</Text></Text></Box>;
$[30] = agent.agentType;
$[31] = backgroundColor;
$[32] = t22;
} else {
t22 = $[32];
}
let t23;
if ($[33] !== agent) {
t23 = !isBuiltInAgent(agent) && <><Box><Text><Text bold={true}>System prompt</Text>:</Text></Box><Box marginLeft={2} marginRight={2}><Markdown>{agent.getSystemPrompt()}</Markdown></Box></>;
$[33] = agent;
$[34] = t23;
} else {
t23 = $[34];
}
let t24;
if ($[35] !== T0 || $[36] !== handleKeyDown || $[37] !== t11 || $[38] !== t14 || $[39] !== t17 || $[40] !== t18 || $[41] !== t19 || $[42] !== t20 || $[43] !== t21 || $[44] !== t22 || $[45] !== t23 || $[46] !== t9) {
t24 = <T0 flexDirection={t5} gap={t6} tabIndex={t7} autoFocus={t8} onKeyDown={handleKeyDown}>{t9}{t11}{t14}{t17}{t18}{t19}{t20}{t21}{t22}{t23}</T0>;
$[35] = T0;
$[36] = handleKeyDown;
$[37] = t11;
$[38] = t14;
$[39] = t17;
$[40] = t18;
$[41] = t19;
$[42] = t20;
$[43] = t21;
$[44] = t22;
$[45] = t23;
$[46] = t9;
$[47] = t24;
} else {
t24 = $[47];
}
return t24;
agent: AgentDefinition
tools: Tools
allAgents?: AgentDefinition[]
onBack: () => void
}
export function AgentDetail({ agent, tools, onBack }: Props): React.ReactNode {
const resolvedTools = resolveAgentTools(agent, tools, false)
const filePath = getActualRelativeAgentFilePath(agent)
const backgroundColor = getAgentColor(agent.agentType)
// Handle Esc to go back
useKeybinding('confirm:no', onBack, { context: 'Confirmation' })
// Handle Enter to go back
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'return') {
e.preventDefault()
onBack()
}
}
function renderToolsList(): React.ReactNode {
if (resolvedTools.hasWildcard) {
return <Text>All tools</Text>
}
if (!agent.tools || agent.tools.length === 0) {
return <Text>None</Text>
}
return (
<>
{resolvedTools.validTools.length > 0 && (
<Text>{resolvedTools.validTools.join(', ')}</Text>
)}
{resolvedTools.invalidTools.length > 0 && (
<Text color="warning">
{figures.warning} Unrecognized:{' '}
{resolvedTools.invalidTools.join(', ')}
</Text>
)}
</>
)
}
return (
<Box
flexDirection="column"
gap={1}
tabIndex={0}
autoFocus
onKeyDown={handleKeyDown}
>
<Text dimColor>{filePath}</Text>
<Box flexDirection="column">
<Text>
<Text bold>Description</Text> (tells Claude when to use this agent):
</Text>
<Box marginLeft={2}>
<Text>{agent.whenToUse}</Text>
</Box>
</Box>
<Box>
<Text>
<Text bold>Tools</Text>:{' '}
</Text>
{renderToolsList()}
</Box>
<Text>
<Text bold>Model</Text>: {getAgentModelDisplay(agent.model)}
</Text>
{agent.permissionMode && (
<Text>
<Text bold>Permission mode</Text>: {agent.permissionMode}
</Text>
)}
{agent.memory && (
<Text>
<Text bold>Memory</Text>: {getMemoryScopeDisplay(agent.memory)}
</Text>
)}
{agent.hooks && Object.keys(agent.hooks).length > 0 && (
<Text>
<Text bold>Hooks</Text>: {Object.keys(agent.hooks).join(', ')}
</Text>
)}
{agent.skills && agent.skills.length > 0 && (
<Text>
<Text bold>Skills</Text>:{' '}
{agent.skills.length > 10
? `${agent.skills.length} skills`
: agent.skills.join(', ')}
</Text>
)}
{backgroundColor && (
<Box>
<Text>
<Text bold>Color</Text>:{' '}
<Text backgroundColor={backgroundColor} color="inverseText">
{' '}
{agent.agentType}{' '}
</Text>
</Text>
</Box>
)}
{!isBuiltInAgent(agent) && (
<>
<Box>
<Text>
<Text bold>System prompt</Text>:
</Text>
</Box>
<Box marginLeft={2} marginRight={2}>
<Markdown>{agent.getSystemPrompt()}</Markdown>
</Box>
</>
)}
</Box>
)
}

View File

@@ -1,177 +1,246 @@
import chalk from 'chalk';
import figures from 'figures';
import * as React from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useSetAppState } from 'src/state/AppState.js';
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
import { Box, Text } from '../../ink.js';
import { useKeybinding } from '../../keybindings/useKeybinding.js';
import type { Tools } from '../../Tool.js';
import { type AgentColorName, setAgentColor } from '../../tools/AgentTool/agentColorManager.js';
import { type AgentDefinition, getActiveAgentsFromList, isCustomAgent, isPluginAgent } from '../../tools/AgentTool/loadAgentsDir.js';
import { editFileInEditor } from '../../utils/promptEditor.js';
import { getActualAgentFilePath, updateAgentFile } from './agentFileUtils.js';
import { ColorPicker } from './ColorPicker.js';
import { ModelSelector } from './ModelSelector.js';
import { ToolSelector } from './ToolSelector.js';
import { getAgentSourceDisplayName } from './utils.js';
import chalk from 'chalk'
import figures from 'figures'
import * as React from 'react'
import { useCallback, useMemo, useState } from 'react'
import { useSetAppState } from 'src/state/AppState.js'
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
import { Box, Text } from '../../ink.js'
import { useKeybinding } from '../../keybindings/useKeybinding.js'
import type { Tools } from '../../Tool.js'
import {
type AgentColorName,
setAgentColor,
} from '../../tools/AgentTool/agentColorManager.js'
import {
type AgentDefinition,
getActiveAgentsFromList,
isCustomAgent,
isPluginAgent,
} from '../../tools/AgentTool/loadAgentsDir.js'
import { editFileInEditor } from '../../utils/promptEditor.js'
import { getActualAgentFilePath, updateAgentFile } from './agentFileUtils.js'
import { ColorPicker } from './ColorPicker.js'
import { ModelSelector } from './ModelSelector.js'
import { ToolSelector } from './ToolSelector.js'
import { getAgentSourceDisplayName } from './utils.js'
type Props = {
agent: AgentDefinition;
tools: Tools;
onSaved: (message: string) => void;
onBack: () => void;
};
type EditMode = 'menu' | 'edit-tools' | 'edit-color' | 'edit-model';
agent: AgentDefinition
tools: Tools
onSaved: (message: string) => void
onBack: () => void
}
type EditMode = 'menu' | 'edit-tools' | 'edit-color' | 'edit-model'
type SaveChanges = {
tools?: string[];
color?: AgentColorName;
model?: string;
};
tools?: string[]
color?: AgentColorName
model?: string
}
export function AgentEditor({
agent,
tools,
onSaved,
onBack
onBack,
}: Props): React.ReactNode {
const setAppState = useSetAppState();
const [editMode, setEditMode] = useState<EditMode>('menu');
const [selectedMenuIndex, setSelectedMenuIndex] = useState(0);
const [error, setError] = useState<string | null>(null);
const [selectedColor, setSelectedColor] = useState<AgentColorName | undefined>(agent.color as AgentColorName | undefined);
const setAppState = useSetAppState()
const [editMode, setEditMode] = useState<EditMode>('menu')
const [selectedMenuIndex, setSelectedMenuIndex] = useState(0)
const [error, setError] = useState<string | null>(null)
const [selectedColor, setSelectedColor] = useState<
AgentColorName | undefined
>(agent.color as AgentColorName | undefined)
const handleOpenInEditor = useCallback(async () => {
const filePath = getActualAgentFilePath(agent);
const result = await editFileInEditor(filePath);
const filePath = getActualAgentFilePath(agent)
const result = await editFileInEditor(filePath)
if (result.error) {
setError(result.error);
setError(result.error)
} else {
onSaved(`Opened ${agent.agentType} in editor. If you made edits, restart to load the latest version.`);
onSaved(
`Opened ${agent.agentType} in editor. If you made edits, restart to load the latest version.`,
)
}
}, [agent, onSaved]);
const handleSave = useCallback(async (changes: SaveChanges = {}) => {
const {
tools: newTools,
color: newColor,
model: newModel
} = changes;
const finalColor = newColor ?? selectedColor;
const hasToolsChanged = newTools !== undefined;
const hasModelChanged = newModel !== undefined;
const hasColorChanged = finalColor !== agent.color;
if (!hasToolsChanged && !hasModelChanged && !hasColorChanged) {
return false;
}
try {
// Only custom/plugin agents can be edited
// this is for type safety; the UI shouldn't allow editing otherwise
if (!isCustomAgent(agent) && !isPluginAgent(agent)) {
return false;
}, [agent, onSaved])
const handleSave = useCallback(
async (changes: SaveChanges = {}) => {
const { tools: newTools, color: newColor, model: newModel } = changes
const finalColor = newColor ?? selectedColor
const hasToolsChanged = newTools !== undefined
const hasModelChanged = newModel !== undefined
const hasColorChanged = finalColor !== agent.color
if (!hasToolsChanged && !hasModelChanged && !hasColorChanged) {
return false
}
await updateAgentFile(agent, agent.whenToUse, newTools ?? agent.tools, agent.getSystemPrompt(), finalColor, newModel ?? agent.model);
if (hasColorChanged && finalColor) {
setAgentColor(agent.agentType, finalColor);
}
setAppState(state => {
const allAgents = state.agentDefinitions.allAgents.map(a => a.agentType === agent.agentType ? {
...a,
tools: newTools ?? a.tools,
color: finalColor,
model: newModel ?? a.model
} : a);
return {
...state,
agentDefinitions: {
...state.agentDefinitions,
activeAgents: getActiveAgentsFromList(allAgents),
allAgents
try {
// Only custom/plugin agents can be edited
// this is for type safety; the UI shouldn't allow editing otherwise
if (!isCustomAgent(agent) && !isPluginAgent(agent)) {
return false
}
await updateAgentFile(
agent,
agent.whenToUse,
newTools ?? agent.tools,
agent.getSystemPrompt(),
finalColor,
newModel ?? agent.model,
)
if (hasColorChanged && finalColor) {
setAgentColor(agent.agentType, finalColor)
}
setAppState(state => {
const allAgents = state.agentDefinitions.allAgents.map(a =>
a.agentType === agent.agentType
? {
...a,
tools: newTools ?? a.tools,
color: finalColor,
model: newModel ?? a.model,
}
: a,
)
return {
...state,
agentDefinitions: {
...state.agentDefinitions,
activeAgents: getActiveAgentsFromList(allAgents),
allAgents,
},
}
};
});
onSaved(`Updated agent: ${chalk.bold(agent.agentType)}`);
return true;
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to save agent');
return false;
}
}, [agent, selectedColor, onSaved, setAppState]);
const menuItems = useMemo(() => [{
label: 'Open in editor',
action: handleOpenInEditor
}, {
label: 'Edit tools',
action: () => setEditMode('edit-tools')
}, {
label: 'Edit model',
action: () => setEditMode('edit-model')
}, {
label: 'Edit color',
action: () => setEditMode('edit-color')
}], [handleOpenInEditor]);
const handleEscape = useCallback(() => {
setError(null);
if (editMode === 'menu') {
onBack();
} else {
setEditMode('menu');
}
}, [editMode, onBack]);
const handleMenuKeyDown = useCallback((e: KeyboardEvent) => {
if (e.key === 'up') {
e.preventDefault();
setSelectedMenuIndex(index => Math.max(0, index - 1));
} else if (e.key === 'down') {
e.preventDefault();
setSelectedMenuIndex(index_0 => Math.min(menuItems.length - 1, index_0 + 1));
} else if (e.key === 'return') {
e.preventDefault();
const selectedItem = menuItems[selectedMenuIndex];
if (selectedItem) {
void selectedItem.action();
})
onSaved(`Updated agent: ${chalk.bold(agent.agentType)}`)
return true
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to save agent')
return false
}
},
[agent, selectedColor, onSaved, setAppState],
)
const menuItems = useMemo(
() => [
{ label: 'Open in editor', action: handleOpenInEditor },
{ label: 'Edit tools', action: () => setEditMode('edit-tools') },
{ label: 'Edit model', action: () => setEditMode('edit-model') },
{ label: 'Edit color', action: () => setEditMode('edit-color') },
],
[handleOpenInEditor],
)
const handleEscape = useCallback(() => {
setError(null)
if (editMode === 'menu') {
onBack()
} else {
setEditMode('menu')
}
}, [menuItems, selectedMenuIndex]);
useKeybinding('confirm:no', handleEscape, {
context: 'Confirmation'
});
const renderMenu = (): React.ReactNode => <Box flexDirection="column" tabIndex={0} autoFocus onKeyDown={handleMenuKeyDown}>
}, [editMode, onBack])
const handleMenuKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'up') {
e.preventDefault()
setSelectedMenuIndex(index => Math.max(0, index - 1))
} else if (e.key === 'down') {
e.preventDefault()
setSelectedMenuIndex(index => Math.min(menuItems.length - 1, index + 1))
} else if (e.key === 'return') {
e.preventDefault()
const selectedItem = menuItems[selectedMenuIndex]
if (selectedItem) {
void selectedItem.action()
}
}
},
[menuItems, selectedMenuIndex],
)
useKeybinding('confirm:no', handleEscape, { context: 'Confirmation' })
const renderMenu = (): React.ReactNode => (
<Box
flexDirection="column"
tabIndex={0}
autoFocus
onKeyDown={handleMenuKeyDown}
>
<Text dimColor>Source: {getAgentSourceDisplayName(agent.source)}</Text>
<Box marginTop={1} flexDirection="column">
{menuItems.map((item, index_1) => <Text key={item.label} color={index_1 === selectedMenuIndex ? 'suggestion' : undefined}>
{index_1 === selectedMenuIndex ? `${figures.pointer} ` : ' '}
{menuItems.map((item, index) => (
<Text
key={item.label}
color={index === selectedMenuIndex ? 'suggestion' : undefined}
>
{index === selectedMenuIndex ? `${figures.pointer} ` : ' '}
{item.label}
</Text>)}
</Text>
))}
</Box>
{error && <Box marginTop={1}>
{error && (
<Box marginTop={1}>
<Text color="error">{error}</Text>
</Box>}
</Box>;
</Box>
)}
</Box>
)
switch (editMode) {
case 'menu':
return renderMenu();
return renderMenu()
case 'edit-tools':
return <ToolSelector tools={tools} initialTools={agent.tools} onComplete={async finalTools => {
setEditMode('menu');
await handleSave({
tools: finalTools
});
}} />;
return (
<ToolSelector
tools={tools}
initialTools={agent.tools}
onComplete={async finalTools => {
setEditMode('menu')
await handleSave({ tools: finalTools })
}}
/>
)
case 'edit-color':
return <ColorPicker agentName={agent.agentType} currentColor={selectedColor || agent.color as AgentColorName || 'automatic'} onConfirm={async color => {
setSelectedColor(color);
setEditMode('menu');
await handleSave({
color
});
}} />;
return (
<ColorPicker
agentName={agent.agentType}
currentColor={
selectedColor || (agent.color as AgentColorName) || 'automatic'
}
onConfirm={async color => {
setSelectedColor(color)
setEditMode('menu')
await handleSave({ color })
}}
/>
)
case 'edit-model':
return <ModelSelector initialModel={agent.model} onComplete={async model => {
setEditMode('menu');
await handleSave({
model
});
}} />;
return (
<ModelSelector
initialModel={agent.model}
onComplete={async model => {
setEditMode('menu')
await handleSave({ model })
}}
/>
)
default:
return null;
return null
}
}

View File

@@ -1,25 +1,23 @@
import { c as _c } from "react/compiler-runtime";
import * as React from 'react';
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js';
import { Box, Text } from '../../ink.js';
import * as React from 'react'
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
import { Box, Text } from '../../ink.js'
type Props = {
instructions?: string;
};
export function AgentNavigationFooter(t0) {
const $ = _c(2);
const {
instructions: t1
} = t0;
const instructions = t1 === undefined ? "Press \u2191\u2193 to navigate \xB7 Enter to select \xB7 Esc to go back" : t1;
const exitState = useExitOnCtrlCDWithKeybindings();
const t2 = exitState.pending ? `Press ${exitState.keyName} again to exit` : instructions;
let t3;
if ($[0] !== t2) {
t3 = <Box marginLeft={2}><Text dimColor={true}>{t2}</Text></Box>;
$[0] = t2;
$[1] = t3;
} else {
t3 = $[1];
}
return t3;
instructions?: string
}
export function AgentNavigationFooter({
instructions = 'Press ↑↓ to navigate · Enter to select · Esc to go back',
}: Props): React.ReactNode {
const exitState = useExitOnCtrlCDWithKeybindings()
return (
<Box marginLeft={2}>
<Text dimColor>
{exitState.pending
? `Press ${exitState.keyName} again to exit`
: instructions}
</Text>
</Box>
)
}

View File

@@ -1,439 +1,342 @@
import { c as _c } from "react/compiler-runtime";
import figures from 'figures';
import * as React from 'react';
import type { SettingSource } from 'src/utils/settings/constants.js';
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
import { Box, Text } from '../../ink.js';
import type { ResolvedAgent } from '../../tools/AgentTool/agentDisplay.js';
import { AGENT_SOURCE_GROUPS, compareAgentsByName, getOverrideSourceLabel, resolveAgentModelDisplay } from '../../tools/AgentTool/agentDisplay.js';
import type { AgentDefinition } from '../../tools/AgentTool/loadAgentsDir.js';
import { count } from '../../utils/array.js';
import { Dialog } from '../design-system/Dialog.js';
import { Divider } from '../design-system/Divider.js';
import { getAgentSourceDisplayName } from './utils.js';
import figures from 'figures'
import * as React from 'react'
import type { SettingSource } from 'src/utils/settings/constants.js'
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
import { Box, Text } from '../../ink.js'
import type { ResolvedAgent } from '../../tools/AgentTool/agentDisplay.js'
import {
AGENT_SOURCE_GROUPS,
compareAgentsByName,
getOverrideSourceLabel,
resolveAgentModelDisplay,
} from '../../tools/AgentTool/agentDisplay.js'
import type { AgentDefinition } from '../../tools/AgentTool/loadAgentsDir.js'
import { count } from '../../utils/array.js'
import { Dialog } from '../design-system/Dialog.js'
import { Divider } from '../design-system/Divider.js'
import { getAgentSourceDisplayName } from './utils.js'
type Props = {
source: SettingSource | 'all' | 'built-in' | 'plugin';
agents: ResolvedAgent[];
onBack: () => void;
onSelect: (agent: AgentDefinition) => void;
onCreateNew?: () => void;
changes?: string[];
};
export function AgentsList(t0) {
const $ = _c(96);
const {
source,
agents,
onBack,
onSelect,
onCreateNew,
changes
} = t0;
const [selectedAgent, setSelectedAgent] = React.useState(null);
const [isCreateNewSelected, setIsCreateNewSelected] = React.useState(true);
let t1;
if ($[0] !== agents) {
t1 = [...agents].sort(compareAgentsByName);
$[0] = agents;
$[1] = t1;
} else {
t1 = $[1];
}
const sortedAgents = t1;
const getOverrideInfo = _temp;
let t2;
if ($[2] !== isCreateNewSelected) {
t2 = () => <Box><Text color={isCreateNewSelected ? "suggestion" : undefined}>{isCreateNewSelected ? `${figures.pointer} ` : " "}</Text><Text color={isCreateNewSelected ? "suggestion" : undefined}>Create new agent</Text></Box>;
$[2] = isCreateNewSelected;
$[3] = t2;
} else {
t2 = $[3];
}
const renderCreateNewOption = t2;
let t3;
if ($[4] !== isCreateNewSelected || $[5] !== selectedAgent?.agentType || $[6] !== selectedAgent?.source) {
t3 = agent_0 => {
const isBuiltIn = agent_0.source === "built-in";
const isSelected = !isBuiltIn && !isCreateNewSelected && selectedAgent?.agentType === agent_0.agentType && selectedAgent?.source === agent_0.source;
const {
isOverridden,
overriddenBy
} = getOverrideInfo(agent_0);
const dimmed = isBuiltIn || isOverridden;
const textColor = !isBuiltIn && isSelected ? "suggestion" : undefined;
const resolvedModel = resolveAgentModelDisplay(agent_0);
return <Box key={`${agent_0.agentType}-${agent_0.source}`}><Text dimColor={dimmed && !isSelected} color={textColor}>{isBuiltIn ? "" : isSelected ? `${figures.pointer} ` : " "}</Text><Text dimColor={dimmed && !isSelected} color={textColor}>{agent_0.agentType}</Text>{resolvedModel && <Text dimColor={true} color={textColor}>{" \xB7 "}{resolvedModel}</Text>}{agent_0.memory && <Text dimColor={true} color={textColor}>{" \xB7 "}{agent_0.memory} memory</Text>}{overriddenBy && <Text dimColor={!isSelected} color={isSelected ? "warning" : undefined}>{" "}{figures.warning} shadowed by {getOverrideSourceLabel(overriddenBy)}</Text>}</Box>;
};
$[4] = isCreateNewSelected;
$[5] = selectedAgent?.agentType;
$[6] = selectedAgent?.source;
$[7] = t3;
} else {
t3 = $[7];
}
const renderAgent = t3;
let t4;
if ($[8] !== sortedAgents || $[9] !== source) {
bb0: {
const nonBuiltIn = sortedAgents.filter(_temp2);
if (source === "all") {
t4 = AGENT_SOURCE_GROUPS.filter(_temp3).flatMap(t5 => {
const {
source: groupSource
} = t5;
return nonBuiltIn.filter(a_0 => a_0.source === groupSource);
});
break bb0;
}
t4 = nonBuiltIn;
source: SettingSource | 'all' | 'built-in' | 'plugin'
agents: ResolvedAgent[]
onBack: () => void
onSelect: (agent: AgentDefinition) => void
onCreateNew?: () => void
changes?: string[]
}
export function AgentsList({
source,
agents,
onBack,
onSelect,
onCreateNew,
changes,
}: Props): React.ReactNode {
const [selectedAgent, setSelectedAgent] =
React.useState<ResolvedAgent | null>(null)
const [isCreateNewSelected, setIsCreateNewSelected] = React.useState(true)
// Sort agents alphabetically by name within each source group
const sortedAgents = React.useMemo(
() => [...agents].sort(compareAgentsByName),
[agents],
)
const getOverrideInfo = (agent: ResolvedAgent) => {
return {
isOverridden: !!agent.overriddenBy,
overriddenBy: agent.overriddenBy || null,
}
$[8] = sortedAgents;
$[9] = source;
$[10] = t4;
} else {
t4 = $[10];
}
const selectableAgentsInOrder = t4;
let t5;
let t6;
if ($[11] !== isCreateNewSelected || $[12] !== onCreateNew || $[13] !== selectableAgentsInOrder || $[14] !== selectedAgent) {
t5 = () => {
if (!selectedAgent && !isCreateNewSelected && selectableAgentsInOrder.length > 0) {
if (onCreateNew) {
setIsCreateNewSelected(true);
} else {
setSelectedAgent(selectableAgentsInOrder[0] || null);
}
}
};
t6 = [selectableAgentsInOrder, selectedAgent, isCreateNewSelected, onCreateNew];
$[11] = isCreateNewSelected;
$[12] = onCreateNew;
$[13] = selectableAgentsInOrder;
$[14] = selectedAgent;
$[15] = t5;
$[16] = t6;
} else {
t5 = $[15];
t6 = $[16];
const renderCreateNewOption = () => {
return (
<Box>
<Text color={isCreateNewSelected ? 'suggestion' : undefined}>
{isCreateNewSelected ? `${figures.pointer} ` : ' '}
</Text>
<Text color={isCreateNewSelected ? 'suggestion' : undefined}>
Create new agent
</Text>
</Box>
)
}
React.useEffect(t5, t6);
let t7;
if ($[17] !== isCreateNewSelected || $[18] !== onCreateNew || $[19] !== onSelect || $[20] !== selectableAgentsInOrder || $[21] !== selectedAgent) {
t7 = e => {
if (e.key === "return") {
e.preventDefault();
if (isCreateNewSelected && onCreateNew) {
onCreateNew();
} else {
if (selectedAgent) {
onSelect(selectedAgent);
}
}
return;
}
if (e.key !== "up" && e.key !== "down") {
return;
}
e.preventDefault();
const hasCreateOption = !!onCreateNew;
const totalItems = selectableAgentsInOrder.length + (hasCreateOption ? 1 : 0);
if (totalItems === 0) {
return;
}
let currentPosition = 0;
if (!isCreateNewSelected && selectedAgent) {
const agentIndex = selectableAgentsInOrder.findIndex(a_1 => a_1.agentType === selectedAgent.agentType && a_1.source === selectedAgent.source);
if (agentIndex >= 0) {
currentPosition = hasCreateOption ? agentIndex + 1 : agentIndex;
}
}
const newPosition = e.key === "up" ? currentPosition === 0 ? totalItems - 1 : currentPosition - 1 : currentPosition === totalItems - 1 ? 0 : currentPosition + 1;
if (hasCreateOption && newPosition === 0) {
setIsCreateNewSelected(true);
setSelectedAgent(null);
} else {
const agentIndex_0 = hasCreateOption ? newPosition - 1 : newPosition;
const newAgent = selectableAgentsInOrder[agentIndex_0];
if (newAgent) {
setIsCreateNewSelected(false);
setSelectedAgent(newAgent);
}
}
};
$[17] = isCreateNewSelected;
$[18] = onCreateNew;
$[19] = onSelect;
$[20] = selectableAgentsInOrder;
$[21] = selectedAgent;
$[22] = t7;
} else {
t7 = $[22];
const renderAgent = (agent: ResolvedAgent) => {
const isBuiltIn = agent.source === 'built-in'
const isSelected =
!isBuiltIn &&
!isCreateNewSelected &&
selectedAgent?.agentType === agent.agentType &&
selectedAgent?.source === agent.source
const { isOverridden, overriddenBy } = getOverrideInfo(agent)
const dimmed = isBuiltIn || isOverridden
const textColor = !isBuiltIn && isSelected ? 'suggestion' : undefined
const resolvedModel = resolveAgentModelDisplay(agent)
return (
<Box key={`${agent.agentType}-${agent.source}`}>
<Text dimColor={dimmed && !isSelected} color={textColor}>
{isBuiltIn ? '' : isSelected ? `${figures.pointer} ` : ' '}
</Text>
<Text dimColor={dimmed && !isSelected} color={textColor}>
{agent.agentType}
</Text>
{resolvedModel && (
<Text dimColor={true} color={textColor}>
{' · '}
{resolvedModel}
</Text>
)}
{agent.memory && (
<Text dimColor={true} color={textColor}>
{' · '}
{agent.memory} memory
</Text>
)}
{overriddenBy && (
<Text
dimColor={!isSelected}
color={isSelected ? 'warning' : undefined}
>
{' '}
{figures.warning} shadowed by {getOverrideSourceLabel(overriddenBy)}
</Text>
)}
</Box>
)
}
const handleKeyDown = t7;
let t8;
if ($[23] !== renderAgent || $[24] !== sortedAgents) {
t8 = t9 => {
const title = t9 === undefined ? "Built-in (always available):" : t9;
const builtInAgents = sortedAgents.filter(_temp4);
return <Box flexDirection="column" marginBottom={1} paddingLeft={2}><Text bold={true} dimColor={true}>{title}</Text>{builtInAgents.map(renderAgent)}</Box>;
};
$[23] = renderAgent;
$[24] = sortedAgents;
$[25] = t8;
} else {
t8 = $[25];
}
const renderBuiltInAgentsSection = t8;
let t9;
if ($[26] !== renderAgent) {
t9 = (title_0, groupAgents) => {
if (!groupAgents.length) {
return null;
}
const folderPath = groupAgents[0]?.baseDir;
return <Box flexDirection="column" marginBottom={1}><Box paddingLeft={2}><Text bold={true} dimColor={true}>{title_0}</Text>{folderPath && <Text dimColor={true}> ({folderPath})</Text>}</Box>{groupAgents.map(agent_1 => renderAgent(agent_1))}</Box>;
};
$[26] = renderAgent;
$[27] = t9;
} else {
t9 = $[27];
}
const renderAgentGroup = t9;
let t10;
if ($[28] !== source) {
t10 = getAgentSourceDisplayName(source);
$[28] = source;
$[29] = t10;
} else {
t10 = $[29];
}
const sourceTitle = t10;
let T0;
let T1;
let t11;
let t12;
let t13;
let t14;
let t15;
let t16;
let t17;
let t18;
let t19;
let t20;
let t21;
let t22;
if ($[30] !== changes || $[31] !== handleKeyDown || $[32] !== onBack || $[33] !== onCreateNew || $[34] !== renderAgent || $[35] !== renderAgentGroup || $[36] !== renderBuiltInAgentsSection || $[37] !== renderCreateNewOption || $[38] !== sortedAgents || $[39] !== source || $[40] !== sourceTitle) {
t22 = Symbol.for("react.early_return_sentinel");
bb1: {
const builtInAgents_0 = sortedAgents.filter(_temp5);
const hasNoAgents = !sortedAgents.length || source !== "built-in" && !sortedAgents.some(_temp6);
if (hasNoAgents) {
let t23;
if ($[55] !== onCreateNew || $[56] !== renderCreateNewOption) {
t23 = onCreateNew && <Box>{renderCreateNewOption()}</Box>;
$[55] = onCreateNew;
$[56] = renderCreateNewOption;
$[57] = t23;
} else {
t23 = $[57];
}
let t24;
let t25;
let t26;
if ($[58] === Symbol.for("react.memo_cache_sentinel")) {
t24 = <Text dimColor={true}>No agents found. Create specialized subagents that Claude can delegate to.</Text>;
t25 = <Text dimColor={true}>Each subagent has its own context window, custom system prompt, and specific tools.</Text>;
t26 = <Text dimColor={true}>Try creating: Code Reviewer, Code Simplifier, Security Reviewer, Tech Lead, or UX Reviewer.</Text>;
$[58] = t24;
$[59] = t25;
$[60] = t26;
} else {
t24 = $[58];
t25 = $[59];
t26 = $[60];
}
let t27;
if ($[61] !== renderBuiltInAgentsSection || $[62] !== sortedAgents || $[63] !== source) {
t27 = source !== "built-in" && sortedAgents.some(_temp7) && <><Divider />{renderBuiltInAgentsSection()}</>;
$[61] = renderBuiltInAgentsSection;
$[62] = sortedAgents;
$[63] = source;
$[64] = t27;
} else {
t27 = $[64];
}
let t28;
if ($[65] !== handleKeyDown || $[66] !== t23 || $[67] !== t27) {
t28 = <Box flexDirection="column" gap={1} tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t23}{t24}{t25}{t26}{t27}</Box>;
$[65] = handleKeyDown;
$[66] = t23;
$[67] = t27;
$[68] = t28;
} else {
t28 = $[68];
}
let t29;
if ($[69] !== onBack || $[70] !== sourceTitle || $[71] !== t28) {
t29 = <Dialog title={sourceTitle} subtitle="No agents found" onCancel={onBack} hideInputGuide={true}>{t28}</Dialog>;
$[69] = onBack;
$[70] = sourceTitle;
$[71] = t28;
$[72] = t29;
} else {
t29 = $[72];
}
t22 = t29;
break bb1;
}
T1 = Dialog;
t17 = sourceTitle;
let t23;
if ($[73] !== sortedAgents) {
t23 = count(sortedAgents, _temp8);
$[73] = sortedAgents;
$[74] = t23;
} else {
t23 = $[74];
}
t18 = `${t23} agents`;
t19 = onBack;
t20 = true;
if ($[75] !== changes) {
t21 = changes && changes.length > 0 && <Box marginTop={1}><Text dimColor={true}>{changes[changes.length - 1]}</Text></Box>;
$[75] = changes;
$[76] = t21;
} else {
t21 = $[76];
}
T0 = Box;
t11 = "column";
t12 = 0;
t13 = true;
t14 = handleKeyDown;
if ($[77] !== onCreateNew || $[78] !== renderCreateNewOption) {
t15 = onCreateNew && <Box marginBottom={1}>{renderCreateNewOption()}</Box>;
$[77] = onCreateNew;
$[78] = renderCreateNewOption;
$[79] = t15;
} else {
t15 = $[79];
}
t16 = source === "all" ? <>{AGENT_SOURCE_GROUPS.filter(_temp9).map(t24 => {
const {
label,
source: groupSource_0
} = t24;
return <React.Fragment key={groupSource_0}>{renderAgentGroup(label, sortedAgents.filter(a_7 => a_7.source === groupSource_0))}</React.Fragment>;
})}{builtInAgents_0.length > 0 && <Box flexDirection="column" marginBottom={1} paddingLeft={2}><Text dimColor={true}><Text bold={true}>Built-in agents</Text> (always available)</Text>{builtInAgents_0.map(renderAgent)}</Box>}</> : source === "built-in" ? <><Text dimColor={true} italic={true}>Built-in agents are provided by default and cannot be modified.</Text><Box marginTop={1} flexDirection="column">{sortedAgents.map(agent_2 => renderAgent(agent_2))}</Box></> : <>{sortedAgents.filter(_temp0).map(agent_3 => renderAgent(agent_3))}{sortedAgents.some(_temp1) && <><Divider />{renderBuiltInAgentsSection()}</>}</>;
const selectableAgentsInOrder = React.useMemo(() => {
const nonBuiltIn = sortedAgents.filter(a => a.source !== 'built-in')
if (source === 'all') {
return AGENT_SOURCE_GROUPS.filter(g => g.source !== 'built-in').flatMap(
({ source: groupSource }) =>
nonBuiltIn.filter(a => a.source === groupSource),
)
}
return nonBuiltIn
}, [sortedAgents, source])
// Set initial selection
React.useEffect(() => {
if (
!selectedAgent &&
!isCreateNewSelected &&
selectableAgentsInOrder.length > 0
) {
if (onCreateNew) {
setIsCreateNewSelected(true)
} else {
setSelectedAgent(selectableAgentsInOrder[0] || null)
}
}
}, [selectableAgentsInOrder, selectedAgent, isCreateNewSelected, onCreateNew])
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'return') {
e.preventDefault()
if (isCreateNewSelected && onCreateNew) {
onCreateNew()
} else if (selectedAgent) {
onSelect(selectedAgent)
}
return
}
if (e.key !== 'up' && e.key !== 'down') return
e.preventDefault()
// Handle navigation with "Create New Agent" option
const hasCreateOption = !!onCreateNew
const totalItems =
selectableAgentsInOrder.length + (hasCreateOption ? 1 : 0)
if (totalItems === 0) return
// Calculate current position in list (0 = create new, 1+ = agents)
let currentPosition = 0
if (!isCreateNewSelected && selectedAgent) {
const agentIndex = selectableAgentsInOrder.findIndex(
a =>
a.agentType === selectedAgent.agentType &&
a.source === selectedAgent.source,
)
if (agentIndex >= 0) {
currentPosition = hasCreateOption ? agentIndex + 1 : agentIndex
}
}
// Calculate new position with wrap-around
const newPosition =
e.key === 'up'
? currentPosition === 0
? totalItems - 1
: currentPosition - 1
: currentPosition === totalItems - 1
? 0
: currentPosition + 1
// Update selection based on new position
if (hasCreateOption && newPosition === 0) {
setIsCreateNewSelected(true)
setSelectedAgent(null)
} else {
const agentIndex = hasCreateOption ? newPosition - 1 : newPosition
const newAgent = selectableAgentsInOrder[agentIndex]
if (newAgent) {
setIsCreateNewSelected(false)
setSelectedAgent(newAgent)
}
}
$[30] = changes;
$[31] = handleKeyDown;
$[32] = onBack;
$[33] = onCreateNew;
$[34] = renderAgent;
$[35] = renderAgentGroup;
$[36] = renderBuiltInAgentsSection;
$[37] = renderCreateNewOption;
$[38] = sortedAgents;
$[39] = source;
$[40] = sourceTitle;
$[41] = T0;
$[42] = T1;
$[43] = t11;
$[44] = t12;
$[45] = t13;
$[46] = t14;
$[47] = t15;
$[48] = t16;
$[49] = t17;
$[50] = t18;
$[51] = t19;
$[52] = t20;
$[53] = t21;
$[54] = t22;
} else {
T0 = $[41];
T1 = $[42];
t11 = $[43];
t12 = $[44];
t13 = $[45];
t14 = $[46];
t15 = $[47];
t16 = $[48];
t17 = $[49];
t18 = $[50];
t19 = $[51];
t20 = $[52];
t21 = $[53];
t22 = $[54];
}
if (t22 !== Symbol.for("react.early_return_sentinel")) {
return t22;
const renderBuiltInAgentsSection = (
title = 'Built-in (always available):',
) => {
const builtInAgents = sortedAgents.filter(a => a.source === 'built-in')
return (
<Box flexDirection="column" marginBottom={1} paddingLeft={2}>
<Text bold dimColor>
{title}
</Text>
{builtInAgents.map(renderAgent)}
</Box>
)
}
let t23;
if ($[80] !== T0 || $[81] !== t11 || $[82] !== t12 || $[83] !== t13 || $[84] !== t14 || $[85] !== t15 || $[86] !== t16) {
t23 = <T0 flexDirection={t11} tabIndex={t12} autoFocus={t13} onKeyDown={t14}>{t15}{t16}</T0>;
$[80] = T0;
$[81] = t11;
$[82] = t12;
$[83] = t13;
$[84] = t14;
$[85] = t15;
$[86] = t16;
$[87] = t23;
} else {
t23 = $[87];
const renderAgentGroup = (title: string, groupAgents: ResolvedAgent[]) => {
if (!groupAgents.length) return null
const folderPath = groupAgents[0]?.baseDir
return (
<Box flexDirection="column" marginBottom={1}>
<Box paddingLeft={2}>
<Text bold dimColor>
{title}
</Text>
{folderPath && <Text dimColor> ({folderPath})</Text>}
</Box>
{groupAgents.map(agent => renderAgent(agent))}
</Box>
)
}
let t24;
if ($[88] !== T1 || $[89] !== t17 || $[90] !== t18 || $[91] !== t19 || $[92] !== t20 || $[93] !== t21 || $[94] !== t23) {
t24 = <T1 title={t17} subtitle={t18} onCancel={t19} hideInputGuide={t20}>{t21}{t23}</T1>;
$[88] = T1;
$[89] = t17;
$[90] = t18;
$[91] = t19;
$[92] = t20;
$[93] = t21;
$[94] = t23;
$[95] = t24;
} else {
t24 = $[95];
const sourceTitle = getAgentSourceDisplayName(source)
const builtInAgents = sortedAgents.filter(a => a.source === 'built-in')
const hasNoAgents =
!sortedAgents.length ||
(source !== 'built-in' && !sortedAgents.some(a => a.source !== 'built-in'))
if (hasNoAgents) {
return (
<Dialog
title={sourceTitle}
subtitle="No agents found"
onCancel={onBack}
hideInputGuide
>
<Box
flexDirection="column"
gap={1}
tabIndex={0}
autoFocus
onKeyDown={handleKeyDown}
>
{onCreateNew && <Box>{renderCreateNewOption()}</Box>}
<Text dimColor>
No agents found. Create specialized subagents that Claude can
delegate to.
</Text>
<Text dimColor>
Each subagent has its own context window, custom system prompt, and
specific tools.
</Text>
<Text dimColor>
Try creating: Code Reviewer, Code Simplifier, Security Reviewer,
Tech Lead, or UX Reviewer.
</Text>
{source !== 'built-in' &&
sortedAgents.some(a => a.source === 'built-in') && (
<>
<Divider />
{renderBuiltInAgentsSection()}
</>
)}
</Box>
</Dialog>
)
}
return t24;
}
function _temp1(a_9) {
return a_9.source === "built-in";
}
function _temp0(a_8) {
return a_8.source !== "built-in";
}
function _temp9(g_0) {
return g_0.source !== "built-in";
}
function _temp8(a_6) {
return !a_6.overriddenBy;
}
function _temp7(a_5) {
return a_5.source === "built-in";
}
function _temp6(a_4) {
return a_4.source !== "built-in";
}
function _temp5(a_3) {
return a_3.source === "built-in";
}
function _temp4(a_2) {
return a_2.source === "built-in";
}
function _temp3(g) {
return g.source !== "built-in";
}
function _temp2(a) {
return a.source !== "built-in";
}
function _temp(agent) {
return {
isOverridden: !!agent.overriddenBy,
overriddenBy: agent.overriddenBy || null
};
return (
<Dialog
title={sourceTitle}
subtitle={`${count(sortedAgents, a => !a.overriddenBy)} agents`}
onCancel={onBack}
hideInputGuide
>
{changes && changes.length > 0 && (
<Box marginTop={1}>
<Text dimColor>{changes[changes.length - 1]}</Text>
</Box>
)}
<Box
flexDirection="column"
tabIndex={0}
autoFocus
onKeyDown={handleKeyDown}
>
{onCreateNew && <Box marginBottom={1}>{renderCreateNewOption()}</Box>}
{source === 'all' ? (
<>
{AGENT_SOURCE_GROUPS.filter(g => g.source !== 'built-in').map(
({ label, source: groupSource }) => (
<React.Fragment key={groupSource}>
{renderAgentGroup(
label,
sortedAgents.filter(a => a.source === groupSource),
)}
</React.Fragment>
),
)}
{builtInAgents.length > 0 && (
<Box flexDirection="column" marginBottom={1} paddingLeft={2}>
<Text dimColor>
<Text bold>Built-in agents</Text> (always available)
</Text>
{builtInAgents.map(renderAgent)}
</Box>
)}
</>
) : source === 'built-in' ? (
<>
<Text dimColor italic>
Built-in agents are provided by default and cannot be modified.
</Text>
<Box marginTop={1} flexDirection="column">
{sortedAgents.map(agent => renderAgent(agent))}
</Box>
</>
) : (
<>
{sortedAgents
.filter(a => a.source !== 'built-in')
.map(agent => renderAgent(agent))}
{sortedAgents.some(a => a.source === 'built-in') && (
<>
<Divider />
{renderBuiltInAgentsSection()}
</>
)}
</>
)}
</Box>
</Dialog>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,111 +1,106 @@
import { c as _c } from "react/compiler-runtime";
import figures from 'figures';
import React, { useState } from 'react';
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
import { Box, Text } from '../../ink.js';
import { AGENT_COLOR_TO_THEME_COLOR, AGENT_COLORS, type AgentColorName } from '../../tools/AgentTool/agentColorManager.js';
import { capitalize } from '../../utils/stringUtils.js';
type ColorOption = AgentColorName | 'automatic';
const COLOR_OPTIONS: ColorOption[] = ['automatic', ...AGENT_COLORS];
import figures from 'figures'
import React, { useState } from 'react'
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
import { Box, Text } from '../../ink.js'
import {
AGENT_COLOR_TO_THEME_COLOR,
AGENT_COLORS,
type AgentColorName,
} from '../../tools/AgentTool/agentColorManager.js'
import { capitalize } from '../../utils/stringUtils.js'
type ColorOption = AgentColorName | 'automatic'
const COLOR_OPTIONS: ColorOption[] = ['automatic', ...AGENT_COLORS]
type Props = {
agentName: string;
currentColor?: AgentColorName | 'automatic';
onConfirm: (color: AgentColorName | undefined) => void;
};
export function ColorPicker(t0) {
const $ = _c(17);
const {
agentName,
currentColor: t1,
onConfirm
} = t0;
const currentColor = t1 === undefined ? "automatic" : t1;
let t2;
if ($[0] !== currentColor) {
t2 = COLOR_OPTIONS.findIndex(opt => opt === currentColor);
$[0] = currentColor;
$[1] = t2;
} else {
t2 = $[1];
}
const [selectedIndex, setSelectedIndex] = useState(Math.max(0, t2));
let t3;
if ($[2] !== onConfirm || $[3] !== selectedIndex) {
t3 = e => {
if (e.key === "up") {
e.preventDefault();
setSelectedIndex(_temp);
} else {
if (e.key === "down") {
e.preventDefault();
setSelectedIndex(_temp2);
} else {
if (e.key === "return") {
e.preventDefault();
const selected = COLOR_OPTIONS[selectedIndex];
onConfirm(selected === "automatic" ? undefined : selected);
}
}
}
};
$[2] = onConfirm;
$[3] = selectedIndex;
$[4] = t3;
} else {
t3 = $[4];
}
const handleKeyDown = t3;
const selectedValue = COLOR_OPTIONS[selectedIndex];
let t4;
if ($[5] !== selectedIndex) {
t4 = COLOR_OPTIONS.map((option, index) => {
const isSelected = index === selectedIndex;
return <Box key={option} flexDirection="row" gap={1}><Text color={isSelected ? "suggestion" : undefined}>{isSelected ? figures.pointer : " "}</Text>{option === "automatic" ? <Text bold={isSelected}>Automatic color</Text> : <Box gap={1}><Text backgroundColor={AGENT_COLOR_TO_THEME_COLOR[option]} color="inverseText">{" "}</Text><Text bold={isSelected}>{capitalize(option)}</Text></Box>}</Box>;
});
$[5] = selectedIndex;
$[6] = t4;
} else {
t4 = $[6];
}
let t5;
if ($[7] !== t4) {
t5 = <Box flexDirection="column">{t4}</Box>;
$[7] = t4;
$[8] = t5;
} else {
t5 = $[8];
}
let t6;
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
t6 = <Text>Preview: </Text>;
$[9] = t6;
} else {
t6 = $[9];
}
let t7;
if ($[10] !== agentName || $[11] !== selectedValue) {
t7 = <Box marginTop={1}>{t6}{selectedValue === undefined || selectedValue === "automatic" ? <Text inverse={true} bold={true}>{" "}@{agentName}{" "}</Text> : <Text backgroundColor={AGENT_COLOR_TO_THEME_COLOR[selectedValue]} color="inverseText" bold={true}>{" "}@{agentName}{" "}</Text>}</Box>;
$[10] = agentName;
$[11] = selectedValue;
$[12] = t7;
} else {
t7 = $[12];
}
let t8;
if ($[13] !== handleKeyDown || $[14] !== t5 || $[15] !== t7) {
t8 = <Box flexDirection="column" gap={1} tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t5}{t7}</Box>;
$[13] = handleKeyDown;
$[14] = t5;
$[15] = t7;
$[16] = t8;
} else {
t8 = $[16];
}
return t8;
agentName: string
currentColor?: AgentColorName | 'automatic'
onConfirm: (color: AgentColorName | undefined) => void
}
function _temp2(prev_0) {
return prev_0 < COLOR_OPTIONS.length - 1 ? prev_0 + 1 : 0;
}
function _temp(prev) {
return prev > 0 ? prev - 1 : COLOR_OPTIONS.length - 1;
export function ColorPicker({
agentName,
currentColor = 'automatic',
onConfirm,
}: Props): React.ReactNode {
const [selectedIndex, setSelectedIndex] = useState(
Math.max(
0,
COLOR_OPTIONS.findIndex(opt => opt === currentColor),
),
)
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'up') {
e.preventDefault()
setSelectedIndex(prev => (prev > 0 ? prev - 1 : COLOR_OPTIONS.length - 1))
} else if (e.key === 'down') {
e.preventDefault()
setSelectedIndex(prev => (prev < COLOR_OPTIONS.length - 1 ? prev + 1 : 0))
} else if (e.key === 'return') {
e.preventDefault()
const selected = COLOR_OPTIONS[selectedIndex]
onConfirm(selected === 'automatic' ? undefined : selected)
}
}
const selectedValue = COLOR_OPTIONS[selectedIndex]
return (
<Box
flexDirection="column"
gap={1}
tabIndex={0}
autoFocus
onKeyDown={handleKeyDown}
>
<Box flexDirection="column">
{COLOR_OPTIONS.map((option, index) => {
const isSelected = index === selectedIndex
return (
<Box key={option} flexDirection="row" gap={1}>
<Text color={isSelected ? 'suggestion' : undefined}>
{isSelected ? figures.pointer : ' '}
</Text>
{option === 'automatic' ? (
<Text bold={isSelected}>Automatic color</Text>
) : (
<Box gap={1}>
<Text
backgroundColor={AGENT_COLOR_TO_THEME_COLOR[option]}
color="inverseText"
>
{' '}
</Text>
<Text bold={isSelected}>{capitalize(option)}</Text>
</Box>
)}
</Box>
)
})}
</Box>
<Box marginTop={1}>
<Text>Preview: </Text>
{selectedValue === undefined || selectedValue === 'automatic' ? (
<Text inverse bold>
{' '}
@{agentName}{' '}
</Text>
) : (
<Text
backgroundColor={AGENT_COLOR_TO_THEME_COLOR[selectedValue]}
color="inverseText"
bold
>
{' '}
@{agentName}{' '}
</Text>
)}
</Box>
</Box>
)
}

View File

@@ -1,67 +1,52 @@
import { c as _c } from "react/compiler-runtime";
import * as React from 'react';
import { Box, Text } from '../../ink.js';
import { getAgentModelOptions } from '../../utils/model/agent.js';
import { Select } from '../CustomSelect/select.js';
import * as React from 'react'
import { Box, Text } from '../../ink.js'
import { getAgentModelOptions } from '../../utils/model/agent.js'
import { Select } from '../CustomSelect/select.js'
interface ModelSelectorProps {
initialModel?: string;
onComplete: (model?: string) => void;
onCancel?: () => void;
initialModel?: string
onComplete: (model?: string) => void
onCancel?: () => void
}
export function ModelSelector(t0) {
const $ = _c(11);
const {
initialModel,
onComplete,
onCancel
} = t0;
let t1;
if ($[0] !== initialModel) {
bb0: {
const base = getAgentModelOptions();
if (initialModel && !base.some(o => o.value === initialModel)) {
t1 = [{
export function ModelSelector({
initialModel,
onComplete,
onCancel,
}: ModelSelectorProps): React.ReactNode {
const modelOptions = React.useMemo(() => {
const base = getAgentModelOptions()
// If the agent's current model is a full ID (e.g. 'claude-opus-4-5') not
// in the alias list, inject it as an option so it can round-trip through
// confirm without being overwritten.
if (initialModel && !base.some(o => o.value === initialModel)) {
return [
{
value: initialModel,
label: initialModel,
description: "Current model (custom ID)"
}, ...base];
break bb0;
}
t1 = base;
description: 'Current model (custom ID)',
},
...base,
]
}
$[0] = initialModel;
$[1] = t1;
} else {
t1 = $[1];
}
const modelOptions = t1;
const defaultModel = initialModel ?? "sonnet";
let t2;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t2 = <Box marginBottom={1}><Text dimColor={true}>Model determines the agent's reasoning capabilities and speed.</Text></Box>;
$[2] = t2;
} else {
t2 = $[2];
}
let t3;
if ($[3] !== onCancel || $[4] !== onComplete) {
t3 = () => onCancel ? onCancel() : onComplete(undefined);
$[3] = onCancel;
$[4] = onComplete;
$[5] = t3;
} else {
t3 = $[5];
}
let t4;
if ($[6] !== defaultModel || $[7] !== modelOptions || $[8] !== onComplete || $[9] !== t3) {
t4 = <Box flexDirection="column">{t2}<Select options={modelOptions} defaultValue={defaultModel} onChange={onComplete} onCancel={t3} /></Box>;
$[6] = defaultModel;
$[7] = modelOptions;
$[8] = onComplete;
$[9] = t3;
$[10] = t4;
} else {
t4 = $[10];
}
return t4;
return base
}, [initialModel])
const defaultModel = initialModel ?? 'sonnet'
return (
<Box flexDirection="column">
<Box marginBottom={1}>
<Text dimColor>
Model determines the agent&apos;s reasoning capabilities and speed.
</Text>
</Box>
<Select
options={modelOptions}
defaultValue={defaultModel}
onChange={onComplete}
onCancel={() => (onCancel ? onCancel() : onComplete(undefined))}
/>
</Box>
)
}

View File

@@ -1,561 +1,478 @@
import { c as _c } from "react/compiler-runtime";
import figures from 'figures';
import React, { useCallback, useMemo, useState } from 'react';
import { mcpInfoFromString } from 'src/services/mcp/mcpStringUtils.js';
import { isMcpTool } from 'src/services/mcp/utils.js';
import type { Tool, Tools } from 'src/Tool.js';
import { filterToolsForAgent } from 'src/tools/AgentTool/agentToolUtils.js';
import { AGENT_TOOL_NAME } from 'src/tools/AgentTool/constants.js';
import { BashTool } from 'src/tools/BashTool/BashTool.js';
import { ExitPlanModeV2Tool } from 'src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.js';
import { FileEditTool } from 'src/tools/FileEditTool/FileEditTool.js';
import { FileReadTool } from 'src/tools/FileReadTool/FileReadTool.js';
import { FileWriteTool } from 'src/tools/FileWriteTool/FileWriteTool.js';
import { GlobTool } from 'src/tools/GlobTool/GlobTool.js';
import { GrepTool } from 'src/tools/GrepTool/GrepTool.js';
import { ListMcpResourcesTool } from 'src/tools/ListMcpResourcesTool/ListMcpResourcesTool.js';
import { NotebookEditTool } from 'src/tools/NotebookEditTool/NotebookEditTool.js';
import { ReadMcpResourceTool } from 'src/tools/ReadMcpResourceTool/ReadMcpResourceTool.js';
import { TaskOutputTool } from 'src/tools/TaskOutputTool/TaskOutputTool.js';
import { TaskStopTool } from 'src/tools/TaskStopTool/TaskStopTool.js';
import { TodoWriteTool } from 'src/tools/TodoWriteTool/TodoWriteTool.js';
import { TungstenTool } from 'src/tools/TungstenTool/TungstenTool.js';
import { WebFetchTool } from 'src/tools/WebFetchTool/WebFetchTool.js';
import { WebSearchTool } from 'src/tools/WebSearchTool/WebSearchTool.js';
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
import { Box, Text } from '../../ink.js';
import { useKeybinding } from '../../keybindings/useKeybinding.js';
import { count } from '../../utils/array.js';
import { plural } from '../../utils/stringUtils.js';
import { Divider } from '../design-system/Divider.js';
import figures from 'figures'
import React, { useCallback, useMemo, useState } from 'react'
import { mcpInfoFromString } from 'src/services/mcp/mcpStringUtils.js'
import { isMcpTool } from 'src/services/mcp/utils.js'
import type { Tool, Tools } from 'src/Tool.js'
import { filterToolsForAgent } from 'src/tools/AgentTool/agentToolUtils.js'
import { AGENT_TOOL_NAME } from 'src/tools/AgentTool/constants.js'
import { BashTool } from 'src/tools/BashTool/BashTool.js'
import { ExitPlanModeV2Tool } from 'src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.js'
import { FileEditTool } from 'src/tools/FileEditTool/FileEditTool.js'
import { FileReadTool } from 'src/tools/FileReadTool/FileReadTool.js'
import { FileWriteTool } from 'src/tools/FileWriteTool/FileWriteTool.js'
import { GlobTool } from 'src/tools/GlobTool/GlobTool.js'
import { GrepTool } from 'src/tools/GrepTool/GrepTool.js'
import { ListMcpResourcesTool } from 'src/tools/ListMcpResourcesTool/ListMcpResourcesTool.js'
import { NotebookEditTool } from 'src/tools/NotebookEditTool/NotebookEditTool.js'
import { ReadMcpResourceTool } from 'src/tools/ReadMcpResourceTool/ReadMcpResourceTool.js'
import { TaskOutputTool } from 'src/tools/TaskOutputTool/TaskOutputTool.js'
import { TaskStopTool } from 'src/tools/TaskStopTool/TaskStopTool.js'
import { TodoWriteTool } from 'src/tools/TodoWriteTool/TodoWriteTool.js'
import { TungstenTool } from 'src/tools/TungstenTool/TungstenTool.js'
import { WebFetchTool } from 'src/tools/WebFetchTool/WebFetchTool.js'
import { WebSearchTool } from 'src/tools/WebSearchTool/WebSearchTool.js'
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
import { Box, Text } from '../../ink.js'
import { useKeybinding } from '../../keybindings/useKeybinding.js'
import { count } from '../../utils/array.js'
import { plural } from '../../utils/stringUtils.js'
import { Divider } from '../design-system/Divider.js'
type Props = {
tools: Tools;
initialTools: string[] | undefined;
onComplete: (selectedTools: string[] | undefined) => void;
onCancel?: () => void;
};
tools: Tools
initialTools: string[] | undefined
onComplete: (selectedTools: string[] | undefined) => void
onCancel?: () => void
}
type ToolBucket = {
name: string;
toolNames: Set<string>;
isMcp?: boolean;
};
name: string
toolNames: Set<string>
isMcp?: boolean
}
type ToolBuckets = {
READ_ONLY: ToolBucket;
EDIT: ToolBucket;
EXECUTION: ToolBucket;
MCP: ToolBucket;
OTHER: ToolBucket;
};
READ_ONLY: ToolBucket
EDIT: ToolBucket
EXECUTION: ToolBucket
MCP: ToolBucket
OTHER: ToolBucket
}
function getToolBuckets(): ToolBuckets {
return {
READ_ONLY: {
name: 'Read-only tools',
toolNames: new Set([GlobTool.name, GrepTool.name, ExitPlanModeV2Tool.name, FileReadTool.name, WebFetchTool.name, TodoWriteTool.name, WebSearchTool.name, TaskStopTool.name, TaskOutputTool.name, ListMcpResourcesTool.name, ReadMcpResourceTool.name])
toolNames: new Set([
GlobTool.name,
GrepTool.name,
ExitPlanModeV2Tool.name,
FileReadTool.name,
WebFetchTool.name,
TodoWriteTool.name,
WebSearchTool.name,
TaskStopTool.name,
TaskOutputTool.name,
ListMcpResourcesTool.name,
ReadMcpResourceTool.name,
]),
},
EDIT: {
name: 'Edit tools',
toolNames: new Set([FileEditTool.name, FileWriteTool.name, NotebookEditTool.name])
toolNames: new Set([
FileEditTool.name,
FileWriteTool.name,
NotebookEditTool.name,
]),
},
EXECUTION: {
name: 'Execution tools',
toolNames: new Set([BashTool.name, (process.env.USER_TYPE) === 'ant' ? TungstenTool.name : undefined].filter(n => n !== undefined))
toolNames: new Set(
[
BashTool.name,
process.env.USER_TYPE === 'ant' ? TungstenTool.name : undefined,
].filter(n => n !== undefined),
),
},
MCP: {
name: 'MCP tools',
toolNames: new Set(),
// Dynamic - no static list
isMcp: true
toolNames: new Set(), // Dynamic - no static list
isMcp: true,
},
OTHER: {
name: 'Other tools',
toolNames: new Set() // Dynamic - catch-all for uncategorized tools
}
};
toolNames: new Set(), // Dynamic - catch-all for uncategorized tools
},
}
}
// Helper to get MCP server buckets dynamically
function getMcpServerBuckets(tools: Tools): Array<{
serverName: string;
tools: Tools;
serverName: string
tools: Tools
}> {
const serverMap = new Map<string, Tool[]>();
const serverMap = new Map<string, Tool[]>()
tools.forEach(tool => {
if (isMcpTool(tool)) {
const mcpInfo = mcpInfoFromString(tool.name);
const mcpInfo = mcpInfoFromString(tool.name)
if (mcpInfo?.serverName) {
const existing = serverMap.get(mcpInfo.serverName) || [];
existing.push(tool);
serverMap.set(mcpInfo.serverName, existing);
const existing = serverMap.get(mcpInfo.serverName) || []
existing.push(tool)
serverMap.set(mcpInfo.serverName, existing)
}
}
});
return Array.from(serverMap.entries()).map(([serverName, tools]) => ({
serverName,
tools
})).sort((a, b) => a.serverName.localeCompare(b.serverName));
})
return Array.from(serverMap.entries())
.map(([serverName, tools]) => ({ serverName, tools }))
.sort((a, b) => a.serverName.localeCompare(b.serverName))
}
export function ToolSelector(t0) {
const $ = _c(69);
const {
tools,
initialTools,
onComplete,
onCancel
} = t0;
let t1;
if ($[0] !== tools) {
t1 = filterToolsForAgent({
tools,
isBuiltIn: false,
isAsync: false
});
$[0] = tools;
$[1] = t1;
} else {
t1 = $[1];
export function ToolSelector({
tools,
initialTools,
onComplete,
onCancel,
}: Props): React.ReactNode {
// Filter tools for custom agents
const customAgentTools = useMemo(
() => filterToolsForAgent({ tools, isBuiltIn: false, isAsync: false }),
[tools],
)
// Expand wildcard or undefined to explicit tool list for internal state
const expandedInitialTools =
!initialTools || initialTools.includes('*')
? customAgentTools.map(t => t.name)
: initialTools
const [selectedTools, setSelectedTools] =
useState<string[]>(expandedInitialTools)
const [focusIndex, setFocusIndex] = useState(0)
const [showIndividualTools, setShowIndividualTools] = useState(false)
// Filter selectedTools to only include tools that currently exist
// This handles MCP tools that disconnect while selected
const validSelectedTools = useMemo(() => {
const toolNames = new Set(customAgentTools.map(t => t.name))
return selectedTools.filter(name => toolNames.has(name))
}, [selectedTools, customAgentTools])
const selectedSet = new Set(validSelectedTools)
const isAllSelected =
validSelectedTools.length === customAgentTools.length &&
customAgentTools.length > 0
const handleToggleTool = (toolName: string) => {
if (!toolName) return
setSelectedTools(current =>
current.includes(toolName)
? current.filter(t => t !== toolName)
: [...current, toolName],
)
}
const customAgentTools = t1;
let t2;
if ($[2] !== customAgentTools || $[3] !== initialTools) {
t2 = !initialTools || initialTools.includes("*") ? customAgentTools.map(_temp) : initialTools;
$[2] = customAgentTools;
$[3] = initialTools;
$[4] = t2;
} else {
t2 = $[4];
}
const expandedInitialTools = t2;
const [selectedTools, setSelectedTools] = useState(expandedInitialTools);
const [focusIndex, setFocusIndex] = useState(0);
const [showIndividualTools, setShowIndividualTools] = useState(false);
let t3;
if ($[5] !== customAgentTools) {
t3 = new Set(customAgentTools.map(_temp2));
$[5] = customAgentTools;
$[6] = t3;
} else {
t3 = $[6];
}
const toolNames = t3;
let t4;
if ($[7] !== selectedTools || $[8] !== toolNames) {
let t5;
if ($[10] !== toolNames) {
t5 = name => toolNames.has(name);
$[10] = toolNames;
$[11] = t5;
} else {
t5 = $[11];
}
t4 = selectedTools.filter(t5);
$[7] = selectedTools;
$[8] = toolNames;
$[9] = t4;
} else {
t4 = $[9];
}
const validSelectedTools = t4;
let t5;
if ($[12] !== validSelectedTools) {
t5 = new Set(validSelectedTools);
$[12] = validSelectedTools;
$[13] = t5;
} else {
t5 = $[13];
}
const selectedSet = t5;
const isAllSelected = validSelectedTools.length === customAgentTools.length && customAgentTools.length > 0;
let t6;
if ($[14] === Symbol.for("react.memo_cache_sentinel")) {
t6 = toolName => {
if (!toolName) {
return;
const handleToggleTools = (toolNames: string[], select: boolean) => {
setSelectedTools(current => {
if (select) {
const toolsToAdd = toolNames.filter(t => !current.includes(t))
return [...current, ...toolsToAdd]
} else {
return current.filter(t => !toolNames.includes(t))
}
setSelectedTools(current => current.includes(toolName) ? current.filter(t_1 => t_1 !== toolName) : [...current, toolName]);
};
$[14] = t6;
} else {
t6 = $[14];
})
}
const handleToggleTool = t6;
let t7;
if ($[15] === Symbol.for("react.memo_cache_sentinel")) {
t7 = (toolNames_0, select) => {
setSelectedTools(current_0 => {
if (select) {
const toolsToAdd = toolNames_0.filter(t_2 => !current_0.includes(t_2));
return [...current_0, ...toolsToAdd];
} else {
return current_0.filter(t_3 => !toolNames_0.includes(t_3));
}
});
};
$[15] = t7;
} else {
t7 = $[15];
const handleConfirm = () => {
// Convert to undefined if all tools are selected (for cleaner file format)
const allToolNames = customAgentTools.map(t => t.name)
const areAllToolsSelected =
validSelectedTools.length === allToolNames.length &&
allToolNames.every(name => validSelectedTools.includes(name))
const finalTools = areAllToolsSelected ? undefined : validSelectedTools
onComplete(finalTools)
}
const handleToggleTools = t7;
let t8;
if ($[16] !== customAgentTools || $[17] !== onComplete || $[18] !== validSelectedTools) {
t8 = () => {
const allToolNames = customAgentTools.map(_temp3);
const areAllToolsSelected = validSelectedTools.length === allToolNames.length && allToolNames.every(name_0 => validSelectedTools.includes(name_0));
const finalTools = areAllToolsSelected ? undefined : validSelectedTools;
onComplete(finalTools);
};
$[16] = customAgentTools;
$[17] = onComplete;
$[18] = validSelectedTools;
$[19] = t8;
} else {
t8 = $[19];
}
const handleConfirm = t8;
let buckets;
if ($[20] !== customAgentTools) {
const toolBuckets = getToolBuckets();
buckets = {
// Group tools by bucket
const toolsByBucket = useMemo(() => {
const toolBuckets = getToolBuckets()
const buckets = {
readOnly: [] as Tool[],
edit: [] as Tool[],
execution: [] as Tool[],
mcp: [] as Tool[],
other: [] as Tool[]
};
other: [] as Tool[],
}
customAgentTools.forEach(tool => {
// Check if it's an MCP tool first
if (isMcpTool(tool)) {
buckets.mcp.push(tool);
} else {
if (toolBuckets.READ_ONLY.toolNames.has(tool.name)) {
buckets.readOnly.push(tool);
} else {
if (toolBuckets.EDIT.toolNames.has(tool.name)) {
buckets.edit.push(tool);
} else {
if (toolBuckets.EXECUTION.toolNames.has(tool.name)) {
buckets.execution.push(tool);
} else {
if (tool.name !== AGENT_TOOL_NAME) {
buckets.other.push(tool);
}
}
}
}
buckets.mcp.push(tool)
} else if (toolBuckets.READ_ONLY.toolNames.has(tool.name)) {
buckets.readOnly.push(tool)
} else if (toolBuckets.EDIT.toolNames.has(tool.name)) {
buckets.edit.push(tool)
} else if (toolBuckets.EXECUTION.toolNames.has(tool.name)) {
buckets.execution.push(tool)
} else if (tool.name !== AGENT_TOOL_NAME) {
// Catch-all for uncategorized tools (except Task)
buckets.other.push(tool)
}
});
$[20] = customAgentTools;
$[21] = buckets;
} else {
buckets = $[21];
}
const toolsByBucket = buckets;
let t9;
if ($[22] !== selectedSet) {
t9 = bucketTools => {
const selected = count(bucketTools, (t_5: Tool) => selectedSet.has(t_5.name));
const needsSelection = selected < bucketTools.length;
return () => {
const toolNames_1 = bucketTools.map(_temp4);
handleToggleTools(toolNames_1, needsSelection);
};
};
$[22] = selectedSet;
$[23] = t9;
} else {
t9 = $[23];
}
const createBucketToggleAction = t9;
let navigableItems;
if ($[24] !== createBucketToggleAction || $[25] !== customAgentTools || $[26] !== focusIndex || $[27] !== handleConfirm || $[28] !== isAllSelected || $[29] !== selectedSet || $[30] !== showIndividualTools || $[31] !== toolsByBucket.edit || $[32] !== toolsByBucket.execution || $[33] !== toolsByBucket.mcp || $[34] !== toolsByBucket.other || $[35] !== toolsByBucket.readOnly) {
navigableItems = [];
navigableItems.push({
id: "continue",
label: "Continue",
action: handleConfirm,
isContinue: true
});
let t10;
if ($[37] !== customAgentTools || $[38] !== isAllSelected) {
t10 = () => {
const allToolNames_0 = customAgentTools.map(_temp5);
handleToggleTools(allToolNames_0, !isAllSelected);
};
$[37] = customAgentTools;
$[38] = isAllSelected;
$[39] = t10;
} else {
t10 = $[39];
})
return buckets
}, [customAgentTools])
const createBucketToggleAction = (bucketTools: Tool[]) => {
const selected = count(bucketTools, t => selectedSet.has(t.name))
const needsSelection = selected < bucketTools.length
return () => {
const toolNames = bucketTools.map(t => t.name)
handleToggleTools(toolNames, needsSelection)
}
}
// Build navigable items (no separators)
const navigableItems: Array<{
id: string
label: string
action: () => void
isContinue?: boolean
isToggle?: boolean
isHeader?: boolean
}> = []
// Continue button
navigableItems.push({
id: 'continue',
label: 'Continue',
action: handleConfirm,
isContinue: true,
})
// All tools
navigableItems.push({
id: 'bucket-all',
label: `${isAllSelected ? figures.checkboxOn : figures.checkboxOff} All tools`,
action: () => {
const allToolNames = customAgentTools.map(t => t.name)
handleToggleTools(allToolNames, !isAllSelected)
},
})
// Create bucket menu items
const toolBuckets = getToolBuckets()
const bucketConfigs = [
{
id: 'bucket-readonly',
name: toolBuckets.READ_ONLY.name,
tools: toolsByBucket.readOnly,
},
{
id: 'bucket-edit',
name: toolBuckets.EDIT.name,
tools: toolsByBucket.edit,
},
{
id: 'bucket-execution',
name: toolBuckets.EXECUTION.name,
tools: toolsByBucket.execution,
},
{
id: 'bucket-mcp',
name: toolBuckets.MCP.name,
tools: toolsByBucket.mcp,
},
{
id: 'bucket-other',
name: toolBuckets.OTHER.name,
tools: toolsByBucket.other,
},
]
bucketConfigs.forEach(({ id, name, tools: bucketTools }) => {
if (bucketTools.length === 0) return
const selected = count(bucketTools, t => selectedSet.has(t.name))
const isFullySelected = selected === bucketTools.length
navigableItems.push({
id: "bucket-all",
label: `${isAllSelected ? figures.checkboxOn : figures.checkboxOff} All tools`,
action: t10
});
const toolBuckets_0 = getToolBuckets();
const bucketConfigs = [{
id: "bucket-readonly",
name: toolBuckets_0.READ_ONLY.name,
tools: toolsByBucket.readOnly
}, {
id: "bucket-edit",
name: toolBuckets_0.EDIT.name,
tools: toolsByBucket.edit
}, {
id: "bucket-execution",
name: toolBuckets_0.EXECUTION.name,
tools: toolsByBucket.execution
}, {
id: "bucket-mcp",
name: toolBuckets_0.MCP.name,
tools: toolsByBucket.mcp
}, {
id: "bucket-other",
name: toolBuckets_0.OTHER.name,
tools: toolsByBucket.other
}];
bucketConfigs.forEach(t11 => {
const {
id,
name: name_1,
tools: bucketTools_0
} = t11;
if (bucketTools_0.length === 0) {
return;
id,
label: `${isFullySelected ? figures.checkboxOn : figures.checkboxOff} ${name}`,
action: createBucketToggleAction(bucketTools),
})
})
// Toggle button for individual tools
const toggleButtonIndex = navigableItems.length
navigableItems.push({
id: 'toggle-individual',
label: showIndividualTools
? 'Hide advanced options'
: 'Show advanced options',
action: () => {
setShowIndividualTools(!showIndividualTools)
// If hiding tools and focus is on an individual tool, move focus to toggle button
if (showIndividualTools && focusIndex > toggleButtonIndex) {
setFocusIndex(toggleButtonIndex)
}
const selected_0 = count(bucketTools_0, (t_8: Tool) => selectedSet.has(t_8.name));
const isFullySelected = selected_0 === bucketTools_0.length;
},
isToggle: true,
})
// Memoize MCP server buckets (must be outside conditional for hooks rules)
const mcpServerBuckets = useMemo(
() => getMcpServerBuckets(customAgentTools),
[customAgentTools],
)
// Individual tools (only if expanded)
if (showIndividualTools) {
// Add MCP server buckets if any exist
if (mcpServerBuckets.length > 0) {
navigableItems.push({
id,
label: `${isFullySelected ? figures.checkboxOn : figures.checkboxOff} ${name_1}`,
action: createBucketToggleAction(bucketTools_0)
});
});
const toggleButtonIndex = navigableItems.length;
let t12;
if ($[40] !== focusIndex || $[41] !== showIndividualTools || $[42] !== toggleButtonIndex) {
t12 = () => {
setShowIndividualTools(!showIndividualTools);
if (showIndividualTools && focusIndex > toggleButtonIndex) {
setFocusIndex(toggleButtonIndex);
}
};
$[40] = focusIndex;
$[41] = showIndividualTools;
$[42] = toggleButtonIndex;
$[43] = t12;
id: 'mcp-servers-header',
label: 'MCP Servers:',
action: () => {}, // No action - just a header
isHeader: true,
})
mcpServerBuckets.forEach(({ serverName, tools: serverTools }) => {
const selected = count(serverTools, t => selectedSet.has(t.name))
const isFullySelected = selected === serverTools.length
navigableItems.push({
id: `mcp-server-${serverName}`,
label: `${isFullySelected ? figures.checkboxOn : figures.checkboxOff} ${serverName} (${serverTools.length} ${plural(serverTools.length, 'tool')})`,
action: () => {
const toolNames = serverTools.map(t => t.name)
handleToggleTools(toolNames, !isFullySelected)
},
})
})
// Add separator header before individual tools
navigableItems.push({
id: 'tools-header',
label: 'Individual Tools:',
action: () => {},
isHeader: true,
})
}
// Add individual tools
customAgentTools.forEach(tool => {
let displayName = tool.name
if (tool.name.startsWith('mcp__')) {
const mcpInfo = mcpInfoFromString(tool.name)
displayName = mcpInfo
? `${mcpInfo.toolName} (${mcpInfo.serverName})`
: tool.name
}
navigableItems.push({
id: `tool-${tool.name}`,
label: `${selectedSet.has(tool.name) ? figures.checkboxOn : figures.checkboxOff} ${displayName}`,
action: () => handleToggleTool(tool.name),
})
})
}
const handleCancel = useCallback(() => {
if (onCancel) {
onCancel()
} else {
t12 = $[43];
onComplete(initialTools)
}
navigableItems.push({
id: "toggle-individual",
label: showIndividualTools ? "Hide advanced options" : "Show advanced options",
action: t12,
isToggle: true
});
const mcpServerBuckets = getMcpServerBuckets(customAgentTools);
if (showIndividualTools) {
if (mcpServerBuckets.length > 0) {
navigableItems.push({
id: "mcp-servers-header",
label: "MCP Servers:",
action: _temp6,
isHeader: true
});
mcpServerBuckets.forEach(t13 => {
const {
serverName,
tools: serverTools
} = t13;
const selected_1 = count(serverTools, t_9 => selectedSet.has(t_9.name));
const isFullySelected_0 = selected_1 === serverTools.length;
navigableItems.push({
id: `mcp-server-${serverName}`,
label: `${isFullySelected_0 ? figures.checkboxOn : figures.checkboxOff} ${serverName} (${serverTools.length} ${plural(serverTools.length, "tool")})`,
action: () => {
const toolNames_2 = serverTools.map(_temp7);
handleToggleTools(toolNames_2, !isFullySelected_0);
}
});
});
navigableItems.push({
id: "tools-header",
label: "Individual Tools:",
action: _temp8,
isHeader: true
});
}, [onCancel, onComplete, initialTools])
useKeybinding('confirm:no', handleCancel, { context: 'Confirmation' })
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'return') {
e.preventDefault()
const item = navigableItems[focusIndex]
if (item && !item.isHeader) {
item.action()
}
customAgentTools.forEach(tool_0 => {
let displayName = tool_0.name;
if (tool_0.name.startsWith("mcp__")) {
const mcpInfo = mcpInfoFromString(tool_0.name);
displayName = mcpInfo ? `${mcpInfo.toolName} (${mcpInfo.serverName})` : tool_0.name;
}
navigableItems.push({
id: `tool-${tool_0.name}`,
label: `${selectedSet.has(tool_0.name) ? figures.checkboxOn : figures.checkboxOff} ${displayName}`,
action: () => handleToggleTool(tool_0.name)
});
});
} else if (e.key === 'up') {
e.preventDefault()
let newIndex = focusIndex - 1
// Skip headers when navigating up
while (newIndex > 0 && navigableItems[newIndex]?.isHeader) {
newIndex--
}
setFocusIndex(Math.max(0, newIndex))
} else if (e.key === 'down') {
e.preventDefault()
let newIndex = focusIndex + 1
// Skip headers when navigating down
while (
newIndex < navigableItems.length - 1 &&
navigableItems[newIndex]?.isHeader
) {
newIndex++
}
setFocusIndex(Math.min(navigableItems.length - 1, newIndex))
}
$[24] = createBucketToggleAction;
$[25] = customAgentTools;
$[26] = focusIndex;
$[27] = handleConfirm;
$[28] = isAllSelected;
$[29] = selectedSet;
$[30] = showIndividualTools;
$[31] = toolsByBucket.edit;
$[32] = toolsByBucket.execution;
$[33] = toolsByBucket.mcp;
$[34] = toolsByBucket.other;
$[35] = toolsByBucket.readOnly;
$[36] = navigableItems;
} else {
navigableItems = $[36];
}
let t10;
if ($[44] !== initialTools || $[45] !== onCancel || $[46] !== onComplete) {
t10 = () => {
if (onCancel) {
onCancel();
} else {
onComplete(initialTools);
}
};
$[44] = initialTools;
$[45] = onCancel;
$[46] = onComplete;
$[47] = t10;
} else {
t10 = $[47];
}
const handleCancel = t10;
let t11;
if ($[48] === Symbol.for("react.memo_cache_sentinel")) {
t11 = {
context: "Confirmation"
};
$[48] = t11;
} else {
t11 = $[48];
}
useKeybinding("confirm:no", handleCancel, t11);
let t12;
if ($[49] !== focusIndex || $[50] !== navigableItems) {
t12 = e => {
if (e.key === "return") {
e.preventDefault();
const item = navigableItems[focusIndex];
if (item && !item.isHeader) {
item.action();
}
} else {
if (e.key === "up") {
e.preventDefault();
let newIndex = focusIndex - 1;
while (newIndex > 0 && navigableItems[newIndex]?.isHeader) {
newIndex--;
}
setFocusIndex(Math.max(0, newIndex));
} else {
if (e.key === "down") {
e.preventDefault();
let newIndex_0 = focusIndex + 1;
while (newIndex_0 < navigableItems.length - 1 && navigableItems[newIndex_0]?.isHeader) {
newIndex_0++;
}
setFocusIndex(Math.min(navigableItems.length - 1, newIndex_0));
}
}
}
};
$[49] = focusIndex;
$[50] = navigableItems;
$[51] = t12;
} else {
t12 = $[51];
}
const handleKeyDown = t12;
const t13 = focusIndex === 0 ? "suggestion" : undefined;
const t14 = focusIndex === 0;
const t15 = focusIndex === 0 ? `${figures.pointer} ` : " ";
let t16;
if ($[52] !== t13 || $[53] !== t14 || $[54] !== t15) {
t16 = <Text color={t13} bold={t14}>{t15}[ Continue ]</Text>;
$[52] = t13;
$[53] = t14;
$[54] = t15;
$[55] = t16;
} else {
t16 = $[55];
}
let t17;
if ($[56] === Symbol.for("react.memo_cache_sentinel")) {
t17 = <Divider width={40} />;
$[56] = t17;
} else {
t17 = $[56];
}
let t18;
if ($[57] !== navigableItems) {
t18 = navigableItems.slice(1);
$[57] = navigableItems;
$[58] = t18;
} else {
t18 = $[58];
}
let t19;
if ($[59] !== focusIndex || $[60] !== t18) {
t19 = t18.map((item_0, index) => {
const isCurrentlyFocused = index + 1 === focusIndex;
const isToggleButton = item_0.isToggle;
const isHeader = item_0.isHeader;
return <React.Fragment key={item_0.id}>{isToggleButton && <Divider width={40} />}{isHeader && index > 0 && <Box marginTop={1} />}<Text color={isHeader ? undefined : isCurrentlyFocused ? "suggestion" : undefined} dimColor={isHeader} bold={isToggleButton && isCurrentlyFocused}>{isHeader ? "" : isCurrentlyFocused ? `${figures.pointer} ` : " "}{isToggleButton ? `[ ${item_0.label} ]` : item_0.label}</Text></React.Fragment>;
});
$[59] = focusIndex;
$[60] = t18;
$[61] = t19;
} else {
t19 = $[61];
}
const t20 = isAllSelected ? "All tools selected" : `${selectedSet.size} of ${customAgentTools.length} tools selected`;
let t21;
if ($[62] !== t20) {
t21 = <Box marginTop={1} flexDirection="column"><Text dimColor={true}>{t20}</Text></Box>;
$[62] = t20;
$[63] = t21;
} else {
t21 = $[63];
}
let t22;
if ($[64] !== handleKeyDown || $[65] !== t16 || $[66] !== t19 || $[67] !== t21) {
t22 = <Box flexDirection="column" marginTop={1} tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t16}{t17}{t19}{t21}</Box>;
$[64] = handleKeyDown;
$[65] = t16;
$[66] = t19;
$[67] = t21;
$[68] = t22;
} else {
t22 = $[68];
}
return t22;
}
function _temp8() {}
function _temp7(t_10) {
return t_10.name;
}
function _temp6() {}
function _temp5(t_7) {
return t_7.name;
}
function _temp4(t_6) {
return t_6.name;
}
function _temp3(t_4) {
return t_4.name;
}
function _temp2(t_0) {
return t_0.name;
}
function _temp(t) {
return t.name;
return (
<Box
flexDirection="column"
marginTop={1}
tabIndex={0}
autoFocus
onKeyDown={handleKeyDown}
>
{/* Render Continue button */}
<Text
color={focusIndex === 0 ? 'suggestion' : undefined}
bold={focusIndex === 0}
>
{focusIndex === 0 ? `${figures.pointer} ` : ' '}[ Continue ]
</Text>
{/* Separator */}
<Divider width={40} />
{/* Render all navigable items except Continue (which is at index 0) */}
{navigableItems.slice(1).map((item, index) => {
const isCurrentlyFocused = index + 1 === focusIndex
const isToggleButton = item.isToggle
const isHeader = item.isHeader
return (
<React.Fragment key={item.id}>
{/* Add separator before toggle button */}
{isToggleButton && <Divider width={40} />}
{/* Add margin before headers */}
{isHeader && index > 0 && <Box marginTop={1} />}
<Text
color={
isHeader
? undefined
: isCurrentlyFocused
? 'suggestion'
: undefined
}
dimColor={isHeader}
bold={isToggleButton && isCurrentlyFocused}
>
{isHeader
? ''
: isCurrentlyFocused
? `${figures.pointer} `
: ' '}
{isToggleButton ? `[ ${item.label} ]` : item.label}
</Text>
</React.Fragment>
)
})}
<Box marginTop={1} flexDirection="column">
<Text dimColor>
{isAllSelected
? 'All tools selected'
: `${selectedSet.size} of ${customAgentTools.length} tools selected`}
</Text>
</Box>
</Box>
)
}

View File

@@ -1,96 +1,68 @@
import { c as _c } from "react/compiler-runtime";
import React, { type ReactNode } from 'react';
import { isAutoMemoryEnabled } from '../../../memdir/paths.js';
import type { Tools } from '../../../Tool.js';
import type { AgentDefinition } from '../../../tools/AgentTool/loadAgentsDir.js';
import { WizardProvider } from '../../wizard/index.js';
import type { WizardStepComponent } from '../../wizard/types.js';
import type { AgentWizardData } from './types.js';
import { ColorStep } from './wizard-steps/ColorStep.js';
import { ConfirmStepWrapper } from './wizard-steps/ConfirmStepWrapper.js';
import { DescriptionStep } from './wizard-steps/DescriptionStep.js';
import { GenerateStep } from './wizard-steps/GenerateStep.js';
import { LocationStep } from './wizard-steps/LocationStep.js';
import { MemoryStep } from './wizard-steps/MemoryStep.js';
import { MethodStep } from './wizard-steps/MethodStep.js';
import { ModelStep } from './wizard-steps/ModelStep.js';
import { PromptStep } from './wizard-steps/PromptStep.js';
import { ToolsStep } from './wizard-steps/ToolsStep.js';
import { TypeStep } from './wizard-steps/TypeStep.js';
import React, { type ReactNode } from 'react'
import { isAutoMemoryEnabled } from '../../../memdir/paths.js'
import type { Tools } from '../../../Tool.js'
import type { AgentDefinition } from '../../../tools/AgentTool/loadAgentsDir.js'
import { WizardProvider } from '../../wizard/index.js'
import type { WizardStepComponent } from '../../wizard/types.js'
import type { AgentWizardData } from './types.js'
import { ColorStep } from './wizard-steps/ColorStep.js'
import { ConfirmStepWrapper } from './wizard-steps/ConfirmStepWrapper.js'
import { DescriptionStep } from './wizard-steps/DescriptionStep.js'
import { GenerateStep } from './wizard-steps/GenerateStep.js'
import { LocationStep } from './wizard-steps/LocationStep.js'
import { MemoryStep } from './wizard-steps/MemoryStep.js'
import { MethodStep } from './wizard-steps/MethodStep.js'
import { ModelStep } from './wizard-steps/ModelStep.js'
import { PromptStep } from './wizard-steps/PromptStep.js'
import { ToolsStep } from './wizard-steps/ToolsStep.js'
import { TypeStep } from './wizard-steps/TypeStep.js'
type Props = {
tools: Tools;
existingAgents: AgentDefinition[];
onComplete: (message: string) => void;
onCancel: () => void;
};
export function CreateAgentWizard(t0) {
const $ = _c(17);
const {
tools,
existingAgents,
onComplete,
onCancel
} = t0;
let t1;
if ($[0] !== existingAgents) {
t1 = () => <TypeStep existingAgents={existingAgents} />;
$[0] = existingAgents;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] !== tools) {
t2 = () => <ToolsStep tools={tools} />;
$[2] = tools;
$[3] = t2;
} else {
t2 = $[3];
}
let t3;
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
t3 = isAutoMemoryEnabled() ? [MemoryStep] : [];
$[4] = t3;
} else {
t3 = $[4];
}
let t4;
if ($[5] !== existingAgents || $[6] !== onComplete || $[7] !== tools) {
t4 = () => <ConfirmStepWrapper tools={tools} existingAgents={existingAgents} onComplete={onComplete} />;
$[5] = existingAgents;
$[6] = onComplete;
$[7] = tools;
$[8] = t4;
} else {
t4 = $[8];
}
let t5;
if ($[9] !== t1 || $[10] !== t2 || $[11] !== t4) {
t5 = [LocationStep, MethodStep, GenerateStep, t1, PromptStep, DescriptionStep, t2, ModelStep, ColorStep, ...t3, t4];
$[9] = t1;
$[10] = t2;
$[11] = t4;
$[12] = t5;
} else {
t5 = $[12];
}
const steps = t5;
let t6;
if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
t6 = {};
$[13] = t6;
} else {
t6 = $[13];
}
let t7;
if ($[14] !== onCancel || $[15] !== steps) {
t7 = <WizardProvider steps={steps} initialData={t6} onComplete={_temp} onCancel={onCancel} title="Create new agent" showStepCounter={false} />;
$[14] = onCancel;
$[15] = steps;
$[16] = t7;
} else {
t7 = $[16];
}
return t7;
tools: Tools
existingAgents: AgentDefinition[]
onComplete: (message: string) => void
onCancel: () => void
}
export function CreateAgentWizard({
tools,
existingAgents,
onComplete,
onCancel,
}: Props): ReactNode {
// Create step components with props
const steps: WizardStepComponent<AgentWizardData>[] = [
LocationStep, // 0
MethodStep, // 1
GenerateStep, // 2
() => <TypeStep existingAgents={existingAgents} />, // 3
PromptStep, // 4
DescriptionStep, // 5
() => <ToolsStep tools={tools} />, // 6
ModelStep, // 7
ColorStep, // 8
// MemoryStep is conditionally included based on GrowthBook gate
...(isAutoMemoryEnabled() ? [MemoryStep] : []),
() => (
<ConfirmStepWrapper
tools={tools}
existingAgents={existingAgents}
onComplete={onComplete}
/>
),
]
return (
<WizardProvider<AgentWizardData>
steps={steps}
initialData={{}}
onComplete={() => {
// Wizard completion is handled by ConfirmStepWrapper
// which calls onComplete with the appropriate message
}}
onCancel={onCancel}
title="Create new agent"
showStepCounter={false}
/>
)
}
function _temp() {}

View File

@@ -1,83 +1,64 @@
import { c as _c } from "react/compiler-runtime";
import React, { type ReactNode } from 'react';
import { Box } from '../../../../ink.js';
import { useKeybinding } from '../../../../keybindings/useKeybinding.js';
import type { AgentColorName } from '../../../../tools/AgentTool/agentColorManager.js';
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
import { Byline } from '../../../design-system/Byline.js';
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
import { useWizard } from '../../../wizard/index.js';
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
import { ColorPicker } from '../../ColorPicker.js';
import type { AgentWizardData } from '../types.js';
export function ColorStep() {
const $ = _c(14);
const {
goNext,
goBack,
updateWizardData,
wizardData
} = useWizard();
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = {
context: "Confirmation"
};
$[0] = t0;
} else {
t0 = $[0];
import React, { type ReactNode } from 'react'
import { Box } from '../../../../ink.js'
import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
import type { AgentColorName } from '../../../../tools/AgentTool/agentColorManager.js'
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
import { Byline } from '../../../design-system/Byline.js'
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
import { useWizard } from '../../../wizard/index.js'
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
import { ColorPicker } from '../../ColorPicker.js'
import type { AgentWizardData } from '../types.js'
export function ColorStep(): ReactNode {
const { goNext, goBack, updateWizardData, wizardData } =
useWizard<AgentWizardData>()
// Handle escape key - ColorPicker handles its own escape internally
useKeybinding('confirm:no', goBack, { context: 'Confirmation' })
const handleConfirm = (color?: string): void => {
updateWizardData({
selectedColor: color,
// Prepare final agent for confirmation
finalAgent: {
agentType: wizardData.agentType!,
whenToUse: wizardData.whenToUse!,
getSystemPrompt: () => wizardData.systemPrompt!,
tools: wizardData.selectedTools,
...(wizardData.selectedModel
? { model: wizardData.selectedModel }
: {}),
...(color ? { color: color as AgentColorName } : {}),
source: wizardData.location!,
},
})
goNext()
}
useKeybinding("confirm:no", goBack, t0);
let t1;
if ($[1] !== goNext || $[2] !== updateWizardData || $[3] !== wizardData.agentType || $[4] !== wizardData.location || $[5] !== wizardData.selectedModel || $[6] !== wizardData.selectedTools || $[7] !== wizardData.systemPrompt || $[8] !== wizardData.whenToUse) {
t1 = color => {
updateWizardData({
selectedColor: color,
finalAgent: {
agentType: wizardData.agentType,
whenToUse: wizardData.whenToUse,
getSystemPrompt: () => wizardData.systemPrompt,
tools: wizardData.selectedTools,
...(wizardData.selectedModel ? {
model: wizardData.selectedModel
} : {}),
...(color ? {
color: color as AgentColorName
} : {}),
source: wizardData.location
}
});
goNext();
};
$[1] = goNext;
$[2] = updateWizardData;
$[3] = wizardData.agentType;
$[4] = wizardData.location;
$[5] = wizardData.selectedModel;
$[6] = wizardData.selectedTools;
$[7] = wizardData.systemPrompt;
$[8] = wizardData.whenToUse;
$[9] = t1;
} else {
t1 = $[9];
}
const handleConfirm = t1;
let t2;
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
t2 = <Byline><KeyboardShortcutHint shortcut={"\u2191\u2193"} action="navigate" /><KeyboardShortcutHint shortcut="Enter" action="select" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" /></Byline>;
$[10] = t2;
} else {
t2 = $[10];
}
const t3 = wizardData.agentType || "agent";
let t4;
if ($[11] !== handleConfirm || $[12] !== t3) {
t4 = <WizardDialogLayout subtitle="Choose background color" footerText={t2}><Box><ColorPicker agentName={t3} currentColor="automatic" onConfirm={handleConfirm} /></Box></WizardDialogLayout>;
$[11] = handleConfirm;
$[12] = t3;
$[13] = t4;
} else {
t4 = $[13];
}
return t4;
return (
<WizardDialogLayout
subtitle="Choose background color"
footerText={
<Byline>
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
<KeyboardShortcutHint shortcut="Enter" action="select" />
<ConfigurableShortcutHint
action="confirm:no"
context="Confirmation"
fallback="Esc"
description="go back"
/>
</Byline>
}
>
<Box>
<ColorPicker
agentName={wizardData.agentType || 'agent'}
currentColor="automatic"
onConfirm={handleConfirm}
/>
</Box>
</WizardDialogLayout>
)
}

View File

@@ -1,377 +1,168 @@
import { c as _c } from "react/compiler-runtime";
import React, { type ReactNode } from 'react';
import type { KeyboardEvent } from '../../../../ink/events/keyboard-event.js';
import { Box, Text } from '../../../../ink.js';
import { useKeybinding } from '../../../../keybindings/useKeybinding.js';
import { isAutoMemoryEnabled } from '../../../../memdir/paths.js';
import type { Tools } from '../../../../Tool.js';
import { getMemoryScopeDisplay } from '../../../../tools/AgentTool/agentMemory.js';
import type { AgentDefinition } from '../../../../tools/AgentTool/loadAgentsDir.js';
import { truncateToWidth } from '../../../../utils/format.js';
import { getAgentModelDisplay } from '../../../../utils/model/agent.js';
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
import { Byline } from '../../../design-system/Byline.js';
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
import { useWizard } from '../../../wizard/index.js';
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
import { getNewRelativeAgentFilePath } from '../../agentFileUtils.js';
import { validateAgent } from '../../validateAgent.js';
import type { AgentWizardData } from '../types.js';
import React, { type ReactNode } from 'react'
import type { KeyboardEvent } from '../../../../ink/events/keyboard-event.js'
import { Box, Text } from '../../../../ink.js'
import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
import { isAutoMemoryEnabled } from '../../../../memdir/paths.js'
import type { Tools } from '../../../../Tool.js'
import { getMemoryScopeDisplay } from '../../../../tools/AgentTool/agentMemory.js'
import type { AgentDefinition } from '../../../../tools/AgentTool/loadAgentsDir.js'
import { truncateToWidth } from '../../../../utils/format.js'
import { getAgentModelDisplay } from '../../../../utils/model/agent.js'
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
import { Byline } from '../../../design-system/Byline.js'
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
import { useWizard } from '../../../wizard/index.js'
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
import { getNewRelativeAgentFilePath } from '../../agentFileUtils.js'
import { validateAgent } from '../../validateAgent.js'
import type { AgentWizardData } from '../types.js'
type Props = {
tools: Tools;
existingAgents: AgentDefinition[];
onSave: () => void;
onSaveAndEdit: () => void;
error?: string | null;
};
export function ConfirmStep(t0) {
const $ = _c(88);
const {
tools,
existingAgents,
onSave,
onSaveAndEdit,
error
} = t0;
const {
goBack,
wizardData
} = useWizard();
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = {
context: "Confirmation"
};
$[0] = t1;
} else {
t1 = $[0];
tools: Tools
existingAgents: AgentDefinition[]
onSave: () => void
onSaveAndEdit: () => void
error?: string | null
}
export function ConfirmStep({
tools,
existingAgents,
onSave,
onSaveAndEdit,
error,
}: Props): ReactNode {
const { goBack, wizardData } = useWizard<AgentWizardData>()
useKeybinding('confirm:no', goBack, { context: 'Confirmation' })
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 's' || e.key === 'return') {
e.preventDefault()
onSave()
} else if (e.key === 'e') {
e.preventDefault()
onSaveAndEdit()
}
}
useKeybinding("confirm:no", goBack, t1);
let t2;
if ($[1] !== onSave || $[2] !== onSaveAndEdit) {
t2 = e => {
if (e.key === "s" || e.key === "return") {
e.preventDefault();
onSave();
} else {
if (e.key === "e") {
e.preventDefault();
onSaveAndEdit();
}
const agent = wizardData.finalAgent!
const validation = validateAgent(agent, tools, existingAgents)
const systemPromptPreview = truncateToWidth(agent.getSystemPrompt(), 240)
const whenToUsePreview = truncateToWidth(agent.whenToUse, 240)
const getToolsDisplay = (toolNames: string[] | undefined): string => {
// undefined means "all tools" per PR semantic
if (toolNames === undefined) return 'All tools'
if (toolNames.length === 0) return 'None'
if (toolNames.length === 1) return toolNames[0] || 'None'
if (toolNames.length === 2) return toolNames.join(' and ')
return `${toolNames.slice(0, -1).join(', ')}, and ${toolNames[toolNames.length - 1]}`
}
// Compute memory display outside JSX
const memoryDisplayElement = isAutoMemoryEnabled() ? (
<Text>
<Text bold>Memory</Text>: {getMemoryScopeDisplay(agent.memory)}
</Text>
) : null
return (
<WizardDialogLayout
subtitle="Confirm and save"
footerText={
<Byline>
<KeyboardShortcutHint shortcut="s/Enter" action="save" />
<KeyboardShortcutHint shortcut="e" action="edit in your editor" />
<ConfigurableShortcutHint
action="confirm:no"
context="Confirmation"
fallback="Esc"
description="cancel"
/>
</Byline>
}
};
$[1] = onSave;
$[2] = onSaveAndEdit;
$[3] = t2;
} else {
t2 = $[3];
}
const handleKeyDown = t2;
const agent = wizardData.finalAgent;
let T0;
let T1;
let t10;
let t11;
let t12;
let t13;
let t14;
let t15;
let t16;
let t17;
let t18;
let t19;
let t3;
let t4;
let t5;
let t6;
let t7;
let t8;
let t9;
if ($[4] !== agent || $[5] !== existingAgents || $[6] !== handleKeyDown || $[7] !== tools || $[8] !== wizardData.location) {
const validation = validateAgent(agent, tools, existingAgents);
let t20;
if ($[28] !== agent) {
t20 = truncateToWidth(agent.getSystemPrompt(), 240);
$[28] = agent;
$[29] = t20;
} else {
t20 = $[29];
}
const systemPromptPreview = t20;
let t21;
if ($[30] !== agent.whenToUse) {
t21 = truncateToWidth(agent.whenToUse, 240);
$[30] = agent.whenToUse;
$[31] = t21;
} else {
t21 = $[31];
}
const whenToUsePreview = t21;
const getToolsDisplay = _temp;
let t22;
if ($[32] !== agent.memory) {
t22 = isAutoMemoryEnabled() ? <Text><Text bold={true}>Memory</Text>: {getMemoryScopeDisplay(agent.memory)}</Text> : null;
$[32] = agent.memory;
$[33] = t22;
} else {
t22 = $[33];
}
const memoryDisplayElement = t22;
T1 = WizardDialogLayout;
t18 = "Confirm and save";
if ($[34] === Symbol.for("react.memo_cache_sentinel")) {
t19 = <Byline><KeyboardShortcutHint shortcut="s/Enter" action="save" /><KeyboardShortcutHint shortcut="e" action="edit in your editor" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="cancel" /></Byline>;
$[34] = t19;
} else {
t19 = $[34];
}
T0 = Box;
t3 = "column";
t4 = 0;
t5 = true;
t6 = handleKeyDown;
let t23;
if ($[35] === Symbol.for("react.memo_cache_sentinel")) {
t23 = <Text bold={true}>Name</Text>;
$[35] = t23;
} else {
t23 = $[35];
}
if ($[36] !== agent.agentType) {
t7 = <Text>{t23}: {agent.agentType}</Text>;
$[36] = agent.agentType;
$[37] = t7;
} else {
t7 = $[37];
}
let t24;
if ($[38] === Symbol.for("react.memo_cache_sentinel")) {
t24 = <Text bold={true}>Location</Text>;
$[38] = t24;
} else {
t24 = $[38];
}
let t25;
if ($[39] !== agent.agentType || $[40] !== wizardData.location) {
t25 = getNewRelativeAgentFilePath({
source: wizardData.location,
agentType: agent.agentType
});
$[39] = agent.agentType;
$[40] = wizardData.location;
$[41] = t25;
} else {
t25 = $[41];
}
if ($[42] !== t25) {
t8 = <Text>{t24}:{" "}{t25}</Text>;
$[42] = t25;
$[43] = t8;
} else {
t8 = $[43];
}
let t26;
if ($[44] === Symbol.for("react.memo_cache_sentinel")) {
t26 = <Text bold={true}>Tools</Text>;
$[44] = t26;
} else {
t26 = $[44];
}
let t27;
if ($[45] !== agent.tools) {
t27 = getToolsDisplay(agent.tools);
$[45] = agent.tools;
$[46] = t27;
} else {
t27 = $[46];
}
if ($[47] !== t27) {
t9 = <Text>{t26}: {t27}</Text>;
$[47] = t27;
$[48] = t9;
} else {
t9 = $[48];
}
let t28;
if ($[49] === Symbol.for("react.memo_cache_sentinel")) {
t28 = <Text bold={true}>Model</Text>;
$[49] = t28;
} else {
t28 = $[49];
}
let t29;
if ($[50] !== agent.model) {
t29 = getAgentModelDisplay(agent.model);
$[50] = agent.model;
$[51] = t29;
} else {
t29 = $[51];
}
if ($[52] !== t29) {
t10 = <Text>{t28}: {t29}</Text>;
$[52] = t29;
$[53] = t10;
} else {
t10 = $[53];
}
t11 = memoryDisplayElement;
if ($[54] === Symbol.for("react.memo_cache_sentinel")) {
t12 = <Box marginTop={1}><Text><Text bold={true}>Description</Text> (tells Claude when to use this agent):</Text></Box>;
$[54] = t12;
} else {
t12 = $[54];
}
if ($[55] !== whenToUsePreview) {
t13 = <Box marginLeft={2} marginTop={1}><Text>{whenToUsePreview}</Text></Box>;
$[55] = whenToUsePreview;
$[56] = t13;
} else {
t13 = $[56];
}
if ($[57] === Symbol.for("react.memo_cache_sentinel")) {
t14 = <Box marginTop={1}><Text><Text bold={true}>System prompt</Text>:</Text></Box>;
$[57] = t14;
} else {
t14 = $[57];
}
if ($[58] !== systemPromptPreview) {
t15 = <Box marginLeft={2} marginTop={1}><Text>{systemPromptPreview}</Text></Box>;
$[58] = systemPromptPreview;
$[59] = t15;
} else {
t15 = $[59];
}
t16 = validation.warnings.length > 0 && <Box marginTop={1} flexDirection="column"><Text color="warning">Warnings:</Text>{validation.warnings.map(_temp2)}</Box>;
t17 = validation.errors.length > 0 && <Box marginTop={1} flexDirection="column"><Text color="error">Errors:</Text>{validation.errors.map(_temp3)}</Box>;
$[4] = agent;
$[5] = existingAgents;
$[6] = handleKeyDown;
$[7] = tools;
$[8] = wizardData.location;
$[9] = T0;
$[10] = T1;
$[11] = t10;
$[12] = t11;
$[13] = t12;
$[14] = t13;
$[15] = t14;
$[16] = t15;
$[17] = t16;
$[18] = t17;
$[19] = t18;
$[20] = t19;
$[21] = t3;
$[22] = t4;
$[23] = t5;
$[24] = t6;
$[25] = t7;
$[26] = t8;
$[27] = t9;
} else {
T0 = $[9];
T1 = $[10];
t10 = $[11];
t11 = $[12];
t12 = $[13];
t13 = $[14];
t14 = $[15];
t15 = $[16];
t16 = $[17];
t17 = $[18];
t18 = $[19];
t19 = $[20];
t3 = $[21];
t4 = $[22];
t5 = $[23];
t6 = $[24];
t7 = $[25];
t8 = $[26];
t9 = $[27];
}
let t20;
if ($[60] !== error) {
t20 = error && <Box marginTop={1}><Text color="error">{error}</Text></Box>;
$[60] = error;
$[61] = t20;
} else {
t20 = $[61];
}
let t21;
if ($[62] === Symbol.for("react.memo_cache_sentinel")) {
t21 = <Text bold={true}>s</Text>;
$[62] = t21;
} else {
t21 = $[62];
}
let t22;
if ($[63] === Symbol.for("react.memo_cache_sentinel")) {
t22 = <Text bold={true}>Enter</Text>;
$[63] = t22;
} else {
t22 = $[63];
}
let t23;
if ($[64] === Symbol.for("react.memo_cache_sentinel")) {
t23 = <Box marginTop={2}><Text color="success">Press {t21} or {t22} to save,{" "}<Text bold={true}>e</Text> to save and edit</Text></Box>;
$[64] = t23;
} else {
t23 = $[64];
}
let t24;
if ($[65] !== T0 || $[66] !== t10 || $[67] !== t11 || $[68] !== t12 || $[69] !== t13 || $[70] !== t14 || $[71] !== t15 || $[72] !== t16 || $[73] !== t17 || $[74] !== t20 || $[75] !== t3 || $[76] !== t4 || $[77] !== t5 || $[78] !== t6 || $[79] !== t7 || $[80] !== t8 || $[81] !== t9) {
t24 = <T0 flexDirection={t3} tabIndex={t4} autoFocus={t5} onKeyDown={t6}>{t7}{t8}{t9}{t10}{t11}{t12}{t13}{t14}{t15}{t16}{t17}{t20}{t23}</T0>;
$[65] = T0;
$[66] = t10;
$[67] = t11;
$[68] = t12;
$[69] = t13;
$[70] = t14;
$[71] = t15;
$[72] = t16;
$[73] = t17;
$[74] = t20;
$[75] = t3;
$[76] = t4;
$[77] = t5;
$[78] = t6;
$[79] = t7;
$[80] = t8;
$[81] = t9;
$[82] = t24;
} else {
t24 = $[82];
}
let t25;
if ($[83] !== T1 || $[84] !== t18 || $[85] !== t19 || $[86] !== t24) {
t25 = <T1 subtitle={t18} footerText={t19}>{t24}</T1>;
$[83] = T1;
$[84] = t18;
$[85] = t19;
$[86] = t24;
$[87] = t25;
} else {
t25 = $[87];
}
return t25;
}
function _temp3(err, i_0) {
return <Text key={i_0} color="error">{" "} {err}</Text>;
}
function _temp2(warning, i) {
return <Text key={i} dimColor={true}>{" "} {warning}</Text>;
}
function _temp(toolNames) {
if (toolNames === undefined) {
return "All tools";
}
if (toolNames.length === 0) {
return "None";
}
if (toolNames.length === 1) {
return toolNames[0] || "None";
}
if (toolNames.length === 2) {
return toolNames.join(" and ");
}
return `${toolNames.slice(0, -1).join(", ")}, and ${toolNames[toolNames.length - 1]}`;
>
<Box
flexDirection="column"
tabIndex={0}
autoFocus
onKeyDown={handleKeyDown}
>
<Text>
<Text bold>Name</Text>: {agent.agentType}
</Text>
<Text>
<Text bold>Location</Text>:{' '}
{getNewRelativeAgentFilePath({
source: wizardData.location!,
agentType: agent.agentType,
})}
</Text>
<Text>
<Text bold>Tools</Text>: {getToolsDisplay(agent.tools)}
</Text>
<Text>
<Text bold>Model</Text>: {getAgentModelDisplay(agent.model)}
</Text>
{memoryDisplayElement}
<Box marginTop={1}>
<Text>
<Text bold>Description</Text> (tells Claude when to use this agent):
</Text>
</Box>
<Box marginLeft={2} marginTop={1}>
<Text>{whenToUsePreview}</Text>
</Box>
<Box marginTop={1}>
<Text>
<Text bold>System prompt</Text>:
</Text>
</Box>
<Box marginLeft={2} marginTop={1}>
<Text>{systemPromptPreview}</Text>
</Box>
{validation.warnings.length > 0 && (
<Box marginTop={1} flexDirection="column">
<Text color="warning">Warnings:</Text>
{validation.warnings.map((warning, i) => (
<Text key={i} dimColor>
{' '}
{warning}
</Text>
))}
</Box>
)}
{validation.errors.length > 0 && (
<Box marginTop={1} flexDirection="column">
<Text color="error">Errors:</Text>
{validation.errors.map((err, i) => (
<Text key={i} color="error">
{' '}
{err}
</Text>
))}
</Box>
)}
{error && (
<Box marginTop={1}>
<Text color="error">{error}</Text>
</Box>
)}
<Box marginTop={2}>
<Text color="success">
Press <Text bold>s</Text> or <Text bold>Enter</Text> to save,{' '}
<Text bold>e</Text> to save and edit
</Text>
</Box>
</Box>
</WizardDialogLayout>
)
}

View File

@@ -1,73 +1,112 @@
import chalk from 'chalk';
import React, { type ReactNode, useCallback, useState } from 'react';
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
import { useSetAppState } from 'src/state/AppState.js';
import type { Tools } from '../../../../Tool.js';
import type { AgentDefinition } from '../../../../tools/AgentTool/loadAgentsDir.js';
import { getActiveAgentsFromList } from '../../../../tools/AgentTool/loadAgentsDir.js';
import { editFileInEditor } from '../../../../utils/promptEditor.js';
import { useWizard } from '../../../wizard/index.js';
import { getNewAgentFilePath, saveAgentToFile } from '../../agentFileUtils.js';
import type { AgentWizardData } from '../types.js';
import { ConfirmStep } from './ConfirmStep.js';
import chalk from 'chalk'
import React, { type ReactNode, useCallback, useState } from 'react'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from 'src/services/analytics/index.js'
import { useSetAppState } from 'src/state/AppState.js'
import type { Tools } from '../../../../Tool.js'
import type { AgentDefinition } from '../../../../tools/AgentTool/loadAgentsDir.js'
import { getActiveAgentsFromList } from '../../../../tools/AgentTool/loadAgentsDir.js'
import { editFileInEditor } from '../../../../utils/promptEditor.js'
import { useWizard } from '../../../wizard/index.js'
import { getNewAgentFilePath, saveAgentToFile } from '../../agentFileUtils.js'
import type { AgentWizardData } from '../types.js'
import { ConfirmStep } from './ConfirmStep.js'
type Props = {
tools: Tools;
existingAgents: AgentDefinition[];
onComplete: (message: string) => void;
};
tools: Tools
existingAgents: AgentDefinition[]
onComplete: (message: string) => void
}
export function ConfirmStepWrapper({
tools,
existingAgents,
onComplete
onComplete,
}: Props): ReactNode {
const {
wizardData
} = useWizard<AgentWizardData>();
const [saveError, setSaveError] = useState<string | null>(null);
const setAppState = useSetAppState();
const saveAgent = useCallback(async (openInEditor: boolean): Promise<void> => {
if (!wizardData?.finalAgent) return;
try {
await saveAgentToFile(wizardData.location!, wizardData.finalAgent.agentType, wizardData.finalAgent.whenToUse, wizardData.finalAgent.tools, wizardData.finalAgent.getSystemPrompt(), true, wizardData.finalAgent.color, wizardData.finalAgent.model, wizardData.finalAgent.memory);
setAppState(state => {
if (!wizardData.finalAgent) return state;
const allAgents = state.agentDefinitions.allAgents.concat(wizardData.finalAgent);
return {
...state,
agentDefinitions: {
...state.agentDefinitions,
activeAgents: getActiveAgentsFromList(allAgents),
allAgents
const { wizardData } = useWizard<AgentWizardData>()
const [saveError, setSaveError] = useState<string | null>(null)
const setAppState = useSetAppState()
const saveAgent = useCallback(
async (openInEditor: boolean): Promise<void> => {
if (!wizardData?.finalAgent) return
try {
await saveAgentToFile(
wizardData.location!,
wizardData.finalAgent.agentType,
wizardData.finalAgent.whenToUse,
wizardData.finalAgent.tools,
wizardData.finalAgent.getSystemPrompt(),
true,
wizardData.finalAgent.color,
wizardData.finalAgent.model,
wizardData.finalAgent.memory,
)
setAppState(state => {
if (!wizardData.finalAgent) return state
const allAgents = state.agentDefinitions.allAgents.concat(
wizardData.finalAgent,
)
return {
...state,
agentDefinitions: {
...state.agentDefinitions,
activeAgents: getActiveAgentsFromList(allAgents),
allAgents,
},
}
};
});
if (openInEditor) {
const filePath = getNewAgentFilePath({
})
if (openInEditor) {
const filePath = getNewAgentFilePath({
source: wizardData.location!,
agentType: wizardData.finalAgent.agentType,
})
await editFileInEditor(filePath)
}
logEvent('tengu_agent_created', {
agent_type: wizardData.finalAgent.agentType,
generation_method: wizardData.wasGenerated ? 'generated' : 'manual',
source: wizardData.location!,
agentType: wizardData.finalAgent.agentType
});
await editFileInEditor(filePath);
tool_count: wizardData.finalAgent.tools?.length ?? 'all',
has_custom_model: !!wizardData.finalAgent.model,
has_custom_color: !!wizardData.finalAgent.color,
has_memory: !!wizardData.finalAgent.memory,
memory_scope: wizardData.finalAgent.memory ?? 'none',
...(openInEditor ? { opened_in_editor: true } : {}),
} as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS)
const message = openInEditor
? `Created agent: ${chalk.bold(wizardData.finalAgent.agentType)} and opened in editor. ` +
`If you made edits, restart to load the latest version.`
: `Created agent: ${chalk.bold(wizardData.finalAgent.agentType)}`
onComplete(message)
} catch (err) {
setSaveError(
err instanceof Error ? err.message : 'Failed to save agent',
)
}
logEvent('tengu_agent_created', {
agent_type: wizardData.finalAgent.agentType,
generation_method: wizardData.wasGenerated ? 'generated' : 'manual',
source: wizardData.location!,
tool_count: wizardData.finalAgent.tools?.length ?? 'all',
has_custom_model: !!wizardData.finalAgent.model,
has_custom_color: !!wizardData.finalAgent.color,
has_memory: !!wizardData.finalAgent.memory,
memory_scope: wizardData.finalAgent.memory ?? 'none',
...(openInEditor ? {
opened_in_editor: true
} : {})
} as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS);
const message = openInEditor ? `Created agent: ${chalk.bold(wizardData.finalAgent.agentType)} and opened in editor. ` + `If you made edits, restart to load the latest version.` : `Created agent: ${chalk.bold(wizardData.finalAgent.agentType)}`;
onComplete(message);
} catch (err) {
setSaveError(err instanceof Error ? err.message : 'Failed to save agent');
}
}, [wizardData, onComplete, setAppState]);
const handleSave = useCallback(() => saveAgent(false), [saveAgent]);
const handleSaveAndEdit = useCallback(() => saveAgent(true), [saveAgent]);
return <ConfirmStep tools={tools} existingAgents={existingAgents} onSave={handleSave} onSaveAndEdit={handleSaveAndEdit} error={saveError} />;
},
[wizardData, onComplete, setAppState],
)
const handleSave = useCallback(() => saveAgent(false), [saveAgent])
const handleSaveAndEdit = useCallback(() => saveAgent(true), [saveAgent])
return (
<ConfirmStep
tools={tools}
existingAgents={existingAgents}
onSave={handleSave}
onSaveAndEdit={handleSaveAndEdit}
error={saveError}
/>
)
}

View File

@@ -1,122 +1,94 @@
import { c as _c } from "react/compiler-runtime";
import React, { type ReactNode, useCallback, useState } from 'react';
import { Box, Text } from '../../../../ink.js';
import { useKeybinding } from '../../../../keybindings/useKeybinding.js';
import { editPromptInEditor } from '../../../../utils/promptEditor.js';
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
import { Byline } from '../../../design-system/Byline.js';
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
import TextInput from '../../../TextInput.js';
import { useWizard } from '../../../wizard/index.js';
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
import type { AgentWizardData } from '../types.js';
export function DescriptionStep() {
const $ = _c(18);
const {
goNext,
goBack,
updateWizardData,
wizardData
} = useWizard();
const [whenToUse, setWhenToUse] = useState(wizardData.whenToUse || "");
const [cursorOffset, setCursorOffset] = useState(whenToUse.length);
const [error, setError] = useState(null);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = {
context: "Settings"
};
$[0] = t0;
} else {
t0 = $[0];
import React, { type ReactNode, useCallback, useState } from 'react'
import { Box, Text } from '../../../../ink.js'
import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
import { editPromptInEditor } from '../../../../utils/promptEditor.js'
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
import { Byline } from '../../../design-system/Byline.js'
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
import TextInput from '../../../TextInput.js'
import { useWizard } from '../../../wizard/index.js'
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
import type { AgentWizardData } from '../types.js'
export function DescriptionStep(): ReactNode {
const { goNext, goBack, updateWizardData, wizardData } =
useWizard<AgentWizardData>()
const [whenToUse, setWhenToUse] = useState(wizardData.whenToUse || '')
const [cursorOffset, setCursorOffset] = useState(whenToUse.length)
const [error, setError] = useState<string | null>(null)
// Handle escape key - use Settings context so 'n' key doesn't cancel (allows typing 'n' in input)
useKeybinding('confirm:no', goBack, { context: 'Settings' })
const handleExternalEditor = useCallback(async () => {
const result = await editPromptInEditor(whenToUse)
if (result.content !== null) {
setWhenToUse(result.content)
setCursorOffset(result.content.length)
}
}, [whenToUse])
useKeybinding('chat:externalEditor', handleExternalEditor, {
context: 'Chat',
})
const handleSubmit = (value: string): void => {
const trimmedValue = value.trim()
if (!trimmedValue) {
setError('Description is required')
return
}
setError(null)
updateWizardData({ whenToUse: trimmedValue })
goNext()
}
useKeybinding("confirm:no", goBack, t0);
let t1;
if ($[1] !== whenToUse) {
t1 = async () => {
const result = await editPromptInEditor(whenToUse);
if (result.content !== null) {
setWhenToUse(result.content);
setCursorOffset(result.content.length);
return (
<WizardDialogLayout
subtitle="Description (tell Claude when to use this agent)"
footerText={
<Byline>
<KeyboardShortcutHint shortcut="Type" action="enter text" />
<KeyboardShortcutHint shortcut="Enter" action="continue" />
<ConfigurableShortcutHint
action="chat:externalEditor"
context="Chat"
fallback="ctrl+g"
description="open in editor"
/>
<ConfigurableShortcutHint
action="confirm:no"
context="Settings"
fallback="Esc"
description="go back"
/>
</Byline>
}
};
$[1] = whenToUse;
$[2] = t1;
} else {
t1 = $[2];
}
const handleExternalEditor = t1;
let t2;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t2 = {
context: "Chat"
};
$[3] = t2;
} else {
t2 = $[3];
}
useKeybinding("chat:externalEditor", handleExternalEditor, t2);
let t3;
if ($[4] !== goNext || $[5] !== updateWizardData) {
t3 = value => {
const trimmedValue = value.trim();
if (!trimmedValue) {
setError("Description is required");
return;
}
setError(null);
updateWizardData({
whenToUse: trimmedValue
});
goNext();
};
$[4] = goNext;
$[5] = updateWizardData;
$[6] = t3;
} else {
t3 = $[6];
}
const handleSubmit = t3;
let t4;
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
t4 = <Byline><KeyboardShortcutHint shortcut="Type" action="enter text" /><KeyboardShortcutHint shortcut="Enter" action="continue" /><ConfigurableShortcutHint action="chat:externalEditor" context="Chat" fallback="ctrl+g" description="open in editor" /><ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="go back" /></Byline>;
$[7] = t4;
} else {
t4 = $[7];
}
let t5;
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
t5 = <Text>When should Claude use this agent?</Text>;
$[8] = t5;
} else {
t5 = $[8];
}
let t6;
if ($[9] !== cursorOffset || $[10] !== handleSubmit || $[11] !== whenToUse) {
t6 = <Box marginTop={1}><TextInput value={whenToUse} onChange={setWhenToUse} onSubmit={handleSubmit} placeholder="e.g., use this agent after you're done writing code..." columns={80} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} focus={true} showCursor={true} /></Box>;
$[9] = cursorOffset;
$[10] = handleSubmit;
$[11] = whenToUse;
$[12] = t6;
} else {
t6 = $[12];
}
let t7;
if ($[13] !== error) {
t7 = error && <Box marginTop={1}><Text color="error">{error}</Text></Box>;
$[13] = error;
$[14] = t7;
} else {
t7 = $[14];
}
let t8;
if ($[15] !== t6 || $[16] !== t7) {
t8 = <WizardDialogLayout subtitle="Description (tell Claude when to use this agent)" footerText={t4}><Box flexDirection="column">{t5}{t6}{t7}</Box></WizardDialogLayout>;
$[15] = t6;
$[16] = t7;
$[17] = t8;
} else {
t8 = $[17];
}
return t8;
>
<Box flexDirection="column">
<Text>When should Claude use this agent?</Text>
<Box marginTop={1}>
<TextInput
value={whenToUse}
onChange={setWhenToUse}
onSubmit={handleSubmit}
placeholder="e.g., use this agent after you're done writing code..."
columns={80}
cursorOffset={cursorOffset}
onChangeCursorOffset={setCursorOffset}
focus
showCursor
/>
</Box>
{error && (
<Box marginTop={1}>
<Text color="error">{error}</Text>
</Box>
)}
</Box>
</WizardDialogLayout>
)
}

View File

@@ -1,58 +1,57 @@
import { APIUserAbortError } from '@anthropic-ai/sdk';
import React, { type ReactNode, useCallback, useRef, useState } from 'react';
import { useMainLoopModel } from '../../../../hooks/useMainLoopModel.js';
import { Box, Text } from '../../../../ink.js';
import { useKeybinding } from '../../../../keybindings/useKeybinding.js';
import { createAbortController } from '../../../../utils/abortController.js';
import { editPromptInEditor } from '../../../../utils/promptEditor.js';
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
import { Byline } from '../../../design-system/Byline.js';
import { Spinner } from '../../../Spinner.js';
import TextInput from '../../../TextInput.js';
import { useWizard } from '../../../wizard/index.js';
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
import { generateAgent } from '../../generateAgent.js';
import type { AgentWizardData } from '../types.js';
import { APIUserAbortError } from '@anthropic-ai/sdk'
import React, { type ReactNode, useCallback, useRef, useState } from 'react'
import { useMainLoopModel } from '../../../../hooks/useMainLoopModel.js'
import { Box, Text } from '../../../../ink.js'
import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
import { createAbortController } from '../../../../utils/abortController.js'
import { editPromptInEditor } from '../../../../utils/promptEditor.js'
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
import { Byline } from '../../../design-system/Byline.js'
import { Spinner } from '../../../Spinner.js'
import TextInput from '../../../TextInput.js'
import { useWizard } from '../../../wizard/index.js'
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
import { generateAgent } from '../../generateAgent.js'
import type { AgentWizardData } from '../types.js'
export function GenerateStep(): ReactNode {
const {
updateWizardData,
goBack,
goToStep,
wizardData
} = useWizard<AgentWizardData>();
const [prompt, setPrompt] = useState(wizardData.generationPrompt || '');
const [isGenerating, setIsGenerating] = useState(false);
const [error, setError] = useState<string | null>(null);
const [cursorOffset, setCursorOffset] = useState(prompt.length);
const model = useMainLoopModel();
const abortControllerRef = useRef<AbortController | null>(null);
const { updateWizardData, goBack, goToStep, wizardData } =
useWizard<AgentWizardData>()
const [prompt, setPrompt] = useState(wizardData.generationPrompt || '')
const [isGenerating, setIsGenerating] = useState(false)
const [error, setError] = useState<string | null>(null)
const [cursorOffset, setCursorOffset] = useState(prompt.length)
const model = useMainLoopModel()
const abortControllerRef = useRef<AbortController | null>(null)
// Cancel generation when escape pressed during generation
const handleCancelGeneration = useCallback(() => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
abortControllerRef.current = null;
setIsGenerating(false);
setError('Generation cancelled');
abortControllerRef.current.abort()
abortControllerRef.current = null
setIsGenerating(false)
setError('Generation cancelled')
}
}, []);
}, [])
// Use Settings context so 'n' key doesn't cancel (allows typing 'n' in prompt input)
useKeybinding('confirm:no', handleCancelGeneration, {
context: 'Settings',
isActive: isGenerating
});
isActive: isGenerating,
})
const handleExternalEditor = useCallback(async () => {
const result = await editPromptInEditor(prompt);
const result = await editPromptInEditor(prompt)
if (result.content !== null) {
setPrompt(result.content);
setCursorOffset(result.content.length);
setPrompt(result.content)
setCursorOffset(result.content.length)
}
}, [prompt]);
}, [prompt])
useKeybinding('chat:externalEditor', handleExternalEditor, {
context: 'Chat',
isActive: !isGenerating
});
isActive: !isGenerating,
})
// Go back when escape pressed while not generating
const handleGoBack = useCallback(() => {
@@ -62,81 +61,141 @@ export function GenerateStep(): ReactNode {
systemPrompt: '',
whenToUse: '',
generatedAgent: undefined,
wasGenerated: false
});
setPrompt('');
setError(null);
goBack();
}, [updateWizardData, goBack]);
wasGenerated: false,
})
setPrompt('')
setError(null)
goBack()
}, [updateWizardData, goBack])
// Use Settings context so 'n' key doesn't cancel (allows typing 'n' in prompt input)
useKeybinding('confirm:no', handleGoBack, {
context: 'Settings',
isActive: !isGenerating
});
isActive: !isGenerating,
})
const handleGenerate = async (): Promise<void> => {
const trimmedPrompt = prompt.trim();
const trimmedPrompt = prompt.trim()
if (!trimmedPrompt) {
setError('Please describe what the agent should do');
return;
setError('Please describe what the agent should do')
return
}
setError(null);
setIsGenerating(true);
setError(null)
setIsGenerating(true)
updateWizardData({
generationPrompt: trimmedPrompt,
isGenerating: true
});
isGenerating: true,
})
// Create abort controller for this generation
const controller = createAbortController();
abortControllerRef.current = controller;
const controller = createAbortController()
abortControllerRef.current = controller
try {
const generated = await generateAgent(trimmedPrompt, model, [], controller.signal);
const generated = await generateAgent(
trimmedPrompt,
model,
[],
controller.signal,
)
updateWizardData({
agentType: generated.identifier,
whenToUse: generated.whenToUse,
systemPrompt: generated.systemPrompt,
generatedAgent: generated,
isGenerating: false,
wasGenerated: true
});
wasGenerated: true,
})
// Skip directly to ToolsStep (index 6) - matching original flow
goToStep(6);
goToStep(6)
} catch (err) {
// Don't show error if it was cancelled (already set in escape handler)
if (err instanceof APIUserAbortError) {
// User cancelled - no error to show
} else if (err instanceof Error && !err.message.includes('No assistant message found')) {
setError(err.message || 'Failed to generate agent');
} else if (
err instanceof Error &&
!err.message.includes('No assistant message found')
) {
setError(err.message || 'Failed to generate agent')
}
updateWizardData({
isGenerating: false
});
updateWizardData({ isGenerating: false })
} finally {
setIsGenerating(false);
abortControllerRef.current = null;
setIsGenerating(false)
abortControllerRef.current = null
}
};
const subtitle = 'Describe what this agent should do and when it should be used (be comprehensive for best results)';
}
const subtitle =
'Describe what this agent should do and when it should be used (be comprehensive for best results)'
if (isGenerating) {
return <WizardDialogLayout subtitle={subtitle} footerText={<ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="cancel" />}>
return (
<WizardDialogLayout
subtitle={subtitle}
footerText={
<ConfigurableShortcutHint
action="confirm:no"
context="Settings"
fallback="Esc"
description="cancel"
/>
}
>
<Box flexDirection="row" alignItems="center">
<Spinner />
<Text color="suggestion"> Generating agent from description...</Text>
</Box>
</WizardDialogLayout>;
</WizardDialogLayout>
)
}
return <WizardDialogLayout subtitle={subtitle} footerText={<Byline>
<ConfigurableShortcutHint action="confirm:yes" context="Confirmation" fallback="Enter" description="submit" />
<ConfigurableShortcutHint action="chat:externalEditor" context="Chat" fallback="ctrl+g" description="open in editor" />
<ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="go back" />
</Byline>}>
return (
<WizardDialogLayout
subtitle={subtitle}
footerText={
<Byline>
<ConfigurableShortcutHint
action="confirm:yes"
context="Confirmation"
fallback="Enter"
description="submit"
/>
<ConfigurableShortcutHint
action="chat:externalEditor"
context="Chat"
fallback="ctrl+g"
description="open in editor"
/>
<ConfigurableShortcutHint
action="confirm:no"
context="Settings"
fallback="Esc"
description="go back"
/>
</Byline>
}
>
<Box flexDirection="column">
{error && <Box marginBottom={1}>
{error && (
<Box marginBottom={1}>
<Text color="error">{error}</Text>
</Box>}
<TextInput value={prompt} onChange={setPrompt} onSubmit={handleGenerate} placeholder="e.g., Help me write unit tests for my code..." columns={80} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} focus showCursor />
</Box>
)}
<TextInput
value={prompt}
onChange={setPrompt}
onSubmit={handleGenerate}
placeholder="e.g., Help me write unit tests for my code..."
columns={80}
cursorOffset={cursorOffset}
onChangeCursorOffset={setCursorOffset}
focus
showCursor
/>
</Box>
</WizardDialogLayout>;
</WizardDialogLayout>
)
}

View File

@@ -1,79 +1,55 @@
import { c as _c } from "react/compiler-runtime";
import React, { type ReactNode } from 'react';
import { Box } from '../../../../ink.js';
import type { SettingSource } from '../../../../utils/settings/constants.js';
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
import { Select } from '../../../CustomSelect/select.js';
import { Byline } from '../../../design-system/Byline.js';
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
import { useWizard } from '../../../wizard/index.js';
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
import type { AgentWizardData } from '../types.js';
export function LocationStep() {
const $ = _c(11);
const {
goNext,
updateWizardData,
cancel
} = useWizard();
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = {
label: "Project (.claude/agents/)",
value: "projectSettings" as SettingSource
};
$[0] = t0;
} else {
t0 = $[0];
}
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = [t0, {
label: "Personal (~/.claude/agents/)",
value: "userSettings" as SettingSource
}];
$[1] = t1;
} else {
t1 = $[1];
}
const locationOptions = t1;
let t2;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t2 = <Byline><KeyboardShortcutHint shortcut={"\u2191\u2193"} action="navigate" /><KeyboardShortcutHint shortcut="Enter" action="select" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="cancel" /></Byline>;
$[2] = t2;
} else {
t2 = $[2];
}
let t3;
if ($[3] !== goNext || $[4] !== updateWizardData) {
t3 = value => {
updateWizardData({
location: value as SettingSource
});
goNext();
};
$[3] = goNext;
$[4] = updateWizardData;
$[5] = t3;
} else {
t3 = $[5];
}
let t4;
if ($[6] !== cancel) {
t4 = () => cancel();
$[6] = cancel;
$[7] = t4;
} else {
t4 = $[7];
}
let t5;
if ($[8] !== t3 || $[9] !== t4) {
t5 = <WizardDialogLayout subtitle="Choose location" footerText={t2}><Box><Select key="location-select" options={locationOptions} onChange={t3} onCancel={t4} /></Box></WizardDialogLayout>;
$[8] = t3;
$[9] = t4;
$[10] = t5;
} else {
t5 = $[10];
}
return t5;
import React, { type ReactNode } from 'react'
import { Box } from '../../../../ink.js'
import type { SettingSource } from '../../../../utils/settings/constants.js'
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
import { Select } from '../../../CustomSelect/select.js'
import { Byline } from '../../../design-system/Byline.js'
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
import { useWizard } from '../../../wizard/index.js'
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
import type { AgentWizardData } from '../types.js'
export function LocationStep(): ReactNode {
const { goNext, updateWizardData, cancel } = useWizard<AgentWizardData>()
const locationOptions = [
{
label: 'Project (.claude/agents/)',
value: 'projectSettings' as SettingSource,
},
{
label: 'Personal (~/.claude/agents/)',
value: 'userSettings' as SettingSource,
},
]
return (
<WizardDialogLayout
subtitle="Choose location"
footerText={
<Byline>
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
<KeyboardShortcutHint shortcut="Enter" action="select" />
<ConfigurableShortcutHint
action="confirm:no"
context="Confirmation"
fallback="Esc"
description="cancel"
/>
</Byline>
}
>
<Box>
<Select
key="location-select"
options={locationOptions}
onChange={(value: string) => {
updateWizardData({ location: value as SettingSource })
goNext()
}}
onCancel={() => cancel()}
/>
</Box>
</WizardDialogLayout>
)
}

View File

@@ -1,112 +1,102 @@
import { c as _c } from "react/compiler-runtime";
import React, { type ReactNode } from 'react';
import { Box } from '../../../../ink.js';
import { useKeybinding } from '../../../../keybindings/useKeybinding.js';
import { isAutoMemoryEnabled } from '../../../../memdir/paths.js';
import { type AgentMemoryScope, loadAgentMemoryPrompt } from '../../../../tools/AgentTool/agentMemory.js';
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
import { Select } from '../../../CustomSelect/select.js';
import { Byline } from '../../../design-system/Byline.js';
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
import { useWizard } from '../../../wizard/index.js';
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
import type { AgentWizardData } from '../types.js';
import React, { type ReactNode } from 'react'
import { Box } from '../../../../ink.js'
import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
import { isAutoMemoryEnabled } from '../../../../memdir/paths.js'
import {
type AgentMemoryScope,
loadAgentMemoryPrompt,
} from '../../../../tools/AgentTool/agentMemory.js'
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
import { Select } from '../../../CustomSelect/select.js'
import { Byline } from '../../../design-system/Byline.js'
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
import { useWizard } from '../../../wizard/index.js'
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
import type { AgentWizardData } from '../types.js'
type MemoryOption = {
label: string;
value: AgentMemoryScope | 'none';
};
export function MemoryStep() {
const $ = _c(13);
const {
goNext,
goBack,
updateWizardData,
wizardData
} = useWizard();
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = {
context: "Confirmation"
};
$[0] = t0;
} else {
t0 = $[0];
}
useKeybinding("confirm:no", goBack, t0);
const isUserScope = wizardData.location === "userSettings";
let t1;
if ($[1] !== isUserScope) {
t1 = isUserScope ? [{
label: "User scope (~/.claude/agent-memory/) (Recommended)",
value: "user"
}, {
label: "None (no persistent memory)",
value: "none"
}, {
label: "Project scope (.claude/agent-memory/)",
value: "project"
}, {
label: "Local scope (.claude/agent-memory-local/)",
value: "local"
}] : [{
label: "Project scope (.claude/agent-memory/) (Recommended)",
value: "project"
}, {
label: "None (no persistent memory)",
value: "none"
}, {
label: "User scope (~/.claude/agent-memory/)",
value: "user"
}, {
label: "Local scope (.claude/agent-memory-local/)",
value: "local"
}];
$[1] = isUserScope;
$[2] = t1;
} else {
t1 = $[2];
}
const memoryOptions = t1;
let t2;
if ($[3] !== goNext || $[4] !== updateWizardData || $[5] !== wizardData.finalAgent || $[6] !== wizardData.systemPrompt) {
t2 = value => {
const memory = value === "none" ? undefined : value as AgentMemoryScope;
const agentType = wizardData.finalAgent?.agentType;
updateWizardData({
selectedMemory: memory,
finalAgent: wizardData.finalAgent ? {
...wizardData.finalAgent,
memory,
getSystemPrompt: isAutoMemoryEnabled() && memory && agentType ? () => wizardData.systemPrompt + "\n\n" + loadAgentMemoryPrompt(agentType, memory) : () => wizardData.systemPrompt
} : undefined
});
goNext();
};
$[3] = goNext;
$[4] = updateWizardData;
$[5] = wizardData.finalAgent;
$[6] = wizardData.systemPrompt;
$[7] = t2;
} else {
t2 = $[7];
}
const handleSelect = t2;
let t3;
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
t3 = <Byline><KeyboardShortcutHint shortcut={"\u2191\u2193"} action="navigate" /><KeyboardShortcutHint shortcut="Enter" action="select" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" /></Byline>;
$[8] = t3;
} else {
t3 = $[8];
}
let t4;
if ($[9] !== goBack || $[10] !== handleSelect || $[11] !== memoryOptions) {
t4 = <WizardDialogLayout subtitle="Configure agent memory" footerText={t3}><Box><Select key="memory-select" options={memoryOptions} onChange={handleSelect} onCancel={goBack} /></Box></WizardDialogLayout>;
$[9] = goBack;
$[10] = handleSelect;
$[11] = memoryOptions;
$[12] = t4;
} else {
t4 = $[12];
}
return t4;
label: string
value: AgentMemoryScope | 'none'
}
export function MemoryStep(): ReactNode {
const { goNext, goBack, updateWizardData, wizardData } =
useWizard<AgentWizardData>()
useKeybinding('confirm:no', goBack, { context: 'Confirmation' })
const isUserScope = wizardData.location === 'userSettings'
// Build options with the recommended default first, then alternatives
// The recommended scope matches the agent's location (project agent → project memory, user agent → user memory)
const memoryOptions: MemoryOption[] = isUserScope
? [
{
label: 'User scope (~/.claude/agent-memory/) (Recommended)',
value: 'user',
},
{ label: 'None (no persistent memory)', value: 'none' },
{ label: 'Project scope (.claude/agent-memory/)', value: 'project' },
{ label: 'Local scope (.claude/agent-memory-local/)', value: 'local' },
]
: [
{
label: 'Project scope (.claude/agent-memory/) (Recommended)',
value: 'project',
},
{ label: 'None (no persistent memory)', value: 'none' },
{ label: 'User scope (~/.claude/agent-memory/)', value: 'user' },
{ label: 'Local scope (.claude/agent-memory-local/)', value: 'local' },
]
const handleSelect = (value: string): void => {
const memory = value === 'none' ? undefined : (value as AgentMemoryScope)
const agentType = wizardData.finalAgent?.agentType
updateWizardData({
selectedMemory: memory,
// Update finalAgent with memory and rewire getSystemPrompt to include memory loading.
// Explicitly set memory (not conditional spread) so selecting 'none' after going back clears it.
finalAgent: wizardData.finalAgent
? {
...wizardData.finalAgent,
memory,
getSystemPrompt:
isAutoMemoryEnabled() && memory && agentType
? () =>
wizardData.systemPrompt! +
'\n\n' +
loadAgentMemoryPrompt(agentType, memory)
: () => wizardData.systemPrompt!,
}
: undefined,
})
goNext()
}
return (
<WizardDialogLayout
subtitle="Configure agent memory"
footerText={
<Byline>
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
<KeyboardShortcutHint shortcut="Enter" action="select" />
<ConfigurableShortcutHint
action="confirm:no"
context="Confirmation"
fallback="Esc"
description="go back"
/>
</Byline>
}
>
<Box>
<Select
key="memory-select"
options={memoryOptions}
onChange={handleSelect}
onCancel={goBack}
/>
</Box>
</WizardDialogLayout>
)
}

View File

@@ -1,79 +1,65 @@
import { c as _c } from "react/compiler-runtime";
import React, { type ReactNode } from 'react';
import { Box } from '../../../../ink.js';
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
import { Select } from '../../../CustomSelect/select.js';
import { Byline } from '../../../design-system/Byline.js';
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
import { useWizard } from '../../../wizard/index.js';
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
import type { AgentWizardData } from '../types.js';
export function MethodStep() {
const $ = _c(11);
const {
goNext,
goBack,
updateWizardData,
goToStep
} = useWizard();
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = [{
label: "Generate with Claude (recommended)",
value: "generate"
}, {
label: "Manual configuration",
value: "manual"
}];
$[0] = t0;
} else {
t0 = $[0];
}
const methodOptions = t0;
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <Byline><KeyboardShortcutHint shortcut={"\u2191\u2193"} action="navigate" /><KeyboardShortcutHint shortcut="Enter" action="select" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" /></Byline>;
$[1] = t1;
} else {
t1 = $[1];
}
let t2;
if ($[2] !== goNext || $[3] !== goToStep || $[4] !== updateWizardData) {
t2 = value => {
const method = value as 'generate' | 'manual';
updateWizardData({
method,
wasGenerated: method === "generate"
});
if (method === "generate") {
goNext();
} else {
goToStep(3);
import React, { type ReactNode } from 'react'
import { Box } from '../../../../ink.js'
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
import { Select } from '../../../CustomSelect/select.js'
import { Byline } from '../../../design-system/Byline.js'
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
import { useWizard } from '../../../wizard/index.js'
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
import type { AgentWizardData } from '../types.js'
export function MethodStep(): ReactNode {
const { goNext, goBack, updateWizardData, goToStep } =
useWizard<AgentWizardData>()
const methodOptions = [
{
label: 'Generate with Claude (recommended)',
value: 'generate',
},
{
label: 'Manual configuration',
value: 'manual',
},
]
return (
<WizardDialogLayout
subtitle="Creation method"
footerText={
<Byline>
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
<KeyboardShortcutHint shortcut="Enter" action="select" />
<ConfigurableShortcutHint
action="confirm:no"
context="Confirmation"
fallback="Esc"
description="go back"
/>
</Byline>
}
};
$[2] = goNext;
$[3] = goToStep;
$[4] = updateWizardData;
$[5] = t2;
} else {
t2 = $[5];
}
let t3;
if ($[6] !== goBack) {
t3 = () => goBack();
$[6] = goBack;
$[7] = t3;
} else {
t3 = $[7];
}
let t4;
if ($[8] !== t2 || $[9] !== t3) {
t4 = <WizardDialogLayout subtitle="Creation method" footerText={t1}><Box><Select key="method-select" options={methodOptions} onChange={t2} onCancel={t3} /></Box></WizardDialogLayout>;
$[8] = t2;
$[9] = t3;
$[10] = t4;
} else {
t4 = $[10];
}
return t4;
>
<Box>
<Select
key="method-select"
options={methodOptions}
onChange={(value: string) => {
const method = value as 'generate' | 'manual'
updateWizardData({
method,
wasGenerated: method === 'generate',
})
// Dynamic navigation based on method
if (method === 'generate') {
goNext() // Go to GenerateStep (index 2)
} else {
goToStep(3) // Skip to TypeStep (index 3)
}
}}
onCancel={() => goBack()}
/>
</Box>
</WizardDialogLayout>
)
}

View File

@@ -1,51 +1,42 @@
import { c as _c } from "react/compiler-runtime";
import React, { type ReactNode } from 'react';
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
import { Byline } from '../../../design-system/Byline.js';
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
import { useWizard } from '../../../wizard/index.js';
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
import { ModelSelector } from '../../ModelSelector.js';
import type { AgentWizardData } from '../types.js';
export function ModelStep() {
const $ = _c(8);
const {
goNext,
goBack,
updateWizardData,
wizardData
} = useWizard();
let t0;
if ($[0] !== goNext || $[1] !== updateWizardData) {
t0 = model => {
updateWizardData({
selectedModel: model
});
goNext();
};
$[0] = goNext;
$[1] = updateWizardData;
$[2] = t0;
} else {
t0 = $[2];
import React, { type ReactNode } from 'react'
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
import { Byline } from '../../../design-system/Byline.js'
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
import { useWizard } from '../../../wizard/index.js'
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
import { ModelSelector } from '../../ModelSelector.js'
import type { AgentWizardData } from '../types.js'
export function ModelStep(): ReactNode {
const { goNext, goBack, updateWizardData, wizardData } =
useWizard<AgentWizardData>()
const handleComplete = (model?: string): void => {
updateWizardData({ selectedModel: model })
goNext()
}
const handleComplete = t0;
let t1;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <Byline><KeyboardShortcutHint shortcut={"\u2191\u2193"} action="navigate" /><KeyboardShortcutHint shortcut="Enter" action="select" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" /></Byline>;
$[3] = t1;
} else {
t1 = $[3];
}
let t2;
if ($[4] !== goBack || $[5] !== handleComplete || $[6] !== wizardData.selectedModel) {
t2 = <WizardDialogLayout subtitle="Select model" footerText={t1}><ModelSelector initialModel={wizardData.selectedModel} onComplete={handleComplete} onCancel={goBack} /></WizardDialogLayout>;
$[4] = goBack;
$[5] = handleComplete;
$[6] = wizardData.selectedModel;
$[7] = t2;
} else {
t2 = $[7];
}
return t2;
return (
<WizardDialogLayout
subtitle="Select model"
footerText={
<Byline>
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
<KeyboardShortcutHint shortcut="Enter" action="select" />
<ConfigurableShortcutHint
action="confirm:no"
context="Confirmation"
fallback="Esc"
description="go back"
/>
</Byline>
}
>
<ModelSelector
initialModel={wizardData.selectedModel}
onComplete={handleComplete}
onCancel={goBack}
/>
</WizardDialogLayout>
)
}

View File

@@ -1,127 +1,97 @@
import { c as _c } from "react/compiler-runtime";
import React, { type ReactNode, useCallback, useState } from 'react';
import { Box, Text } from '../../../../ink.js';
import { useKeybinding } from '../../../../keybindings/useKeybinding.js';
import { editPromptInEditor } from '../../../../utils/promptEditor.js';
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
import { Byline } from '../../../design-system/Byline.js';
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
import TextInput from '../../../TextInput.js';
import { useWizard } from '../../../wizard/index.js';
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
import type { AgentWizardData } from '../types.js';
export function PromptStep() {
const $ = _c(20);
const {
goNext,
goBack,
updateWizardData,
wizardData
} = useWizard();
const [systemPrompt, setSystemPrompt] = useState(wizardData.systemPrompt || "");
const [cursorOffset, setCursorOffset] = useState(systemPrompt.length);
const [error, setError] = useState(null);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = {
context: "Settings"
};
$[0] = t0;
} else {
t0 = $[0];
import React, { type ReactNode, useCallback, useState } from 'react'
import { Box, Text } from '../../../../ink.js'
import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
import { editPromptInEditor } from '../../../../utils/promptEditor.js'
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
import { Byline } from '../../../design-system/Byline.js'
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
import TextInput from '../../../TextInput.js'
import { useWizard } from '../../../wizard/index.js'
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
import type { AgentWizardData } from '../types.js'
export function PromptStep(): ReactNode {
const { goNext, goBack, updateWizardData, wizardData } =
useWizard<AgentWizardData>()
const [systemPrompt, setSystemPrompt] = useState(
wizardData.systemPrompt || '',
)
const [cursorOffset, setCursorOffset] = useState(systemPrompt.length)
const [error, setError] = useState<string | null>(null)
// Handle escape key - use Settings context so 'n' key doesn't cancel (allows typing 'n' in input)
useKeybinding('confirm:no', goBack, { context: 'Settings' })
const handleExternalEditor = useCallback(async () => {
const result = await editPromptInEditor(systemPrompt)
if (result.content !== null) {
setSystemPrompt(result.content)
setCursorOffset(result.content.length)
}
}, [systemPrompt])
useKeybinding('chat:externalEditor', handleExternalEditor, {
context: 'Chat',
})
const handleSubmit = (): void => {
const trimmedPrompt = systemPrompt.trim()
if (!trimmedPrompt) {
setError('System prompt is required')
return
}
setError(null)
updateWizardData({ systemPrompt: trimmedPrompt })
goNext()
}
useKeybinding("confirm:no", goBack, t0);
let t1;
if ($[1] !== systemPrompt) {
t1 = async () => {
const result = await editPromptInEditor(systemPrompt);
if (result.content !== null) {
setSystemPrompt(result.content);
setCursorOffset(result.content.length);
return (
<WizardDialogLayout
subtitle="System prompt"
footerText={
<Byline>
<KeyboardShortcutHint shortcut="Type" action="enter text" />
<KeyboardShortcutHint shortcut="Enter" action="continue" />
<ConfigurableShortcutHint
action="chat:externalEditor"
context="Chat"
fallback="ctrl+g"
description="open in editor"
/>
<ConfigurableShortcutHint
action="confirm:no"
context="Settings"
fallback="Esc"
description="go back"
/>
</Byline>
}
};
$[1] = systemPrompt;
$[2] = t1;
} else {
t1 = $[2];
}
const handleExternalEditor = t1;
let t2;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t2 = {
context: "Chat"
};
$[3] = t2;
} else {
t2 = $[3];
}
useKeybinding("chat:externalEditor", handleExternalEditor, t2);
let t3;
if ($[4] !== goNext || $[5] !== systemPrompt || $[6] !== updateWizardData) {
t3 = () => {
const trimmedPrompt = systemPrompt.trim();
if (!trimmedPrompt) {
setError("System prompt is required");
return;
}
setError(null);
updateWizardData({
systemPrompt: trimmedPrompt
});
goNext();
};
$[4] = goNext;
$[5] = systemPrompt;
$[6] = updateWizardData;
$[7] = t3;
} else {
t3 = $[7];
}
const handleSubmit = t3;
let t4;
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
t4 = <Byline><KeyboardShortcutHint shortcut="Type" action="enter text" /><KeyboardShortcutHint shortcut="Enter" action="continue" /><ConfigurableShortcutHint action="chat:externalEditor" context="Chat" fallback="ctrl+g" description="open in editor" /><ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="go back" /></Byline>;
$[8] = t4;
} else {
t4 = $[8];
}
let t5;
let t6;
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
t5 = <Text>Enter the system prompt for your agent:</Text>;
t6 = <Text dimColor={true}>Be comprehensive for best results</Text>;
$[9] = t5;
$[10] = t6;
} else {
t5 = $[9];
t6 = $[10];
}
let t7;
if ($[11] !== cursorOffset || $[12] !== handleSubmit || $[13] !== systemPrompt) {
t7 = <Box marginTop={1}><TextInput value={systemPrompt} onChange={setSystemPrompt} onSubmit={handleSubmit} placeholder="You are a helpful code reviewer who..." columns={80} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} focus={true} showCursor={true} /></Box>;
$[11] = cursorOffset;
$[12] = handleSubmit;
$[13] = systemPrompt;
$[14] = t7;
} else {
t7 = $[14];
}
let t8;
if ($[15] !== error) {
t8 = error && <Box marginTop={1}><Text color="error">{error}</Text></Box>;
$[15] = error;
$[16] = t8;
} else {
t8 = $[16];
}
let t9;
if ($[17] !== t7 || $[18] !== t8) {
t9 = <WizardDialogLayout subtitle="System prompt" footerText={t4}><Box flexDirection="column">{t5}{t6}{t7}{t8}</Box></WizardDialogLayout>;
$[17] = t7;
$[18] = t8;
$[19] = t9;
} else {
t9 = $[19];
}
return t9;
>
<Box flexDirection="column">
<Text>Enter the system prompt for your agent:</Text>
<Text dimColor>Be comprehensive for best results</Text>
<Box marginTop={1}>
<TextInput
value={systemPrompt}
onChange={setSystemPrompt}
onSubmit={handleSubmit}
placeholder="You are a helpful code reviewer who..."
columns={80}
cursorOffset={cursorOffset}
onChangeCursorOffset={setCursorOffset}
focus
showCursor
/>
</Box>
{error && (
<Box marginTop={1}>
<Text color="error">{error}</Text>
</Box>
)}
</Box>
</WizardDialogLayout>
)
}

View File

@@ -1,60 +1,52 @@
import { c as _c } from "react/compiler-runtime";
import React, { type ReactNode } from 'react';
import type { Tools } from '../../../../Tool.js';
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
import { Byline } from '../../../design-system/Byline.js';
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
import { useWizard } from '../../../wizard/index.js';
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
import { ToolSelector } from '../../ToolSelector.js';
import type { AgentWizardData } from '../types.js';
import React, { type ReactNode } from 'react'
import type { Tools } from '../../../../Tool.js'
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
import { Byline } from '../../../design-system/Byline.js'
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
import { useWizard } from '../../../wizard/index.js'
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
import { ToolSelector } from '../../ToolSelector.js'
import type { AgentWizardData } from '../types.js'
type Props = {
tools: Tools;
};
export function ToolsStep(t0) {
const $ = _c(9);
const {
tools
} = t0;
const {
goNext,
goBack,
updateWizardData,
wizardData
} = useWizard();
let t1;
if ($[0] !== goNext || $[1] !== updateWizardData) {
t1 = selectedTools => {
updateWizardData({
selectedTools
});
goNext();
};
$[0] = goNext;
$[1] = updateWizardData;
$[2] = t1;
} else {
t1 = $[2];
}
const handleComplete = t1;
const initialTools = wizardData.selectedTools;
let t2;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t2 = <Byline><KeyboardShortcutHint shortcut="Enter" action="toggle selection" /><KeyboardShortcutHint shortcut={"\u2191\u2193"} action="navigate" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" /></Byline>;
$[3] = t2;
} else {
t2 = $[3];
}
let t3;
if ($[4] !== goBack || $[5] !== handleComplete || $[6] !== initialTools || $[7] !== tools) {
t3 = <WizardDialogLayout subtitle="Select tools" footerText={t2}><ToolSelector tools={tools} initialTools={initialTools} onComplete={handleComplete} onCancel={goBack} /></WizardDialogLayout>;
$[4] = goBack;
$[5] = handleComplete;
$[6] = initialTools;
$[7] = tools;
$[8] = t3;
} else {
t3 = $[8];
}
return t3;
tools: Tools
}
export function ToolsStep({ tools }: Props): ReactNode {
const { goNext, goBack, updateWizardData, wizardData } =
useWizard<AgentWizardData>()
const handleComplete = (selectedTools: string[] | undefined): void => {
updateWizardData({ selectedTools })
goNext()
}
// Pass through undefined to preserve "all tools" semantic
// ToolSelector will expand it internally for display purposes
const initialTools = wizardData.selectedTools
return (
<WizardDialogLayout
subtitle="Select tools"
footerText={
<Byline>
<KeyboardShortcutHint shortcut="Enter" action="toggle selection" />
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
<ConfigurableShortcutHint
action="confirm:no"
context="Confirmation"
fallback="Esc"
description="go back"
/>
</Byline>
}
>
<ToolSelector
tools={tools}
initialTools={initialTools}
onComplete={handleComplete}
onCancel={goBack}
/>
</WizardDialogLayout>
)
}

View File

@@ -1,102 +1,83 @@
import { c as _c } from "react/compiler-runtime";
import React, { type ReactNode, useState } from 'react';
import { Box, Text } from '../../../../ink.js';
import { useKeybinding } from '../../../../keybindings/useKeybinding.js';
import type { AgentDefinition } from '../../../../tools/AgentTool/loadAgentsDir.js';
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js';
import { Byline } from '../../../design-system/Byline.js';
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js';
import TextInput from '../../../TextInput.js';
import { useWizard } from '../../../wizard/index.js';
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js';
import { validateAgentType } from '../../validateAgent.js';
import type { AgentWizardData } from '../types.js';
import React, { type ReactNode, useState } from 'react'
import { Box, Text } from '../../../../ink.js'
import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
import type { AgentDefinition } from '../../../../tools/AgentTool/loadAgentsDir.js'
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
import { Byline } from '../../../design-system/Byline.js'
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
import TextInput from '../../../TextInput.js'
import { useWizard } from '../../../wizard/index.js'
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
import { validateAgentType } from '../../validateAgent.js'
import type { AgentWizardData } from '../types.js'
type Props = {
existingAgents: AgentDefinition[];
};
export function TypeStep(_props) {
const $ = _c(15);
const {
goNext,
goBack,
updateWizardData,
wizardData
} = useWizard();
const [agentType, setAgentType] = useState(wizardData.agentType || "");
const [error, setError] = useState(null);
const [cursorOffset, setCursorOffset] = useState(agentType.length);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = {
context: "Settings"
};
$[0] = t0;
} else {
t0 = $[0];
}
useKeybinding("confirm:no", goBack, t0);
let t1;
if ($[1] !== goNext || $[2] !== updateWizardData) {
t1 = value => {
const trimmedValue = value.trim();
const validationError = validateAgentType(trimmedValue);
if (validationError) {
setError(validationError);
return;
}
setError(null);
updateWizardData({
agentType: trimmedValue
});
goNext();
};
$[1] = goNext;
$[2] = updateWizardData;
$[3] = t1;
} else {
t1 = $[3];
}
const handleSubmit = t1;
let t2;
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
t2 = <Byline><KeyboardShortcutHint shortcut="Type" action="enter text" /><KeyboardShortcutHint shortcut="Enter" action="continue" /><ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="go back" /></Byline>;
$[4] = t2;
} else {
t2 = $[4];
}
let t3;
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
t3 = <Text>Enter a unique identifier for your agent:</Text>;
$[5] = t3;
} else {
t3 = $[5];
}
let t4;
if ($[6] !== agentType || $[7] !== cursorOffset || $[8] !== handleSubmit) {
t4 = <Box marginTop={1}><TextInput value={agentType} onChange={setAgentType} onSubmit={handleSubmit} placeholder="e.g., test-runner, tech-lead, etc" columns={60} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} focus={true} showCursor={true} /></Box>;
$[6] = agentType;
$[7] = cursorOffset;
$[8] = handleSubmit;
$[9] = t4;
} else {
t4 = $[9];
}
let t5;
if ($[10] !== error) {
t5 = error && <Box marginTop={1}><Text color="error">{error}</Text></Box>;
$[10] = error;
$[11] = t5;
} else {
t5 = $[11];
}
let t6;
if ($[12] !== t4 || $[13] !== t5) {
t6 = <WizardDialogLayout subtitle="Agent type (identifier)" footerText={t2}><Box flexDirection="column">{t3}{t4}{t5}</Box></WizardDialogLayout>;
$[12] = t4;
$[13] = t5;
$[14] = t6;
} else {
t6 = $[14];
}
return t6;
existingAgents: AgentDefinition[]
}
export function TypeStep(_props: Props): ReactNode {
const { goNext, goBack, updateWizardData, wizardData } =
useWizard<AgentWizardData>()
const [agentType, setAgentType] = useState(wizardData.agentType || '')
const [error, setError] = useState<string | null>(null)
const [cursorOffset, setCursorOffset] = useState(agentType.length)
// Handle escape key - Go back to MethodStep
// Use Settings context so 'n' key doesn't cancel (allows typing 'n' in input)
useKeybinding('confirm:no', goBack, { context: 'Settings' })
const handleSubmit = (value: string): void => {
const trimmedValue = value.trim()
const validationError = validateAgentType(trimmedValue)
if (validationError) {
setError(validationError)
return
}
setError(null)
updateWizardData({ agentType: trimmedValue })
goNext()
}
return (
<WizardDialogLayout
subtitle="Agent type (identifier)"
footerText={
<Byline>
<KeyboardShortcutHint shortcut="Type" action="enter text" />
<KeyboardShortcutHint shortcut="Enter" action="continue" />
<ConfigurableShortcutHint
action="confirm:no"
context="Settings"
fallback="Esc"
description="go back"
/>
</Byline>
}
>
<Box flexDirection="column">
<Text>Enter a unique identifier for your agent:</Text>
<Box marginTop={1}>
<TextInput
value={agentType}
onChange={setAgentType}
onSubmit={handleSubmit}
placeholder="e.g., test-runner, tech-lead, etc"
columns={60}
cursorOffset={cursorOffset}
onChangeCursorOffset={setCursorOffset}
focus
showCursor
/>
</Box>
{error && (
<Box marginTop={1}>
<Text color="error">{error}</Text>
</Box>
)}
</Box>
</WizardDialogLayout>
)
}

View File

@@ -1,10 +1,10 @@
import { c as _c } from "react/compiler-runtime";
import React, { Children, isValidElement } from 'react';
import { Text } from '../../ink.js';
import React, { Children, isValidElement } from 'react'
import { Text } from '../../ink.js'
type Props = {
/** The items to join with a middot separator */
children: React.ReactNode;
};
children: React.ReactNode
}
/**
* Joins children with a middot separator (" · ") for inline metadata display.
@@ -34,43 +34,24 @@ type Props = {
* </Text>
*
*/
export function Byline(t0) {
const $ = _c(5);
const {
children
} = t0;
let t1;
let t2;
if ($[0] !== children) {
t2 = Symbol.for("react.early_return_sentinel");
bb0: {
const validChildren = Children.toArray(children);
if (validChildren.length === 0) {
t2 = null;
break bb0;
}
t1 = validChildren.map(_temp);
}
$[0] = children;
$[1] = t1;
$[2] = t2;
} else {
t1 = $[1];
t2 = $[2];
export function Byline({ children }: Props): React.ReactNode {
// Children.toArray already filters out null, undefined, and booleans
const validChildren = Children.toArray(children)
if (validChildren.length === 0) {
return null
}
if (t2 !== Symbol.for("react.early_return_sentinel")) {
return t2;
}
let t3;
if ($[3] !== t1) {
t3 = <>{t1}</>;
$[3] = t1;
$[4] = t3;
} else {
t3 = $[4];
}
return t3;
}
function _temp(child, index) {
return <React.Fragment key={isValidElement(child) ? child.key ?? index : index}>{index > 0 && <Text dimColor={true}> · </Text>}{child}</React.Fragment>;
return (
<>
{validChildren.map((child, index) => (
<React.Fragment
key={isValidElement(child) ? (child.key ?? index) : index}
>
{index > 0 && <Text dimColor> · </Text>}
{child}
</React.Fragment>
))}
</>
)
}

View File

@@ -1,23 +1,26 @@
import { c as _c } from "react/compiler-runtime";
import React from 'react';
import { type ExitState, useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js';
import { Box, Text } from '../../ink.js';
import { useKeybinding } from '../../keybindings/useKeybinding.js';
import type { Theme } from '../../utils/theme.js';
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
import { Byline } from './Byline.js';
import { KeyboardShortcutHint } from './KeyboardShortcutHint.js';
import { Pane } from './Pane.js';
import React from 'react'
import {
type ExitState,
useExitOnCtrlCDWithKeybindings,
} from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
import { Box, Text } from '../../ink.js'
import { useKeybinding } from '../../keybindings/useKeybinding.js'
import type { Theme } from '../../utils/theme.js'
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
import { Byline } from './Byline.js'
import { KeyboardShortcutHint } from './KeyboardShortcutHint.js'
import { Pane } from './Pane.js'
type DialogProps = {
title: React.ReactNode;
subtitle?: React.ReactNode;
children: React.ReactNode;
onCancel: () => void;
color?: keyof Theme;
hideInputGuide?: boolean;
hideBorder?: boolean;
title: React.ReactNode
subtitle?: React.ReactNode
children: React.ReactNode
onCancel: () => void
color?: keyof Theme
hideInputGuide?: boolean
hideBorder?: boolean
/** Custom input guide content. Receives exitState for Ctrl+C/D pending display. */
inputGuide?: (exitState: ExitState) => React.ReactNode;
inputGuide?: (exitState: ExitState) => React.ReactNode
/**
* Controls whether Dialog's built-in confirm:no (Esc/n) and app:exit/interrupt
* (Ctrl-C/D) keybindings are active. Set to `false` while an embedded text
@@ -25,113 +28,73 @@ type DialogProps = {
* consumed by Dialog. TextInput has its own ctrl+c/d handlers (cancel on
* press, delete-forward on ctrl+d with text). Defaults to `true`.
*/
isCancelActive?: boolean;
};
export function Dialog(t0) {
const $ = _c(27);
const {
title,
subtitle,
children,
onCancel,
color: t1,
hideInputGuide,
hideBorder,
inputGuide,
isCancelActive: t2
} = t0;
const color = t1 === undefined ? "permission" : t1;
const isCancelActive = t2 === undefined ? true : t2;
const exitState = useExitOnCtrlCDWithKeybindings(undefined, undefined, isCancelActive);
let t3;
if ($[0] !== isCancelActive) {
t3 = {
context: "Confirmation",
isActive: isCancelActive
};
$[0] = isCancelActive;
$[1] = t3;
} else {
t3 = $[1];
}
useKeybinding("confirm:no", onCancel, t3);
let t4;
if ($[2] !== exitState.keyName || $[3] !== exitState.pending) {
t4 = exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline><KeyboardShortcutHint shortcut="Enter" action="confirm" /><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="cancel" /></Byline>;
$[2] = exitState.keyName;
$[3] = exitState.pending;
$[4] = t4;
} else {
t4 = $[4];
}
const defaultInputGuide = t4;
let t5;
if ($[5] !== color || $[6] !== title) {
t5 = <Text bold={true} color={color}>{title}</Text>;
$[5] = color;
$[6] = title;
$[7] = t5;
} else {
t5 = $[7];
}
let t6;
if ($[8] !== subtitle) {
t6 = subtitle && <Text dimColor={true}>{subtitle}</Text>;
$[8] = subtitle;
$[9] = t6;
} else {
t6 = $[9];
}
let t7;
if ($[10] !== t5 || $[11] !== t6) {
t7 = <Box flexDirection="column">{t5}{t6}</Box>;
$[10] = t5;
$[11] = t6;
$[12] = t7;
} else {
t7 = $[12];
}
let t8;
if ($[13] !== children || $[14] !== t7) {
t8 = <Box flexDirection="column" gap={1}>{t7}{children}</Box>;
$[13] = children;
$[14] = t7;
$[15] = t8;
} else {
t8 = $[15];
}
let t9;
if ($[16] !== defaultInputGuide || $[17] !== exitState || $[18] !== hideInputGuide || $[19] !== inputGuide) {
t9 = !hideInputGuide && <Box marginTop={1}><Text dimColor={true} italic={true}>{inputGuide ? inputGuide(exitState) : defaultInputGuide}</Text></Box>;
$[16] = defaultInputGuide;
$[17] = exitState;
$[18] = hideInputGuide;
$[19] = inputGuide;
$[20] = t9;
} else {
t9 = $[20];
}
let t10;
if ($[21] !== t8 || $[22] !== t9) {
t10 = <>{t8}{t9}</>;
$[21] = t8;
$[22] = t9;
$[23] = t10;
} else {
t10 = $[23];
}
const content = t10;
if (hideBorder) {
return content;
}
let t11;
if ($[24] !== color || $[25] !== content) {
t11 = <Pane color={color}>{content}</Pane>;
$[24] = color;
$[25] = content;
$[26] = t11;
} else {
t11 = $[26];
}
return t11;
isCancelActive?: boolean
}
export function Dialog({
title,
subtitle,
children,
onCancel,
color = 'permission',
hideInputGuide,
hideBorder,
inputGuide,
isCancelActive = true,
}: DialogProps): React.ReactNode {
const exitState = useExitOnCtrlCDWithKeybindings(
undefined,
undefined,
isCancelActive,
)
// Use configurable keybinding for ESC to cancel.
// isCancelActive lets consumers (e.g. ElicitationDialog) disable this while
// an embedded TextInput is focused, so that keys like 'n' reach the field
// instead of being consumed here.
useKeybinding('confirm:no', onCancel, {
context: 'Confirmation',
isActive: isCancelActive,
})
const defaultInputGuide = exitState.pending ? (
<Text>Press {exitState.keyName} again to exit</Text>
) : (
<Byline>
<KeyboardShortcutHint shortcut="Enter" action="confirm" />
<ConfigurableShortcutHint
action="confirm:no"
context="Confirmation"
fallback="Esc"
description="cancel"
/>
</Byline>
)
const content = (
<>
<Box flexDirection="column" gap={1}>
<Box flexDirection="column">
<Text bold color={color}>
{title}
</Text>
{subtitle && <Text dimColor>{subtitle}</Text>}
</Box>
{children}
</Box>
{!hideInputGuide && (
<Box marginTop={1}>
<Text dimColor italic>
{inputGuide ? inputGuide(exitState) : defaultInputGuide}
</Text>
</Box>
)}
</>
)
if (hideBorder) {
return content
}
return <Pane color={color}>{content}</Pane>
}

View File

@@ -1,33 +1,33 @@
import { c as _c } from "react/compiler-runtime";
import React from 'react';
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
import { stringWidth } from '../../ink/stringWidth.js';
import { Ansi, Text } from '../../ink.js';
import type { Theme } from '../../utils/theme.js';
import React from 'react'
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
import { stringWidth } from '../../ink/stringWidth.js'
import { Ansi, Text } from '../../ink.js'
import type { Theme } from '../../utils/theme.js'
type DividerProps = {
/**
* Width of the divider in characters.
* Defaults to terminal width.
*/
width?: number;
width?: number
/**
* Theme color for the divider.
* If not provided, dimColor is used.
*/
color?: keyof Theme;
color?: keyof Theme
/**
* Character to use for the divider line.
* @default '─'
*/
char?: string;
char?: string
/**
* Padding to subtract from the width (e.g., for indentation).
* @default 0
*/
padding?: number;
padding?: number
/**
* Title shown in the middle of the divider.
@@ -37,8 +37,8 @@ type DividerProps = {
* // ─────────── Title ───────────
* <Divider title="Title" />
*/
title?: string;
};
title?: string
}
/**
* A horizontal divider line.
@@ -63,86 +63,35 @@ type DividerProps = {
* // With centered title
* <Divider title="3 new messages" />
*/
export function Divider(t0) {
const $ = _c(21);
const {
width,
color,
char: t1,
padding: t2,
title
} = t0;
const char = t1 === undefined ? "\u2500" : t1;
const padding = t2 === undefined ? 0 : t2;
const {
columns: terminalWidth
} = useTerminalSize();
const effectiveWidth = Math.max(0, (width ?? terminalWidth) - padding);
export function Divider({
width,
color,
char = '─',
padding = 0,
title,
}: DividerProps): React.ReactNode {
const { columns: terminalWidth } = useTerminalSize()
const effectiveWidth = Math.max(0, (width ?? terminalWidth) - padding)
if (title) {
const titleWidth = stringWidth(title) + 2;
const sideWidth = Math.max(0, effectiveWidth - titleWidth);
const leftWidth = Math.floor(sideWidth / 2);
const rightWidth = sideWidth - leftWidth;
const t3 = !color;
let t4;
if ($[0] !== char || $[1] !== leftWidth) {
t4 = char.repeat(leftWidth);
$[0] = char;
$[1] = leftWidth;
$[2] = t4;
} else {
t4 = $[2];
}
let t5;
if ($[3] !== title) {
t5 = <Text dimColor={true}><Ansi>{title}</Ansi></Text>;
$[3] = title;
$[4] = t5;
} else {
t5 = $[4];
}
let t6;
if ($[5] !== char || $[6] !== rightWidth) {
t6 = char.repeat(rightWidth);
$[5] = char;
$[6] = rightWidth;
$[7] = t6;
} else {
t6 = $[7];
}
let t7;
if ($[8] !== color || $[9] !== t3 || $[10] !== t4 || $[11] !== t5 || $[12] !== t6) {
t7 = <Text color={color} dimColor={t3}>{t4}{" "}{t5}{" "}{t6}</Text>;
$[8] = color;
$[9] = t3;
$[10] = t4;
$[11] = t5;
$[12] = t6;
$[13] = t7;
} else {
t7 = $[13];
}
return t7;
const titleWidth = stringWidth(title) + 2 // +2 for spaces around title
const sideWidth = Math.max(0, effectiveWidth - titleWidth)
const leftWidth = Math.floor(sideWidth / 2)
const rightWidth = sideWidth - leftWidth
return (
<Text color={color} dimColor={!color}>
{char.repeat(leftWidth)}{' '}
<Text dimColor>
<Ansi>{title}</Ansi>
</Text>{' '}
{char.repeat(rightWidth)}
</Text>
)
}
const t3 = !color;
let t4;
if ($[14] !== char || $[15] !== effectiveWidth) {
t4 = char.repeat(effectiveWidth);
$[14] = char;
$[15] = effectiveWidth;
$[16] = t4;
} else {
t4 = $[16];
}
let t5;
if ($[17] !== color || $[18] !== t3 || $[19] !== t4) {
t5 = <Text color={color} dimColor={t3}>{t4}</Text>;
$[17] = color;
$[18] = t3;
$[19] = t4;
$[20] = t5;
} else {
t5 = $[20];
}
return t5;
return (
<Text color={color} dimColor={!color}>
{char.repeat(effectiveWidth)}
</Text>
)
}

View File

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

View File

@@ -1,16 +1,16 @@
import { c as _c } from "react/compiler-runtime";
import React from 'react';
import Text from '../../ink/components/Text.js';
import React from 'react'
import Text from '../../ink/components/Text.js'
type Props = {
/** The key or chord to display (e.g., "ctrl+o", "Enter", "↑/↓") */
shortcut: string;
shortcut: string
/** The action the key performs (e.g., "expand", "select", "navigate") */
action: string;
action: string
/** Whether to wrap the hint in parentheses. Default: false */
parens?: boolean;
parens?: boolean
/** Whether to render the shortcut in bold. Default: false */
bold?: boolean;
};
bold?: boolean
}
/**
* Renders a keyboard shortcut hint like "ctrl+o to expand" or "(tab to toggle)"
@@ -35,46 +35,24 @@ type Props = {
* </Byline>
* </Text>
*/
export function KeyboardShortcutHint(t0) {
const $ = _c(9);
const {
shortcut,
action,
parens: t1,
bold: t2
} = t0;
const parens = t1 === undefined ? false : t1;
const bold = t2 === undefined ? false : t2;
let t3;
if ($[0] !== bold || $[1] !== shortcut) {
t3 = bold ? <Text bold={true}>{shortcut}</Text> : shortcut;
$[0] = bold;
$[1] = shortcut;
$[2] = t3;
} else {
t3 = $[2];
}
const shortcutText = t3;
export function KeyboardShortcutHint({
shortcut,
action,
parens = false,
bold = false,
}: Props): React.ReactNode {
const shortcutText = bold ? <Text bold>{shortcut}</Text> : shortcut
if (parens) {
let t4;
if ($[3] !== action || $[4] !== shortcutText) {
t4 = <Text>({shortcutText} to {action})</Text>;
$[3] = action;
$[4] = shortcutText;
$[5] = t4;
} else {
t4 = $[5];
}
return t4;
return (
<Text>
({shortcutText} to {action})
</Text>
)
}
let t4;
if ($[6] !== action || $[7] !== shortcutText) {
t4 = <Text>{shortcutText} to {action}</Text>;
$[6] = action;
$[7] = shortcutText;
$[8] = t4;
} else {
t4 = $[8];
}
return t4;
return (
<Text>
{shortcutText} to {action}
</Text>
)
}

View File

@@ -1,44 +1,44 @@
import { c as _c } from "react/compiler-runtime";
import figures from 'figures';
import type { ReactNode } from 'react';
import React from 'react';
import { useDeclaredCursor } from '../../ink/hooks/use-declared-cursor.js';
import { Box, Text } from '../../ink.js';
import figures from 'figures'
import type { ReactNode } from 'react'
import React from 'react'
import { useDeclaredCursor } from '../../ink/hooks/use-declared-cursor.js'
import { Box, Text } from '../../ink.js'
type ListItemProps = {
/**
* Whether this item is currently focused (keyboard selection).
* Shows the pointer indicator () when true.
*/
isFocused: boolean;
isFocused: boolean
/**
* Whether this item is selected (chosen/checked).
* Shows the checkmark indicator (✓) when true.
* @default false
*/
isSelected?: boolean;
isSelected?: boolean
/**
* The content to display for this item.
*/
children: ReactNode;
children: ReactNode
/**
* Optional description text displayed below the main content.
*/
description?: string;
description?: string
/**
* Show a down arrow indicator instead of pointer (for scroll hints).
* Only applies when not focused.
*/
showScrollDown?: boolean;
showScrollDown?: boolean
/**
* Show an up arrow indicator instead of pointer (for scroll hints).
* Only applies when not focused.
*/
showScrollUp?: boolean;
showScrollUp?: boolean
/**
* Whether to apply automatic styling to the children based on focus/selection state.
@@ -46,21 +46,21 @@ type ListItemProps = {
* - When false: children are rendered as-is, allowing custom styling
* @default true
*/
styled?: boolean;
styled?: boolean
/**
* Whether this item is disabled. Disabled items show dimmed text and no indicators.
* @default false
*/
disabled?: boolean;
disabled?: boolean
/**
* Whether this ListItem should declare the terminal cursor position.
* Set false when a child (e.g. BaseTextInput) declares its own cursor.
* @default true
*/
declareCursor?: boolean;
};
declareCursor?: boolean
}
/**
* A list item component for selection UIs (dropdowns, multi-selects, menus).
@@ -101,143 +101,88 @@ type ListItemProps = {
* <Text color="claude">Custom styled content</Text>
* </ListItem>
*/
export function ListItem(t0) {
const $ = _c(32);
const {
isFocused,
isSelected: t1,
children,
description,
showScrollDown,
showScrollUp,
styled: t2,
disabled: t3,
declareCursor
} = t0;
const isSelected = t1 === undefined ? false : t1;
const styled = t2 === undefined ? true : t2;
const disabled = t3 === undefined ? false : t3;
let t4;
if ($[0] !== disabled || $[1] !== isFocused || $[2] !== showScrollDown || $[3] !== showScrollUp) {
t4 = function renderIndicator() {
if (disabled) {
return <Text> </Text>;
}
if (isFocused) {
return <Text color="suggestion">{figures.pointer}</Text>;
}
if (showScrollDown) {
return <Text dimColor={true}>{figures.arrowDown}</Text>;
}
if (showScrollUp) {
return <Text dimColor={true}>{figures.arrowUp}</Text>;
}
return <Text> </Text>;
};
$[0] = disabled;
$[1] = isFocused;
$[2] = showScrollDown;
$[3] = showScrollUp;
$[4] = t4;
} else {
t4 = $[4];
export function ListItem({
isFocused,
isSelected = false,
children,
description,
showScrollDown,
showScrollUp,
styled = true,
disabled = false,
declareCursor,
}: ListItemProps): React.ReactNode {
// Determine which indicator to show
function renderIndicator(): ReactNode {
if (disabled) {
return <Text> </Text>
}
if (isFocused) {
return <Text color="suggestion">{figures.pointer}</Text>
}
if (showScrollDown) {
return <Text dimColor>{figures.arrowDown}</Text>
}
if (showScrollUp) {
return <Text dimColor>{figures.arrowUp}</Text>
}
return <Text> </Text>
}
const renderIndicator = t4;
let t5;
if ($[5] !== disabled || $[6] !== isFocused || $[7] !== isSelected || $[8] !== styled) {
const getTextColor = function getTextColor() {
if (disabled) {
return "inactive";
}
if (!styled) {
return;
}
if (isSelected) {
return "success";
}
if (isFocused) {
return "suggestion";
}
};
t5 = getTextColor();
$[5] = disabled;
$[6] = isFocused;
$[7] = isSelected;
$[8] = styled;
$[9] = t5;
} else {
t5 = $[9];
// Determine text color based on state
function getTextColor(): 'success' | 'suggestion' | 'inactive' | undefined {
if (disabled) {
return 'inactive'
}
if (!styled) {
return undefined
}
if (isSelected) {
return 'success'
}
if (isFocused) {
return 'suggestion'
}
return undefined
}
const textColor = t5;
const t6 = isFocused && !disabled && declareCursor !== false;
let t7;
if ($[10] !== t6) {
t7 = {
line: 0,
column: 0,
active: t6
};
$[10] = t6;
$[11] = t7;
} else {
t7 = $[11];
}
const cursorRef = useDeclaredCursor(t7);
let t8;
if ($[12] !== renderIndicator) {
t8 = renderIndicator();
$[12] = renderIndicator;
$[13] = t8;
} else {
t8 = $[13];
}
let t9;
if ($[14] !== children || $[15] !== disabled || $[16] !== styled || $[17] !== textColor) {
t9 = styled ? <Text color={textColor} dimColor={disabled}>{children}</Text> : children;
$[14] = children;
$[15] = disabled;
$[16] = styled;
$[17] = textColor;
$[18] = t9;
} else {
t9 = $[18];
}
let t10;
if ($[19] !== disabled || $[20] !== isSelected) {
t10 = isSelected && !disabled && <Text color="success">{figures.tick}</Text>;
$[19] = disabled;
$[20] = isSelected;
$[21] = t10;
} else {
t10 = $[21];
}
let t11;
if ($[22] !== t10 || $[23] !== t8 || $[24] !== t9) {
t11 = <Box flexDirection="row" gap={1}>{t8}{t9}{t10}</Box>;
$[22] = t10;
$[23] = t8;
$[24] = t9;
$[25] = t11;
} else {
t11 = $[25];
}
let t12;
if ($[26] !== description) {
t12 = description && <Box paddingLeft={2}><Text color="inactive">{description}</Text></Box>;
$[26] = description;
$[27] = t12;
} else {
t12 = $[27];
}
let t13;
if ($[28] !== cursorRef || $[29] !== t11 || $[30] !== t12) {
t13 = <Box ref={cursorRef} flexDirection="column">{t11}{t12}</Box>;
$[28] = cursorRef;
$[29] = t11;
$[30] = t12;
$[31] = t13;
} else {
t13 = $[31];
}
return t13;
const textColor = getTextColor()
// Park the native terminal cursor on the pointer indicator so screen
// readers / magnifiers track the focused item. (0,0) is the top-left of
// this Box, where the pointer renders.
const cursorRef = useDeclaredCursor({
line: 0,
column: 0,
active: isFocused && !disabled && declareCursor !== false,
})
return (
<Box ref={cursorRef} flexDirection="column">
<Box flexDirection="row" gap={1}>
{renderIndicator()}
{styled ? (
<Text color={textColor} dimColor={disabled}>
{children}
</Text>
) : (
children
)}
{isSelected && !disabled && <Text color="success">{figures.tick}</Text>}
</Box>
{description && (
<Box paddingLeft={2}>
<Text color="inactive">{description}</Text>
</Box>
)}
</Box>
)
}

View File

@@ -1,30 +1,30 @@
import { c as _c } from "react/compiler-runtime";
import React from 'react';
import { Box, Text } from '../../ink.js';
import { Spinner } from '../Spinner.js';
import React from 'react'
import { Box, Text } from '../../ink.js'
import { Spinner } from '../Spinner.js'
type LoadingStateProps = {
/**
* The loading message to display next to the spinner.
*/
message: string;
message: string
/**
* Display the message in bold.
* @default false
*/
bold?: boolean;
bold?: boolean
/**
* Display the message in dimmed color.
* @default false
*/
dimColor?: boolean;
dimColor?: boolean
/**
* Optional subtitle displayed below the main message.
*/
subtitle?: string;
};
subtitle?: string
}
/**
* A spinner with loading message for async operations.
@@ -45,49 +45,22 @@ type LoadingStateProps = {
* subtitle="Fetching your Claude Code sessions..."
* />
*/
export function LoadingState(t0) {
const $ = _c(10);
const {
message,
bold: t1,
dimColor: t2,
subtitle
} = t0;
const bold = t1 === undefined ? false : t1;
const dimColor = t2 === undefined ? false : t2;
let t3;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t3 = <Spinner />;
$[0] = t3;
} else {
t3 = $[0];
}
let t4;
if ($[1] !== bold || $[2] !== dimColor || $[3] !== message) {
t4 = <Box flexDirection="row">{t3}<Text bold={bold} dimColor={dimColor}>{" "}{message}</Text></Box>;
$[1] = bold;
$[2] = dimColor;
$[3] = message;
$[4] = t4;
} else {
t4 = $[4];
}
let t5;
if ($[5] !== subtitle) {
t5 = subtitle && <Text dimColor={true}>{subtitle}</Text>;
$[5] = subtitle;
$[6] = t5;
} else {
t5 = $[6];
}
let t6;
if ($[7] !== t4 || $[8] !== t5) {
t6 = <Box flexDirection="column">{t4}{t5}</Box>;
$[7] = t4;
$[8] = t5;
$[9] = t6;
} else {
t6 = $[9];
}
return t6;
export function LoadingState({
message,
bold = false,
dimColor = false,
subtitle,
}: LoadingStateProps): React.ReactNode {
return (
<Box flexDirection="column">
<Box flexDirection="row">
<Spinner />
<Text bold={bold} dimColor={dimColor}>
{' '}
{message}
</Text>
</Box>
{subtitle && <Text dimColor>{subtitle}</Text>}
</Box>
)
}

View File

@@ -1,16 +1,16 @@
import { c as _c } from "react/compiler-runtime";
import React from 'react';
import { useIsInsideModal } from '../../context/modalContext.js';
import { Box } from '../../ink.js';
import type { Theme } from '../../utils/theme.js';
import { Divider } from './Divider.js';
import React from 'react'
import { useIsInsideModal } from '../../context/modalContext.js'
import { Box } from '../../ink.js'
import type { Theme } from '../../utils/theme.js'
import { Divider } from './Divider.js'
type PaneProps = {
children: React.ReactNode;
children: React.ReactNode
/**
* Theme color for the top border line.
*/
color?: keyof Theme;
};
color?: keyof Theme
}
/**
* A pane — a region of the terminal that appears below the REPL prompt,
@@ -30,47 +30,28 @@ type PaneProps = {
* <Tabs title="Sandbox:">...</Tabs>
* </Pane>
*/
export function Pane(t0) {
const $ = _c(9);
const {
children,
color
} = t0;
export function Pane({ children, color }: PaneProps): React.ReactNode {
// When rendered inside FullscreenLayout's modal slot, its ▔ divider IS
// the frame. Skip our own Divider (would double-frame) and the extra top
// padding. This lets slash-command screens that wrap in Pane (e.g.
// /model → ModelPicker) route through the modal slot unchanged.
if (useIsInsideModal()) {
let t1;
if ($[0] !== children) {
t1 = <Box flexDirection="column" paddingX={1} flexShrink={0}>{children}</Box>;
$[0] = children;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
// flexShrink=0: the modal slot's absolute Box has no explicit height
// (grows to fit, maxHeight cap). With flexGrow=1, re-renders cause
// yoga to resolve this Box's height to 0 against the undetermined
// parent — /permissions body blanks on Down arrow. See #23592.
return (
<Box flexDirection="column" paddingX={1} flexShrink={0}>
{children}
</Box>
)
}
let t1;
if ($[2] !== color) {
t1 = <Divider color={color} />;
$[2] = color;
$[3] = t1;
} else {
t1 = $[3];
}
let t2;
if ($[4] !== children) {
t2 = <Box flexDirection="column" paddingX={2}>{children}</Box>;
$[4] = children;
$[5] = t2;
} else {
t2 = $[5];
}
let t3;
if ($[6] !== t1 || $[7] !== t2) {
t3 = <Box flexDirection="column" paddingTop={1}>{t1}{t2}</Box>;
$[6] = t1;
$[7] = t2;
$[8] = t3;
} else {
t3 = $[8];
}
return t3;
return (
<Box flexDirection="column" paddingTop={1}>
<Divider color={color} />
<Box flexDirection="column" paddingX={2}>
{children}
</Box>
</Box>
)
}

View File

@@ -1,85 +1,54 @@
import { c as _c } from "react/compiler-runtime";
import React from 'react';
import { Text } from '../../ink.js';
import type { Theme } from '../../utils/theme.js';
import React from 'react'
import { Text } from '../../ink.js'
import type { Theme } from '../../utils/theme.js'
type Props = {
/**
* How much progress to display, between 0 and 1 inclusive
*/
ratio: number; // [0, 1]
ratio: number // [0, 1]
/**
* How many characters wide to draw the progress bar
*/
width: number; // how many characters wide
width: number // how many characters wide
/**
* Optional color for the filled portion of the bar
*/
fillColor?: keyof Theme;
fillColor?: keyof Theme
/**
* Optional color for the empty portion of the bar
*/
emptyColor?: keyof Theme;
};
const BLOCKS = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
export function ProgressBar(t0) {
const $ = _c(13);
const {
ratio: inputRatio,
width,
fillColor,
emptyColor
} = t0;
const ratio = Math.min(1, Math.max(0, inputRatio));
const whole = Math.floor(ratio * width);
let t1;
if ($[0] !== whole) {
t1 = BLOCKS[BLOCKS.length - 1].repeat(whole);
$[0] = whole;
$[1] = t1;
} else {
t1 = $[1];
}
let segments;
if ($[2] !== ratio || $[3] !== t1 || $[4] !== whole || $[5] !== width) {
segments = [t1];
if (whole < width) {
const remainder = ratio * width - whole;
const middle = Math.floor(remainder * BLOCKS.length);
segments.push(BLOCKS[middle]);
const empty = width - whole - 1;
if (empty > 0) {
let t2;
if ($[7] !== empty) {
t2 = BLOCKS[0].repeat(empty);
$[7] = empty;
$[8] = t2;
} else {
t2 = $[8];
}
segments.push(t2);
}
}
$[2] = ratio;
$[3] = t1;
$[4] = whole;
$[5] = width;
$[6] = segments;
} else {
segments = $[6];
}
const t2 = segments.join("");
let t3;
if ($[9] !== emptyColor || $[10] !== fillColor || $[11] !== t2) {
t3 = <Text color={fillColor} backgroundColor={emptyColor}>{t2}</Text>;
$[9] = emptyColor;
$[10] = fillColor;
$[11] = t2;
$[12] = t3;
} else {
t3 = $[12];
}
return t3;
emptyColor?: keyof Theme
}
const BLOCKS = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█']
export function ProgressBar({
ratio: inputRatio,
width,
fillColor,
emptyColor,
}: Props): React.ReactNode {
const ratio = Math.min(1, Math.max(0, inputRatio))
const whole = Math.floor(ratio * width)
const segments = [BLOCKS[BLOCKS.length - 1]!.repeat(whole)]
if (whole < width) {
const remainder = ratio * width - whole
const middle = Math.floor(remainder * BLOCKS.length)
segments.push(BLOCKS[middle]!)
const empty = width - whole - 1
if (empty > 0) {
segments.push(BLOCKS[0]!.repeat(empty))
}
}
return (
<Text color={fillColor} backgroundColor={emptyColor}>
{segments.join('')}
</Text>
)
}

View File

@@ -1,79 +1,45 @@
import { c as _c } from "react/compiler-runtime";
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
import { useTerminalViewport } from '../../ink/hooks/use-terminal-viewport.js';
import { Box, type DOMElement, measureElement } from '../../ink.js';
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react'
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
import { useTerminalViewport } from '../../ink/hooks/use-terminal-viewport.js'
import { Box, type DOMElement, measureElement } from '../../ink.js'
type Props = {
children: React.ReactNode;
lock?: 'always' | 'offscreen';
};
export function Ratchet(t0) {
const $ = _c(10);
const {
children,
lock: t1
} = t0;
const lock = t1 === undefined ? "always" : t1;
const [viewportRef, t2] = useTerminalViewport();
const {
isVisible
} = t2;
const {
rows
} = useTerminalSize();
const innerRef = useRef(null);
const maxHeight = useRef(0);
const [minHeight, setMinHeight] = useState(0);
let t3;
if ($[0] !== viewportRef) {
t3 = el => {
viewportRef(el);
};
$[0] = viewportRef;
$[1] = t3;
} else {
t3 = $[1];
}
const outerRef = t3;
const engaged = lock === "always" || !isVisible;
let t4;
if ($[2] !== rows) {
t4 = () => {
if (!innerRef.current) {
return;
}
const {
height
} = measureElement(innerRef.current);
if (height > maxHeight.current) {
maxHeight.current = Math.min(height, rows);
setMinHeight(maxHeight.current);
}
};
$[2] = rows;
$[3] = t4;
} else {
t4 = $[3];
}
useLayoutEffect(t4);
const t5 = engaged ? minHeight : undefined;
let t6;
if ($[4] !== children) {
t6 = <Box ref={innerRef} flexDirection="column">{children}</Box>;
$[4] = children;
$[5] = t6;
} else {
t6 = $[5];
}
let t7;
if ($[6] !== outerRef || $[7] !== t5 || $[8] !== t6) {
t7 = <Box minHeight={t5} ref={outerRef}>{t6}</Box>;
$[6] = outerRef;
$[7] = t5;
$[8] = t6;
$[9] = t7;
} else {
t7 = $[9];
}
return t7;
children: React.ReactNode
lock?: 'always' | 'offscreen'
}
export function Ratchet({ children, lock = 'always' }: Props): React.ReactNode {
const [viewportRef, { isVisible }] = useTerminalViewport()
const { rows } = useTerminalSize()
const innerRef = useRef<DOMElement | null>(null)
const maxHeight = useRef(0)
const [minHeight, setMinHeight] = useState(0)
const outerRef = useCallback(
(el: DOMElement | null) => {
viewportRef(el)
},
[viewportRef],
)
const engaged = lock === 'always' || !isVisible
useLayoutEffect(() => {
if (!innerRef.current) {
return
}
const { height } = measureElement(innerRef.current)
if (height > maxHeight.current) {
maxHeight.current = Math.min(height, rows)
setMinHeight(maxHeight.current)
}
})
return (
<Box minHeight={engaged ? minHeight : undefined} ref={outerRef}>
<Box ref={innerRef} flexDirection="column">
{children}
</Box>
</Box>
)
}

View File

@@ -1,8 +1,9 @@
import { c as _c } from "react/compiler-runtime";
import figures from 'figures';
import React from 'react';
import { Text } from '../../ink.js';
type Status = 'success' | 'error' | 'warning' | 'info' | 'pending' | 'loading';
import figures from 'figures'
import React from 'react'
import { Text } from '../../ink.js'
type Status = 'success' | 'error' | 'warning' | 'info' | 'pending' | 'loading'
type Props = {
/**
* The status to display. Determines both the icon and color.
@@ -14,42 +15,28 @@ type Props = {
* - `pending`: Dimmed circle (○)
* - `loading`: Dimmed ellipsis (…)
*/
status: Status;
status: Status
/**
* Include a trailing space after the icon. Useful when followed by text.
* @default false
*/
withSpace?: boolean;
};
const STATUS_CONFIG: Record<Status, {
icon: string;
color: 'success' | 'error' | 'warning' | 'suggestion' | undefined;
}> = {
success: {
icon: figures.tick,
color: 'success'
},
error: {
icon: figures.cross,
color: 'error'
},
warning: {
icon: figures.warning,
color: 'warning'
},
info: {
icon: figures.info,
color: 'suggestion'
},
pending: {
icon: figures.circle,
color: undefined
},
loading: {
icon: '…',
color: undefined
withSpace?: boolean
}
const STATUS_CONFIG: Record<
Status,
{
icon: string
color: 'success' | 'error' | 'warning' | 'suggestion' | undefined
}
};
> = {
success: { icon: figures.tick, color: 'success' },
error: { icon: figures.cross, color: 'error' },
warning: { icon: figures.warning, color: 'warning' },
info: { icon: figures.info, color: 'suggestion' },
pending: { icon: figures.circle, color: undefined },
loading: { icon: '…', color: undefined },
}
/**
* Renders a status indicator icon with appropriate color.
@@ -69,26 +56,16 @@ const STATUS_CONFIG: Record<Status, {
* Waiting for response
* </Text>
*/
export function StatusIcon(t0) {
const $ = _c(5);
const {
status,
withSpace: t1
} = t0;
const withSpace = t1 === undefined ? false : t1;
const config = STATUS_CONFIG[status];
const t2 = !config.color;
const t3 = withSpace && " ";
let t4;
if ($[0] !== config.color || $[1] !== config.icon || $[2] !== t2 || $[3] !== t3) {
t4 = <Text color={config.color} dimColor={t2}>{config.icon}{t3}</Text>;
$[0] = config.color;
$[1] = config.icon;
$[2] = t2;
$[3] = t3;
$[4] = t4;
} else {
t4 = $[4];
}
return t4;
export function StatusIcon({
status,
withSpace = false,
}: Props): React.ReactNode {
const config = STATUS_CONFIG[status]
return (
<Text color={config.color} dimColor={!config.color}>
{config.icon}
{withSpace && ' '}
</Text>
)
}

View File

@@ -1,28 +1,37 @@
import { c as _c } from "react/compiler-runtime";
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
import { useIsInsideModal, useModalScrollRef } from '../../context/modalContext.js';
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
import ScrollBox from '../../ink/components/ScrollBox.js';
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
import { stringWidth } from '../../ink/stringWidth.js';
import { Box, Text } from '../../ink.js';
import { useKeybindings } from '../../keybindings/useKeybinding.js';
import type { Theme } from '../../utils/theme.js';
import React, {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from 'react'
import {
useIsInsideModal,
useModalScrollRef,
} from '../../context/modalContext.js'
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
import ScrollBox from '../../ink/components/ScrollBox.js'
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
import { stringWidth } from '../../ink/stringWidth.js'
import { Box, Text } from '../../ink.js'
import { useKeybindings } from '../../keybindings/useKeybinding.js'
import type { Theme } from '../../utils/theme.js'
type TabsProps = {
children: Array<React.ReactElement<TabProps>>;
title?: string;
color?: keyof Theme;
defaultTab?: string;
hidden?: boolean;
useFullWidth?: boolean;
children: Array<React.ReactElement<TabProps>>
title?: string
color?: keyof Theme
defaultTab?: string
hidden?: boolean
useFullWidth?: boolean
/** Controlled mode: current selected tab id/title */
selectedTab?: string;
selectedTab?: string
/** Controlled mode: callback when tab changes */
onTabChange?: (tabId: string) => void;
onTabChange?: (tabId: string) => void
/** Optional banner to display below tabs header */
banner?: React.ReactNode;
banner?: React.ReactNode
/** Disable keyboard navigation (e.g. when a child component handles arrow keys) */
disableNavigation?: boolean;
disableNavigation?: boolean
/**
* Initial focus state for the tab header row. Defaults to true (header
* focused, nav always works). Keep the default for Select/list content —
@@ -31,28 +40,30 @@ type TabsProps = {
* content actually binds left/right/tab (e.g. enum cycling), and show a
* "↑ tabs" footer hint — without it tabs look broken.
*/
initialHeaderFocused?: boolean;
initialHeaderFocused?: boolean
/**
* Fixed height for the content area. When set, all tabs render within the
* same height (overflow hidden) so switching tabs doesn't cause layout
* shifts. Shorter tabs get whitespace; taller tabs are clipped.
*/
contentHeight?: number;
contentHeight?: number
/**
* Let Tab/←/→ switch tabs from focused content. Opt-in since some
* content uses those keys; pass a reactive boolean to cede them when
* needed. Switching from content focuses the header.
*/
navFromContent?: boolean;
};
navFromContent?: boolean
}
type TabsContextValue = {
selectedTab: string | undefined;
width: number | undefined;
headerFocused: boolean;
focusHeader: () => void;
blurHeader: () => void;
registerOptIn: () => () => void;
};
selectedTab: string | undefined
width: number | undefined
headerFocused: boolean
focusHeader: () => void
blurHeader: () => void
registerOptIn: () => () => void
}
const TabsContext = createContext<TabsContextValue>({
selectedTab: undefined,
width: undefined,
@@ -61,236 +72,248 @@ const TabsContext = createContext<TabsContextValue>({
headerFocused: false,
focusHeader: () => {},
blurHeader: () => {},
registerOptIn: () => () => {}
});
export function Tabs(t0) {
const $ = _c(25);
const {
title,
color,
defaultTab,
children,
hidden,
useFullWidth,
selectedTab: controlledSelectedTab,
onTabChange,
banner,
disableNavigation,
initialHeaderFocused: t1,
contentHeight,
navFromContent: t2
} = t0;
const initialHeaderFocused = t1 === undefined ? true : t1;
const navFromContent = t2 === undefined ? false : t2;
const {
columns: terminalWidth
} = useTerminalSize();
const tabs = children.map(_temp);
const defaultTabIndex = defaultTab ? tabs.findIndex(tab => defaultTab === tab[0]) : 0;
const isControlled = controlledSelectedTab !== undefined;
const [internalSelectedTab, setInternalSelectedTab] = useState(defaultTabIndex !== -1 ? defaultTabIndex : 0);
const controlledTabIndex = isControlled ? tabs.findIndex(tab_0 => tab_0[0] === controlledSelectedTab) : -1;
const selectedTabIndex = isControlled ? controlledTabIndex !== -1 ? controlledTabIndex : 0 : internalSelectedTab;
const modalScrollRef = useModalScrollRef();
const [headerFocused, setHeaderFocused] = useState(initialHeaderFocused);
let t3;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t3 = () => setHeaderFocused(true);
$[0] = t3;
} else {
t3 = $[0];
}
const focusHeader = t3;
let t4;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t4 = () => setHeaderFocused(false);
$[1] = t4;
} else {
t4 = $[1];
}
const blurHeader = t4;
const [optInCount, setOptInCount] = useState(0);
let t5;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t5 = () => {
setOptInCount(_temp2);
return () => setOptInCount(_temp3);
};
$[2] = t5;
} else {
t5 = $[2];
}
const registerOptIn = t5;
const optedIn = optInCount > 0;
const handleTabChange = offset => {
const newIndex = (selectedTabIndex + tabs.length + offset) % tabs.length;
const newTabId = tabs[newIndex]?.[0];
registerOptIn: () => () => {},
})
export function Tabs({
title,
color,
defaultTab,
children,
hidden,
useFullWidth,
selectedTab: controlledSelectedTab,
onTabChange,
banner,
disableNavigation,
initialHeaderFocused = true,
contentHeight,
navFromContent = false,
}: TabsProps): React.ReactNode {
const { columns: terminalWidth } = useTerminalSize()
const tabs = children.map(child => [
child.props.id ?? child.props.title,
child.props.title,
])
const defaultTabIndex = defaultTab
? tabs.findIndex(tab => defaultTab === tab[0])
: 0
// Support both controlled and uncontrolled modes
const isControlled = controlledSelectedTab !== undefined
const [internalSelectedTab, setInternalSelectedTab] = useState(
defaultTabIndex !== -1 ? defaultTabIndex : 0,
)
// In controlled mode, find the index of the controlled tab
const controlledTabIndex = isControlled
? tabs.findIndex(tab => tab[0] === controlledSelectedTab)
: -1
const selectedTabIndex = isControlled
? controlledTabIndex !== -1
? controlledTabIndex
: 0
: internalSelectedTab
const modalScrollRef = useModalScrollRef()
// Header focus: left/right/tab only switch tabs when the header row is
// focused. Children with interactive content call focusHeader() (via
// useTabHeaderFocus) on up-arrow to hand focus back here; down-arrow
// returns it. Tabs that never call the hook see no behavior change —
// initialHeaderFocused defaults to true so nav always works.
const [headerFocused, setHeaderFocused] = useState(initialHeaderFocused)
const focusHeader = useCallback(() => setHeaderFocused(true), [])
const blurHeader = useCallback(() => setHeaderFocused(false), [])
// Count of mounted children using useTabHeaderFocus(). Down-arrow blur and
// the ↓ hint only engage when at least one child has opted in — otherwise
// pressing down on a legacy tab would strand the user with nav disabled.
const [optInCount, setOptInCount] = useState(0)
const registerOptIn = useCallback(() => {
setOptInCount(n => n + 1)
return () => setOptInCount(n => n - 1)
}, [])
const optedIn = optInCount > 0
const handleTabChange = (offset: number) => {
const newIndex = (selectedTabIndex + tabs.length + offset) % tabs.length
const newTabId = tabs[newIndex]?.[0]
if (isControlled && onTabChange && newTabId) {
onTabChange(newTabId);
onTabChange(newTabId)
} else {
setInternalSelectedTab(newIndex);
setInternalSelectedTab(newIndex)
}
setHeaderFocused(true);
};
const t6 = !hidden && !disableNavigation && headerFocused;
let t7;
if ($[3] !== t6) {
t7 = {
context: "Tabs",
isActive: t6
};
$[3] = t6;
$[4] = t7;
} else {
t7 = $[4];
// Tab switching is a header action — stay focused so the user can keep
// cycling. The newly mounted tab can blur via its own interaction.
setHeaderFocused(true)
}
useKeybindings({
"tabs:next": () => handleTabChange(1),
"tabs:previous": () => handleTabChange(-1)
}, t7);
let t8;
if ($[5] !== headerFocused || $[6] !== hidden || $[7] !== optedIn) {
t8 = e => {
if (!headerFocused || !optedIn || hidden) {
return;
}
if (e.key === "down") {
e.preventDefault();
setHeaderFocused(false);
}
};
$[5] = headerFocused;
$[6] = hidden;
$[7] = optedIn;
$[8] = t8;
} else {
t8 = $[8];
}
const handleKeyDown = t8;
const t9 = navFromContent && !headerFocused && optedIn && !hidden && !disableNavigation;
let t10;
if ($[9] !== t9) {
t10 = {
context: "Tabs",
isActive: t9
};
$[9] = t9;
$[10] = t10;
} else {
t10 = $[10];
}
useKeybindings({
"tabs:next": () => {
handleTabChange(1);
setHeaderFocused(true);
useKeybindings(
{
'tabs:next': () => handleTabChange(1),
'tabs:previous': () => handleTabChange(-1),
},
"tabs:previous": () => {
handleTabChange(-1);
setHeaderFocused(true);
{
context: 'Tabs',
isActive: !hidden && !disableNavigation && headerFocused,
},
)
// When the header is focused, down-arrow returns focus to content. Only
// active when the selected tab has opted in via useTabHeaderFocus() —
// legacy tabs have nowhere to return focus to.
const handleKeyDown = (e: KeyboardEvent) => {
if (!headerFocused || !optedIn || hidden) return
if (e.key === 'down') {
e.preventDefault()
setHeaderFocused(false)
}
}, t10);
const titleWidth = title ? stringWidth(title) + 1 : 0;
const tabsWidth = tabs.reduce(_temp4, 0);
const usedWidth = titleWidth + tabsWidth;
const spacerWidth = useFullWidth ? Math.max(0, terminalWidth - usedWidth) : 0;
const contentWidth = useFullWidth ? terminalWidth : undefined;
const T0 = Box;
const t11 = "column";
const t12 = 0;
const t13 = true;
const t14 = modalScrollRef ? 0 : undefined;
const t15 = !hidden && <Box flexDirection="row" gap={1} flexShrink={modalScrollRef ? 0 : undefined}>{title !== undefined && <Text bold={true} color={color}>{title}</Text>}{tabs.map((t16, i) => {
const [id, title_0] = t16;
const isCurrent = selectedTabIndex === i;
const hasColorCursor = color && isCurrent && headerFocused;
return <Text key={id} backgroundColor={hasColorCursor ? color : undefined} color={hasColorCursor ? "inverseText" : undefined} inverse={isCurrent && !hasColorCursor} bold={isCurrent}>{" "}{title_0}{" "}</Text>;
})}{spacerWidth > 0 && <Text>{" ".repeat(spacerWidth)}</Text>}</Box>;
let t17;
if ($[11] !== children || $[12] !== contentHeight || $[13] !== contentWidth || $[14] !== hidden || $[15] !== modalScrollRef || $[16] !== selectedTabIndex) {
t17 = modalScrollRef ? <Box width={contentWidth} marginTop={hidden ? 0 : 1} flexShrink={0}><ScrollBox key={selectedTabIndex} ref={modalScrollRef} flexDirection="column" flexShrink={0}>{children}</ScrollBox></Box> : <Box width={contentWidth} marginTop={hidden ? 0 : 1} height={contentHeight} overflowY={contentHeight !== undefined ? "hidden" : undefined}>{children}</Box>;
$[11] = children;
$[12] = contentHeight;
$[13] = contentWidth;
$[14] = hidden;
$[15] = modalScrollRef;
$[16] = selectedTabIndex;
$[17] = t17;
} else {
t17 = $[17];
}
let t18;
if ($[18] !== T0 || $[19] !== banner || $[20] !== handleKeyDown || $[21] !== t14 || $[22] !== t15 || $[23] !== t17) {
t18 = <T0 flexDirection={t11} tabIndex={t12} autoFocus={t13} onKeyDown={handleKeyDown} flexShrink={t14}>{t15}{banner}{t17}</T0>;
$[18] = T0;
$[19] = banner;
$[20] = handleKeyDown;
$[21] = t14;
$[22] = t15;
$[23] = t17;
$[24] = t18;
} else {
t18 = $[24];
}
return <TabsContext.Provider value={{
selectedTab: tabs[selectedTabIndex][0],
width: contentWidth,
headerFocused,
focusHeader,
blurHeader,
registerOptIn
}}>{t18}</TabsContext.Provider>;
}
function _temp4(sum, t0) {
const [, tabTitle] = t0;
return sum + (tabTitle ? stringWidth(tabTitle) : 0) + 2 + 1;
}
function _temp3(n_0) {
return n_0 - 1;
}
function _temp2(n) {
return n + 1;
}
function _temp(child) {
return [child.props.id ?? child.props.title, child.props.title];
// Opt-in: same tabs:next/previous actions, active from content. Focuses
// the header so subsequent presses cycle via the handler above.
useKeybindings(
{
'tabs:next': () => {
handleTabChange(1)
setHeaderFocused(true)
},
'tabs:previous': () => {
handleTabChange(-1)
setHeaderFocused(true)
},
},
{
context: 'Tabs',
isActive:
navFromContent &&
!headerFocused &&
optedIn &&
!hidden &&
!disableNavigation,
},
)
// Calculate spacing to fill the available width. No keyboard hint in the
// header row — content footers own hints (see useTabHeaderFocus docs).
const titleWidth = title ? stringWidth(title) + 1 : 0 // +1 for gap
const tabsWidth = tabs.reduce(
(sum, [, tabTitle]) => sum + (tabTitle ? stringWidth(tabTitle) : 0) + 2 + 1, // +2 for padding, +1 for gap
0,
)
const usedWidth = titleWidth + tabsWidth
const spacerWidth = useFullWidth ? Math.max(0, terminalWidth - usedWidth) : 0
const contentWidth = useFullWidth ? terminalWidth : undefined
return (
<TabsContext.Provider
value={{
selectedTab: tabs[selectedTabIndex]![0],
width: contentWidth,
headerFocused,
focusHeader,
blurHeader,
registerOptIn,
}}
>
<Box
flexDirection="column"
tabIndex={0}
autoFocus
onKeyDown={handleKeyDown}
// flexShrink=0 inside modal slot — the modal's absolute Box has no
// explicit height (grows to fit, maxHeight cap), so flexGrow=1 here
// resolves to 0 on re-render and the body blanks on Down arrow.
// See #23592. Outside modal, leave layout alone.
flexShrink={modalScrollRef ? 0 : undefined}
>
{!hidden && (
<Box
flexDirection="row"
gap={1}
flexShrink={modalScrollRef ? 0 : undefined}
>
{title !== undefined && (
<Text bold color={color}>
{title}
</Text>
)}
{tabs.map(([id, title], i) => {
const isCurrent = selectedTabIndex === i
const hasColorCursor = color && isCurrent && headerFocused
return (
<Text
key={id}
backgroundColor={hasColorCursor ? color : undefined}
color={hasColorCursor ? 'inverseText' : undefined}
inverse={isCurrent && !hasColorCursor}
bold={isCurrent}
>
{' '}
{title}{' '}
</Text>
)
})}
{spacerWidth > 0 && <Text>{' '.repeat(spacerWidth)}</Text>}
</Box>
)}
{banner}
{modalScrollRef ? (
// Inside the modal slot: own the ScrollBox here so the tabs
// header row above sits OUTSIDE the scroll area — it can never
// scroll off. The ref reaches REPL's ScrollKeybindingHandler via
// ModalContext. Keyed by selectedTabIndex → remounts on tab
// switch, resetting scrollTop to 0 without scrollTo() timing games.
<Box width={contentWidth} marginTop={hidden ? 0 : 1} flexShrink={0}>
<ScrollBox
key={selectedTabIndex}
ref={modalScrollRef}
flexDirection="column"
flexShrink={0}
>
{children}
</ScrollBox>
</Box>
) : (
<Box
width={contentWidth}
marginTop={hidden ? 0 : 1}
height={contentHeight}
overflowY={contentHeight !== undefined ? 'hidden' : undefined}
>
{children}
</Box>
)}
</Box>
</TabsContext.Provider>
)
}
type TabProps = {
title: string;
id?: string;
children: React.ReactNode;
};
export function Tab(t0) {
const $ = _c(4);
const {
title,
id,
children
} = t0;
const {
selectedTab,
width
} = useContext(TabsContext);
const insideModal = useIsInsideModal();
if (selectedTab !== (id ?? title)) {
return null;
}
const t1 = insideModal ? 0 : undefined;
let t2;
if ($[0] !== children || $[1] !== t1 || $[2] !== width) {
t2 = <Box width={width} flexShrink={t1}>{children}</Box>;
$[0] = children;
$[1] = t1;
$[2] = width;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
title: string
id?: string
children: React.ReactNode
}
export function useTabsWidth() {
const {
width
} = useContext(TabsContext);
return width;
export function Tab({ title, id, children }: TabProps): React.ReactNode {
const { selectedTab, width } = useContext(TabsContext)
const insideModal = useIsInsideModal()
if (selectedTab !== (id ?? title)) {
return null
}
return (
<Box width={width} flexShrink={insideModal ? 0 : undefined}>
{children}
</Box>
)
}
export function useTabsWidth(): number | undefined {
const { width } = useContext(TabsContext)
return width
}
/**
@@ -304,36 +327,13 @@ export function useTabsWidth() {
* no onUpFromFirstItem to recover. Split the component so the hook only runs
* when the Select renders.
*/
export function useTabHeaderFocus() {
const $ = _c(6);
const {
headerFocused,
focusHeader,
blurHeader,
registerOptIn
} = useContext(TabsContext);
let t0;
if ($[0] !== registerOptIn) {
t0 = [registerOptIn];
$[0] = registerOptIn;
$[1] = t0;
} else {
t0 = $[1];
}
useEffect(registerOptIn, t0);
let t1;
if ($[2] !== blurHeader || $[3] !== focusHeader || $[4] !== headerFocused) {
t1 = {
headerFocused,
focusHeader,
blurHeader
};
$[2] = blurHeader;
$[3] = focusHeader;
$[4] = headerFocused;
$[5] = t1;
} else {
t1 = $[5];
}
return t1;
export function useTabHeaderFocus(): {
headerFocused: boolean
focusHeader: () => void
blurHeader: () => void
} {
const { headerFocused, focusHeader, blurHeader, registerOptIn } =
useContext(TabsContext)
useEffect(registerOptIn, [registerOptIn])
return { headerFocused, focusHeader, blurHeader }
}

View File

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

View File

@@ -1,155 +1,112 @@
import { c as _c } from "react/compiler-runtime";
import React, { type PropsWithChildren, type Ref } from 'react';
import Box from '../../ink/components/Box.js';
import type { DOMElement } from '../../ink/dom.js';
import type { ClickEvent } from '../../ink/events/click-event.js';
import type { FocusEvent } from '../../ink/events/focus-event.js';
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
import type { Color, Styles } from '../../ink/styles.js';
import { getTheme, type Theme } from '../../utils/theme.js';
import { useTheme } from './ThemeProvider.js';
import React, { type PropsWithChildren, type Ref } from 'react'
import Box from '../../ink/components/Box.js'
import type { DOMElement } from '../../ink/dom.js'
import type { ClickEvent } from '../../ink/events/click-event.js'
import type { FocusEvent } from '../../ink/events/focus-event.js'
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
import type { Color, Styles } from '../../ink/styles.js'
import { getTheme, type Theme } from '../../utils/theme.js'
import { useTheme } from './ThemeProvider.js'
// Color props that accept theme keys
type ThemedColorProps = {
readonly borderColor?: keyof Theme | Color;
readonly borderTopColor?: keyof Theme | Color;
readonly borderBottomColor?: keyof Theme | Color;
readonly borderLeftColor?: keyof Theme | Color;
readonly borderRightColor?: keyof Theme | Color;
readonly backgroundColor?: keyof Theme | Color;
};
readonly borderColor?: keyof Theme | Color
readonly borderTopColor?: keyof Theme | Color
readonly borderBottomColor?: keyof Theme | Color
readonly borderLeftColor?: keyof Theme | Color
readonly borderRightColor?: keyof Theme | Color
readonly backgroundColor?: keyof Theme | Color
}
// Base Styles without color props (they'll be overridden)
type BaseStylesWithoutColors = Omit<Styles, 'textWrap' | 'borderColor' | 'borderTopColor' | 'borderBottomColor' | 'borderLeftColor' | 'borderRightColor' | 'backgroundColor'>;
export type Props = BaseStylesWithoutColors & ThemedColorProps & {
ref?: Ref<DOMElement>;
tabIndex?: number;
autoFocus?: boolean;
onClick?: (event: ClickEvent) => void;
onFocus?: (event: FocusEvent) => void;
onFocusCapture?: (event: FocusEvent) => void;
onBlur?: (event: FocusEvent) => void;
onBlurCapture?: (event: FocusEvent) => void;
onKeyDown?: (event: KeyboardEvent) => void;
onKeyDownCapture?: (event: KeyboardEvent) => void;
onMouseEnter?: () => void;
onMouseLeave?: () => void;
};
type BaseStylesWithoutColors = Omit<
Styles,
| 'textWrap'
| 'borderColor'
| 'borderTopColor'
| 'borderBottomColor'
| 'borderLeftColor'
| 'borderRightColor'
| 'backgroundColor'
>
export type Props = BaseStylesWithoutColors &
ThemedColorProps & {
ref?: Ref<DOMElement>
tabIndex?: number
autoFocus?: boolean
onClick?: (event: ClickEvent) => void
onFocus?: (event: FocusEvent) => void
onFocusCapture?: (event: FocusEvent) => void
onBlur?: (event: FocusEvent) => void
onBlurCapture?: (event: FocusEvent) => void
onKeyDown?: (event: KeyboardEvent) => void
onKeyDownCapture?: (event: KeyboardEvent) => void
onMouseEnter?: () => void
onMouseLeave?: () => void
}
/**
* Resolves a color value that may be a theme key to a raw Color.
*/
function resolveColor(color: keyof Theme | Color | undefined, theme: Theme): Color | undefined {
if (!color) return undefined;
function resolveColor(
color: keyof Theme | Color | undefined,
theme: Theme,
): Color | undefined {
if (!color) return undefined
// Check if it's a raw color (starts with rgb(, #, ansi256(, or ansi:)
if (color.startsWith('rgb(') || color.startsWith('#') || color.startsWith('ansi256(') || color.startsWith('ansi:')) {
return color as Color;
if (
color.startsWith('rgb(') ||
color.startsWith('#') ||
color.startsWith('ansi256(') ||
color.startsWith('ansi:')
) {
return color as Color
}
// It's a theme key - resolve it
return theme[color as keyof Theme] as Color;
return theme[color as keyof Theme] as Color
}
/**
* Theme-aware Box component that resolves theme color keys to raw colors.
* This wraps the base Box component with theme resolution for border colors.
*/
function ThemedBox(t0) {
const $ = _c(33);
let backgroundColor;
let borderBottomColor;
let borderColor;
let borderLeftColor;
let borderRightColor;
let borderTopColor;
let children;
let ref;
let rest;
if ($[0] !== t0) {
({
borderColor,
borderTopColor,
borderBottomColor,
borderLeftColor,
borderRightColor,
backgroundColor,
children,
ref,
...rest
} = t0);
$[0] = t0;
$[1] = backgroundColor;
$[2] = borderBottomColor;
$[3] = borderColor;
$[4] = borderLeftColor;
$[5] = borderRightColor;
$[6] = borderTopColor;
$[7] = children;
$[8] = ref;
$[9] = rest;
} else {
backgroundColor = $[1];
borderBottomColor = $[2];
borderColor = $[3];
borderLeftColor = $[4];
borderRightColor = $[5];
borderTopColor = $[6];
children = $[7];
ref = $[8];
rest = $[9];
}
const [themeName] = useTheme();
let resolvedBorderBottomColor;
let resolvedBorderColor;
let resolvedBorderLeftColor;
let resolvedBorderRightColor;
let resolvedBorderTopColor;
let t1;
if ($[10] !== backgroundColor || $[11] !== borderBottomColor || $[12] !== borderColor || $[13] !== borderLeftColor || $[14] !== borderRightColor || $[15] !== borderTopColor || $[16] !== themeName) {
const theme = getTheme(themeName);
resolvedBorderColor = resolveColor(borderColor, theme);
resolvedBorderTopColor = resolveColor(borderTopColor, theme);
resolvedBorderBottomColor = resolveColor(borderBottomColor, theme);
resolvedBorderLeftColor = resolveColor(borderLeftColor, theme);
resolvedBorderRightColor = resolveColor(borderRightColor, theme);
t1 = resolveColor(backgroundColor, theme);
$[10] = backgroundColor;
$[11] = borderBottomColor;
$[12] = borderColor;
$[13] = borderLeftColor;
$[14] = borderRightColor;
$[15] = borderTopColor;
$[16] = themeName;
$[17] = resolvedBorderBottomColor;
$[18] = resolvedBorderColor;
$[19] = resolvedBorderLeftColor;
$[20] = resolvedBorderRightColor;
$[21] = resolvedBorderTopColor;
$[22] = t1;
} else {
resolvedBorderBottomColor = $[17];
resolvedBorderColor = $[18];
resolvedBorderLeftColor = $[19];
resolvedBorderRightColor = $[20];
resolvedBorderTopColor = $[21];
t1 = $[22];
}
const resolvedBackgroundColor = t1;
let t2;
if ($[23] !== children || $[24] !== ref || $[25] !== resolvedBackgroundColor || $[26] !== resolvedBorderBottomColor || $[27] !== resolvedBorderColor || $[28] !== resolvedBorderLeftColor || $[29] !== resolvedBorderRightColor || $[30] !== resolvedBorderTopColor || $[31] !== rest) {
t2 = <Box ref={ref} borderColor={resolvedBorderColor} borderTopColor={resolvedBorderTopColor} borderBottomColor={resolvedBorderBottomColor} borderLeftColor={resolvedBorderLeftColor} borderRightColor={resolvedBorderRightColor} backgroundColor={resolvedBackgroundColor} {...rest}>{children}</Box>;
$[23] = children;
$[24] = ref;
$[25] = resolvedBackgroundColor;
$[26] = resolvedBorderBottomColor;
$[27] = resolvedBorderColor;
$[28] = resolvedBorderLeftColor;
$[29] = resolvedBorderRightColor;
$[30] = resolvedBorderTopColor;
$[31] = rest;
$[32] = t2;
} else {
t2 = $[32];
}
return t2;
function ThemedBox({
borderColor,
borderTopColor,
borderBottomColor,
borderLeftColor,
borderRightColor,
backgroundColor,
children,
ref,
...rest
}: PropsWithChildren<Props>): React.ReactNode {
const [themeName] = useTheme()
const theme = getTheme(themeName)
// Resolve theme keys to raw colors
const resolvedBorderColor = resolveColor(borderColor, theme)
const resolvedBorderTopColor = resolveColor(borderTopColor, theme)
const resolvedBorderBottomColor = resolveColor(borderBottomColor, theme)
const resolvedBorderLeftColor = resolveColor(borderLeftColor, theme)
const resolvedBorderRightColor = resolveColor(borderRightColor, theme)
const resolvedBackgroundColor = resolveColor(backgroundColor, theme)
return (
<Box
ref={ref}
borderColor={resolvedBorderColor}
borderTopColor={resolvedBorderTopColor}
borderBottomColor={resolvedBorderBottomColor}
borderLeftColor={resolvedBorderLeftColor}
borderRightColor={resolvedBorderRightColor}
backgroundColor={resolvedBackgroundColor}
{...rest}
>
{children}
</Box>
)
}
export default ThemedBox;
export default ThemedBox

View File

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

View File

@@ -1,236 +1,205 @@
import { c as _c } from "react/compiler-runtime";
import capitalize from 'lodash-es/capitalize.js';
import * as React from 'react';
import { useMemo } from 'react';
import { type Command, type CommandBase, type CommandResultDisplay, getCommandName, type PromptCommand } from '../../commands.js';
import { Box, Text } from '../../ink.js';
import { estimateSkillFrontmatterTokens, getSkillsPath } from '../../skills/loadSkillsDir.js';
import { getDisplayPath } from '../../utils/file.js';
import { formatTokens } from '../../utils/format.js';
import { getSettingSourceName, type SettingSource } from '../../utils/settings/constants.js';
import { plural } from '../../utils/stringUtils.js';
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
import { Dialog } from '../design-system/Dialog.js';
import capitalize from 'lodash-es/capitalize.js'
import * as React from 'react'
import { useMemo } from 'react'
import {
type Command,
type CommandBase,
type CommandResultDisplay,
getCommandName,
type PromptCommand,
} from '../../commands.js'
import { Box, Text } from '../../ink.js'
import {
estimateSkillFrontmatterTokens,
getSkillsPath,
} from '../../skills/loadSkillsDir.js'
import { getDisplayPath } from '../../utils/file.js'
import { formatTokens } from '../../utils/format.js'
import {
getSettingSourceName,
type SettingSource,
} from '../../utils/settings/constants.js'
import { plural } from '../../utils/stringUtils.js'
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
import { Dialog } from '../design-system/Dialog.js'
// Skills are always PromptCommands with CommandBase properties
type SkillCommand = CommandBase & PromptCommand;
type SkillSource = SettingSource | 'plugin' | 'mcp';
type SkillCommand = CommandBase & PromptCommand
type SkillSource = SettingSource | 'plugin' | 'mcp'
type Props = {
onExit: (result?: string, options?: {
display?: CommandResultDisplay;
}) => void;
commands: Command[];
};
onExit: (
result?: string,
options?: { display?: CommandResultDisplay },
) => void
commands: Command[]
}
function getSourceTitle(source: SkillSource): string {
if (source === 'plugin') {
return 'Plugin skills';
return 'Plugin skills'
}
if (source === 'mcp') {
return 'MCP skills';
return 'MCP skills'
}
return `${capitalize(getSettingSourceName(source))} skills`;
return `${capitalize(getSettingSourceName(source))} skills`
}
function getSourceSubtitle(source: SkillSource, skills: SkillCommand[]): string | undefined {
function getSourceSubtitle(
source: SkillSource,
skills: SkillCommand[],
): string | undefined {
// MCP skills show server names; file-based skills show filesystem paths.
// Skill names are `<server>:<skill>`, not `mcp__<server>__…`.
if (source === 'mcp') {
const servers = [...new Set(skills.map(s => {
const idx = s.name.indexOf(':');
return idx > 0 ? s.name.slice(0, idx) : null;
}).filter((n): n is string => n != null))];
return servers.length > 0 ? servers.join(', ') : undefined;
const servers = [
...new Set(
skills
.map(s => {
const idx = s.name.indexOf(':')
return idx > 0 ? s.name.slice(0, idx) : null
})
.filter((n): n is string => n != null),
),
]
return servers.length > 0 ? servers.join(', ') : undefined
}
const skillsPath = getDisplayPath(getSkillsPath(source, 'skills'));
const hasCommandsSkills = skills.some(s => s.loadedFrom === 'commands_DEPRECATED');
return hasCommandsSkills ? `${skillsPath}, ${getDisplayPath(getSkillsPath(source, 'commands'))}` : skillsPath;
const skillsPath = getDisplayPath(getSkillsPath(source, 'skills'))
const hasCommandsSkills = skills.some(
s => s.loadedFrom === 'commands_DEPRECATED',
)
return hasCommandsSkills
? `${skillsPath}, ${getDisplayPath(getSkillsPath(source, 'commands'))}`
: skillsPath
}
export function SkillsMenu(t0) {
const $ = _c(35);
const {
onExit,
commands
} = t0;
let t1;
if ($[0] !== commands) {
t1 = commands.filter(_temp);
$[0] = commands;
$[1] = t1;
} else {
t1 = $[1];
}
const skills = t1;
let groups;
if ($[2] !== skills) {
groups = {
export function SkillsMenu({ onExit, commands }: Props): React.ReactNode {
// Filter commands for skills and cast to SkillCommand
const skills = useMemo(() => {
return commands.filter(
(cmd): cmd is SkillCommand =>
cmd.type === 'prompt' &&
(cmd.loadedFrom === 'skills' ||
cmd.loadedFrom === 'commands_DEPRECATED' ||
cmd.loadedFrom === 'plugin' ||
cmd.loadedFrom === 'mcp'),
)
}, [commands])
const skillsBySource = useMemo((): Record<SkillSource, SkillCommand[]> => {
const groups: Record<SkillSource, SkillCommand[]> = {
policySettings: [],
userSettings: [],
projectSettings: [],
localSettings: [],
flagSettings: [],
plugin: [],
mcp: []
};
mcp: [],
}
for (const skill of skills) {
const source = skill.source as SkillSource;
const source = skill.source as SkillSource
if (source in groups) {
groups[source].push(skill);
groups[source].push(skill)
}
}
for (const group of Object.values(groups)) {
(group as Array<{ name: string }>).sort(_temp2);
group.sort((a, b) => getCommandName(a).localeCompare(getCommandName(b)))
}
$[2] = skills;
$[3] = groups;
} else {
groups = $[3];
return groups
}, [skills])
const handleCancel = (): void => {
onExit('Skills dialog dismissed', { display: 'system' })
}
const skillsBySource = groups;
let t2;
if ($[4] !== onExit) {
t2 = () => {
onExit("Skills dialog dismissed", {
display: "system"
});
};
$[4] = onExit;
$[5] = t2;
} else {
t2 = $[5];
}
const handleCancel = t2;
if (skills.length === 0) {
let t3;
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
t3 = <Text dimColor={true}>Create skills in .claude/skills/ or ~/.claude/skills/</Text>;
$[6] = t3;
} else {
t3 = $[6];
}
let t4;
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
t4 = <Text dimColor={true} italic={true}><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="close" /></Text>;
$[7] = t4;
} else {
t4 = $[7];
}
let t5;
if ($[8] !== handleCancel) {
t5 = <Dialog title="Skills" subtitle="No skills found" onCancel={handleCancel} hideInputGuide={true}>{t3}{t4}</Dialog>;
$[8] = handleCancel;
$[9] = t5;
} else {
t5 = $[9];
}
return t5;
return (
<Dialog
title="Skills"
subtitle="No skills found"
onCancel={handleCancel}
hideInputGuide
>
<Text dimColor>
Create skills in .claude/skills/ or ~/.claude/skills/
</Text>
<Text dimColor italic>
<ConfigurableShortcutHint
action="confirm:no"
context="Confirmation"
fallback="Esc"
description="close"
/>
</Text>
</Dialog>
)
}
const renderSkill = _temp3;
let t3;
if ($[10] !== skillsBySource) {
t3 = source_0 => {
const groupSkills = skillsBySource[source_0];
if (groupSkills.length === 0) {
return null;
}
const title = getSourceTitle(source_0);
const subtitle = getSourceSubtitle(source_0, groupSkills);
return <Box flexDirection="column" key={source_0}><Box><Text bold={true} dimColor={true}>{title}</Text>{subtitle && <Text dimColor={true}> ({subtitle})</Text>}</Box>{groupSkills.map(skill_1 => renderSkill(skill_1))}</Box>;
};
$[10] = skillsBySource;
$[11] = t3;
} else {
t3 = $[11];
const renderSkill = (skill: SkillCommand) => {
const estimatedTokens = estimateSkillFrontmatterTokens(skill)
const tokenDisplay = `~${formatTokens(estimatedTokens)}`
const pluginName =
skill.source === 'plugin'
? skill.pluginInfo?.pluginManifest.name
: undefined
return (
<Box key={`${skill.name}-${skill.source}`}>
<Text>{getCommandName(skill)}</Text>
<Text dimColor>
{pluginName ? ` · ${pluginName}` : ''} · {tokenDisplay} description
tokens
</Text>
</Box>
)
}
const renderSkillGroup = t3;
const t4 = skills.length;
let t5;
if ($[12] !== skills.length) {
t5 = plural(skills.length, "skill");
$[12] = skills.length;
$[13] = t5;
} else {
t5 = $[13];
const renderSkillGroup = (source: SkillSource) => {
const groupSkills = skillsBySource[source]
if (groupSkills.length === 0) return null
const title = getSourceTitle(source)
const subtitle = getSourceSubtitle(source, groupSkills)
return (
<Box flexDirection="column" key={source}>
<Box>
<Text bold dimColor>
{title}
</Text>
{subtitle && <Text dimColor> ({subtitle})</Text>}
</Box>
{groupSkills.map(skill => renderSkill(skill))}
</Box>
)
}
const t6 = `${t4} ${t5}`;
let t7;
if ($[14] !== renderSkillGroup) {
t7 = renderSkillGroup("projectSettings");
$[14] = renderSkillGroup;
$[15] = t7;
} else {
t7 = $[15];
}
let t8;
if ($[16] !== renderSkillGroup) {
t8 = renderSkillGroup("userSettings");
$[16] = renderSkillGroup;
$[17] = t8;
} else {
t8 = $[17];
}
let t9;
if ($[18] !== renderSkillGroup) {
t9 = renderSkillGroup("policySettings");
$[18] = renderSkillGroup;
$[19] = t9;
} else {
t9 = $[19];
}
let t10;
if ($[20] !== renderSkillGroup) {
t10 = renderSkillGroup("plugin");
$[20] = renderSkillGroup;
$[21] = t10;
} else {
t10 = $[21];
}
let t11;
if ($[22] !== renderSkillGroup) {
t11 = renderSkillGroup("mcp");
$[22] = renderSkillGroup;
$[23] = t11;
} else {
t11 = $[23];
}
let t12;
if ($[24] !== t10 || $[25] !== t11 || $[26] !== t7 || $[27] !== t8 || $[28] !== t9) {
t12 = <Box flexDirection="column" gap={1}>{t7}{t8}{t9}{t10}{t11}</Box>;
$[24] = t10;
$[25] = t11;
$[26] = t7;
$[27] = t8;
$[28] = t9;
$[29] = t12;
} else {
t12 = $[29];
}
let t13;
if ($[30] === Symbol.for("react.memo_cache_sentinel")) {
t13 = <Text dimColor={true} italic={true}><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="close" /></Text>;
$[30] = t13;
} else {
t13 = $[30];
}
let t14;
if ($[31] !== handleCancel || $[32] !== t12 || $[33] !== t6) {
t14 = <Dialog title="Skills" subtitle={t6} onCancel={handleCancel} hideInputGuide={true}>{t12}{t13}</Dialog>;
$[31] = handleCancel;
$[32] = t12;
$[33] = t6;
$[34] = t14;
} else {
t14 = $[34];
}
return t14;
}
function _temp3(skill_0) {
const estimatedTokens = estimateSkillFrontmatterTokens(skill_0);
const tokenDisplay = `~${formatTokens(estimatedTokens)}`;
const pluginName = skill_0.source === "plugin" ? skill_0.pluginInfo?.pluginManifest.name : undefined;
return <Box key={`${skill_0.name}-${skill_0.source}`}><Text>{getCommandName(skill_0)}</Text><Text dimColor={true}>{pluginName ? ` · ${pluginName}` : ""} · {tokenDisplay} description tokens</Text></Box>;
}
function _temp2(a, b) {
return getCommandName(a).localeCompare(getCommandName(b));
}
function _temp(cmd) {
return cmd.type === "prompt" && (cmd.loadedFrom === "skills" || cmd.loadedFrom === "commands_DEPRECATED" || cmd.loadedFrom === "plugin" || cmd.loadedFrom === "mcp");
return (
<Dialog
title="Skills"
subtitle={`${skills.length} ${plural(skills.length, 'skill')}`}
onCancel={handleCancel}
hideInputGuide
>
<Box flexDirection="column" gap={1}>
{renderSkillGroup('projectSettings')}
{renderSkillGroup('userSettings')}
{renderSkillGroup('policySettings')}
{renderSkillGroup('plugin')}
{renderSkillGroup('mcp')}
</Box>
<Text dimColor italic>
<ConfigurableShortcutHint
action="confirm:no"
context="Confirmation"
fallback="Esc"
description="close"
/>
</Text>
</Dialog>
)
}

View File

@@ -1,228 +1,200 @@
import { c as _c } from "react/compiler-runtime";
import React, { useMemo } from 'react';
import type { DeepImmutable } from 'src/types/utils.js';
import { useElapsedTime } from '../../hooks/useElapsedTime.js';
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
import { Box, Text, useTheme } from '../../ink.js';
import { useKeybindings } from '../../keybindings/useKeybinding.js';
import { getEmptyToolPermissionContext } from '../../Tool.js';
import type { LocalAgentTaskState } from '../../tasks/LocalAgentTask/LocalAgentTask.js';
import { getTools } from '../../tools.js';
import { formatNumber } from '../../utils/format.js';
import { extractTag } from '../../utils/messages.js';
import { Byline } from '../design-system/Byline.js';
import { Dialog } from '../design-system/Dialog.js';
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
import { UserPlanMessage } from '../messages/UserPlanMessage.js';
import { renderToolActivity } from './renderToolActivity.js';
import { getTaskStatusColor, getTaskStatusIcon } from './taskStatusUtils.js';
import React, { useMemo } from 'react'
import type { DeepImmutable } from 'src/types/utils.js'
import { useElapsedTime } from '../../hooks/useElapsedTime.js'
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
import { Box, Text, useTheme } from '../../ink.js'
import { useKeybindings } from '../../keybindings/useKeybinding.js'
import { getEmptyToolPermissionContext } from '../../Tool.js'
import type { LocalAgentTaskState } from '../../tasks/LocalAgentTask/LocalAgentTask.js'
import { getTools } from '../../tools.js'
import { formatNumber } from '../../utils/format.js'
import { extractTag } from '../../utils/messages.js'
import { Byline } from '../design-system/Byline.js'
import { Dialog } from '../design-system/Dialog.js'
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
import { UserPlanMessage } from '../messages/UserPlanMessage.js'
import { renderToolActivity } from './renderToolActivity.js'
import { getTaskStatusColor, getTaskStatusIcon } from './taskStatusUtils.js'
type Props = {
agent: DeepImmutable<LocalAgentTaskState>;
onDone: () => void;
onKillAgent?: () => void;
onBack?: () => void;
};
export function AsyncAgentDetailDialog(t0) {
const $ = _c(54);
const {
agent,
onDone,
onKillAgent,
onBack
} = t0;
const [theme] = useTheme();
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = getTools(getEmptyToolPermissionContext());
$[0] = t1;
} else {
t1 = $[0];
}
const tools = t1;
const elapsedTime = useElapsedTime(agent.startTime, agent.status === "running", 1000, agent.totalPausedMs ?? 0);
let t2;
if ($[1] !== onDone) {
t2 = {
"confirm:yes": onDone
};
$[1] = onDone;
$[2] = t2;
} else {
t2 = $[2];
}
let t3;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t3 = {
context: "Confirmation"
};
$[3] = t3;
} else {
t3 = $[3];
}
useKeybindings(t2, t3);
let t4;
if ($[4] !== agent.status || $[5] !== onBack || $[6] !== onDone || $[7] !== onKillAgent) {
t4 = e => {
if (e.key === " ") {
e.preventDefault();
onDone();
} else {
if (e.key === "left" && onBack) {
e.preventDefault();
onBack();
} else {
if (e.key === "x" && agent.status === "running" && onKillAgent) {
e.preventDefault();
onKillAgent();
}
}
}
};
$[4] = agent.status;
$[5] = onBack;
$[6] = onDone;
$[7] = onKillAgent;
$[8] = t4;
} else {
t4 = $[8];
}
const handleKeyDown = t4;
let t5;
if ($[9] !== agent.prompt) {
t5 = extractTag(agent.prompt, "plan");
$[9] = agent.prompt;
$[10] = t5;
} else {
t5 = $[10];
}
const planContent = t5;
const displayPrompt = agent.prompt.length > 300 ? agent.prompt.substring(0, 297) + "\u2026" : agent.prompt;
const tokenCount = agent.result?.totalTokens ?? agent.progress?.tokenCount;
const toolUseCount = agent.result?.totalToolUseCount ?? agent.progress?.toolUseCount;
const t6 = agent.selectedAgent?.agentType ?? "agent";
const t7 = agent.description || "Async agent";
let t8;
if ($[11] !== t6 || $[12] !== t7) {
t8 = <Text>{t6} {" "}{t7}</Text>;
$[11] = t6;
$[12] = t7;
$[13] = t8;
} else {
t8 = $[13];
}
const title = t8;
let t9;
if ($[14] !== agent.status) {
t9 = agent.status !== "running" && <Text color={getTaskStatusColor(agent.status)}>{getTaskStatusIcon(agent.status)}{" "}{agent.status === "completed" ? "Completed" : agent.status === "failed" ? "Failed" : "Stopped"}{" \xB7 "}</Text>;
$[14] = agent.status;
$[15] = t9;
} else {
t9 = $[15];
}
let t10;
if ($[16] !== tokenCount) {
t10 = tokenCount !== undefined && tokenCount > 0 && <> · {formatNumber(tokenCount)} tokens</>;
$[16] = tokenCount;
$[17] = t10;
} else {
t10 = $[17];
}
let t11;
if ($[18] !== toolUseCount) {
t11 = toolUseCount !== undefined && toolUseCount > 0 && <>{" "}· {toolUseCount} {toolUseCount === 1 ? "tool" : "tools"}</>;
$[18] = toolUseCount;
$[19] = t11;
} else {
t11 = $[19];
}
let t12;
if ($[20] !== elapsedTime || $[21] !== t10 || $[22] !== t11) {
t12 = <Text dimColor={true}>{elapsedTime}{t10}{t11}</Text>;
$[20] = elapsedTime;
$[21] = t10;
$[22] = t11;
$[23] = t12;
} else {
t12 = $[23];
}
let t13;
if ($[24] !== t12 || $[25] !== t9) {
t13 = <Text>{t9}{t12}</Text>;
$[24] = t12;
$[25] = t9;
$[26] = t13;
} else {
t13 = $[26];
}
const subtitle = t13;
let t14;
if ($[27] !== agent.status || $[28] !== onBack || $[29] !== onKillAgent) {
t14 = exitState => exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline>{onBack && <KeyboardShortcutHint shortcut={"\u2190"} action="go back" />}<KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />{agent.status === "running" && onKillAgent && <KeyboardShortcutHint shortcut="x" action="stop" />}</Byline>;
$[27] = agent.status;
$[28] = onBack;
$[29] = onKillAgent;
$[30] = t14;
} else {
t14 = $[30];
}
let t15;
if ($[31] !== agent.progress || $[32] !== agent.status || $[33] !== theme) {
t15 = agent.status === "running" && agent.progress?.recentActivities && agent.progress.recentActivities.length > 0 && <Box flexDirection="column"><Text bold={true} dimColor={true}>Progress</Text>{agent.progress.recentActivities.map((activity, i) => <Text key={i} dimColor={i < agent.progress.recentActivities.length - 1} wrap="truncate-end">{i === agent.progress.recentActivities.length - 1 ? "\u203A " : " "}{renderToolActivity(activity, tools, theme)}</Text>)}</Box>;
$[31] = agent.progress;
$[32] = agent.status;
$[33] = theme;
$[34] = t15;
} else {
t15 = $[34];
}
let t16;
if ($[35] !== displayPrompt || $[36] !== planContent) {
t16 = planContent ? <Box marginTop={1}><UserPlanMessage addMargin={false} planContent={planContent} /></Box> : <Box flexDirection="column" marginTop={1}><Text bold={true} dimColor={true}>Prompt</Text><Text wrap="wrap">{displayPrompt}</Text></Box>;
$[35] = displayPrompt;
$[36] = planContent;
$[37] = t16;
} else {
t16 = $[37];
}
let t17;
if ($[38] !== agent.error || $[39] !== agent.status) {
t17 = agent.status === "failed" && agent.error && <Box flexDirection="column" marginTop={1}><Text bold={true} color="error">Error</Text><Text color="error" wrap="wrap">{agent.error}</Text></Box>;
$[38] = agent.error;
$[39] = agent.status;
$[40] = t17;
} else {
t17 = $[40];
}
let t18;
if ($[41] !== t15 || $[42] !== t16 || $[43] !== t17) {
t18 = <Box flexDirection="column">{t15}{t16}{t17}</Box>;
$[41] = t15;
$[42] = t16;
$[43] = t17;
$[44] = t18;
} else {
t18 = $[44];
}
let t19;
if ($[45] !== onDone || $[46] !== subtitle || $[47] !== t14 || $[48] !== t18 || $[49] !== title) {
t19 = <Dialog title={title} subtitle={subtitle} onCancel={onDone} color="background" inputGuide={t14}>{t18}</Dialog>;
$[45] = onDone;
$[46] = subtitle;
$[47] = t14;
$[48] = t18;
$[49] = title;
$[50] = t19;
} else {
t19 = $[50];
}
let t20;
if ($[51] !== handleKeyDown || $[52] !== t19) {
t20 = <Box flexDirection="column" tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t19}</Box>;
$[51] = handleKeyDown;
$[52] = t19;
$[53] = t20;
} else {
t20 = $[53];
}
return t20;
agent: DeepImmutable<LocalAgentTaskState>
onDone: () => void
onKillAgent?: () => void
onBack?: () => void
}
export function AsyncAgentDetailDialog({
agent,
onDone,
onKillAgent,
onBack,
}: Props): React.ReactNode {
const [theme] = useTheme()
// Get tools for rendering activity messages
const tools = useMemo(() => getTools(getEmptyToolPermissionContext()), [])
const elapsedTime = useElapsedTime(
agent.startTime,
agent.status === 'running',
1000,
agent.totalPausedMs ?? 0,
)
// Restore confirm:yes (Enter/y) dismissal — Dialog handles confirm:no (Esc)
// internally but does NOT auto-wire confirm:yes.
useKeybindings(
{
'confirm:yes': onDone,
},
{ context: 'Confirmation' },
)
// Component-specific shortcuts shown in UI hints (x=stop) and
// navigation keys (space=dismiss, left=back). These are context-dependent
// actions tied to agent state, not standard dialog keybindings.
// Note: Dialog component already handles ESC via confirm:no keybinding;
// confirm:yes (Enter/y) is handled by useKeybindings above.
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === ' ') {
e.preventDefault()
onDone()
} else if (e.key === 'left' && onBack) {
e.preventDefault()
onBack()
} else if (e.key === 'x' && agent.status === 'running' && onKillAgent) {
e.preventDefault()
onKillAgent()
}
}
// Extract plan from prompt - if present, we show the plan instead of the prompt
const planContent = extractTag(agent.prompt, 'plan')
const displayPrompt =
agent.prompt.length > 300
? agent.prompt.substring(0, 297) + '…'
: agent.prompt
// Get tokens and tool uses (from result if completed, otherwise from progress)
const tokenCount = agent.result?.totalTokens ?? agent.progress?.tokenCount
const toolUseCount =
agent.result?.totalToolUseCount ?? agent.progress?.toolUseCount
const title = (
<Text>
{agent.selectedAgent?.agentType ?? 'agent'} {' '}
{agent.description || 'Async agent'}
</Text>
)
// Build subtitle with status and stats
const subtitle = (
<Text>
{agent.status !== 'running' && (
<Text color={getTaskStatusColor(agent.status)}>
{getTaskStatusIcon(agent.status)}{' '}
{agent.status === 'completed'
? 'Completed'
: agent.status === 'failed'
? 'Failed'
: 'Stopped'}
{' · '}
</Text>
)}
<Text dimColor>
{elapsedTime}
{tokenCount !== undefined && tokenCount > 0 && (
<> · {formatNumber(tokenCount)} tokens</>
)}
{toolUseCount !== undefined && toolUseCount > 0 && (
<>
{' '}
· {toolUseCount} {toolUseCount === 1 ? 'tool' : 'tools'}
</>
)}
</Text>
</Text>
)
return (
<Box
flexDirection="column"
tabIndex={0}
autoFocus
onKeyDown={handleKeyDown}
>
<Dialog
title={title}
subtitle={subtitle}
onCancel={onDone}
color="background"
inputGuide={exitState =>
exitState.pending ? (
<Text>Press {exitState.keyName} again to exit</Text>
) : (
<Byline>
{onBack && <KeyboardShortcutHint shortcut="←" action="go back" />}
<KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />
{agent.status === 'running' && onKillAgent && (
<KeyboardShortcutHint shortcut="x" action="stop" />
)}
</Byline>
)
}
>
<Box flexDirection="column">
{/* Recent activities for running agents */}
{agent.status === 'running' &&
agent.progress?.recentActivities &&
agent.progress.recentActivities.length > 0 && (
<Box flexDirection="column">
<Text bold dimColor>
Progress
</Text>
{agent.progress.recentActivities.map((activity, i) => (
<Text
key={i}
dimColor={i < agent.progress!.recentActivities!.length - 1}
wrap="truncate-end"
>
{i === agent.progress!.recentActivities!.length - 1
? ' '
: ' '}
{renderToolActivity(activity, tools, theme)}
</Text>
))}
</Box>
)}
{/* Plan section (if present) - shown instead of prompt */}
{planContent ? (
<Box marginTop={1}>
<UserPlanMessage addMargin={false} planContent={planContent} />
</Box>
) : (
/* Prompt section - only shown when no plan */
<Box flexDirection="column" marginTop={1}>
<Text bold dimColor>
Prompt
</Text>
<Text wrap="wrap">{displayPrompt}</Text>
</Box>
)}
{/* Error details if failed */}
{agent.status === 'failed' && agent.error && (
<Box flexDirection="column" marginTop={1}>
<Text bold color="error">
Error
</Text>
<Text color="error" wrap="wrap">
{agent.error}
</Text>
</Box>
)}
</Box>
</Dialog>
</Box>
)
}

View File

@@ -1,344 +1,146 @@
import { c as _c } from "react/compiler-runtime";
import * as React from 'react';
import { Text } from 'src/ink.js';
import type { BackgroundTaskState } from 'src/tasks/types.js';
import type { DeepImmutable } from 'src/types/utils.js';
import { truncate } from 'src/utils/format.js';
import { toInkColor } from 'src/utils/ink.js';
import { plural } from 'src/utils/stringUtils.js';
import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js';
import { RemoteSessionProgress } from './RemoteSessionProgress.js';
import { ShellProgress, TaskStatusText } from './ShellProgress.js';
import { describeTeammateActivity } from './taskStatusUtils.js';
import * as React from 'react'
import { Text } from 'src/ink.js'
import type { BackgroundTaskState } from 'src/tasks/types.js'
import type { DeepImmutable } from 'src/types/utils.js'
import { truncate } from 'src/utils/format.js'
import { toInkColor } from 'src/utils/ink.js'
import { plural } from 'src/utils/stringUtils.js'
import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js'
import { RemoteSessionProgress } from './RemoteSessionProgress.js'
import { ShellProgress, TaskStatusText } from './ShellProgress.js'
import { describeTeammateActivity } from './taskStatusUtils.js'
type Props = {
task: DeepImmutable<BackgroundTaskState>;
maxActivityWidth?: number;
};
export function BackgroundTask(t0) {
const $ = _c(92);
const {
task,
maxActivityWidth
} = t0;
const activityLimit = maxActivityWidth ?? 40;
task: DeepImmutable<BackgroundTaskState>
maxActivityWidth?: number
}
export function BackgroundTask({
task,
maxActivityWidth,
}: Props): React.ReactNode {
const activityLimit = maxActivityWidth ?? 40
switch (task.type) {
case "local_bash":
{
const t1 = task.kind === "monitor" ? task.description : task.command;
let t2;
if ($[0] !== activityLimit || $[1] !== t1) {
t2 = truncate(t1, activityLimit, true);
$[0] = activityLimit;
$[1] = t1;
$[2] = t2;
} else {
t2 = $[2];
}
let t3;
if ($[3] !== task) {
t3 = <ShellProgress shell={task} />;
$[3] = task;
$[4] = t3;
} else {
t3 = $[4];
}
let t4;
if ($[5] !== t2 || $[6] !== t3) {
t4 = <Text>{t2}{" "}{t3}</Text>;
$[5] = t2;
$[6] = t3;
$[7] = t4;
} else {
t4 = $[7];
}
return t4;
}
case "remote_agent":
{
if (task.isRemoteReview) {
let t1;
if ($[8] !== task) {
t1 = <Text><RemoteSessionProgress session={task} /></Text>;
$[8] = task;
$[9] = t1;
} else {
t1 = $[9];
}
return t1;
}
const running = task.status === "running" || task.status === "pending";
const t1 = running ? DIAMOND_OPEN : DIAMOND_FILLED;
let t2;
if ($[10] !== t1) {
t2 = <Text dimColor={true}>{t1} </Text>;
$[10] = t1;
$[11] = t2;
} else {
t2 = $[11];
}
let t3;
if ($[12] !== activityLimit || $[13] !== task.title) {
t3 = truncate(task.title, activityLimit, true);
$[12] = activityLimit;
$[13] = task.title;
$[14] = t3;
} else {
t3 = $[14];
}
let t4;
if ($[15] === Symbol.for("react.memo_cache_sentinel")) {
t4 = <Text dimColor={true}> · </Text>;
$[15] = t4;
} else {
t4 = $[15];
}
let t5;
if ($[16] !== task) {
t5 = <RemoteSessionProgress session={task} />;
$[16] = task;
$[17] = t5;
} else {
t5 = $[17];
}
let t6;
if ($[18] !== t2 || $[19] !== t3 || $[20] !== t5) {
t6 = <Text>{t2}{t3}{t4}{t5}</Text>;
$[18] = t2;
$[19] = t3;
$[20] = t5;
$[21] = t6;
} else {
t6 = $[21];
}
return t6;
}
case "local_agent":
{
let t1;
if ($[22] !== activityLimit || $[23] !== task.description) {
t1 = truncate(task.description, activityLimit, true);
$[22] = activityLimit;
$[23] = task.description;
$[24] = t1;
} else {
t1 = $[24];
}
const t2 = task.status === "completed" ? "done" : undefined;
const t3 = task.status === "completed" && !task.notified ? ", unread" : undefined;
let t4;
if ($[25] !== t2 || $[26] !== t3 || $[27] !== task.status) {
t4 = <TaskStatusText status={task.status} label={t2} suffix={t3} />;
$[25] = t2;
$[26] = t3;
$[27] = task.status;
$[28] = t4;
} else {
t4 = $[28];
}
let t5;
if ($[29] !== t1 || $[30] !== t4) {
t5 = <Text>{t1}{" "}{t4}</Text>;
$[29] = t1;
$[30] = t4;
$[31] = t5;
} else {
t5 = $[31];
}
return t5;
}
case "in_process_teammate":
{
let T0;
let T1;
let t1;
let t2;
let t3;
let t4;
if ($[32] !== activityLimit || $[33] !== task) {
const activity = describeTeammateActivity(task);
T1 = Text;
let t5;
if ($[40] !== task.identity.color) {
t5 = toInkColor(task.identity.color);
$[40] = task.identity.color;
$[41] = t5;
} else {
t5 = $[41];
}
if ($[42] !== t5 || $[43] !== task.identity.agentName) {
t4 = <Text color={t5}>@{task.identity.agentName}</Text>;
$[42] = t5;
$[43] = task.identity.agentName;
$[44] = t4;
} else {
t4 = $[44];
}
T0 = Text;
t1 = true;
t2 = ": ";
t3 = truncate(activity, activityLimit, true);
$[32] = activityLimit;
$[33] = task;
$[34] = T0;
$[35] = T1;
$[36] = t1;
$[37] = t2;
$[38] = t3;
$[39] = t4;
} else {
T0 = $[34];
T1 = $[35];
t1 = $[36];
t2 = $[37];
t3 = $[38];
t4 = $[39];
}
let t5;
if ($[45] !== T0 || $[46] !== t1 || $[47] !== t2 || $[48] !== t3) {
t5 = <T0 dimColor={t1}>{t2}{t3}</T0>;
$[45] = T0;
$[46] = t1;
$[47] = t2;
$[48] = t3;
$[49] = t5;
} else {
t5 = $[49];
}
let t6;
if ($[50] !== T1 || $[51] !== t4 || $[52] !== t5) {
t6 = <T1>{t4}{t5}</T1>;
$[50] = T1;
$[51] = t4;
$[52] = t5;
$[53] = t6;
} else {
t6 = $[53];
}
return t6;
}
case "local_workflow":
{
const t1 = task.workflowName ?? task.summary ?? task.description;
let t2;
if ($[54] !== activityLimit || $[55] !== t1) {
t2 = truncate(t1, activityLimit, true);
$[54] = activityLimit;
$[55] = t1;
$[56] = t2;
} else {
t2 = $[56];
}
let t3;
if ($[57] !== task.agentCount || $[58] !== task.status) {
t3 = task.status === "running" ? `${task.agentCount} ${plural(task.agentCount, "agent")}` : task.status === "completed" ? "done" : undefined;
$[57] = task.agentCount;
$[58] = task.status;
$[59] = t3;
} else {
t3 = $[59];
}
const t4 = task.status === "completed" && !task.notified ? ", unread" : undefined;
let t5;
if ($[60] !== t3 || $[61] !== t4 || $[62] !== task.status) {
t5 = <TaskStatusText status={task.status} label={t3} suffix={t4} />;
$[60] = t3;
$[61] = t4;
$[62] = task.status;
$[63] = t5;
} else {
t5 = $[63];
}
let t6;
if ($[64] !== t2 || $[65] !== t5) {
t6 = <Text>{t2}{" "}{t5}</Text>;
$[64] = t2;
$[65] = t5;
$[66] = t6;
} else {
t6 = $[66];
}
return t6;
}
case "monitor_mcp":
{
let t1;
if ($[67] !== activityLimit || $[68] !== task.description) {
t1 = truncate(task.description, activityLimit, true);
$[67] = activityLimit;
$[68] = task.description;
$[69] = t1;
} else {
t1 = $[69];
}
const t2 = task.status === "completed" ? "done" : undefined;
const t3 = task.status === "completed" && !task.notified ? ", unread" : undefined;
let t4;
if ($[70] !== t2 || $[71] !== t3 || $[72] !== task.status) {
t4 = <TaskStatusText status={task.status} label={t2} suffix={t3} />;
$[70] = t2;
$[71] = t3;
$[72] = task.status;
$[73] = t4;
} else {
t4 = $[73];
}
let t5;
if ($[74] !== t1 || $[75] !== t4) {
t5 = <Text>{t1}{" "}{t4}</Text>;
$[74] = t1;
$[75] = t4;
$[76] = t5;
} else {
t5 = $[76];
}
return t5;
}
case "dream":
{
const n = task.filesTouched.length;
let t1;
if ($[77] !== n || $[78] !== task.phase || $[79] !== task.sessionsReviewing) {
t1 = task.phase === "updating" && n > 0 ? `${n} ${plural(n, "file")}` : `${task.sessionsReviewing} ${plural(task.sessionsReviewing, "session")}`;
$[77] = n;
$[78] = task.phase;
$[79] = task.sessionsReviewing;
$[80] = t1;
} else {
t1 = $[80];
}
const detail = t1;
let t2;
if ($[81] !== detail || $[82] !== task.phase) {
t2 = <Text dimColor={true}>· {task.phase} · {detail}</Text>;
$[81] = detail;
$[82] = task.phase;
$[83] = t2;
} else {
t2 = $[83];
}
const t3 = task.status === "completed" ? "done" : undefined;
const t4 = task.status === "completed" && !task.notified ? ", unread" : undefined;
let t5;
if ($[84] !== t3 || $[85] !== t4 || $[86] !== task.status) {
t5 = <TaskStatusText status={task.status} label={t3} suffix={t4} />;
$[84] = t3;
$[85] = t4;
$[86] = task.status;
$[87] = t5;
} else {
t5 = $[87];
}
let t6;
if ($[88] !== t2 || $[89] !== t5 || $[90] !== task.description) {
t6 = <Text>{task.description}{" "}{t2}{" "}{t5}</Text>;
$[88] = t2;
$[89] = t5;
$[90] = task.description;
$[91] = t6;
} else {
t6 = $[91];
}
return t6;
case 'local_bash':
return (
<Text>
{truncate(
task.kind === 'monitor' ? task.description : task.command,
activityLimit,
true,
)}{' '}
<ShellProgress shell={task} />
</Text>
)
case 'remote_agent': {
// Lite-review renders its own rainbow line (title + live counts),
// so we don't prefix the title — the rainbow already includes it.
if (task.isRemoteReview) {
return (
<Text>
<RemoteSessionProgress session={task} />
</Text>
)
}
const running = task.status === 'running' || task.status === 'pending'
return (
<Text>
<Text dimColor>{running ? DIAMOND_OPEN : DIAMOND_FILLED} </Text>
{truncate(task.title, activityLimit, true)}
<Text dimColor> · </Text>
<RemoteSessionProgress session={task} />
</Text>
)
}
case 'local_agent':
return (
<Text>
{truncate(task.description, activityLimit, true)}{' '}
<TaskStatusText
status={task.status}
label={task.status === 'completed' ? 'done' : undefined}
suffix={
task.status === 'completed' && !task.notified
? ', unread'
: undefined
}
/>
</Text>
)
case 'in_process_teammate': {
const activity = describeTeammateActivity(task)
return (
<Text>
<Text color={toInkColor(task.identity.color)}>
@{task.identity.agentName}
</Text>
<Text dimColor>: {truncate(activity, activityLimit, true)}</Text>
</Text>
)
}
case 'local_workflow':
return (
<Text>
{truncate(
task.workflowName ?? task.summary ?? task.description,
activityLimit,
true,
)}{' '}
<TaskStatusText
status={task.status}
label={
task.status === 'running'
? `${task.agentCount} ${plural(task.agentCount, 'agent')}`
: task.status === 'completed'
? 'done'
: undefined
}
suffix={
task.status === 'completed' && !task.notified
? ', unread'
: undefined
}
/>
</Text>
)
case 'monitor_mcp':
return (
<Text>
{truncate(task.description, activityLimit, true)}{' '}
<TaskStatusText
status={task.status}
label={task.status === 'completed' ? 'done' : undefined}
suffix={
task.status === 'completed' && !task.notified
? ', unread'
: undefined
}
/>
</Text>
)
case 'dream': {
const n = task.filesTouched.length
const detail =
task.phase === 'updating' && n > 0
? `${n} ${plural(n, 'file')}`
: `${task.sessionsReviewing} ${plural(task.sessionsReviewing, 'session')}`
return (
<Text>
{task.description}{' '}
<Text dimColor>
· {task.phase} · {detail}
</Text>{' '}
<TaskStatusText
status={task.status}
label={task.status === 'completed' ? 'done' : undefined}
suffix={
task.status === 'completed' && !task.notified
? ', unread'
: undefined
}
/>
</Text>
)
}
}
}

View File

@@ -1,428 +1,310 @@
import { c as _c } from "react/compiler-runtime";
import figures from 'figures';
import * as React from 'react';
import { useMemo, useState } from 'react';
import { useTerminalSize } from 'src/hooks/useTerminalSize.js';
import { stringWidth } from 'src/ink/stringWidth.js';
import { useAppState, useSetAppState } from 'src/state/AppState.js';
import { enterTeammateView, exitTeammateView } from 'src/state/teammateViewHelpers.js';
import { isPanelAgentTask } from 'src/tasks/LocalAgentTask/LocalAgentTask.js';
import { getPillLabel, pillNeedsCta } from 'src/tasks/pillLabel.js';
import { type BackgroundTaskState, isBackgroundTask, type TaskState } from 'src/tasks/types.js';
import { calculateHorizontalScrollWindow } from 'src/utils/horizontalScroll.js';
import { Box, Text } from '../../ink.js';
import { AGENT_COLOR_TO_THEME_COLOR, AGENT_COLORS, type AgentColorName } from '../../tools/AgentTool/agentColorManager.js';
import type { Theme } from '../../utils/theme.js';
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
import { shouldHideTasksFooter } from './taskStatusUtils.js';
import figures from 'figures'
import * as React from 'react'
import { useMemo, useState } from 'react'
import { useTerminalSize } from 'src/hooks/useTerminalSize.js'
import { stringWidth } from 'src/ink/stringWidth.js'
import { useAppState, useSetAppState } from 'src/state/AppState.js'
import {
enterTeammateView,
exitTeammateView,
} from 'src/state/teammateViewHelpers.js'
import { isPanelAgentTask } from 'src/tasks/LocalAgentTask/LocalAgentTask.js'
import { getPillLabel, pillNeedsCta } from 'src/tasks/pillLabel.js'
import {
type BackgroundTaskState,
isBackgroundTask,
type TaskState,
} from 'src/tasks/types.js'
import { calculateHorizontalScrollWindow } from 'src/utils/horizontalScroll.js'
import { Box, Text } from '../../ink.js'
import {
AGENT_COLOR_TO_THEME_COLOR,
AGENT_COLORS,
type AgentColorName,
} from '../../tools/AgentTool/agentColorManager.js'
import type { Theme } from '../../utils/theme.js'
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
import { shouldHideTasksFooter } from './taskStatusUtils.js'
type Props = {
tasksSelected: boolean;
isViewingTeammate?: boolean;
teammateFooterIndex?: number;
isLeaderIdle?: boolean;
onOpenDialog?: (taskId?: string) => void;
};
export function BackgroundTaskStatus(t0) {
const $ = _c(48);
const {
tasksSelected,
isViewingTeammate,
teammateFooterIndex: t1,
isLeaderIdle: t2,
onOpenDialog
} = t0;
const teammateFooterIndex = t1 === undefined ? 0 : t1;
const isLeaderIdle = t2 === undefined ? false : t2;
const setAppState = useSetAppState();
const {
columns
} = useTerminalSize();
const tasks = useAppState(_temp);
const viewingAgentTaskId = useAppState(_temp2);
let t3;
if ($[0] !== tasks) {
t3 = (Object.values(tasks ?? {}) as TaskState[]).filter(_temp3);
$[0] = tasks;
$[1] = t3;
} else {
t3 = $[1];
}
const runningTasks = t3;
const expandedView = useAppState(_temp4);
const showSpinnerTree = expandedView === "teammates";
const allTeammates = !showSpinnerTree && runningTasks.length > 0 && runningTasks.every(_temp5);
let t4;
if ($[2] !== runningTasks) {
t4 = runningTasks.filter(_temp6).sort(_temp7);
$[2] = runningTasks;
$[3] = t4;
} else {
t4 = $[3];
}
const teammateEntries = t4;
let t5;
if ($[4] !== isLeaderIdle) {
t5 = {
name: "main",
tasksSelected: boolean
isViewingTeammate?: boolean
teammateFooterIndex?: number
isLeaderIdle?: boolean
onOpenDialog?: (taskId?: string) => void
}
export function BackgroundTaskStatus({
tasksSelected,
isViewingTeammate,
teammateFooterIndex = 0,
isLeaderIdle = false,
onOpenDialog,
}: Props): React.ReactNode {
const setAppState = useSetAppState()
const { columns } = useTerminalSize()
const tasks = useAppState(s => s.tasks)
const viewingAgentTaskId = useAppState(s => s.viewingAgentTaskId)
const runningTasks = useMemo(
() =>
(Object.values(tasks ?? {}) as TaskState[]).filter(
t =>
isBackgroundTask(t) &&
!(process.env.USER_TYPE === 'ant' && isPanelAgentTask(t)),
),
[tasks],
)
// Check if all tasks are in-process teammates (team mode)
// In spinner-tree mode, don't show teammate pills (teammates appear in the spinner tree)
const expandedView = useAppState(s => s.expandedView)
const showSpinnerTree = expandedView === 'teammates'
const allTeammates =
!showSpinnerTree &&
runningTasks.length > 0 &&
runningTasks.every(t => t.type === 'in_process_teammate')
// Memoize teammate-related computations at the top level (rules of hooks)
const teammateEntries = useMemo(
() =>
runningTasks
.filter(
(t): t is BackgroundTaskState & { type: 'in_process_teammate' } =>
t.type === 'in_process_teammate',
)
.sort((a, b) =>
a.identity.agentName.localeCompare(b.identity.agentName),
),
[runningTasks],
)
// Build array of all pills with their activity state
// Each pill is "@{name}" and separator is " " (1 char)
// Sort idle agents to the end, but only when not in selection mode
// to avoid reordering while user is arrowing through the list
// "main" always stays first regardless of idle state
const allPills = useMemo(() => {
const mainPill = {
name: 'main',
color: undefined as keyof Theme | undefined,
isIdle: isLeaderIdle,
taskId: undefined as string | undefined
};
$[4] = isLeaderIdle;
$[5] = t5;
} else {
t5 = $[5];
}
const mainPill = t5;
let t6;
if ($[6] !== mainPill || $[7] !== tasksSelected || $[8] !== teammateEntries) {
const teammatePills = teammateEntries.map(_temp8);
taskId: undefined as string | undefined,
}
const teammatePills = teammateEntries.map(t => ({
name: t.identity.agentName,
color: getAgentThemeColor(t.identity.color),
isIdle: t.isIdle,
taskId: t.id,
}))
// Only sort teammates when not selecting to avoid reordering during navigation
if (!tasksSelected) {
teammatePills.sort(_temp9);
teammatePills.sort((a, b) => {
// Active agents first, idle agents last
if (a.isIdle !== b.isIdle) return a.isIdle ? 1 : -1
return 0 // Keep original order within each group
})
}
const pills = [mainPill, ...teammatePills];
t6 = pills.map(_temp0);
$[6] = mainPill;
$[7] = tasksSelected;
$[8] = teammateEntries;
$[9] = t6;
} else {
t6 = $[9];
}
const allPills = t6;
let t7;
if ($[10] !== allPills) {
t7 = allPills.map(_temp1);
$[10] = allPills;
$[11] = t7;
} else {
t7 = $[11];
}
const pillWidths = t7;
if (allTeammates || !showSpinnerTree && isViewingTeammate) {
const selectedIdx = tasksSelected ? teammateFooterIndex : -1;
let t8;
if ($[12] !== teammateEntries || $[13] !== viewingAgentTaskId) {
t8 = viewingAgentTaskId ? teammateEntries.findIndex(t_3 => t_3.id === viewingAgentTaskId) + 1 : 0;
$[12] = teammateEntries;
$[13] = viewingAgentTaskId;
$[14] = t8;
} else {
t8 = $[14];
}
const viewedIdx = t8;
const availableWidth = Math.max(20, columns - 20 - 4);
const t9 = selectedIdx >= 0 ? selectedIdx : 0;
let t10;
if ($[15] !== availableWidth || $[16] !== pillWidths || $[17] !== t9) {
t10 = calculateHorizontalScrollWindow(pillWidths, availableWidth, 2, t9);
$[15] = availableWidth;
$[16] = pillWidths;
$[17] = t9;
$[18] = t10;
} else {
t10 = $[18];
}
const {
startIndex,
endIndex,
showLeftArrow,
showRightArrow
} = t10;
let t11;
if ($[19] !== allPills || $[20] !== endIndex || $[21] !== startIndex) {
t11 = allPills.slice(startIndex, endIndex);
$[19] = allPills;
$[20] = endIndex;
$[21] = startIndex;
$[22] = t11;
} else {
t11 = $[22];
}
const visiblePills = t11;
let t12;
if ($[23] !== showLeftArrow) {
t12 = showLeftArrow && <Text dimColor={true}>{figures.arrowLeft} </Text>;
$[23] = showLeftArrow;
$[24] = t12;
} else {
t12 = $[24];
}
let t13;
if ($[25] !== selectedIdx || $[26] !== setAppState || $[27] !== viewedIdx || $[28] !== visiblePills) {
t13 = visiblePills.map((pill_1, i_1) => {
const needsSeparator = i_1 > 0;
return <React.Fragment key={pill_1.name}>{needsSeparator && <Text> </Text>}<AgentPill name={pill_1.name} color={pill_1.color} isSelected={selectedIdx === pill_1.idx} isViewed={viewedIdx === pill_1.idx} isIdle={pill_1.isIdle} onClick={() => pill_1.taskId ? enterTeammateView(pill_1.taskId, setAppState) : exitTeammateView(setAppState)} /></React.Fragment>;
});
$[25] = selectedIdx;
$[26] = setAppState;
$[27] = viewedIdx;
$[28] = visiblePills;
$[29] = t13;
} else {
t13 = $[29];
}
let t14;
if ($[30] !== showRightArrow) {
t14 = showRightArrow && <Text dimColor={true}> {figures.arrowRight}</Text>;
$[30] = showRightArrow;
$[31] = t14;
} else {
t14 = $[31];
}
let t15;
if ($[32] === Symbol.for("react.memo_cache_sentinel")) {
t15 = <Text dimColor={true}>{" \xB7 "}<KeyboardShortcutHint shortcut={"shift + \u2193"} action="expand" /></Text>;
$[32] = t15;
} else {
t15 = $[32];
}
let t16;
if ($[33] !== t12 || $[34] !== t13 || $[35] !== t14) {
t16 = <>{t12}{t13}{t14}{t15}</>;
$[33] = t12;
$[34] = t13;
$[35] = t14;
$[36] = t16;
} else {
t16 = $[36];
}
return t16;
// main always first, then sorted teammates
const pills = [mainPill, ...teammatePills]
// Add idx after sorting
return pills.map((pill, i) => ({ ...pill, idx: i }))
}, [teammateEntries, isLeaderIdle, tasksSelected])
// Calculate pill widths (including separator space, except first)
const pillWidths = useMemo(
() =>
allPills.map((pill, i) => {
const pillText = `@${pill.name}`
// First pill has no leading space, others have 1 space separator
return stringWidth(pillText) + (i > 0 ? 1 : 0)
}),
[allPills],
)
if (allTeammates || (!showSpinnerTree && isViewingTeammate)) {
const selectedIdx = tasksSelected ? teammateFooterIndex : -1
// Which agent is currently foregrounded (bold)
const viewedIdx = viewingAgentTaskId
? teammateEntries.findIndex(t => t.id === viewingAgentTaskId) + 1
: 0 // 0 = main/leader
// Calculate available width for pills
// Reserve space for: arrows, hint, and minimal padding
// Pills are rendered on their own line when in team mode
const ARROW_WIDTH = 2 // arrow char + space
const HINT_WIDTH = 20 // shift+↓ to expand
const PADDING = 4 // minimal safety margin
const availableWidth = Math.max(20, columns - HINT_WIDTH - PADDING)
// Calculate visible window of pills
const { startIndex, endIndex, showLeftArrow, showRightArrow } =
calculateHorizontalScrollWindow(
pillWidths,
availableWidth,
ARROW_WIDTH,
selectedIdx >= 0 ? selectedIdx : 0,
)
const visiblePills = allPills.slice(startIndex, endIndex)
return (
<>
{showLeftArrow && <Text dimColor>{figures.arrowLeft} </Text>}
{visiblePills.map((pill, i) => {
// First visible pill has no leading separator
// (left arrow already provides spacing if present)
const needsSeparator = i > 0
return (
<React.Fragment key={pill.name}>
{needsSeparator && <Text> </Text>}
<AgentPill
name={pill.name}
color={pill.color}
isSelected={selectedIdx === pill.idx}
isViewed={viewedIdx === pill.idx}
isIdle={pill.isIdle}
onClick={() =>
pill.taskId
? enterTeammateView(pill.taskId, setAppState)
: exitTeammateView(setAppState)
}
/>
</React.Fragment>
)
})}
{showRightArrow && <Text dimColor> {figures.arrowRight}</Text>}
<Text dimColor>
{' · '}
<KeyboardShortcutHint shortcut="shift + ↓" action="expand" />
</Text>
</>
)
}
// In spinner-tree mode, don't show any footer status for teammates
// (they appear in the spinner tree above)
if (shouldHideTasksFooter(tasks ?? {}, showSpinnerTree)) {
return null;
return null
}
if (runningTasks.length === 0) {
return null;
return null
}
let t8;
if ($[37] !== runningTasks) {
t8 = getPillLabel(runningTasks);
$[37] = runningTasks;
$[38] = t8;
} else {
t8 = $[38];
}
let t9;
if ($[39] !== onOpenDialog || $[40] !== t8 || $[41] !== tasksSelected) {
t9 = <SummaryPill selected={tasksSelected} onClick={onOpenDialog}>{t8}</SummaryPill>;
$[39] = onOpenDialog;
$[40] = t8;
$[41] = tasksSelected;
$[42] = t9;
} else {
t9 = $[42];
}
let t10;
if ($[43] !== runningTasks) {
t10 = pillNeedsCta(runningTasks) && <Text dimColor={true}> · {figures.arrowDown} to view</Text>;
$[43] = runningTasks;
$[44] = t10;
} else {
t10 = $[44];
}
let t11;
if ($[45] !== t10 || $[46] !== t9) {
t11 = <>{t9}{t10}</>;
$[45] = t10;
$[46] = t9;
$[47] = t11;
} else {
t11 = $[47];
}
return t11;
}
function _temp1(pill_0, i_0) {
const pillText = `@${pill_0.name}`;
return stringWidth(pillText) + (i_0 > 0 ? 1 : 0);
}
function _temp0(pill, i) {
return {
...pill,
idx: i
};
}
function _temp9(a_0, b_0) {
if (a_0.isIdle !== b_0.isIdle) {
return a_0.isIdle ? 1 : -1;
}
return 0;
}
function _temp8(t_2) {
return {
name: t_2.identity.agentName,
color: getAgentThemeColor(t_2.identity.color),
isIdle: t_2.isIdle,
taskId: t_2.id
};
}
function _temp7(a, b) {
return a.identity.agentName.localeCompare(b.identity.agentName);
}
function _temp6(t_1) {
return t_1.type === "in_process_teammate";
}
function _temp5(t_0) {
return t_0.type === "in_process_teammate";
}
function _temp4(s_1) {
return s_1.expandedView;
}
function _temp3(t) {
return isBackgroundTask(t) && !(false && isPanelAgentTask(t));
}
function _temp2(s_0) {
return s_0.viewingAgentTaskId;
}
function _temp(s) {
return s.tasks;
return (
<>
<SummaryPill selected={tasksSelected} onClick={onOpenDialog}>
{getPillLabel(runningTasks)}
</SummaryPill>
{pillNeedsCta(runningTasks) && (
<Text dimColor> · {figures.arrowDown} to view</Text>
)}
</>
)
}
type AgentPillProps = {
name: string;
color?: keyof Theme;
isSelected: boolean;
isViewed: boolean;
isIdle: boolean;
onClick?: () => void;
};
function AgentPill(t0) {
const $ = _c(19);
const {
name,
color,
isSelected,
isViewed,
isIdle,
onClick
} = t0;
const [hover, setHover] = useState(false);
const highlighted = isSelected || hover;
let label;
name: string
color?: keyof Theme
isSelected: boolean
isViewed: boolean
isIdle: boolean
onClick?: () => void
}
function AgentPill({
name,
color,
isSelected,
isViewed,
isIdle,
onClick,
}: AgentPillProps): React.ReactNode {
const [hover, setHover] = useState(false)
// Hover mirrors the keyboard-selected look so the affordance is familiar.
const highlighted = isSelected || hover
let label: React.ReactNode
if (highlighted) {
let t1;
if ($[0] !== color || $[1] !== isViewed || $[2] !== name) {
t1 = color ? <Text backgroundColor={color} color="inverseText" bold={isViewed}>@{name}</Text> : <Text color="background" inverse={true} bold={isViewed}>@{name}</Text>;
$[0] = color;
$[1] = isViewed;
$[2] = name;
$[3] = t1;
} else {
t1 = $[3];
}
label = t1;
label = color ? (
<Text backgroundColor={color} color="inverseText" bold={isViewed}>
@{name}
</Text>
) : (
<Text color="background" inverse bold={isViewed}>
@{name}
</Text>
)
} else if (isIdle) {
label = (
<Text dimColor bold={isViewed}>
@{name}
</Text>
)
} else if (isViewed) {
label = (
<Text color={color} bold>
@{name}
</Text>
)
} else {
if (isIdle) {
let t1;
if ($[4] !== isViewed || $[5] !== name) {
t1 = <Text dimColor={true} bold={isViewed}>@{name}</Text>;
$[4] = isViewed;
$[5] = name;
$[6] = t1;
} else {
t1 = $[6];
}
label = t1;
} else {
if (isViewed) {
let t1;
if ($[7] !== color || $[8] !== name) {
t1 = <Text color={color} bold={true}>@{name}</Text>;
$[7] = color;
$[8] = name;
$[9] = t1;
} else {
t1 = $[9];
}
label = t1;
} else {
const t1 = !color;
let t2;
if ($[10] !== color || $[11] !== name || $[12] !== t1) {
t2 = <Text color={color} dimColor={t1}>@{name}</Text>;
$[10] = color;
$[11] = name;
$[12] = t1;
$[13] = t2;
} else {
t2 = $[13];
}
label = t2;
}
}
label = (
<Text color={color} dimColor={!color}>
@{name}
</Text>
)
}
if (!onClick) {
return label;
}
let t1;
let t2;
if ($[14] === Symbol.for("react.memo_cache_sentinel")) {
t1 = () => setHover(true);
t2 = () => setHover(false);
$[14] = t1;
$[15] = t2;
} else {
t1 = $[14];
t2 = $[15];
}
let t3;
if ($[16] !== label || $[17] !== onClick) {
t3 = <Box onClick={onClick} onMouseEnter={t1} onMouseLeave={t2}>{label}</Box>;
$[16] = label;
$[17] = onClick;
$[18] = t3;
} else {
t3 = $[18];
}
return t3;
if (!onClick) return label
return (
<Box
onClick={onClick}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
{label}
</Box>
)
}
function SummaryPill(t0) {
const $ = _c(8);
const {
selected,
onClick,
children
} = t0;
const [hover, setHover] = useState(false);
const t1 = selected || hover;
let t2;
if ($[0] !== children || $[1] !== t1) {
t2 = <Text color="background" inverse={t1}>{children}</Text>;
$[0] = children;
$[1] = t1;
$[2] = t2;
} else {
t2 = $[2];
}
const label = t2;
if (!onClick) {
return label;
}
let t3;
let t4;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t3 = () => setHover(true);
t4 = () => setHover(false);
$[3] = t3;
$[4] = t4;
} else {
t3 = $[3];
t4 = $[4];
}
let t5;
if ($[5] !== label || $[6] !== onClick) {
t5 = <Box onClick={onClick} onMouseEnter={t3} onMouseLeave={t4}>{label}</Box>;
$[5] = label;
$[6] = onClick;
$[7] = t5;
} else {
t5 = $[7];
}
return t5;
function SummaryPill({
selected,
onClick,
children,
}: {
selected: boolean
onClick?: () => void
children: React.ReactNode
}): React.ReactNode {
const [hover, setHover] = useState(false)
const label = (
<Text color="background" inverse={selected || hover}>
{children}
</Text>
)
if (!onClick) return label
return (
<Box
onClick={onClick}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
{label}
</Box>
)
}
function getAgentThemeColor(colorName: string | undefined): keyof Theme | undefined {
if (!colorName) return undefined;
function getAgentThemeColor(
colorName: string | undefined,
): keyof Theme | undefined {
if (!colorName) return undefined
if (AGENT_COLORS.includes(colorName as AgentColorName)) {
return AGENT_COLOR_TO_THEME_COLOR[colorName as AgentColorName];
return AGENT_COLOR_TO_THEME_COLOR[colorName as AgentColorName]
}
return undefined;
return undefined
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,250 +1,136 @@
import { c as _c } from "react/compiler-runtime";
import React from 'react';
import type { DeepImmutable } from 'src/types/utils.js';
import { useElapsedTime } from '../../hooks/useElapsedTime.js';
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
import { Box, Text } from '../../ink.js';
import { useKeybindings } from '../../keybindings/useKeybinding.js';
import type { DreamTaskState } from '../../tasks/DreamTask/DreamTask.js';
import { plural } from '../../utils/stringUtils.js';
import { Byline } from '../design-system/Byline.js';
import { Dialog } from '../design-system/Dialog.js';
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
import React from 'react'
import type { DeepImmutable } from 'src/types/utils.js'
import { useElapsedTime } from '../../hooks/useElapsedTime.js'
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
import { Box, Text } from '../../ink.js'
import { useKeybindings } from '../../keybindings/useKeybinding.js'
import type { DreamTaskState } from '../../tasks/DreamTask/DreamTask.js'
import { plural } from '../../utils/stringUtils.js'
import { Byline } from '../design-system/Byline.js'
import { Dialog } from '../design-system/Dialog.js'
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
type Props = {
task: DeepImmutable<DreamTaskState>;
onDone: () => void;
onBack?: () => void;
onKill?: () => void;
};
task: DeepImmutable<DreamTaskState>
onDone: () => void
onBack?: () => void
onKill?: () => void
}
// How many recent turns to render. Earlier turns collapse to a count.
const VISIBLE_TURNS = 6;
export function DreamDetailDialog(t0) {
const $ = _c(70);
const {
task,
onDone,
onBack,
onKill
} = t0;
const elapsedTime = useElapsedTime(task.startTime, task.status === "running", 1000, 0);
let t1;
if ($[0] !== onDone) {
t1 = {
"confirm:yes": onDone
};
$[0] = onDone;
$[1] = t1;
} else {
t1 = $[1];
const VISIBLE_TURNS = 6
export function DreamDetailDialog({
task,
onDone,
onBack,
onKill,
}: Props): React.ReactNode {
const elapsedTime = useElapsedTime(
task.startTime,
task.status === 'running',
1000,
0,
)
// Dialog handles confirm:no (Esc) → onCancel. Wire confirm:yes (Enter/y) too.
useKeybindings({ 'confirm:yes': onDone }, { context: 'Confirmation' })
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === ' ') {
e.preventDefault()
onDone()
} else if (e.key === 'left' && onBack) {
e.preventDefault()
onBack()
} else if (e.key === 'x' && task.status === 'running' && onKill) {
e.preventDefault()
onKill()
}
}
let t2;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t2 = {
context: "Confirmation"
};
$[2] = t2;
} else {
t2 = $[2];
}
useKeybindings(t1, t2);
let t3;
if ($[3] !== onBack || $[4] !== onDone || $[5] !== onKill || $[6] !== task.status) {
t3 = e => {
if (e.key === " ") {
e.preventDefault();
onDone();
} else {
if (e.key === "left" && onBack) {
e.preventDefault();
onBack();
} else {
if (e.key === "x" && task.status === "running" && onKill) {
e.preventDefault();
onKill();
}
// Turns with text to show. Tool-only turns (text='') are dropped entirely —
// the per-turn toolUseCount already captures that work.
const visibleTurns = task.turns.filter(t => t.text !== '')
const shown = visibleTurns.slice(-VISIBLE_TURNS)
const hidden = visibleTurns.length - shown.length
return (
<Box
flexDirection="column"
tabIndex={0}
autoFocus
onKeyDown={handleKeyDown}
>
<Dialog
title="Memory consolidation"
subtitle={
<Text dimColor>
{elapsedTime} · reviewing {task.sessionsReviewing}{' '}
{plural(task.sessionsReviewing, 'session')}
{task.filesTouched.length > 0 && (
<>
{' '}
· {task.filesTouched.length}{' '}
{plural(task.filesTouched.length, 'file')} touched
</>
)}
</Text>
}
}
};
$[3] = onBack;
$[4] = onDone;
$[5] = onKill;
$[6] = task.status;
$[7] = t3;
} else {
t3 = $[7];
}
const handleKeyDown = t3;
let T0;
let T1;
let T2;
let t10;
let t11;
let t12;
let t13;
let t14;
let t15;
let t16;
let t4;
let t5;
let t6;
let t7;
let t8;
let t9;
if ($[8] !== elapsedTime || $[9] !== handleKeyDown || $[10] !== onBack || $[11] !== onDone || $[12] !== onKill || $[13] !== task.filesTouched.length || $[14] !== task.sessionsReviewing || $[15] !== task.status || $[16] !== task.turns) {
const visibleTurns = task.turns.filter(_temp);
const shown = visibleTurns.slice(-VISIBLE_TURNS);
const hidden = visibleTurns.length - shown.length;
T2 = Box;
t13 = "column";
t14 = 0;
t15 = true;
t16 = handleKeyDown;
T1 = Dialog;
t8 = "Memory consolidation";
const t17 = task.sessionsReviewing;
let t18;
if ($[33] !== task.sessionsReviewing) {
t18 = plural(task.sessionsReviewing, "session");
$[33] = task.sessionsReviewing;
$[34] = t18;
} else {
t18 = $[34];
}
let t19;
if ($[35] !== task.filesTouched.length) {
t19 = task.filesTouched.length > 0 && <>{" "}· {task.filesTouched.length}{" "}{plural(task.filesTouched.length, "file")} touched</>;
$[35] = task.filesTouched.length;
$[36] = t19;
} else {
t19 = $[36];
}
if ($[37] !== elapsedTime || $[38] !== t18 || $[39] !== t19 || $[40] !== task.sessionsReviewing) {
t9 = <Text dimColor={true}>{elapsedTime} · reviewing {t17}{" "}{t18}{t19}</Text>;
$[37] = elapsedTime;
$[38] = t18;
$[39] = t19;
$[40] = task.sessionsReviewing;
$[41] = t9;
} else {
t9 = $[41];
}
t10 = onDone;
t11 = "background";
if ($[42] !== onBack || $[43] !== onKill || $[44] !== task.status) {
t12 = exitState => exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline>{onBack && <KeyboardShortcutHint shortcut={"\u2190"} action="go back" />}<KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />{task.status === "running" && onKill && <KeyboardShortcutHint shortcut="x" action="stop" />}</Byline>;
$[42] = onBack;
$[43] = onKill;
$[44] = task.status;
$[45] = t12;
} else {
t12 = $[45];
}
T0 = Box;
t4 = "column";
t5 = 1;
let t20;
if ($[46] === Symbol.for("react.memo_cache_sentinel")) {
t20 = <Text bold={true}>Status:</Text>;
$[46] = t20;
} else {
t20 = $[46];
}
if ($[47] !== task.status) {
t6 = <Text>{t20}{" "}{task.status === "running" ? <Text color="background">running</Text> : task.status === "completed" ? <Text color="success">{task.status}</Text> : <Text color="error">{task.status}</Text>}</Text>;
$[47] = task.status;
$[48] = t6;
} else {
t6 = $[48];
}
t7 = shown.length === 0 ? <Text dimColor={true}>{task.status === "running" ? "Starting\u2026" : "(no text output)"}</Text> : <>{hidden > 0 && <Text dimColor={true}>({hidden} earlier {plural(hidden, "turn")})</Text>}{shown.map(_temp2)}</>;
$[8] = elapsedTime;
$[9] = handleKeyDown;
$[10] = onBack;
$[11] = onDone;
$[12] = onKill;
$[13] = task.filesTouched.length;
$[14] = task.sessionsReviewing;
$[15] = task.status;
$[16] = task.turns;
$[17] = T0;
$[18] = T1;
$[19] = T2;
$[20] = t10;
$[21] = t11;
$[22] = t12;
$[23] = t13;
$[24] = t14;
$[25] = t15;
$[26] = t16;
$[27] = t4;
$[28] = t5;
$[29] = t6;
$[30] = t7;
$[31] = t8;
$[32] = t9;
} else {
T0 = $[17];
T1 = $[18];
T2 = $[19];
t10 = $[20];
t11 = $[21];
t12 = $[22];
t13 = $[23];
t14 = $[24];
t15 = $[25];
t16 = $[26];
t4 = $[27];
t5 = $[28];
t6 = $[29];
t7 = $[30];
t8 = $[31];
t9 = $[32];
}
let t17;
if ($[49] !== T0 || $[50] !== t4 || $[51] !== t5 || $[52] !== t6 || $[53] !== t7) {
t17 = <T0 flexDirection={t4} gap={t5}>{t6}{t7}</T0>;
$[49] = T0;
$[50] = t4;
$[51] = t5;
$[52] = t6;
$[53] = t7;
$[54] = t17;
} else {
t17 = $[54];
}
let t18;
if ($[55] !== T1 || $[56] !== t10 || $[57] !== t11 || $[58] !== t12 || $[59] !== t17 || $[60] !== t8 || $[61] !== t9) {
t18 = <T1 title={t8} subtitle={t9} onCancel={t10} color={t11} inputGuide={t12}>{t17}</T1>;
$[55] = T1;
$[56] = t10;
$[57] = t11;
$[58] = t12;
$[59] = t17;
$[60] = t8;
$[61] = t9;
$[62] = t18;
} else {
t18 = $[62];
}
let t19;
if ($[63] !== T2 || $[64] !== t13 || $[65] !== t14 || $[66] !== t15 || $[67] !== t16 || $[68] !== t18) {
t19 = <T2 flexDirection={t13} tabIndex={t14} autoFocus={t15} onKeyDown={t16}>{t18}</T2>;
$[63] = T2;
$[64] = t13;
$[65] = t14;
$[66] = t15;
$[67] = t16;
$[68] = t18;
$[69] = t19;
} else {
t19 = $[69];
}
return t19;
}
function _temp2(turn, i) {
return <Box key={i} flexDirection="column"><Text wrap="wrap">{turn.text}</Text>{turn.toolUseCount > 0 && <Text dimColor={true}>{" "}({turn.toolUseCount}{" "}{plural(turn.toolUseCount, "tool")})</Text>}</Box>;
}
function _temp(t) {
return t.text !== "";
onCancel={onDone}
color="background"
inputGuide={exitState =>
exitState.pending ? (
<Text>Press {exitState.keyName} again to exit</Text>
) : (
<Byline>
{onBack && <KeyboardShortcutHint shortcut="←" action="go back" />}
<KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />
{task.status === 'running' && onKill && (
<KeyboardShortcutHint shortcut="x" action="stop" />
)}
</Byline>
)
}
>
<Box flexDirection="column" gap={1}>
<Text>
<Text bold>Status:</Text>{' '}
{task.status === 'running' ? (
<Text color="background">running</Text>
) : task.status === 'completed' ? (
<Text color="success">{task.status}</Text>
) : (
<Text color="error">{task.status}</Text>
)}
</Text>
{shown.length === 0 ? (
<Text dimColor>
{task.status === 'running' ? 'Starting…' : '(no text output)'}
</Text>
) : (
<>
{hidden > 0 && (
<Text dimColor>
({hidden} earlier {plural(hidden, 'turn')})
</Text>
)}
{shown.map((turn, i) => (
<Box key={i} flexDirection="column">
<Text wrap="wrap">{turn.text}</Text>
{turn.toolUseCount > 0 && (
<Text dimColor>
{' '}({turn.toolUseCount}{' '}
{plural(turn.toolUseCount, 'tool')})
</Text>
)}
</Box>
))}
</>
)}
</Box>
</Dialog>
</Box>
)
}

View File

@@ -1,265 +1,193 @@
import { c as _c } from "react/compiler-runtime";
import React, { useMemo } from 'react';
import type { DeepImmutable } from 'src/types/utils.js';
import { useElapsedTime } from '../../hooks/useElapsedTime.js';
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
import { Box, Text, useTheme } from '../../ink.js';
import { useKeybindings } from '../../keybindings/useKeybinding.js';
import { getEmptyToolPermissionContext } from '../../Tool.js';
import type { InProcessTeammateTaskState } from '../../tasks/InProcessTeammateTask/types.js';
import { getTools } from '../../tools.js';
import { formatNumber, truncateToWidth } from '../../utils/format.js';
import { toInkColor } from '../../utils/ink.js';
import { Byline } from '../design-system/Byline.js';
import { Dialog } from '../design-system/Dialog.js';
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
import { renderToolActivity } from './renderToolActivity.js';
import { describeTeammateActivity } from './taskStatusUtils.js';
import React, { useMemo } from 'react'
import type { DeepImmutable } from 'src/types/utils.js'
import { useElapsedTime } from '../../hooks/useElapsedTime.js'
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
import { Box, Text, useTheme } from '../../ink.js'
import { useKeybindings } from '../../keybindings/useKeybinding.js'
import { getEmptyToolPermissionContext } from '../../Tool.js'
import type { InProcessTeammateTaskState } from '../../tasks/InProcessTeammateTask/types.js'
import { getTools } from '../../tools.js'
import { formatNumber, truncateToWidth } from '../../utils/format.js'
import { toInkColor } from '../../utils/ink.js'
import { Byline } from '../design-system/Byline.js'
import { Dialog } from '../design-system/Dialog.js'
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
import { renderToolActivity } from './renderToolActivity.js'
import { describeTeammateActivity } from './taskStatusUtils.js'
type Props = {
teammate: DeepImmutable<InProcessTeammateTaskState>;
onDone: () => void;
onKill?: () => void;
onBack?: () => void;
onForeground?: () => void;
};
export function InProcessTeammateDetailDialog(t0) {
const $ = _c(63);
const {
teammate,
onDone,
onKill,
onBack,
onForeground
} = t0;
const [theme] = useTheme();
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = getTools(getEmptyToolPermissionContext());
$[0] = t1;
} else {
t1 = $[0];
}
const tools = t1;
const elapsedTime = useElapsedTime(teammate.startTime, teammate.status === "running", 1000, teammate.totalPausedMs ?? 0);
let t2;
if ($[1] !== onDone) {
t2 = {
"confirm:yes": onDone
};
$[1] = onDone;
$[2] = t2;
} else {
t2 = $[2];
}
let t3;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t3 = {
context: "Confirmation"
};
$[3] = t3;
} else {
t3 = $[3];
}
useKeybindings(t2, t3);
let t4;
if ($[4] !== onBack || $[5] !== onDone || $[6] !== onForeground || $[7] !== onKill || $[8] !== teammate.status) {
t4 = e => {
if (e.key === " ") {
e.preventDefault();
onDone();
} else {
if (e.key === "left" && onBack) {
e.preventDefault();
onBack();
} else {
if (e.key === "x" && teammate.status === "running" && onKill) {
e.preventDefault();
onKill();
} else {
if (e.key === "f" && teammate.status === "running" && onForeground) {
e.preventDefault();
onForeground();
}
}
}
}
};
$[4] = onBack;
$[5] = onDone;
$[6] = onForeground;
$[7] = onKill;
$[8] = teammate.status;
$[9] = t4;
} else {
t4 = $[9];
}
const handleKeyDown = t4;
let t5;
if ($[10] !== teammate) {
t5 = describeTeammateActivity(teammate);
$[10] = teammate;
$[11] = t5;
} else {
t5 = $[11];
}
const activity = t5;
const tokenCount = teammate.result?.totalTokens ?? teammate.progress?.tokenCount;
const toolUseCount = teammate.result?.totalToolUseCount ?? teammate.progress?.toolUseCount;
let t6;
if ($[12] !== teammate.prompt) {
t6 = truncateToWidth(teammate.prompt, 300);
$[12] = teammate.prompt;
$[13] = t6;
} else {
t6 = $[13];
}
const displayPrompt = t6;
let t7;
if ($[14] !== teammate.identity.color) {
t7 = toInkColor(teammate.identity.color);
$[14] = teammate.identity.color;
$[15] = t7;
} else {
t7 = $[15];
}
let t8;
if ($[16] !== t7 || $[17] !== teammate.identity.agentName) {
t8 = <Text color={t7}>@{teammate.identity.agentName}</Text>;
$[16] = t7;
$[17] = teammate.identity.agentName;
$[18] = t8;
} else {
t8 = $[18];
}
let t9;
if ($[19] !== activity) {
t9 = activity && <Text dimColor={true}> ({activity})</Text>;
$[19] = activity;
$[20] = t9;
} else {
t9 = $[20];
}
let t10;
if ($[21] !== t8 || $[22] !== t9) {
t10 = <Text>{t8}{t9}</Text>;
$[21] = t8;
$[22] = t9;
$[23] = t10;
} else {
t10 = $[23];
}
const title = t10;
let t11;
if ($[24] !== teammate.status) {
t11 = teammate.status !== "running" && <Text color={teammate.status === "completed" ? "success" : teammate.status === "killed" ? "warning" : "error"}>{teammate.status === "completed" ? "Completed" : teammate.status === "failed" ? "Failed" : "Stopped"}{" \xB7 "}</Text>;
$[24] = teammate.status;
$[25] = t11;
} else {
t11 = $[25];
}
let t12;
if ($[26] !== tokenCount) {
t12 = tokenCount !== undefined && tokenCount > 0 && <> · {formatNumber(tokenCount)} tokens</>;
$[26] = tokenCount;
$[27] = t12;
} else {
t12 = $[27];
}
let t13;
if ($[28] !== toolUseCount) {
t13 = toolUseCount !== undefined && toolUseCount > 0 && <>{" "}· {toolUseCount} {toolUseCount === 1 ? "tool" : "tools"}</>;
$[28] = toolUseCount;
$[29] = t13;
} else {
t13 = $[29];
}
let t14;
if ($[30] !== elapsedTime || $[31] !== t12 || $[32] !== t13) {
t14 = <Text dimColor={true}>{elapsedTime}{t12}{t13}</Text>;
$[30] = elapsedTime;
$[31] = t12;
$[32] = t13;
$[33] = t14;
} else {
t14 = $[33];
}
let t15;
if ($[34] !== t11 || $[35] !== t14) {
t15 = <Text>{t11}{t14}</Text>;
$[34] = t11;
$[35] = t14;
$[36] = t15;
} else {
t15 = $[36];
}
const subtitle = t15;
let t16;
if ($[37] !== onBack || $[38] !== onForeground || $[39] !== onKill || $[40] !== teammate.status) {
t16 = exitState => exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline>{onBack && <KeyboardShortcutHint shortcut={"\u2190"} action="go back" />}<KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />{teammate.status === "running" && onKill && <KeyboardShortcutHint shortcut="x" action="stop" />}{teammate.status === "running" && onForeground && <KeyboardShortcutHint shortcut="f" action="foreground" />}</Byline>;
$[37] = onBack;
$[38] = onForeground;
$[39] = onKill;
$[40] = teammate.status;
$[41] = t16;
} else {
t16 = $[41];
}
let t17;
if ($[42] !== teammate.progress || $[43] !== teammate.status || $[44] !== theme) {
t17 = teammate.status === "running" && teammate.progress?.recentActivities && teammate.progress.recentActivities.length > 0 && <Box flexDirection="column"><Text bold={true} dimColor={true}>Progress</Text>{teammate.progress.recentActivities.map((activity_0, i) => <Text key={i} dimColor={i < teammate.progress.recentActivities.length - 1} wrap="truncate-end">{i === teammate.progress.recentActivities.length - 1 ? "\u203A " : " "}{renderToolActivity(activity_0, tools, theme)}</Text>)}</Box>;
$[42] = teammate.progress;
$[43] = teammate.status;
$[44] = theme;
$[45] = t17;
} else {
t17 = $[45];
}
let t18;
if ($[46] === Symbol.for("react.memo_cache_sentinel")) {
t18 = <Text bold={true} dimColor={true}>Prompt</Text>;
$[46] = t18;
} else {
t18 = $[46];
}
let t19;
if ($[47] !== displayPrompt) {
t19 = <Box flexDirection="column" marginTop={1}>{t18}<Text wrap="wrap">{displayPrompt}</Text></Box>;
$[47] = displayPrompt;
$[48] = t19;
} else {
t19 = $[48];
}
let t20;
if ($[49] !== teammate.error || $[50] !== teammate.status) {
t20 = teammate.status === "failed" && teammate.error && <Box flexDirection="column" marginTop={1}><Text bold={true} color="error">Error</Text><Text color="error" wrap="wrap">{teammate.error}</Text></Box>;
$[49] = teammate.error;
$[50] = teammate.status;
$[51] = t20;
} else {
t20 = $[51];
}
let t21;
if ($[52] !== onDone || $[53] !== subtitle || $[54] !== t16 || $[55] !== t17 || $[56] !== t19 || $[57] !== t20 || $[58] !== title) {
t21 = <Dialog title={title} subtitle={subtitle} onCancel={onDone} color="background" inputGuide={t16}>{t17}{t19}{t20}</Dialog>;
$[52] = onDone;
$[53] = subtitle;
$[54] = t16;
$[55] = t17;
$[56] = t19;
$[57] = t20;
$[58] = title;
$[59] = t21;
} else {
t21 = $[59];
}
let t22;
if ($[60] !== handleKeyDown || $[61] !== t21) {
t22 = <Box flexDirection="column" tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t21}</Box>;
$[60] = handleKeyDown;
$[61] = t21;
$[62] = t22;
} else {
t22 = $[62];
}
return t22;
teammate: DeepImmutable<InProcessTeammateTaskState>
onDone: () => void
onKill?: () => void
onBack?: () => void
onForeground?: () => void
}
export function InProcessTeammateDetailDialog({
teammate,
onDone,
onKill,
onBack,
onForeground,
}: Props): React.ReactNode {
const [theme] = useTheme()
const tools = useMemo(() => getTools(getEmptyToolPermissionContext()), [])
const elapsedTime = useElapsedTime(
teammate.startTime,
teammate.status === 'running',
1000,
teammate.totalPausedMs ?? 0,
)
// Restore confirm:yes (Enter/y) dismissal — Dialog handles confirm:no (Esc)
useKeybindings(
{
'confirm:yes': onDone,
},
{ context: 'Confirmation' },
)
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === ' ') {
e.preventDefault()
onDone()
} else if (e.key === 'left' && onBack) {
e.preventDefault()
onBack()
} else if (e.key === 'x' && teammate.status === 'running' && onKill) {
e.preventDefault()
onKill()
} else if (e.key === 'f' && teammate.status === 'running' && onForeground) {
e.preventDefault()
onForeground()
}
}
const activity = describeTeammateActivity(teammate)
const tokenCount =
teammate.result?.totalTokens ?? teammate.progress?.tokenCount
const toolUseCount =
teammate.result?.totalToolUseCount ?? teammate.progress?.toolUseCount
const displayPrompt = truncateToWidth(teammate.prompt, 300)
const title = (
<Text>
<Text color={toInkColor(teammate.identity.color)}>
@{teammate.identity.agentName}
</Text>
{activity && <Text dimColor> ({activity})</Text>}
</Text>
)
const subtitle = (
<Text>
{teammate.status !== 'running' && (
<Text
color={
teammate.status === 'completed'
? 'success'
: teammate.status === 'killed'
? 'warning'
: 'error'
}
>
{teammate.status === 'completed'
? 'Completed'
: teammate.status === 'failed'
? 'Failed'
: 'Stopped'}
{' · '}
</Text>
)}
<Text dimColor>
{elapsedTime}
{tokenCount !== undefined && tokenCount > 0 && (
<> · {formatNumber(tokenCount)} tokens</>
)}
{toolUseCount !== undefined && toolUseCount > 0 && (
<>
{' '}
· {toolUseCount} {toolUseCount === 1 ? 'tool' : 'tools'}
</>
)}
</Text>
</Text>
)
return (
<Box
flexDirection="column"
tabIndex={0}
autoFocus
onKeyDown={handleKeyDown}
>
<Dialog
title={title}
subtitle={subtitle}
onCancel={onDone}
color="background"
inputGuide={exitState =>
exitState.pending ? (
<Text>Press {exitState.keyName} again to exit</Text>
) : (
<Byline>
{onBack && <KeyboardShortcutHint shortcut="←" action="go back" />}
<KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />
{teammate.status === 'running' && onKill && (
<KeyboardShortcutHint shortcut="x" action="stop" />
)}
{teammate.status === 'running' && onForeground && (
<KeyboardShortcutHint shortcut="f" action="foreground" />
)}
</Byline>
)
}
>
{/* Recent activities for running teammates */}
{teammate.status === 'running' &&
teammate.progress?.recentActivities &&
teammate.progress.recentActivities.length > 0 && (
<Box flexDirection="column">
<Text bold dimColor>
Progress
</Text>
{teammate.progress.recentActivities.map((activity, i) => (
<Text
key={i}
dimColor={i < teammate.progress!.recentActivities!.length - 1}
wrap="truncate-end"
>
{i === teammate.progress!.recentActivities!.length - 1
? ' '
: ' '}
{renderToolActivity(activity, tools, theme)}
</Text>
))}
</Box>
)}
{/* Prompt section */}
<Box flexDirection="column" marginTop={1}>
<Text bold dimColor>
Prompt
</Text>
<Text wrap="wrap">{displayPrompt}</Text>
</Box>
{/* Error details if failed */}
{teammate.status === 'failed' && teammate.error && (
<Box flexDirection="column" marginTop={1}>
<Text bold color="error">
Error
</Text>
<Text color="error" wrap="wrap">
{teammate.error}
</Text>
</Box>
)}
</Dialog>
</Box>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,17 @@
import { c as _c } from "react/compiler-runtime";
import React, { useRef } from 'react';
import type { RemoteAgentTaskState } from 'src/tasks/RemoteAgentTask/RemoteAgentTask.js';
import type { DeepImmutable } from 'src/types/utils.js';
import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js';
import { useSettings } from '../../hooks/useSettings.js';
import { Text, useAnimationFrame } from '../../ink.js';
import { count } from '../../utils/array.js';
import { getRainbowColor } from '../../utils/thinking.js';
const TICK_MS = 80;
type ReviewStage = NonNullable<NonNullable<RemoteAgentTaskState['reviewProgress']>['stage']>;
import React, { useRef } from 'react'
import type { RemoteAgentTaskState } from 'src/tasks/RemoteAgentTask/RemoteAgentTask.js'
import type { DeepImmutable } from 'src/types/utils.js'
import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js'
import { useSettings } from '../../hooks/useSettings.js'
import { Text, useAnimationFrame } from '../../ink.js'
import { count } from '../../utils/array.js'
import { getRainbowColor } from '../../utils/thinking.js'
const TICK_MS = 80
type ReviewStage = NonNullable<
NonNullable<RemoteAgentTaskState['reviewProgress']>['stage']
>
/**
* Stage-appropriate counts line for a running review. Shared between the
@@ -19,52 +22,48 @@ type ReviewStage = NonNullable<NonNullable<RemoteAgentTaskState['reviewProgress'
* Canonical behavior: word labels (not ✓/✗), hide refuted when 0, "deduping"
* for the synthesizing stage (matches STAGE_LABELS in the detail dialog).
*/
export function formatReviewStageCounts(stage: ReviewStage | undefined, found: number, verified: number, refuted: number): string {
export function formatReviewStageCounts(
stage: ReviewStage | undefined,
found: number,
verified: number,
refuted: number,
): string {
// Pre-stage orchestrator images don't write the stage field.
if (!stage) return `${found} found · ${verified} verified`;
if (!stage) return `${found} found · ${verified} verified`
if (stage === 'synthesizing') {
const parts = [`${verified} verified`];
if (refuted > 0) parts.push(`${refuted} refuted`);
parts.push('deduping');
return parts.join(' · ');
const parts = [`${verified} verified`]
if (refuted > 0) parts.push(`${refuted} refuted`)
parts.push('deduping')
return parts.join(' · ')
}
if (stage === 'verifying') {
const parts = [`${found} found`, `${verified} verified`];
if (refuted > 0) parts.push(`${refuted} refuted`);
return parts.join(' · ');
const parts = [`${found} found`, `${verified} verified`]
if (refuted > 0) parts.push(`${refuted} refuted`)
return parts.join(' · ')
}
// stage === 'finding'
return found > 0 ? `${found} found` : 'finding';
return found > 0 ? `${found} found` : 'finding'
}
// Per-character rainbow gradient, same treatment as the ultraplan keyword.
// The phase offset lets the gradient cycle — so the colors sweep along the
// text on each animation frame instead of being static.
function RainbowText(t0) {
const $ = _c(5);
const {
text,
phase: t1
} = t0;
const phase = t1 === undefined ? 0 : t1;
let t2;
if ($[0] !== text) {
t2 = [...text];
$[0] = text;
$[1] = t2;
} else {
t2 = $[1];
}
let t3;
if ($[2] !== phase || $[3] !== t2) {
t3 = <>{t2.map((ch, i) => <Text key={i} color={getRainbowColor(i + phase)}>{ch}</Text>)}</>;
$[2] = phase;
$[3] = t2;
$[4] = t3;
} else {
t3 = $[4];
}
return t3;
function RainbowText({
text,
phase = 0,
}: {
text: string
phase?: number
}): React.ReactNode {
return (
<>
{[...text].map((ch, i) => (
<Text key={i} color={getRainbowColor(i + phase)}>
{ch}
</Text>
))}
</>
)
}
// Smooth-tick a count toward target, +1 per frame. Same pattern as the
@@ -74,169 +73,129 @@ function RainbowText(t0) {
// the clock is frozen), bypass the tick and jump straight to target —
// otherwise a frozen `time` would leave the ref stuck at its init value.
function useSmoothCount(target: number, time: number, snap: boolean): number {
const displayed = useRef(target);
const lastTick = useRef(time);
const displayed = useRef(target)
const lastTick = useRef(time)
if (snap || target < displayed.current) {
displayed.current = target;
displayed.current = target
} else if (target > displayed.current && time !== lastTick.current) {
displayed.current += 1;
lastTick.current = time;
displayed.current += 1
lastTick.current = time
}
return displayed.current;
return displayed.current
}
function ReviewRainbowLine(t0) {
const $ = _c(15);
const {
session
} = t0;
const settings = useSettings();
const reducedMotion = settings.prefersReducedMotion ?? false;
const p = session.reviewProgress;
const running = session.status === "running";
const [, time] = useAnimationFrame(running && !reducedMotion ? TICK_MS : null);
const targetFound = p?.bugsFound ?? 0;
const targetVerified = p?.bugsVerified ?? 0;
const targetRefuted = p?.bugsRefuted ?? 0;
const snap = reducedMotion || !running;
const found = useSmoothCount(targetFound, time, snap);
const verified = useSmoothCount(targetVerified, time, snap);
const refuted = useSmoothCount(targetRefuted, time, snap);
const phase = Math.floor(time / (TICK_MS * 3)) % 7;
if (session.status === "completed") {
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <><Text color="background">{DIAMOND_FILLED} </Text><RainbowText text="ultrareview" phase={0} /><Text dimColor={true}> ready · shift+ to view</Text></>;
$[0] = t1;
} else {
t1 = $[0];
}
return t1;
function ReviewRainbowLine({
session,
}: {
session: DeepImmutable<RemoteAgentTaskState>
}): React.ReactNode {
const settings = useSettings()
const reducedMotion = settings.prefersReducedMotion ?? false
const p = session.reviewProgress
const running = session.status === 'running'
// Animation clock runs only while running — completed/failed are static.
// Disabled entirely when the user prefers reduced motion.
//
// The ref is intentionally discarded: this component is rendered inside
// <Text> wrappers (BackgroundTasksDialog, RemoteSessionDetailDialog), and
// Ink can't nest <Box> inside <Text>. Dropping the ref means
// useTerminalViewport's isVisible stays true, so the clock ticks even when
// scrolled off-screen — acceptable for a single 30-char line.
const [, time] = useAnimationFrame(running && !reducedMotion ? TICK_MS : null)
const targetFound = p?.bugsFound ?? 0
const targetVerified = p?.bugsVerified ?? 0
const targetRefuted = p?.bugsRefuted ?? 0
// snap when the clock isn't advancing (reduced motion, or not running) —
// useAnimationFrame(null) freezes `time` at its mount value, which would
// leave the tick-gate permanently false.
const snap = reducedMotion || !running
const found = useSmoothCount(targetFound, time, snap)
const verified = useSmoothCount(targetVerified, time, snap)
const refuted = useSmoothCount(targetRefuted, time, snap)
// Phase advances every 3 ticks so the gradient sweep is visible but
// not frantic. Modulo keeps it in the 7-color cycle.
const phase = Math.floor(time / (TICK_MS * 3)) % 7
// ◇ open diamond while running (teal, matches cloud-session accent), ◆
// filled when terminal. Rainbow is scoped to the word `ultrareview` only —
// per design feedback, "there is a limit to the glittering rainbow".
// Counts stay dimColor.
if (session.status === 'completed') {
return (
<>
<Text color="background">{DIAMOND_FILLED} </Text>
<RainbowText text="ultrareview" phase={0} />
<Text dimColor> ready · shift+ to view</Text>
</>
)
}
if (session.status === "failed") {
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <><Text color="background">{DIAMOND_FILLED} </Text><RainbowText text="ultrareview" phase={0} /><Text color="error" dimColor={true}>{" \xB7 "}error</Text></>;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
if (session.status === 'failed') {
return (
<>
<Text color="background">{DIAMOND_FILLED} </Text>
<RainbowText text="ultrareview" phase={0} />
<Text color="error" dimColor>
{' · '}
error
</Text>
</>
)
}
let t1;
if ($[2] !== found || $[3] !== p || $[4] !== refuted || $[5] !== verified) {
t1 = !p ? "setting up" : formatReviewStageCounts(p.stage, found, verified, refuted);
$[2] = found;
$[3] = p;
$[4] = refuted;
$[5] = verified;
$[6] = t1;
} else {
t1 = $[6];
}
const tail = t1;
let t2;
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
t2 = <Text color="background">{DIAMOND_OPEN} </Text>;
$[7] = t2;
} else {
t2 = $[7];
}
const t3 = running ? phase : 0;
let t4;
if ($[8] !== t3) {
t4 = <RainbowText text="ultrareview" phase={t3} />;
$[8] = t3;
$[9] = t4;
} else {
t4 = $[9];
}
let t5;
if ($[10] !== tail) {
t5 = <Text dimColor={true}> · {tail}</Text>;
$[10] = tail;
$[11] = t5;
} else {
t5 = $[11];
}
let t6;
if ($[12] !== t4 || $[13] !== t5) {
t6 = <>{t2}{t4}{t5}</>;
$[12] = t4;
$[13] = t5;
$[14] = t6;
} else {
t6 = $[14];
}
return t6;
// The !p branch ("setting up") covers the window before the orchestrator
// writes its first progress snapshot — container boot + repo clone can
// take 1-3 min, during which "0 found" looked hung.
const tail = !p
? 'setting up'
: formatReviewStageCounts(p.stage, found, verified, refuted)
return (
<>
<Text color="background">{DIAMOND_OPEN} </Text>
<RainbowText text="ultrareview" phase={running ? phase : 0} />
<Text dimColor> · {tail}</Text>
</>
)
}
export function RemoteSessionProgress(t0) {
const $ = _c(11);
const {
session
} = t0;
export function RemoteSessionProgress({
session,
}: {
session: DeepImmutable<RemoteAgentTaskState>
}): React.ReactNode {
// Lite-review: rainbow gradient over the full line, ultraplan-style.
// BackgroundTask.tsx delegates the whole <Text> wrapper here so the
// gradient spans the title, not just the trailing status.
if (session.isRemoteReview) {
let t1;
if ($[0] !== session) {
t1 = <ReviewRainbowLine session={session} />;
$[0] = session;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
return <ReviewRainbowLine session={session} />
}
if (session.status === "completed") {
let t1;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <Text bold={true} color="success" dimColor={true}>done</Text>;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
if (session.status === 'completed') {
return (
<Text bold color="success" dimColor>
done
</Text>
)
}
if (session.status === "failed") {
let t1;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <Text bold={true} color="error" dimColor={true}>error</Text>;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
if (session.status === 'failed') {
return (
<Text bold color="error" dimColor>
error
</Text>
)
}
if (!session.todoList.length) {
let t1;
if ($[4] !== session.status) {
t1 = <Text dimColor={true}>{session.status}</Text>;
$[4] = session.status;
$[5] = t1;
} else {
t1 = $[5];
}
return t1;
return <Text dimColor>{session.status}</Text>
}
let t1;
if ($[6] !== session.todoList) {
t1 = count(session.todoList, _temp);
$[6] = session.todoList;
$[7] = t1;
} else {
t1 = $[7];
}
const completed = t1;
const total = session.todoList.length;
let t2;
if ($[8] !== completed || $[9] !== total) {
t2 = <Text dimColor={true}>{completed}/{total}</Text>;
$[8] = completed;
$[9] = total;
$[10] = t2;
} else {
t2 = $[10];
}
return t2;
}
function _temp(_) {
return _.status === "completed";
const completed = count(session.todoList, _ => _.status === 'completed')
const total = session.todoList.length
return (
<Text dimColor>
{completed}/{total}
</Text>
)
}

View File

@@ -1,403 +1,247 @@
import { c as _c } from "react/compiler-runtime";
import React, { Suspense, use, useDeferredValue, useEffect, useState } from 'react';
import type { DeepImmutable } from 'src/types/utils.js';
import type { CommandResultDisplay } from '../../commands.js';
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
import { Box, Text } from '../../ink.js';
import { useKeybindings } from '../../keybindings/useKeybinding.js';
import type { LocalShellTaskState } from '../../tasks/LocalShellTask/guards.js';
import { formatDuration, formatFileSize, truncateToWidth } from '../../utils/format.js';
import { tailFile } from '../../utils/fsOperations.js';
import { getTaskOutputPath } from '../../utils/task/diskOutput.js';
import { Byline } from '../design-system/Byline.js';
import { Dialog } from '../design-system/Dialog.js';
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
import React, {
Suspense,
use,
useDeferredValue,
useEffect,
useState,
} from 'react'
import type { DeepImmutable } from 'src/types/utils.js'
import type { CommandResultDisplay } from '../../commands.js'
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
import { Box, Text } from '../../ink.js'
import { useKeybindings } from '../../keybindings/useKeybinding.js'
import type { LocalShellTaskState } from '../../tasks/LocalShellTask/guards.js'
import {
formatDuration,
formatFileSize,
truncateToWidth,
} from '../../utils/format.js'
import { tailFile } from '../../utils/fsOperations.js'
import { getTaskOutputPath } from '../../utils/task/diskOutput.js'
import { Byline } from '../design-system/Byline.js'
import { Dialog } from '../design-system/Dialog.js'
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
type Props = {
shell: DeepImmutable<LocalShellTaskState>;
onDone: (result?: string, options?: {
display?: CommandResultDisplay;
}) => void;
onKillShell?: () => void;
onBack?: () => void;
};
const SHELL_DETAIL_TAIL_BYTES = 8192;
shell: DeepImmutable<LocalShellTaskState>
onDone: (
result?: string,
options?: { display?: CommandResultDisplay },
) => void
onKillShell?: () => void
onBack?: () => void
}
const SHELL_DETAIL_TAIL_BYTES = 8192
type TaskOutputResult = {
content: string;
bytesTotal: number;
};
content: string
bytesTotal: number
}
/**
* Read the tail of the task output file. Only reads the last few KB,
* not the entire file.
*/
async function getTaskOutput(shell: DeepImmutable<LocalShellTaskState>): Promise<TaskOutputResult> {
const path = getTaskOutputPath(shell.id);
async function getTaskOutput(
shell: DeepImmutable<LocalShellTaskState>,
): Promise<TaskOutputResult> {
const path = getTaskOutputPath(shell.id)
try {
const result = await tailFile(path, SHELL_DETAIL_TAIL_BYTES);
return {
content: result.content,
bytesTotal: result.bytesTotal
};
const result = await tailFile(path, SHELL_DETAIL_TAIL_BYTES)
return { content: result.content, bytesTotal: result.bytesTotal }
} catch {
return {
content: '',
bytesTotal: 0
};
return { content: '', bytesTotal: 0 }
}
}
export function ShellDetailDialog(t0) {
const $ = _c(57);
const {
shell,
onDone,
onKillShell,
onBack
} = t0;
const {
columns
} = useTerminalSize();
let t1;
if ($[0] !== shell) {
t1 = () => getTaskOutput(shell);
$[0] = shell;
$[1] = t1;
} else {
t1 = $[1];
export function ShellDetailDialog({
shell,
onDone,
onKillShell,
onBack,
}: Props): React.ReactNode {
const { columns } = useTerminalSize()
// Promise created in initializer (not during render). For running shells,
// the effect timer replaces it periodically to pick up new output.
// useDeferredValue keeps showing the previous output while the new promise
// resolves, preventing the Suspense fallback from flickering.
const [outputPromise, setOutputPromise] = useState<Promise<TaskOutputResult>>(
() => getTaskOutput(shell),
)
const deferredOutputPromise = useDeferredValue(outputPromise)
useEffect(() => {
if (shell.status !== 'running') {
return
}
const timer = setInterval(
(setOutputPromise, shell) => setOutputPromise(getTaskOutput(shell)),
1000,
setOutputPromise,
shell,
)
return () => clearInterval(timer)
}, [shell.id, shell.status])
// Handle standard close action
const handleClose = () =>
onDone('Shell details dismissed', { display: 'system' })
// Handle additional close actions beyond Dialog's built-in Esc handler
useKeybindings(
{
'confirm:yes': handleClose,
},
{ context: 'Confirmation' },
)
// Handle dialog-specific keys
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === ' ') {
e.preventDefault()
onDone('Shell details dismissed', { display: 'system' })
} else if (e.key === 'left' && onBack) {
e.preventDefault()
onBack()
} else if (e.key === 'x' && shell.status === 'running' && onKillShell) {
e.preventDefault()
onKillShell()
}
}
const [outputPromise, setOutputPromise] = useState(t1);
const deferredOutputPromise = useDeferredValue(outputPromise);
let t2;
if ($[2] !== shell) {
t2 = () => {
if (shell.status !== "running") {
return;
}
const timer = setInterval(_temp, 1000, setOutputPromise, shell);
return () => clearInterval(timer);
};
$[2] = shell;
$[3] = t2;
} else {
t2 = $[3];
}
let t3;
if ($[4] !== shell.id || $[5] !== shell.status) {
t3 = [shell.id, shell.status];
$[4] = shell.id;
$[5] = shell.status;
$[6] = t3;
} else {
t3 = $[6];
}
useEffect(t2, t3);
let t4;
if ($[7] !== onDone) {
t4 = () => onDone("Shell details dismissed", {
display: "system"
});
$[7] = onDone;
$[8] = t4;
} else {
t4 = $[8];
}
const handleClose = t4;
let t5;
if ($[9] !== handleClose) {
t5 = {
"confirm:yes": handleClose
};
$[9] = handleClose;
$[10] = t5;
} else {
t5 = $[10];
}
let t6;
if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
t6 = {
context: "Confirmation"
};
$[11] = t6;
} else {
t6 = $[11];
}
useKeybindings(t5, t6);
let t7;
if ($[12] !== onBack || $[13] !== onDone || $[14] !== onKillShell || $[15] !== shell.status) {
t7 = e => {
if (e.key === " ") {
e.preventDefault();
onDone("Shell details dismissed", {
display: "system"
});
} else {
if (e.key === "left" && onBack) {
e.preventDefault();
onBack();
} else {
if (e.key === "x" && shell.status === "running" && onKillShell) {
e.preventDefault();
onKillShell();
}
// Truncate command if too long (for display purposes)
const isMonitor = shell.kind === 'monitor'
const displayCommand = truncateToWidth(shell.command, 280)
return (
<Box
flexDirection="column"
tabIndex={0}
autoFocus
onKeyDown={handleKeyDown}
>
<Dialog
title={isMonitor ? 'Monitor details' : 'Shell details'}
onCancel={handleClose}
color="background"
inputGuide={exitState =>
exitState.pending ? (
<Text>Press {exitState.keyName} again to exit</Text>
) : (
<Byline>
{onBack && <KeyboardShortcutHint shortcut="←" action="go back" />}
<KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />
{shell.status === 'running' && onKillShell && (
<KeyboardShortcutHint shortcut="x" action="stop" />
)}
</Byline>
)
}
}
};
$[12] = onBack;
$[13] = onDone;
$[14] = onKillShell;
$[15] = shell.status;
$[16] = t7;
} else {
t7 = $[16];
}
const handleKeyDown = t7;
const isMonitor = shell.kind === "monitor";
let t8;
if ($[17] !== shell.command) {
t8 = truncateToWidth(shell.command, 280);
$[17] = shell.command;
$[18] = t8;
} else {
t8 = $[18];
}
const displayCommand = t8;
const t9 = isMonitor ? "Monitor details" : "Shell details";
let t10;
if ($[19] !== onBack || $[20] !== onKillShell || $[21] !== shell.status) {
t10 = exitState => exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <Byline>{onBack && <KeyboardShortcutHint shortcut={"\u2190"} action="go back" />}<KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />{shell.status === "running" && onKillShell && <KeyboardShortcutHint shortcut="x" action="stop" />}</Byline>;
$[19] = onBack;
$[20] = onKillShell;
$[21] = shell.status;
$[22] = t10;
} else {
t10 = $[22];
}
let t11;
if ($[23] === Symbol.for("react.memo_cache_sentinel")) {
t11 = <Text bold={true}>Status:</Text>;
$[23] = t11;
} else {
t11 = $[23];
}
let t12;
if ($[24] !== shell.result || $[25] !== shell.status) {
t12 = <Text>{t11}{" "}{shell.status === "running" ? <Text color="background">{shell.status}{shell.result?.code !== undefined && ` (exit code: ${shell.result.code})`}</Text> : shell.status === "completed" ? <Text color="success">{shell.status}{shell.result?.code !== undefined && ` (exit code: ${shell.result.code})`}</Text> : <Text color="error">{shell.status}{shell.result?.code !== undefined && ` (exit code: ${shell.result.code})`}</Text>}</Text>;
$[24] = shell.result;
$[25] = shell.status;
$[26] = t12;
} else {
t12 = $[26];
}
let t13;
if ($[27] === Symbol.for("react.memo_cache_sentinel")) {
t13 = <Text bold={true}>Runtime:</Text>;
$[27] = t13;
} else {
t13 = $[27];
}
let t14;
if ($[28] !== shell.endTime) {
t14 = shell.endTime ?? Date.now();
$[28] = shell.endTime;
$[29] = t14;
} else {
t14 = $[29];
}
const t15 = t14 - shell.startTime;
let t16;
if ($[30] !== t15) {
t16 = formatDuration(t15);
$[30] = t15;
$[31] = t16;
} else {
t16 = $[31];
}
let t17;
if ($[32] !== t16) {
t17 = <Text>{t13}{" "}{t16}</Text>;
$[32] = t16;
$[33] = t17;
} else {
t17 = $[33];
}
const t18 = isMonitor ? "Script:" : "Command:";
let t19;
if ($[34] !== t18) {
t19 = <Text bold={true}>{t18}</Text>;
$[34] = t18;
$[35] = t19;
} else {
t19 = $[35];
}
let t20;
if ($[36] !== displayCommand || $[37] !== t19) {
t20 = <Text wrap="wrap">{t19}{" "}{displayCommand}</Text>;
$[36] = displayCommand;
$[37] = t19;
$[38] = t20;
} else {
t20 = $[38];
}
let t21;
if ($[39] !== t12 || $[40] !== t17 || $[41] !== t20) {
t21 = <Box flexDirection="column">{t12}{t17}{t20}</Box>;
$[39] = t12;
$[40] = t17;
$[41] = t20;
$[42] = t21;
} else {
t21 = $[42];
}
let t22;
if ($[43] === Symbol.for("react.memo_cache_sentinel")) {
t22 = <Text bold={true}>Output:</Text>;
$[43] = t22;
} else {
t22 = $[43];
}
let t23;
if ($[44] === Symbol.for("react.memo_cache_sentinel")) {
t23 = <Text dimColor={true}>Loading output</Text>;
$[44] = t23;
} else {
t23 = $[44];
}
let t24;
if ($[45] !== columns || $[46] !== deferredOutputPromise) {
t24 = <Box flexDirection="column">{t22}<Suspense fallback={t23}><ShellOutputContent outputPromise={deferredOutputPromise} columns={columns} /></Suspense></Box>;
$[45] = columns;
$[46] = deferredOutputPromise;
$[47] = t24;
} else {
t24 = $[47];
}
let t25;
if ($[48] !== handleClose || $[49] !== t10 || $[50] !== t21 || $[51] !== t24 || $[52] !== t9) {
t25 = <Dialog title={t9} onCancel={handleClose} color="background" inputGuide={t10}>{t21}{t24}</Dialog>;
$[48] = handleClose;
$[49] = t10;
$[50] = t21;
$[51] = t24;
$[52] = t9;
$[53] = t25;
} else {
t25 = $[53];
}
let t26;
if ($[54] !== handleKeyDown || $[55] !== t25) {
t26 = <Box flexDirection="column" tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t25}</Box>;
$[54] = handleKeyDown;
$[55] = t25;
$[56] = t26;
} else {
t26 = $[56];
}
return t26;
}
function _temp(setOutputPromise_0, shell_0) {
return setOutputPromise_0(getTaskOutput(shell_0));
>
<Box flexDirection="column">
<Text>
<Text bold>Status:</Text>{' '}
{shell.status === 'running' ? (
<Text color="background">
{shell.status}
{shell.result?.code !== undefined &&
` (exit code: ${shell.result.code})`}
</Text>
) : shell.status === 'completed' ? (
<Text color="success">
{shell.status}
{shell.result?.code !== undefined &&
` (exit code: ${shell.result.code})`}
</Text>
) : (
<Text color="error">
{shell.status}
{shell.result?.code !== undefined &&
` (exit code: ${shell.result.code})`}
</Text>
)}
</Text>
<Text>
<Text bold>Runtime:</Text>{' '}
{formatDuration((shell.endTime ?? Date.now()) - shell.startTime)}
</Text>
<Text wrap="wrap">
<Text bold>{isMonitor ? 'Script:' : 'Command:'}</Text>{' '}
{displayCommand}
</Text>
</Box>
<Box flexDirection="column">
<Text bold>Output:</Text>
<Suspense fallback={<Text dimColor>Loading output</Text>}>
<ShellOutputContent
outputPromise={deferredOutputPromise}
columns={columns}
/>
</Suspense>
</Box>
</Dialog>
</Box>
)
}
type ShellOutputContentProps = {
outputPromise: Promise<TaskOutputResult>;
columns: number;
};
function ShellOutputContent(t0) {
const $ = _c(19);
const {
outputPromise,
columns
} = t0;
const {
content,
bytesTotal
} = use(outputPromise) as any;
outputPromise: Promise<TaskOutputResult>
columns: number
}
function ShellOutputContent({
outputPromise,
columns,
}: ShellOutputContentProps): React.ReactNode {
const { content, bytesTotal } = use(outputPromise)
if (!content) {
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <Text dimColor={true}>No output available</Text>;
$[0] = t1;
} else {
t1 = $[0];
}
return t1;
return <Text dimColor>No output available</Text>
}
let isIncomplete;
let rendered;
if ($[1] !== bytesTotal || $[2] !== content) {
const starts = [];
let pos = content.length;
for (let i = 0; i < 10 && pos > 0; i++) {
const prev = content.lastIndexOf("\n", pos - 1);
starts.push(prev + 1);
pos = prev;
}
starts.reverse();
isIncomplete = bytesTotal > content.length;
rendered = [];
for (let i_0 = 0; i_0 < starts.length; i_0++) {
const start = starts[i_0];
const end = i_0 < starts.length - 1 ? starts[i_0 + 1] - 1 : content.length;
const line = content.slice(start, end);
if (line) {
rendered.push(line);
}
}
$[1] = bytesTotal;
$[2] = content;
$[3] = isIncomplete;
$[4] = rendered;
} else {
isIncomplete = $[3];
rendered = $[4];
// Find last 10 line boundaries via lastIndexOf
const starts: number[] = []
let pos = content.length
for (let i = 0; i < 10 && pos > 0; i++) {
const prev = content.lastIndexOf('\n', pos - 1)
starts.push(prev + 1)
pos = prev
}
const t1 = columns - 6;
let t2;
if ($[5] !== rendered) {
t2 = rendered.map(_temp2);
$[5] = rendered;
$[6] = t2;
} else {
t2 = $[6];
starts.reverse()
const isIncomplete = bytesTotal > content.length
// Build lines, skip empty trailing/leading segments
const rendered: string[] = []
for (let i = 0; i < starts.length; i++) {
const start = starts[i]!
const end = i < starts.length - 1 ? starts[i + 1]! - 1 : content.length
const line = content.slice(start, end)
if (line) rendered.push(line)
}
let t3;
if ($[7] !== t1 || $[8] !== t2) {
t3 = <Box borderStyle="round" paddingX={1} flexDirection="column" height={12} maxWidth={t1}>{t2}</Box>;
$[7] = t1;
$[8] = t2;
$[9] = t3;
} else {
t3 = $[9];
}
const t4 = `Showing ${rendered.length} lines`;
let t5;
if ($[10] !== bytesTotal || $[11] !== isIncomplete) {
t5 = isIncomplete ? ` of ${formatFileSize(bytesTotal)}` : "";
$[10] = bytesTotal;
$[11] = isIncomplete;
$[12] = t5;
} else {
t5 = $[12];
}
let t6;
if ($[13] !== t4 || $[14] !== t5) {
t6 = <Text dimColor={true} italic={true}>{t4}{t5}</Text>;
$[13] = t4;
$[14] = t5;
$[15] = t6;
} else {
t6 = $[15];
}
let t7;
if ($[16] !== t3 || $[17] !== t6) {
t7 = <>{t3}{t6}</>;
$[16] = t3;
$[17] = t6;
$[18] = t7;
} else {
t7 = $[18];
}
return t7;
}
function _temp2(line_0, i_1) {
return <Text key={i_1} wrap="truncate-end">{line_0}</Text>;
return (
<>
<Box
borderStyle="round"
paddingX={1}
flexDirection="column"
height={12}
maxWidth={columns - 6}
>
{rendered.map((line, i) => (
<Text key={i} wrap="truncate-end">
{line}
</Text>
))}
</Box>
<Text dimColor italic>
{`Showing ${rendered.length} lines`}
{isIncomplete ? ` of ${formatFileSize(bytesTotal)}` : ''}
</Text>
</>
)
}

View File

@@ -1,86 +1,52 @@
import { c as _c } from "react/compiler-runtime";
import type { ReactNode } from 'react';
import React from 'react';
import { Text } from 'src/ink.js';
import type { TaskStatus } from 'src/Task.js';
import type { LocalShellTaskState } from 'src/tasks/LocalShellTask/guards.js';
import type { DeepImmutable } from 'src/types/utils.js';
import type { ReactNode } from 'react'
import React from 'react'
import { Text } from 'src/ink.js'
import type { TaskStatus } from 'src/Task.js'
import type { LocalShellTaskState } from 'src/tasks/LocalShellTask/guards.js'
import type { DeepImmutable } from 'src/types/utils.js'
type TaskStatusTextProps = {
status: TaskStatus;
label?: string;
suffix?: string;
};
export function TaskStatusText(t0) {
const $ = _c(4);
const {
status,
label,
suffix
} = t0;
const displayLabel = label ?? status;
const color = status === "completed" ? "success" : status === "failed" ? "error" : status === "killed" ? "warning" : undefined;
let t1;
if ($[0] !== color || $[1] !== displayLabel || $[2] !== suffix) {
t1 = <Text color={color} dimColor={true}>({displayLabel}{suffix})</Text>;
$[0] = color;
$[1] = displayLabel;
$[2] = suffix;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
status: TaskStatus
label?: string
suffix?: string
}
export function ShellProgress(t0) {
const $ = _c(4);
const {
shell
} = t0;
export function TaskStatusText({
status,
label,
suffix,
}: TaskStatusTextProps): ReactNode {
const displayLabel = label ?? status
const color =
status === 'completed'
? 'success'
: status === 'failed'
? 'error'
: status === 'killed'
? 'warning'
: undefined
return (
<Text color={color} dimColor>
({displayLabel}
{suffix})
</Text>
)
}
export function ShellProgress({
shell,
}: {
shell: DeepImmutable<LocalShellTaskState>
}): ReactNode {
switch (shell.status) {
case "completed":
{
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <TaskStatusText status="completed" label="done" />;
$[0] = t1;
} else {
t1 = $[0];
}
return t1;
}
case "failed":
{
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <TaskStatusText status="failed" label="error" />;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}
case "killed":
{
let t1;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <TaskStatusText status="killed" label="stopped" />;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
case "running":
case "pending":
{
let t1;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <TaskStatusText status="running" />;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
case 'completed':
return <TaskStatusText status="completed" label="done" />
case 'failed':
return <TaskStatusText status="failed" label="error" />
case 'killed':
return <TaskStatusText status="killed" label="stopped" />
case 'running':
case 'pending':
return <TaskStatusText status="running" />
}
}

View File

@@ -1,32 +1,39 @@
import React from 'react';
import { Text } from '../../ink.js';
import type { Tools } from '../../Tool.js';
import { findToolByName } from '../../Tool.js';
import type { ToolActivity } from '../../tasks/LocalAgentTask/LocalAgentTask.js';
import type { ThemeName } from '../../utils/theme.js';
export function renderToolActivity(activity: ToolActivity, tools: Tools, theme: ThemeName): React.ReactNode {
const tool = findToolByName(tools, activity.toolName);
import React from 'react'
import { Text } from '../../ink.js'
import type { Tools } from '../../Tool.js'
import { findToolByName } from '../../Tool.js'
import type { ToolActivity } from '../../tasks/LocalAgentTask/LocalAgentTask.js'
import type { ThemeName } from '../../utils/theme.js'
export function renderToolActivity(
activity: ToolActivity,
tools: Tools,
theme: ThemeName,
): React.ReactNode {
const tool = findToolByName(tools, activity.toolName)
if (!tool) {
return activity.toolName;
return activity.toolName
}
try {
const parsed = tool.inputSchema.safeParse(activity.input);
const parsedInput = parsed.success ? parsed.data : {};
const userFacingName = tool.userFacingName(parsedInput);
const parsed = tool.inputSchema.safeParse(activity.input)
const parsedInput = parsed.success ? parsed.data : {}
const userFacingName = tool.userFacingName(parsedInput)
if (!userFacingName) {
return activity.toolName;
return activity.toolName
}
const toolArgs = tool.renderToolUseMessage(parsedInput, {
theme,
verbose: false
});
verbose: false,
})
if (toolArgs) {
return <Text>
return (
<Text>
{userFacingName}({toolArgs})
</Text>;
</Text>
)
}
return userFacingName;
return userFacingName
} catch {
return activity.toolName;
return activity.toolName
}
}

View File

@@ -2,71 +2,73 @@
* Shared utilities for displaying task status across different task types.
*/
import figures from 'figures';
import type { TaskStatus } from 'src/Task.js';
import type { InProcessTeammateTaskState } from 'src/tasks/InProcessTeammateTask/types.js';
import { isPanelAgentTask } from 'src/tasks/LocalAgentTask/LocalAgentTask.js';
import { isBackgroundTask, type TaskState } from 'src/tasks/types.js';
import type { DeepImmutable } from 'src/types/utils.js';
import { summarizeRecentActivities } from 'src/utils/collapseReadSearch.js';
import figures from 'figures'
import type { TaskStatus } from 'src/Task.js'
import type { InProcessTeammateTaskState } from 'src/tasks/InProcessTeammateTask/types.js'
import { isPanelAgentTask } from 'src/tasks/LocalAgentTask/LocalAgentTask.js'
import { isBackgroundTask, type TaskState } from 'src/tasks/types.js'
import type { DeepImmutable } from 'src/types/utils.js'
import { summarizeRecentActivities } from 'src/utils/collapseReadSearch.js'
/**
* Returns true if the given task status represents a terminal (finished) state.
*/
export function isTerminalStatus(status: TaskStatus): boolean {
return status === 'completed' || status === 'failed' || status === 'killed';
return status === 'completed' || status === 'failed' || status === 'killed'
}
/**
* Returns the appropriate icon for a task based on status and state flags.
*/
export function getTaskStatusIcon(status: TaskStatus, options?: {
isIdle?: boolean;
awaitingApproval?: boolean;
hasError?: boolean;
shutdownRequested?: boolean;
}): string {
const {
isIdle,
awaitingApproval,
hasError,
shutdownRequested
} = options ?? {};
if (hasError) return figures.cross;
if (awaitingApproval) return figures.questionMarkPrefix;
if (shutdownRequested) return figures.warning;
export function getTaskStatusIcon(
status: TaskStatus,
options?: {
isIdle?: boolean
awaitingApproval?: boolean
hasError?: boolean
shutdownRequested?: boolean
},
): string {
const { isIdle, awaitingApproval, hasError, shutdownRequested } =
options ?? {}
if (hasError) return figures.cross
if (awaitingApproval) return figures.questionMarkPrefix
if (shutdownRequested) return figures.warning
if (status === 'running') {
if (isIdle) return figures.ellipsis;
return figures.play;
if (isIdle) return figures.ellipsis
return figures.play
}
if (status === 'completed') return figures.tick;
if (status === 'failed' || status === 'killed') return figures.cross;
return figures.bullet;
if (status === 'completed') return figures.tick
if (status === 'failed' || status === 'killed') return figures.cross
return figures.bullet
}
/**
* Returns the appropriate semantic color for a task based on status and state flags.
*/
export function getTaskStatusColor(status: TaskStatus, options?: {
isIdle?: boolean;
awaitingApproval?: boolean;
hasError?: boolean;
shutdownRequested?: boolean;
}): 'success' | 'error' | 'warning' | 'background' {
const {
isIdle,
awaitingApproval,
hasError,
shutdownRequested
} = options ?? {};
if (hasError) return 'error';
if (awaitingApproval) return 'warning';
if (shutdownRequested) return 'warning';
if (isIdle) return 'background';
if (status === 'completed') return 'success';
if (status === 'failed') return 'error';
if (status === 'killed') return 'warning';
return 'background';
export function getTaskStatusColor(
status: TaskStatus,
options?: {
isIdle?: boolean
awaitingApproval?: boolean
hasError?: boolean
shutdownRequested?: boolean
},
): 'success' | 'error' | 'warning' | 'background' {
const { isIdle, awaitingApproval, hasError, shutdownRequested } =
options ?? {}
if (hasError) return 'error'
if (awaitingApproval) return 'warning'
if (shutdownRequested) return 'warning'
if (isIdle) return 'background'
if (status === 'completed') return 'success'
if (status === 'failed') return 'error'
if (status === 'killed') return 'warning'
return 'background'
}
/**
@@ -74,11 +76,18 @@ export function getTaskStatusColor(status: TaskStatus, options?: {
* accounting for shutdown/approval/idle states and falling back through
* recent-activity summary → last activity description → 'working'.
*/
export function describeTeammateActivity(t: DeepImmutable<InProcessTeammateTaskState>): string {
if (t.shutdownRequested) return 'stopping';
if (t.awaitingPlanApproval) return 'awaiting approval';
if (t.isIdle) return 'idle';
return (t.progress?.recentActivities && summarizeRecentActivities(t.progress.recentActivities)) ?? t.progress?.lastActivity?.activityDescription ?? 'working';
export function describeTeammateActivity(
t: DeepImmutable<InProcessTeammateTaskState>,
): string {
if (t.shutdownRequested) return 'stopping'
if (t.awaitingPlanApproval) return 'awaiting approval'
if (t.isIdle) return 'idle'
return (
(t.progress?.recentActivities &&
summarizeRecentActivities(t.progress.recentActivities)) ??
t.progress?.lastActivity?.activityDescription ??
'working'
)
}
/**
@@ -90,17 +99,21 @@ export function describeTeammateActivity(t: DeepImmutable<InProcessTeammateTaskS
* plus exclusion of panel-managed agent tasks for ants (those are shown
* by CoordinatorTaskPanel).
*/
export function shouldHideTasksFooter(tasks: {
[taskId: string]: TaskState;
}, showSpinnerTree: boolean): boolean {
if (!showSpinnerTree) return false;
let hasVisibleTask = false;
export function shouldHideTasksFooter(
tasks: { [taskId: string]: TaskState },
showSpinnerTree: boolean,
): boolean {
if (!showSpinnerTree) return false
let hasVisibleTask = false
for (const t of Object.values(tasks) as TaskState[]) {
if (!isBackgroundTask(t) || (process.env.USER_TYPE) === 'ant' && isPanelAgentTask(t)) {
continue;
if (
!isBackgroundTask(t) ||
(process.env.USER_TYPE === 'ant' && isPanelAgentTask(t))
) {
continue
}
hasVisibleTask = true;
if (t.type !== 'in_process_teammate') return false;
hasVisibleTask = true
if (t.type !== 'in_process_teammate') return false
}
return hasVisibleTask;
return hasVisibleTask
}

View File

@@ -1,64 +1,48 @@
import { c as _c } from "react/compiler-runtime";
import React, { type ReactNode } from 'react';
import type { Theme } from '../../utils/theme.js';
import { Dialog } from '../design-system/Dialog.js';
import { useWizard } from './useWizard.js';
import { WizardNavigationFooter } from './WizardNavigationFooter.js';
import React, { type ReactNode } from 'react'
import type { Theme } from '../../utils/theme.js'
import { Dialog } from '../design-system/Dialog.js'
import { useWizard } from './useWizard.js'
import { WizardNavigationFooter } from './WizardNavigationFooter.js'
type Props = {
title?: string;
color?: keyof Theme;
children: ReactNode;
subtitle?: string;
footerText?: ReactNode;
};
export function WizardDialogLayout(t0) {
const $ = _c(11);
const {
title: titleOverride,
color: t1,
children,
subtitle,
footerText
} = t0;
const color = t1 === undefined ? "suggestion" : t1;
title?: string
color?: keyof Theme
children: ReactNode
subtitle?: string
footerText?: ReactNode
}
export function WizardDialogLayout({
title: titleOverride,
color = 'suggestion',
children,
subtitle,
footerText,
}: Props): ReactNode {
const {
currentStepIndex,
totalSteps,
title: providerTitle,
showStepCounter,
goBack
} = useWizard();
const title = titleOverride || providerTitle || "Wizard";
const stepSuffix = showStepCounter !== false ? ` (${currentStepIndex + 1}/${totalSteps})` : "";
const t2 = `${title}${stepSuffix}`;
let t3;
if ($[0] !== children || $[1] !== color || $[2] !== goBack || $[3] !== subtitle || $[4] !== t2) {
t3 = <Dialog title={t2} subtitle={subtitle} onCancel={goBack} color={color} hideInputGuide={true} isCancelActive={false}>{children}</Dialog>;
$[0] = children;
$[1] = color;
$[2] = goBack;
$[3] = subtitle;
$[4] = t2;
$[5] = t3;
} else {
t3 = $[5];
}
let t4;
if ($[6] !== footerText) {
t4 = <WizardNavigationFooter instructions={footerText} />;
$[6] = footerText;
$[7] = t4;
} else {
t4 = $[7];
}
let t5;
if ($[8] !== t3 || $[9] !== t4) {
t5 = <>{t3}{t4}</>;
$[8] = t3;
$[9] = t4;
$[10] = t5;
} else {
t5 = $[10];
}
return t5;
goBack,
} = useWizard()
const title = titleOverride || providerTitle || 'Wizard'
const stepSuffix =
showStepCounter !== false ? ` (${currentStepIndex + 1}/${totalSteps})` : ''
return (
<>
<Dialog
title={`${title}${stepSuffix}`}
subtitle={subtitle}
onCancel={goBack}
color={color}
hideInputGuide
isCancelActive={false}
>
{children}
</Dialog>
<WizardNavigationFooter instructions={footerText} />
</>
)
}

View File

@@ -1,23 +1,37 @@
import React, { type ReactNode } from 'react';
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js';
import { Box, Text } from '../../ink.js';
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
import { Byline } from '../design-system/Byline.js';
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
import React, { type ReactNode } from 'react'
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
import { Box, Text } from '../../ink.js'
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
import { Byline } from '../design-system/Byline.js'
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
type Props = {
instructions?: ReactNode;
};
instructions?: ReactNode
}
export function WizardNavigationFooter({
instructions = <Byline>
instructions = (
<Byline>
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
<KeyboardShortcutHint shortcut="Enter" action="select" />
<ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" />
<ConfigurableShortcutHint
action="confirm:no"
context="Confirmation"
fallback="Esc"
description="go back"
/>
</Byline>
),
}: Props): ReactNode {
const exitState = useExitOnCtrlCDWithKeybindings();
return <Box marginLeft={3} marginTop={1}>
const exitState = useExitOnCtrlCDWithKeybindings()
return (
<Box marginLeft={3} marginTop={1}>
<Text dimColor>
{exitState.pending ? `Press ${exitState.keyName} again to exit` : instructions}
{exitState.pending
? `Press ${exitState.keyName} again to exit`
: instructions}
</Text>
</Box>;
</Box>
)
}

View File

@@ -1,156 +1,96 @@
import { c as _c } from "react/compiler-runtime";
import React, { createContext, type ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js';
import type { WizardContextValue, WizardProviderProps } from './types.js';
import React, {
createContext,
type ReactNode,
useCallback,
useEffect,
useMemo,
useState,
} from 'react'
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
import type { WizardContextValue, WizardProviderProps } from './types.js'
// Use any here for the context since it will be cast properly when used
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const WizardContext = createContext<WizardContextValue<any> | null>(null);
export function WizardProvider(t0) {
const $ = _c(38);
const {
steps,
initialData: t1,
onComplete,
onCancel,
children,
title,
showStepCounter: t2
} = t0;
let t3;
if ($[0] !== t1) {
t3 = t1 === undefined ? {} as T : t1;
$[0] = t1;
$[1] = t3;
} else {
t3 = $[1];
}
const initialData = t3;
const showStepCounter = t2 === undefined ? true : t2;
const [currentStepIndex, setCurrentStepIndex] = useState(0);
const [wizardData, setWizardData] = useState(initialData);
const [isCompleted, setIsCompleted] = useState(false);
let t4;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t4 = [];
$[2] = t4;
} else {
t4 = $[2];
}
const [navigationHistory, setNavigationHistory] = useState(t4);
useExitOnCtrlCDWithKeybindings();
let t5;
let t6;
if ($[3] !== isCompleted || $[4] !== onComplete || $[5] !== wizardData) {
t5 = () => {
if (isCompleted) {
setNavigationHistory([]);
onComplete(wizardData);
}
};
t6 = [isCompleted, wizardData, onComplete];
$[3] = isCompleted;
$[4] = onComplete;
$[5] = wizardData;
$[6] = t5;
$[7] = t6;
} else {
t5 = $[6];
t6 = $[7];
}
useEffect(t5, t6);
let t7;
if ($[8] !== currentStepIndex || $[9] !== navigationHistory || $[10] !== steps.length) {
t7 = () => {
if (currentStepIndex < steps.length - 1) {
if (navigationHistory.length > 0) {
setNavigationHistory(prev => [...prev, currentStepIndex]);
}
setCurrentStepIndex(_temp);
} else {
setIsCompleted(true);
}
};
$[8] = currentStepIndex;
$[9] = navigationHistory;
$[10] = steps.length;
$[11] = t7;
} else {
t7 = $[11];
}
const goNext = t7;
let t8;
if ($[12] !== currentStepIndex || $[13] !== navigationHistory || $[14] !== onCancel) {
t8 = () => {
export const WizardContext = createContext<WizardContextValue<any> | null>(null)
export function WizardProvider<T extends Record<string, unknown>>({
steps,
initialData = {} as T,
onComplete,
onCancel,
children,
title,
showStepCounter = true,
}: WizardProviderProps<T>): ReactNode {
const [currentStepIndex, setCurrentStepIndex] = useState(0)
const [wizardData, setWizardData] = useState<T>(initialData)
const [isCompleted, setIsCompleted] = useState(false)
const [navigationHistory, setNavigationHistory] = useState<number[]>([])
useExitOnCtrlCDWithKeybindings()
// Handle completion in useEffect to avoid updating parent during render
useEffect(() => {
if (isCompleted) {
setNavigationHistory([])
void onComplete(wizardData)
}
}, [isCompleted, wizardData, onComplete])
const goNext = useCallback(() => {
if (currentStepIndex < steps.length - 1) {
// If we have history (non-linear flow), add current step to it
if (navigationHistory.length > 0) {
const previousStep = navigationHistory[navigationHistory.length - 1];
if (previousStep !== undefined) {
setNavigationHistory(_temp2);
setCurrentStepIndex(previousStep);
}
} else {
if (currentStepIndex > 0) {
setCurrentStepIndex(_temp3);
} else {
if (onCancel) {
onCancel();
}
}
setNavigationHistory(prev => [...prev, currentStepIndex])
}
};
$[12] = currentStepIndex;
$[13] = navigationHistory;
$[14] = onCancel;
$[15] = t8;
} else {
t8 = $[15];
}
const goBack = t8;
let t9;
if ($[16] !== currentStepIndex || $[17] !== steps.length) {
t9 = index => {
setCurrentStepIndex(prev => prev + 1)
} else {
// Mark as completed, which will trigger useEffect
setIsCompleted(true)
}
}, [currentStepIndex, steps.length, navigationHistory])
const goBack = useCallback(() => {
// Check if we have navigation history to use
if (navigationHistory.length > 0) {
const previousStep = navigationHistory[navigationHistory.length - 1]
if (previousStep !== undefined) {
setNavigationHistory(prev => prev.slice(0, -1))
setCurrentStepIndex(previousStep)
}
} else if (currentStepIndex > 0) {
// Fallback to simple decrement if no history
setCurrentStepIndex(prev => prev - 1)
} else if (onCancel) {
onCancel()
}
}, [currentStepIndex, navigationHistory, onCancel])
const goToStep = useCallback(
(index: number) => {
if (index >= 0 && index < steps.length) {
setNavigationHistory(prev_3 => [...prev_3, currentStepIndex]);
setCurrentStepIndex(index);
// Push current step to history before jumping
setNavigationHistory(prev => [...prev, currentStepIndex])
setCurrentStepIndex(index)
}
};
$[16] = currentStepIndex;
$[17] = steps.length;
$[18] = t9;
} else {
t9 = $[18];
}
const goToStep = t9;
let t10;
if ($[19] !== onCancel) {
t10 = () => {
setNavigationHistory([]);
if (onCancel) {
onCancel();
}
};
$[19] = onCancel;
$[20] = t10;
} else {
t10 = $[20];
}
const cancel = t10;
let t11;
if ($[21] === Symbol.for("react.memo_cache_sentinel")) {
t11 = updates => {
setWizardData(prev_4 => ({
...prev_4,
...updates
}));
};
$[21] = t11;
} else {
t11 = $[21];
}
const updateWizardData = t11;
let t12;
if ($[22] !== cancel || $[23] !== currentStepIndex || $[24] !== goBack || $[25] !== goNext || $[26] !== goToStep || $[27] !== showStepCounter || $[28] !== steps.length || $[29] !== title || $[30] !== wizardData) {
t12 = {
},
[currentStepIndex, steps.length],
)
const cancel = useCallback(() => {
setNavigationHistory([])
if (onCancel) {
onCancel()
}
}, [onCancel])
const updateWizardData = useCallback((updates: Partial<T>) => {
setWizardData(prev => ({ ...prev, ...updates }))
}, [])
const contextValue = useMemo<WizardContextValue<T>>(
() => ({
currentStepIndex,
totalSteps: steps.length,
wizardData,
@@ -161,52 +101,31 @@ export function WizardProvider(t0) {
goToStep,
cancel,
title,
showStepCounter
};
$[22] = cancel;
$[23] = currentStepIndex;
$[24] = goBack;
$[25] = goNext;
$[26] = goToStep;
$[27] = showStepCounter;
$[28] = steps.length;
$[29] = title;
$[30] = wizardData;
$[31] = t12;
} else {
t12 = $[31];
}
const contextValue = t12;
const CurrentStepComponent = steps[currentStepIndex];
showStepCounter,
}),
[
currentStepIndex,
steps.length,
wizardData,
updateWizardData,
goNext,
goBack,
goToStep,
cancel,
title,
showStepCounter,
],
)
const CurrentStepComponent = steps[currentStepIndex]
if (!CurrentStepComponent || isCompleted) {
return null;
return null
}
let t13;
if ($[32] !== CurrentStepComponent || $[33] !== children) {
t13 = children || <CurrentStepComponent />;
$[32] = CurrentStepComponent;
$[33] = children;
$[34] = t13;
} else {
t13 = $[34];
}
let t14;
if ($[35] !== contextValue || $[36] !== t13) {
t14 = <WizardContext.Provider value={contextValue}>{t13}</WizardContext.Provider>;
$[35] = contextValue;
$[36] = t13;
$[37] = t14;
} else {
t14 = $[37];
}
return t14;
}
function _temp3(prev_2) {
return prev_2 - 1;
}
function _temp2(prev_1) {
return prev_1.slice(0, -1);
}
function _temp(prev_0) {
return prev_0 + 1;
return (
<WizardContext.Provider value={contextValue}>
{children || <CurrentStepComponent />}
</WizardContext.Provider>
)
}