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

View File

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

View File

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

View File

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

View File

@@ -1,212 +1,283 @@
import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useCallback, useEffect, useMemo, useRef } from 'react'
import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js'; import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js'
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.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 {
import { isAutoMemoryEnabled } from '../../memdir/paths.js'; type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
import { isPolicyAllowed } from '../../services/policyLimits/index.js'; logEvent,
import { FILE_READ_TOOL_NAME } from '../../tools/FileReadTool/prompt.js'; } from 'src/services/analytics/index.js'
import type { Message } from '../../types/message.js'; import { isAutoMemoryEnabled } from '../../memdir/paths.js'
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'; import { isPolicyAllowed } from '../../services/policyLimits/index.js'
import { isEnvTruthy } from '../../utils/envUtils.js'; import { FILE_READ_TOOL_NAME } from '../../tools/FileReadTool/prompt.js'
import { isAutoManagedMemoryFile } from '../../utils/memoryFileDetection.js'; import type { Message } from '../../types/message.js'
import { extractTextContent, getLastAssistantMessage } from '../../utils/messages.js'; import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
import { logOTelEvent } from '../../utils/telemetry/events.js'; import { isEnvTruthy } from '../../utils/envUtils.js'
import { submitTranscriptShare } from './submitTranscriptShare.js'; import { isAutoManagedMemoryFile } from '../../utils/memoryFileDetection.js'
import type { TranscriptShareResponse } from './TranscriptSharePrompt.js'; import {
import { useSurveyState } from './useSurveyState.js'; extractTextContent,
import type { FeedbackSurveyResponse } from './utils.js'; getLastAssistantMessage,
const HIDE_THANKS_AFTER_MS = 3000; } from '../../utils/messages.js'
const MEMORY_SURVEY_GATE = 'tengu_dunwich_bell'; import { logOTelEvent } from '../../utils/telemetry/events.js'
const MEMORY_SURVEY_EVENT = 'tengu_memory_survey_event'; import { submitTranscriptShare } from './submitTranscriptShare.js'
const SURVEY_PROBABILITY = 0.2; import type { TranscriptShareResponse } from './TranscriptSharePrompt.js'
const TRANSCRIPT_SHARE_TRIGGER = 'memory_survey'; import { useSurveyState } from './useSurveyState.js'
const MEMORY_WORD_RE = /\bmemor(?:y|ies)\b/i; 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 { function hasMemoryFileRead(messages: Message[]): boolean {
for (const message of messages) { for (const message of messages) {
if (message.type !== 'assistant') { if (message.type !== 'assistant') {
continue; continue
} }
const content = message.message.content; const content = message.message.content
if (!Array.isArray(content)) { if (!Array.isArray(content)) {
continue; continue
} }
for (const block of content) { for (const block of content) {
if (block.type !== 'tool_use' || block.name !== FILE_READ_TOOL_NAME) { if (block.type !== 'tool_use' || block.name !== FILE_READ_TOOL_NAME) {
continue; continue
} }
const input = block.input as { const input = block.input as { file_path?: unknown }
file_path?: unknown; if (
}; typeof input.file_path === 'string' &&
if (typeof input.file_path === 'string' && isAutoManagedMemoryFile(input.file_path)) { isAutoManagedMemoryFile(input.file_path)
return true; ) {
return true
} }
} }
} }
return false; return false
} }
export function useMemorySurvey(messages: Message[], isLoading: boolean, hasActivePrompt = false, {
enabled = true export function useMemorySurvey(
}: { messages: Message[],
enabled?: boolean; isLoading: boolean,
} = {}): { hasActivePrompt = false,
state: 'closed' | 'open' | 'thanks' | 'transcript_prompt' | 'submitting' | 'submitted'; { enabled = true }: { enabled?: boolean } = {},
lastResponse: FeedbackSurveyResponse | null; ): {
handleSelect: (selected: FeedbackSurveyResponse) => void; state:
handleTranscriptSelect: (selected: TranscriptShareResponse) => void; | '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 // 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. // 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 — // Once a memory file read is observed it stays true for the session —
// skip the O(n) scan on subsequent turns. // skip the O(n) scan on subsequent turns.
const memoryReadSeen = useRef(false); const memoryReadSeen = useRef(false)
const messagesRef = useRef(messages); const messagesRef = useRef(messages)
messagesRef.current = messages; messagesRef.current = messages
const onOpen = useCallback((appearanceId: string) => { const onOpen = useCallback((appearanceId: string) => {
logEvent(MEMORY_SURVEY_EVENT, { logEvent(MEMORY_SURVEY_EVENT, {
event_type: 'appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, event_type:
appearance_id: appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS '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', { void logOTelEvent('feedback_survey', {
event_type: 'appeared', event_type: 'appeared',
appearance_id: appearanceId, appearance_id: appearanceId,
survey_type: 'memory' survey_type: 'memory',
}); })
}, []); }, [])
const onSelect = useCallback((appearanceId_0: string, selected: FeedbackSurveyResponse) => {
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, { logEvent(MEMORY_SURVEY_EVENT, {
event_type: 'responded' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, event_type:
appearance_id: appearanceId_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 'transcript_prompt_appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
response: selected 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', { trigger:
event_type: 'responded', TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
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
});
void logOTelEvent('feedback_survey', { void logOTelEvent('feedback_survey', {
event_type: 'transcript_prompt_appeared', event_type: 'transcript_prompt_appeared',
appearance_id: appearanceId_1, appearance_id: appearanceId,
survey_type: 'memory' survey_type: 'memory',
}); })
}, []); }, [])
const onTranscriptSelect = useCallback(async (appearanceId_2: string, selected_1: TranscriptShareResponse): Promise<boolean> => {
logEvent(MEMORY_SURVEY_EVENT, { const onTranscriptSelect = useCallback(
event_type: `transcript_share_${selected_1}` as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, async (
appearance_id: appearanceId_2 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, appearanceId: string,
trigger: TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS selected: TranscriptShareResponse,
}); ): Promise<boolean> => {
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);
logEvent(MEMORY_SURVEY_EVENT, { logEvent(MEMORY_SURVEY_EVENT, {
event_type: (result.success ? 'transcript_share_submitted' : 'transcript_share_failed') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, event_type:
appearance_id: appearanceId_2 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, `transcript_share_${selected}` as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
trigger: TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS appearance_id:
}); appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
return result.success; trigger:
} TRANSCRIPT_SHARE_TRIGGER as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
return false; })
}, []);
const { if (selected === 'dont_ask_again') {
state, saveGlobalConfig(current => ({
lastResponse, ...current,
open, transcriptShareDismissed: true,
handleSelect, }))
handleTranscriptSelect }
} = useSurveyState({
hideThanksAfterMs: HIDE_THANKS_AFTER_MS, if (selected === 'yes') {
onOpen, const result = await submitTranscriptShare(
onSelect, messagesRef.current,
shouldShowTranscriptPrompt, TRANSCRIPT_SHARE_TRIGGER,
onTranscriptPromptShown, appearanceId,
onTranscriptSelect )
}); logEvent(MEMORY_SURVEY_EVENT, {
const lastAssistant = useMemo(() => getLastAssistantMessage(messages), [messages]); 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(() => { useEffect(() => {
if (!enabled) return; if (!enabled) return
// /clear resets messages but REPL stays mounted — reset refs so a memory // /clear resets messages but REPL stays mounted — reset refs so a memory
// read from the previous conversation doesn't leak into the new one. // read from the previous conversation doesn't leak into the new one.
if (messages.length === 0) { if (messages.length === 0) {
memoryReadSeen.current = false; memoryReadSeen.current = false
seenAssistantUuids.current.clear(); seenAssistantUuids.current.clear()
return; return
} }
if (state !== 'closed' || isLoading || hasActivePrompt) { if (state !== 'closed' || isLoading || hasActivePrompt) {
return; return
} }
// 3P default: survey off (no GrowthBook on Bedrock/Vertex/Foundry). // 3P default: survey off (no GrowthBook on Bedrock/Vertex/Foundry).
if (!getFeatureValue_CACHED_MAY_BE_STALE(MEMORY_SURVEY_GATE, false)) { if (!getFeatureValue_CACHED_MAY_BE_STALE(MEMORY_SURVEY_GATE, false)) {
return; return
} }
if (!isAutoMemoryEnabled()) { if (!isAutoMemoryEnabled()) {
return; return
} }
if (isFeedbackSurveyDisabled()) { if (isFeedbackSurveyDisabled()) {
return; return
} }
if (!isPolicyAllowed('allow_product_feedback')) { if (!isPolicyAllowed('allow_product_feedback')) {
return; return
} }
if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY)) { if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY)) {
return; return
} }
if (!lastAssistant || seenAssistantUuids.current.has(lastAssistant.uuid)) { 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)) { if (!MEMORY_WORD_RE.test(text)) {
return; return
} }
// Mark as evaluated before the memory-read scan so a turn that mentions // 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 // "memory" but has no memory read doesn't trigger repeated O(n) scans
// on subsequent renders with the same last assistant message. // on subsequent renders with the same last assistant message.
seenAssistantUuids.current.add(lastAssistant.uuid); seenAssistantUuids.current.add(lastAssistant.uuid)
if (!memoryReadSeen.current) { if (!memoryReadSeen.current) {
memoryReadSeen.current = hasMemoryFileRead(messages); memoryReadSeen.current = hasMemoryFileRead(messages)
} }
if (!memoryReadSeen.current) { if (!memoryReadSeen.current) {
return; return
} }
if (Math.random() < SURVEY_PROBABILITY) { if (Math.random() < SURVEY_PROBABILITY) {
open(); open()
} }
}, [enabled, state, isLoading, hasActivePrompt, lastAssistant, messages, open]); }, [
return { enabled,
state, state,
lastResponse, isLoading,
handleSelect, hasActivePrompt,
handleTranscriptSelect 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 { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js'
import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js'; import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'; import {
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js'; type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
import { shouldUseSessionMemoryCompaction } from '../../services/compact/sessionMemoryCompact.js'; logEvent,
import type { Message } from '../../types/message.js'; } from 'src/services/analytics/index.js'
import { isEnvTruthy } from '../../utils/envUtils.js'; import { shouldUseSessionMemoryCompaction } from '../../services/compact/sessionMemoryCompact.js'
import { isCompactBoundaryMessage } from '../../utils/messages.js'; import type { Message } from '../../types/message.js'
import { logOTelEvent } from '../../utils/telemetry/events.js'; import { isEnvTruthy } from '../../utils/envUtils.js'
import { useSurveyState } from './useSurveyState.js'; import { isCompactBoundaryMessage } from '../../utils/messages.js'
import type { FeedbackSurveyResponse } from './utils.js'; import { logOTelEvent } from '../../utils/telemetry/events.js'
const HIDE_THANKS_AFTER_MS = 3000; import { useSurveyState } from './useSurveyState.js'
const POST_COMPACT_SURVEY_GATE = 'tengu_post_compact_survey'; import type { FeedbackSurveyResponse } from './utils.js'
const SURVEY_PROBABILITY = 0.2; // Show survey 20% of the time after compaction
function hasMessageAfterBoundary(messages: Message[], boundaryUuid: string): boolean { const HIDE_THANKS_AFTER_MS = 3000
const boundaryIndex = messages.findIndex(msg => msg.uuid === boundaryUuid); 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) { if (boundaryIndex === -1) {
return false; return false
} }
// Check if there's a user or assistant message after the boundary // Check if there's a user or assistant message after the boundary
for (let i = boundaryIndex + 1; i < messages.length; i++) { 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')) { 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); export function usePostCompactSurvey(
const hasActivePrompt = t0 === undefined ? false : t0; messages: Message[],
let t2; isLoading: boolean,
if ($[0] !== t1) { hasActivePrompt = false,
t2 = t1 === undefined ? {} : t1; { enabled = true }: { enabled?: boolean } = {},
$[0] = t1; ): {
$[1] = t2; state:
} else { | 'closed'
t2 = $[1]; | 'open'
} | 'thanks'
const { | 'transcript_prompt'
enabled: t3 | 'submitting'
} = t2; | 'submitted'
const enabled = t3 === undefined ? true : t3; lastResponse: FeedbackSurveyResponse | null
const [gateEnabled, setGateEnabled] = useState(null); handleSelect: (selected: FeedbackSurveyResponse) => void
let t4; } {
if ($[2] === Symbol.for("react.memo_cache_sentinel")) { const [gateEnabled, setGateEnabled] = useState<boolean | null>(null)
t4 = new Set(); const seenCompactBoundaries = useRef<Set<string>>(new Set())
$[2] = t4; // Track the compact boundary we're waiting on (to show survey after next message)
} else { const pendingCompactBoundaryUuid = useRef<string | null>(null)
t4 = $[2];
} const onOpen = useCallback((appearanceId: string) => {
const seenCompactBoundaries = useRef(t4); const smCompactionEnabled = shouldUseSessionMemoryCompaction()
const pendingCompactBoundaryUuid = useRef(null); logEvent('tengu_post_compact_survey_event', {
const onOpen = _temp; event_type:
const onSelect = _temp2; 'appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
let t5; appearance_id:
if ($[3] === Symbol.for("react.memo_cache_sentinel")) { appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
t5 = { session_memory_compaction_enabled:
hideThanksAfterMs: HIDE_THANKS_AFTER_MS, smCompactionEnabled as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
onOpen, })
onSelect void logOTelEvent('feedback_survey', {
}; event_type: 'appeared',
$[3] = t5; appearance_id: appearanceId,
} else { survey_type: 'post_compact',
t5 = $[3]; })
} }, [])
const {
state, const onSelect = useCallback(
lastResponse, (appearanceId: string, selected: FeedbackSurveyResponse) => {
open, const smCompactionEnabled = shouldUseSessionMemoryCompaction()
handleSelect logEvent('tengu_post_compact_survey_event', {
} = useSurveyState(t5); event_type:
let t6; 'responded' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
let t7; appearance_id:
if ($[4] !== enabled) { appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
t6 = () => { response:
if (!enabled) { selected as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
return; session_memory_compaction_enabled:
} smCompactionEnabled as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
setGateEnabled(checkStatsigFeatureGate_CACHED_MAY_BE_STALE(POST_COMPACT_SURVEY_GATE)); })
}; void logOTelEvent('feedback_survey', {
t7 = [enabled]; event_type: 'responded',
$[4] = enabled; appearance_id: appearanceId,
$[5] = t6; response: selected,
$[6] = t7; survey_type: 'post_compact',
} else { })
t6 = $[5]; },
t7 = $[6]; [],
} )
useEffect(t6, t7);
let t8; const { state, lastResponse, open, handleSelect } = useSurveyState({
if ($[7] !== messages) { hideThanksAfterMs: HIDE_THANKS_AFTER_MS,
t8 = new Set(messages.filter(_temp3).map(_temp4)); onOpen,
$[7] = messages; onSelect,
$[8] = t8; })
} else {
t8 = $[8]; // Check the feature gate on mount
} useEffect(() => {
const currentCompactBoundaries = t8; if (!enabled) return
let t10; setGateEnabled(
let t9; checkStatsigFeatureGate_CACHED_MAY_BE_STALE(POST_COMPACT_SURVEY_GATE),
if ($[9] !== currentCompactBoundaries || $[10] !== enabled || $[11] !== gateEnabled || $[12] !== hasActivePrompt || $[13] !== isLoading || $[14] !== messages || $[15] !== open || $[16] !== state) { )
t9 = () => { }, [enabled])
if (!enabled) {
return; // Find compact boundary messages
} const currentCompactBoundaries = useMemo(
if (state !== "closed" || isLoading) { () =>
return; new Set(
} messages
if (hasActivePrompt) { .filter(msg => isCompactBoundaryMessage(msg))
return; .map(msg => msg.uuid),
} ),
if (gateEnabled !== true) { [messages],
return; )
}
if (isFeedbackSurveyDisabled()) { // Detect new compact boundaries and defer showing survey until next message
return; useEffect(() => {
} if (!enabled) return
if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY)) {
return; // Don't process if already showing
} if (state !== 'closed' || isLoading) {
if (pendingCompactBoundaryUuid.current !== null) { return
if (hasMessageAfterBoundary(messages, pendingCompactBoundaryUuid.current)) { }
pendingCompactBoundaryUuid.current = null;
if (Math.random() < SURVEY_PROBABILITY) { // Don't show survey when permission or ask question prompts are visible
open(); if (hasActivePrompt) {
} return
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); // Find new compact boundaries that we haven't seen yet
pendingCompactBoundaryUuid.current = newBoundaries[newBoundaries.length - 1]; const newBoundaries = Array.from(currentCompactBoundaries).filter(
} uuid => !seenCompactBoundaries.current.has(uuid),
}; )
t10 = [enabled, currentCompactBoundaries, state, isLoading, hasActivePrompt, gateEnabled, messages, open];
$[9] = currentCompactBoundaries; if (newBoundaries.length > 0) {
$[10] = enabled; // Mark these boundaries as seen
$[11] = gateEnabled; seenCompactBoundaries.current = new Set(currentCompactBoundaries)
$[12] = hasActivePrompt;
$[13] = isLoading; // Don't show survey immediately - wait for next message
$[14] = messages; // Store the most recent new boundary UUID
$[15] = open; pendingCompactBoundaryUuid.current =
$[16] = state; newBoundaries[newBoundaries.length - 1]!
$[17] = t10; }
$[18] = t9; }, [
} else { enabled,
t10 = $[17]; currentCompactBoundaries,
t9 = $[18]; state,
} isLoading,
useEffect(t9, t10); hasActivePrompt,
let t11; gateEnabled,
if ($[19] !== handleSelect || $[20] !== lastResponse || $[21] !== state) { messages,
t11 = { open,
state, ])
lastResponse,
handleSelect return { 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"
});
} }

View File

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

View File

@@ -1,50 +1,38 @@
import { c as _c } from "react/compiler-runtime"; import * as React from 'react'
import * as React from 'react'; import { stringWidth } from '../../ink/stringWidth.js'
import { stringWidth } from '../../ink/stringWidth.js'; import { Box, Text } from '../../ink.js'
import { Box, Text } from '../../ink.js'; import TextInput from '../TextInput.js'
import TextInput from '../TextInput.js';
type Props = { type Props = {
value: string; value: string
onChange: (value: string) => void; onChange: (value: string) => void
historyFailedMatch: boolean; 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;
} }
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 * as React from 'react'
import { FLAG_ICON } from '../../constants/figures.js'; import { FLAG_ICON } from '../../constants/figures.js'
import { Box, Text } from '../../ink.js'; import { Box, Text } from '../../ink.js'
/** /**
* ANT-ONLY: Banner shown in the transcript that prompts users to report * ANT-ONLY: Banner shown in the transcript that prompts users to report
* issues via /issue. Appears when friction is detected in the conversation. * issues via /issue. Appears when friction is detected in the conversation.
*/ */
export function IssueFlagBanner() { export function IssueFlagBanner(): React.ReactNode {
return null; 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 { feature } from 'bun:bundle'; import * as React from 'react'
import * as React from 'react'; import { type ReactNode, useEffect, useMemo, useState } from 'react'
import { type ReactNode, useEffect, useMemo, useState } from 'react'; import {
import { type Notification, useNotifications } from 'src/context/notifications.js'; type Notification,
import { logEvent } from 'src/services/analytics/index.js'; useNotifications,
import { useAppState } from 'src/state/AppState.js'; } from 'src/context/notifications.js'
import { useVoiceState } from '../../context/voice.js'; import { logEvent } from 'src/services/analytics/index.js'
import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js'; import { useAppState } from 'src/state/AppState.js'
import { useIdeConnectionStatus } from '../../hooks/useIdeConnectionStatus.js'; import { useVoiceState } from '../../context/voice.js'
import type { IDESelection } from '../../hooks/useIdeSelection.js'; import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js'
import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'; import { useIdeConnectionStatus } from '../../hooks/useIdeConnectionStatus.js'
import { useVoiceEnabled } from '../../hooks/useVoiceEnabled.js'; import type { IDESelection } from '../../hooks/useIdeSelection.js'
import { Box, Text } from '../../ink.js'; import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'
import { useClaudeAiLimits } from '../../services/claudeAiLimitsHook.js'; import { useVoiceEnabled } from '../../hooks/useVoiceEnabled.js'
import { calculateTokenWarningState } from '../../services/compact/autoCompact.js'; import { Box, Text } from '../../ink.js'
import type { MCPServerConnection } from '../../services/mcp/types.js'; import { useClaudeAiLimits } from '../../services/claudeAiLimitsHook.js'
import type { Message } from '../../types/message.js'; import { calculateTokenWarningState } from '../../services/compact/autoCompact.js'
import { getApiKeyHelperElapsedMs, getConfiguredApiKeyHelper, getSubscriptionType } from '../../utils/auth.js'; import type { MCPServerConnection } from '../../services/mcp/types.js'
import type { AutoUpdaterResult } from '../../utils/autoUpdater.js'; import type { Message } from '../../types/message.js'
import { getExternalEditor } from '../../utils/editor.js'; import {
import { isEnvTruthy } from '../../utils/envUtils.js'; getApiKeyHelperElapsedMs,
import { formatDuration } from '../../utils/format.js'; getConfiguredApiKeyHelper,
import { setEnvHookNotifier } from '../../utils/hooks/fileChangedWatcher.js'; getSubscriptionType,
import { toIDEDisplayName } from '../../utils/ide.js'; } from '../../utils/auth.js'
import { getMessagesAfterCompactBoundary } from '../../utils/messages.js'; import type { AutoUpdaterResult } from '../../utils/autoUpdater.js'
import { tokenCountFromLastAPIResponse } from '../../utils/tokens.js'; import { getExternalEditor } from '../../utils/editor.js'
import { AutoUpdaterWrapper } from '../AutoUpdaterWrapper.js'; import { isEnvTruthy } from '../../utils/envUtils.js'
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'; import { formatDuration } from '../../utils/format.js'
import { IdeStatusIndicator } from '../IdeStatusIndicator.js'; import { setEnvHookNotifier } from '../../utils/hooks/fileChangedWatcher.js'
import { MemoryUsageIndicator } from '../MemoryUsageIndicator.js'; import { toIDEDisplayName } from '../../utils/ide.js'
import { SentryErrorBoundary } from '../SentryErrorBoundary.js'; import { getMessagesAfterCompactBoundary } from '../../utils/messages.js'
import { TokenWarning } from '../TokenWarning.js'; import { tokenCountFromLastAPIResponse } from '../../utils/tokens.js'
import { SandboxPromptFooterHint } from './SandboxPromptFooterHint.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 */ /* 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 */ /* eslint-enable @typescript-eslint/no-require-imports */
export const FOOTER_TEMPORARY_STATUS_TIMEOUT = 5000; export const FOOTER_TEMPORARY_STATUS_TIMEOUT = 5000
type Props = { type Props = {
apiKeyStatus: VerificationStatus; apiKeyStatus: VerificationStatus
autoUpdaterResult: AutoUpdaterResult | null; autoUpdaterResult: AutoUpdaterResult | null
isAutoUpdating: boolean; isAutoUpdating: boolean
debug: boolean; debug: boolean
verbose: boolean; verbose: boolean
messages: Message[]; messages: Message[]
onAutoUpdaterResult: (result: AutoUpdaterResult) => void; onAutoUpdaterResult: (result: AutoUpdaterResult) => void
onChangeIsUpdating: (isUpdating: boolean) => void; onChangeIsUpdating: (isUpdating: boolean) => void
ideSelection: IDESelection | undefined; ideSelection: IDESelection | undefined
mcpClients?: MCPServerConnection[]; mcpClients?: MCPServerConnection[]
isInputWrapped?: boolean; isInputWrapped?: boolean
isNarrow?: boolean; isNarrow?: boolean
}; }
export function Notifications(t0) {
const $ = _c(34); export function Notifications({
const { apiKeyStatus,
apiKeyStatus, autoUpdaterResult,
autoUpdaterResult, debug,
debug, isAutoUpdating,
isAutoUpdating, verbose,
verbose, messages,
messages, onAutoUpdaterResult,
onAutoUpdaterResult, onChangeIsUpdating,
onChangeIsUpdating, ideSelection,
ideSelection, mcpClients,
mcpClients, isInputWrapped = false,
isInputWrapped: t1, isNarrow = false,
isNarrow: t2 }: Props): ReactNode {
} = t0; const tokenUsage = useMemo(() => {
const isInputWrapped = t1 === undefined ? false : t1; const messagesForTokenCount = getMessagesAfterCompactBoundary(messages)
const isNarrow = t2 === undefined ? false : t2; return tokenCountFromLastAPIResponse(messagesForTokenCount)
let t3; }, [messages])
if ($[0] !== messages) {
const messagesForTokenCount = getMessagesAfterCompactBoundary(messages); // AppState-sourced model — same source as API requests. getMainLoopModel()
t3 = tokenCountFromLastAPIResponse(messagesForTokenCount); // re-reads settings.json on every call, so another session's /model write
$[0] = messages; // would leak into this session's display (anthropics/claude-code#37596).
$[1] = t3; const mainLoopModel = useMainLoopModel()
} else { const isShowingCompactMessage = calculateTokenWarningState(
t3 = $[1]; tokenUsage,
} mainLoopModel,
const tokenUsage = t3; ).isAboveWarningThreshold
const mainLoopModel = useMainLoopModel(); const { status: ideStatus } = useIdeConnectionStatus(mcpClients)
let t4; const notifications = useAppState(s => s.notifications)
if ($[2] !== mainLoopModel || $[3] !== tokenUsage) { const { addNotification, removeNotification } = useNotifications()
t4 = calculateTokenWarningState(tokenUsage, mainLoopModel); const claudeAiLimits = useClaudeAiLimits()
$[2] = mainLoopModel;
$[3] = tokenUsage; // Register env hook notifier for CwdChanged/FileChanged feedback
$[4] = t4; useEffect(() => {
} else { setEnvHookNotifier((text, isError) => {
t4 = $[4]; addNotification({
} key: 'env-hook',
const isShowingCompactMessage = t4.isAboveWarningThreshold; text,
const { color: isError ? 'error' : undefined,
status: ideStatus priority: isError ? 'medium' : 'low',
} = useIdeConnectionStatus(mcpClients); timeoutMs: isError ? 8000 : 5000,
const notifications = useAppState(_temp); })
const { })
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, addNotification,
removeNotification removeNotification,
} = useNotifications(); ])
const claudeAiLimits = useClaudeAiLimits();
let t5; return (
let t6; <SentryErrorBoundary>
if ($[5] !== addNotification) { <Box
t5 = () => { flexDirection="column"
setEnvHookNotifier((text, isError) => { alignItems={isNarrow ? 'flex-start' : 'flex-end'}
addNotification({ flexShrink={0}
key: "env-hook", overflowX="hidden"
text, >
color: isError ? "error" : undefined, <NotificationContent
priority: isError ? "medium" : "low", ideSelection={ideSelection}
timeoutMs: isError ? 8000 : 5000 mcpClients={mcpClients}
}); notifications={notifications}
}); isInOverageMode={isInOverageMode ?? false}
return _temp2; isTeamOrEnterprise={isTeamOrEnterprise}
}; apiKeyStatus={apiKeyStatus}
t6 = [addNotification]; debug={debug}
$[5] = addNotification; verbose={verbose}
$[6] = t5; tokenUsage={tokenUsage}
$[7] = t6; mainLoopModel={mainLoopModel}
} else { shouldShowAutoUpdater={shouldShowAutoUpdater}
t5 = $[6]; autoUpdaterResult={autoUpdaterResult}
t6 = $[7]; isAutoUpdating={isAutoUpdating}
} isShowingCompactMessage={isShowingCompactMessage}
useEffect(t5, t6); onAutoUpdaterResult={onAutoUpdaterResult}
const shouldShowIdeSelection = ideStatus === "connected" && (ideSelection?.filePath || ideSelection?.text && ideSelection.lineCount > 0); onChangeIsUpdating={onChangeIsUpdating}
const shouldShowAutoUpdater = !shouldShowIdeSelection || isAutoUpdating || autoUpdaterResult?.status !== "success"; />
const isInOverageMode = claudeAiLimits.isUsingOverage; </Box>
let t7; </SentryErrorBoundary>
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;
} }
function NotificationContent({ function NotificationContent({
ideSelection, ideSelection,
mcpClients, mcpClients,
@@ -229,103 +212,155 @@ function NotificationContent({
isAutoUpdating, isAutoUpdating,
isShowingCompactMessage, isShowingCompactMessage,
onAutoUpdaterResult, onAutoUpdaterResult,
onChangeIsUpdating onChangeIsUpdating,
}: { }: {
ideSelection: IDESelection | undefined; ideSelection: IDESelection | undefined
mcpClients?: MCPServerConnection[]; mcpClients?: MCPServerConnection[]
notifications: { notifications: {
current: Notification | null; current: Notification | null
queue: Notification[]; queue: Notification[]
}; }
isInOverageMode: boolean; isInOverageMode: boolean
isTeamOrEnterprise: boolean; isTeamOrEnterprise: boolean
apiKeyStatus: VerificationStatus; apiKeyStatus: VerificationStatus
debug: boolean; debug: boolean
verbose: boolean; verbose: boolean
tokenUsage: number; tokenUsage: number
mainLoopModel: string; mainLoopModel: string
shouldShowAutoUpdater: boolean; shouldShowAutoUpdater: boolean
autoUpdaterResult: AutoUpdaterResult | null; autoUpdaterResult: AutoUpdaterResult | null
isAutoUpdating: boolean; isAutoUpdating: boolean
isShowingCompactMessage: boolean; isShowingCompactMessage: boolean
onAutoUpdaterResult: (result: AutoUpdaterResult) => void; onAutoUpdaterResult: (result: AutoUpdaterResult) => void
onChangeIsUpdating: (isUpdating: boolean) => void; onChangeIsUpdating: (isUpdating: boolean) => void
}): ReactNode { }): ReactNode {
// Poll apiKeyHelper inflight state to show slow-helper notice. // Poll apiKeyHelper inflight state to show slow-helper notice.
// Gated on configuration — most users never set apiKeyHelper, so the // Gated on configuration — most users never set apiKeyHelper, so the
// effect is a no-op for them (no interval allocated). // 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(() => { useEffect(() => {
if (!getConfiguredApiKeyHelper()) return; if (!getConfiguredApiKeyHelper()) return
const interval = setInterval((setSlow: React.Dispatch<React.SetStateAction<string | null>>) => { const interval = setInterval(
const ms = getApiKeyHelperElapsedMs(); (setSlow: React.Dispatch<React.SetStateAction<string | null>>) => {
const next = ms >= 10_000 ? formatDuration(ms) : null; const ms = getApiKeyHelperElapsedMs()
setSlow(prev => next === prev ? prev : next); const next = ms >= 10_000 ? formatDuration(ms) : null
}, 1000, setApiKeyHelperSlow); setSlow(prev => (next === prev ? prev : next))
return () => clearInterval(interval); },
}, []); 1000,
setApiKeyHelperSlow,
)
return () => clearInterval(interval)
}, [])
// Voice state (VOICE_MODE builds only, runtime-gated by GrowthBook) // 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 // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
useVoiceState(s => s.voiceState) : 'idle' as const; const voiceEnabled = feature('VOICE_MODE') ? useVoiceEnabled() : false
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant const voiceError = feature('VOICE_MODE')
const voiceEnabled = feature('VOICE_MODE') ? useVoiceEnabled() : false; ? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
const voiceError = feature('VOICE_MODE') ? useVoiceState(s => s.voiceError)
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant : null
useVoiceState(s_0 => s_0.voiceError) : null; const isBriefOnly =
const isBriefOnly = feature('KAIROS') || feature('KAIROS_BRIEF') ? feature('KAIROS') || feature('KAIROS_BRIEF')
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant ? // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
useAppState(s_1 => s_1.isBriefOnly) : false; useAppState(s => s.isBriefOnly)
: false
// When voice is actively recording or processing, replace all // When voice is actively recording or processing, replace all
// notifications with just the voice indicator. // notifications with just the voice indicator.
if (feature('VOICE_MODE') && voiceEnabled && (voiceState === 'recording' || voiceState === 'processing')) { if (
return <VoiceIndicator voiceState={voiceState} />; feature('VOICE_MODE') &&
voiceEnabled &&
(voiceState === 'recording' || voiceState === 'processing')
) {
return <VoiceIndicator voiceState={voiceState} />
} }
return <>
return (
<>
<IdeStatusIndicator ideSelection={ideSelection} mcpClients={mcpClients} /> <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} {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} {notifications.current.text}
</Text>)} </Text>
{isInOverageMode && !isTeamOrEnterprise && <Box> ))}
{isInOverageMode && !isTeamOrEnterprise && (
<Box>
<Text dimColor wrap="truncate"> <Text dimColor wrap="truncate">
Now using extra usage Now using extra usage
</Text> </Text>
</Box>} </Box>
{apiKeyHelperSlow && <Box> )}
{apiKeyHelperSlow && (
<Box>
<Text color="warning" wrap="truncate"> <Text color="warning" wrap="truncate">
apiKeyHelper is taking a while{' '} apiKeyHelper is taking a while{' '}
</Text> </Text>
<Text dimColor wrap="truncate"> <Text dimColor wrap="truncate">
({apiKeyHelperSlow}) ({apiKeyHelperSlow})
</Text> </Text>
</Box>} </Box>
{(apiKeyStatus === 'invalid' || apiKeyStatus === 'missing') && <Box> )}
{(apiKeyStatus === 'invalid' || apiKeyStatus === 'missing') && (
<Box>
<Text color="error" wrap="truncate"> <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> </Text>
</Box>} </Box>
{debug && <Box> )}
{debug && (
<Box>
<Text color="warning" wrap="truncate"> <Text color="warning" wrap="truncate">
Debug mode Debug mode
</Text> </Text>
</Box>} </Box>
{apiKeyStatus !== 'invalid' && apiKeyStatus !== 'missing' && verbose && <Box> )}
{apiKeyStatus !== 'invalid' && apiKeyStatus !== 'missing' && verbose && (
<Box>
<Text dimColor wrap="truncate"> <Text dimColor wrap="truncate">
{tokenUsage} tokens {tokenUsage} tokens
</Text> </Text>
</Box>} </Box>
{!isBriefOnly && <TokenWarning tokenUsage={tokenUsage} model={mainLoopModel} />} )}
{shouldShowAutoUpdater && <AutoUpdaterWrapper verbose={verbose} onAutoUpdaterResult={onAutoUpdaterResult} autoUpdaterResult={autoUpdaterResult} isUpdating={isAutoUpdating} onChangeIsUpdating={onChangeIsUpdating} showSuccessMessage={!isShowingCompactMessage} />} {!isBriefOnly && (
{feature('VOICE_MODE') ? voiceEnabled && voiceError && <Box> <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"> <Text color="error" wrap="truncate">
{voiceError} {voiceError}
</Text> </Text>
</Box> : null} </Box>
)
: null}
<MemoryUsageIndicator /> <MemoryUsageIndicator />
<SandboxPromptFooterHint /> <SandboxPromptFooterHint />
</>; </>
)
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,65 +1,77 @@
import { feature } from 'bun:bundle'; import { feature } from 'bun:bundle'
import * as React from 'react'; import * as React from 'react'
import { memo, type ReactNode, useMemo, useRef } from 'react'; import { memo, type ReactNode, useMemo, useRef } from 'react'
import { isBridgeEnabled } from '../../bridge/bridgeEnabled.js'; import { isBridgeEnabled } from '../../bridge/bridgeEnabled.js'
import { getBridgeStatus } from '../../bridge/bridgeStatusUtil.js'; import { getBridgeStatus } from '../../bridge/bridgeStatusUtil.js'
import { useSetPromptOverlay } from '../../context/promptOverlayContext.js'; import { useSetPromptOverlay } from '../../context/promptOverlayContext.js'
import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js'; import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js'
import type { IDESelection } from '../../hooks/useIdeSelection.js'; import type { IDESelection } from '../../hooks/useIdeSelection.js'
import { useSettings } from '../../hooks/useSettings.js'; import { useSettings } from '../../hooks/useSettings.js'
import { useTerminalSize } from '../../hooks/useTerminalSize.js'; import { useTerminalSize } from '../../hooks/useTerminalSize.js'
import { Box, Text } from '../../ink.js'; import { Box, Text } from '../../ink.js'
import type { MCPServerConnection } from '../../services/mcp/types.js'; import type { MCPServerConnection } from '../../services/mcp/types.js'
import { useAppState } from '../../state/AppState.js'; import { useAppState } from '../../state/AppState.js'
import type { ToolPermissionContext } from '../../Tool.js'; import type { ToolPermissionContext } from '../../Tool.js'
import type { Message } from '../../types/message.js'; import type { Message } from '../../types/message.js'
import type { PromptInputMode, VimMode } from '../../types/textInputTypes.js'; import type { PromptInputMode, VimMode } from '../../types/textInputTypes.js'
import type { AutoUpdaterResult } from '../../utils/autoUpdater.js'; import type { AutoUpdaterResult } from '../../utils/autoUpdater.js'
import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'; import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'
import { isUndercover } from '../../utils/undercover.js'; import { isUndercover } from '../../utils/undercover.js'
import { CoordinatorTaskPanel, useCoordinatorTaskCount } from '../CoordinatorAgentStatus.js'; import {
import { getLastAssistantMessageId, StatusLine, statusLineShouldDisplay } from '../StatusLine.js'; CoordinatorTaskPanel,
import { Notifications } from './Notifications.js'; useCoordinatorTaskCount,
import { PromptInputFooterLeftSide } from './PromptInputFooterLeftSide.js'; } from '../CoordinatorAgentStatus.js'
import { PromptInputFooterSuggestions, type SuggestionItem } from './PromptInputFooterSuggestions.js'; import {
import { PromptInputHelpMenu } from './PromptInputHelpMenu.js'; 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 = { type Props = {
apiKeyStatus: VerificationStatus; apiKeyStatus: VerificationStatus
debug: boolean; debug: boolean
exitMessage: { exitMessage: {
show: boolean; show: boolean
key?: string; key?: string
}; }
vimMode: VimMode | undefined; vimMode: VimMode | undefined
mode: PromptInputMode; mode: PromptInputMode
autoUpdaterResult: AutoUpdaterResult | null; autoUpdaterResult: AutoUpdaterResult | null
isAutoUpdating: boolean; isAutoUpdating: boolean
verbose: boolean; verbose: boolean
onAutoUpdaterResult: (result: AutoUpdaterResult) => void; onAutoUpdaterResult: (result: AutoUpdaterResult) => void
onChangeIsUpdating: (isUpdating: boolean) => void; onChangeIsUpdating: (isUpdating: boolean) => void
suggestions: SuggestionItem[]; suggestions: SuggestionItem[]
selectedSuggestion: number; selectedSuggestion: number
maxColumnWidth?: number; maxColumnWidth?: number
toolPermissionContext: ToolPermissionContext; toolPermissionContext: ToolPermissionContext
helpOpen: boolean; helpOpen: boolean
suppressHint: boolean; suppressHint: boolean
isLoading: boolean; isLoading: boolean
tasksSelected: boolean; tasksSelected: boolean
teamsSelected: boolean; teamsSelected: boolean
bridgeSelected: boolean; bridgeSelected: boolean
tmuxSelected: boolean; tmuxSelected: boolean
teammateFooterIndex?: number; teammateFooterIndex?: number
ideSelection: IDESelection | undefined; ideSelection: IDESelection | undefined
mcpClients?: MCPServerConnection[]; mcpClients?: MCPServerConnection[]
isPasting?: boolean; isPasting?: boolean
isInputWrapped?: boolean; isInputWrapped?: boolean
messages: Message[]; messages: Message[]
isSearching: boolean; isSearching: boolean
historyQuery: string; historyQuery: string
setHistoryQuery: (query: string) => void; setHistoryQuery: (query: string) => void
historyFailedMatch: boolean; historyFailedMatch: boolean
onOpenTasksDialog?: (taskId?: string) => void; onOpenTasksDialog?: (taskId?: string) => void
}; }
function PromptInputFooter({ function PromptInputFooter({
apiKeyStatus, apiKeyStatus,
debug, debug,
@@ -92,99 +104,176 @@ function PromptInputFooter({
historyQuery, historyQuery,
setHistoryQuery, setHistoryQuery,
historyFailedMatch, historyFailedMatch,
onOpenTasksDialog onOpenTasksDialog,
}: Props): ReactNode { }: Props): ReactNode {
const settings = useSettings(); const settings = useSettings()
const { const { columns, rows } = useTerminalSize()
columns, const messagesRef = useRef(messages)
rows messagesRef.current = messages
} = useTerminalSize(); const lastAssistantMessageId = useMemo(
const messagesRef = useRef(messages); () => getLastAssistantMessageId(messages),
messagesRef.current = messages; [messages],
const lastAssistantMessageId = useMemo(() => getLastAssistantMessageId(messages), [messages]); )
const isNarrow = columns < 80; const isNarrow = columns < 80
// In fullscreen the bottom slot is flexShrink:0, so every row here is a row // 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 // stolen from the ScrollBox. Drop the optional StatusLine first. Non-fullscreen
// has terminal scrollback to absorb overflow, so we never hide StatusLine there. // has terminal scrollback to absorb overflow, so we never hide StatusLine there.
const isFullscreen = isFullscreenEnvEnabled(); const isFullscreen = isFullscreenEnvEnabled()
const isShort = isFullscreen && rows < 24; const isShort = isFullscreen && rows < 24
// Pill highlights when tasks is the active footer item AND no specific // Pill highlights when tasks is the active footer item AND no specific
// agent row is selected. When coordinatorTaskIndex >= 0 the pointer has // agent row is selected. When coordinatorTaskIndex >= 0 the pointer has
// moved into CoordinatorTaskPanel, so the pill should un-highlight. // moved into CoordinatorTaskPanel, so the pill should un-highlight.
// coordinatorTaskCount === 0 covers the bash-only case (no agent rows // coordinatorTaskCount === 0 covers the bash-only case (no agent rows
// exist, pill is the only selectable item). // exist, pill is the only selectable item).
const coordinatorTaskCount = useCoordinatorTaskCount(); const coordinatorTaskCount = useCoordinatorTaskCount()
const coordinatorTaskIndex = useAppState(s => s.coordinatorTaskIndex); const coordinatorTaskIndex = useAppState(s => s.coordinatorTaskIndex)
const pillSelected = tasksSelected && (coordinatorTaskCount === 0 || coordinatorTaskIndex < 0); const pillSelected =
tasksSelected && (coordinatorTaskCount === 0 || coordinatorTaskIndex < 0)
// Hide `? for shortcuts` if the user has a custom status line, or during ctrl-r // 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 // Fullscreen: portal data to FullscreenLayout — see promptOverlayContext.tsx
const overlayData = useMemo(() => isFullscreen && suggestions.length ? { const overlayData = useMemo(
suggestions, () =>
selectedSuggestion, isFullscreen && suggestions.length
maxColumnWidth ? { suggestions, selectedSuggestion, maxColumnWidth }
} : null, [isFullscreen, suggestions, selectedSuggestion, maxColumnWidth]); : null,
useSetPromptOverlay(overlayData); [isFullscreen, suggestions, selectedSuggestion, maxColumnWidth],
)
useSetPromptOverlay(overlayData)
if (suggestions.length && !isFullscreen) { if (suggestions.length && !isFullscreen) {
return <Box paddingX={2} paddingY={0}> return (
<PromptInputFooterSuggestions suggestions={suggestions} selectedSuggestion={selectedSuggestion} maxColumnWidth={maxColumnWidth} /> <Box paddingX={2} paddingY={0}>
</Box>; <PromptInputFooterSuggestions
suggestions={suggestions}
selectedSuggestion={selectedSuggestion}
maxColumnWidth={maxColumnWidth}
/>
</Box>
)
} }
if (helpOpen) { 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}> <Box flexDirection="column" flexShrink={isNarrow ? 0 : 1}>
{mode === 'prompt' && !isShort && !exitMessage.show && !isPasting && statusLineShouldDisplay(settings) && <StatusLine messagesRef={messagesRef} lastAssistantMessageId={lastAssistantMessageId} vimMode={vimMode} />} {mode === 'prompt' &&
<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} /> !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>
<Box flexShrink={1} gap={1}> <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} />} {isFullscreen ? null : (
{(process.env.USER_TYPE) === 'ant' && isUndercover() && <Text dimColor>undercover</Text>} <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} /> <BridgeStatusIndicator bridgeSelected={bridgeSelected} />
</Box> </Box>
</Box> </Box>
{(process.env.USER_TYPE) === 'ant' && <CoordinatorTaskPanel />} {process.env.USER_TYPE === 'ant' && <CoordinatorTaskPanel />}
</>; </>
)
} }
export default memo(PromptInputFooter);
export default memo(PromptInputFooter)
type BridgeStatusProps = { type BridgeStatusProps = {
bridgeSelected: boolean; bridgeSelected: boolean
}; }
function BridgeStatusIndicator({ function BridgeStatusIndicator({
bridgeSelected bridgeSelected,
}: BridgeStatusProps): React.ReactNode { }: 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 // 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 // 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 // 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 // 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 // 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. // Failed state is surfaced via notification (useReplBridge), not a footer pill.
if (!isBridgeEnabled() || !enabled) return null; if (!isBridgeEnabled() || !enabled) return null
const status = getBridgeStatus({ const status = getBridgeStatus({
error: undefined, error: undefined,
connected, connected,
sessionActive, sessionActive,
reconnecting reconnecting,
}); })
// For implicit (config-driven) remote, only show the reconnecting state // For implicit (config-driven) remote, only show the reconnecting state
if (!explicit && status.label !== 'Remote Control reconnecting') { 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} {status.label}
{bridgeSelected && <Text dimColor> · Enter to view</Text>} {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 * as React from 'react'; import { memo, type ReactNode } from 'react'
import { memo, type ReactNode } from 'react'; import { useTerminalSize } from '../../hooks/useTerminalSize.js'
import { useTerminalSize } from '../../hooks/useTerminalSize.js'; import { stringWidth } from '../../ink/stringWidth.js'
import { stringWidth } from '../../ink/stringWidth.js'; import { Box, Text } from '../../ink.js'
import { Box, Text } from '../../ink.js'; import { truncatePathMiddle, truncateToWidth } from '../../utils/format.js'
import { truncatePathMiddle, truncateToWidth } from '../../utils/format.js'; import type { Theme } from '../../utils/theme.js'
import type { Theme } from '../../utils/theme.js';
export type SuggestionItem = { export type SuggestionItem = {
id: string; id: string
displayText: string; displayText: string
tag?: string; tag?: string
description?: string; description?: string
metadata?: unknown; metadata?: unknown
color?: keyof Theme; color?: keyof Theme
}; }
export type SuggestionType = 'command' | 'file' | 'directory' | 'agent' | 'shell' | 'custom-title' | 'slack-channel' | 'none';
export const OVERLAY_MAX_ITEMS = 5; 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 * Get the icon for a suggestion based on its type
* Icons: + for files, ◇ for MCP resources, * for agents * Icons: + for files, ◇ for MCP resources, * for agents
*/ */
function getIcon(itemId: string): string { function getIcon(itemId: string): string {
if (itemId.startsWith('file-')) return '+'; if (itemId.startsWith('file-')) return '+'
if (itemId.startsWith('mcp-resource-')) return '◇'; if (itemId.startsWith('mcp-resource-')) return '◇'
if (itemId.startsWith('agent-')) return '*'; if (itemId.startsWith('agent-')) return '*'
return '+'; return '+'
} }
/** /**
* Check if an item is a unified suggestion type (file, mcp-resource, or agent) * Check if an item is a unified suggestion type (file, mcp-resource, or agent)
*/ */
function isUnifiedSuggestion(itemId: string): boolean { 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 SuggestionItemRow = memo(function SuggestionItemRow({
const { item,
item, maxColumnWidth,
maxColumnWidth, isSelected,
isSelected }: {
} = t0; item: SuggestionItem
const columns = useTerminalSize().columns; maxColumnWidth?: number
const isUnified = isUnifiedSuggestion(item.id); 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) { if (isUnified) {
let t1; const icon = getIcon(item.id)
if ($[0] !== item.id) { const textColor: keyof Theme | undefined = isSelected
t1 = getIcon(item.id); ? 'suggestion'
$[0] = item.id; : undefined
$[1] = t1; const dimColor = !isSelected
} else {
t1 = $[1]; const isFile = item.id.startsWith('file-')
} const isMcpResource = item.id.startsWith('mcp-resource-')
const icon = t1;
const textColor = isSelected ? "suggestion" : undefined; // Calculate layout widths
const dimColor = !isSelected; // Layout: "X " (2) + displayText + " " (3) + description + padding (4)
const isFile = item.id.startsWith("file-"); const iconWidth = 2 // icon + space (fixed)
const isMcpResource = item.id.startsWith("mcp-resource-"); const paddingWidth = 4
const separatorWidth = item.description ? 3 : 0; const separatorWidth = item.description ? 3 : 0 // ' ' separator
let displayText;
// 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) { if (isFile) {
let t2; // Reserve space for description if present, otherwise use all available space
if ($[2] !== item.description) { const descReserve = item.description
t2 = item.description ? Math.min(20, stringWidth(item.description)) : 0; ? Math.min(20, stringWidth(item.description))
$[2] = item.description; : 0
$[3] = t2; const maxPathLength =
} else { columns - iconWidth - paddingWidth - separatorWidth - descReserve
t2 = $[3]; displayText = truncatePathMiddle(item.displayText, maxPathLength)
} } else if (isMcpResource) {
const descReserve = t2; const maxDisplayTextLength = 30
const maxPathLength = columns - 2 - 4 - separatorWidth - descReserve; displayText = truncateToWidth(item.displayText, maxDisplayTextLength)
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;
} else { } else {
if (isMcpResource) { displayText = item.displayText
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;
}
} }
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) { if (item.description) {
const maxDescLength = Math.max(0, availableWidth); const maxDescLength = Math.max(0, availableWidth)
let t2; const truncatedDesc = truncateToWidth(
if ($[9] !== item.description || $[10] !== maxDescLength) { item.description.replace(/\s+/g, ' '),
t2 = truncateToWidth(item.description.replace(/\s+/g, " "), maxDescLength); maxDescLength,
$[9] = item.description; )
$[10] = maxDescLength; lineContent = `${icon} ${displayText} ${truncatedDesc}`
$[11] = t2;
} else {
t2 = $[11];
}
const truncatedDesc = t2;
lineContent = `${icon} ${displayText} ${truncatedDesc}`;
} else { } else {
lineContent = `${icon} ${displayText}`; lineContent = `${icon} ${displayText}`
} }
let t2;
if ($[12] !== dimColor || $[13] !== lineContent || $[14] !== textColor) { return (
t2 = <Text color={textColor} dimColor={dimColor} wrap="truncate">{lineContent}</Text>; <Text color={textColor} dimColor={dimColor} wrap="truncate">
$[12] = dimColor; {lineContent}
$[13] = lineContent; </Text>
$[14] = textColor; )
$[15] = t2;
} else {
t2 = $[15];
}
return t2;
} }
const maxNameWidth = Math.floor(columns * 0.4);
const displayTextWidth = Math.min(maxColumnWidth ?? stringWidth(item.displayText) + 5, maxNameWidth); // For non-unified suggestions (commands, shell, etc.), use improved layout from main
const textColor_0 = item.color || (isSelected ? "suggestion" : undefined); // Cap the command name column at 40% of terminal width to ensure description has space
const shouldDim = !isSelected; const maxNameWidth = Math.floor(columns * 0.4)
let displayText_0 = item.displayText; const displayTextWidth = Math.min(
if (stringWidth(displayText_0) > displayTextWidth - 2) { maxColumnWidth ?? stringWidth(item.displayText) + 5,
const t1 = displayTextWidth - 2; maxNameWidth,
let t2; )
if ($[16] !== displayText_0 || $[17] !== t1) {
t2 = truncateToWidth(displayText_0, t1); const textColor = item.color || (isSelected ? 'suggestion' : undefined)
$[16] = displayText_0; const shouldDim = !isSelected
$[17] = t1;
$[18] = t2; // Truncate and pad the display text to fixed width
} else { let displayText = item.displayText
t2 = $[18]; if (stringWidth(displayText) > displayTextWidth - 2) {
} displayText = truncateToWidth(displayText, displayTextWidth - 2)
displayText_0 = t2;
} }
const paddedDisplayText = displayText_0 + " ".repeat(Math.max(0, displayTextWidth - stringWidth(displayText_0))); const paddedDisplayText =
const tagText = item.tag ? `[${item.tag}] ` : ""; displayText +
const tagWidth = stringWidth(tagText); ' '.repeat(Math.max(0, displayTextWidth - stringWidth(displayText)))
const descriptionWidth = Math.max(0, columns - displayTextWidth - tagWidth - 4);
let t1; const tagText = item.tag ? `[${item.tag}] ` : ''
if ($[19] !== descriptionWidth || $[20] !== item.description) { const tagWidth = stringWidth(tagText)
t1 = item.description ? truncateToWidth(item.description.replace(/\s+/g, " "), descriptionWidth) : ""; const descriptionWidth = Math.max(
$[19] = descriptionWidth; 0,
$[20] = item.description; columns - displayTextWidth - tagWidth - 4,
$[21] = t1; )
} else { // Skill descriptions can contain newlines (e.g. /claude-api's "TRIGGER
t1 = $[21]; // when:" block). A multi-line row grows the overlay past minHeight; when
} // the filter narrows past that skill, the overlay shrinks and leaves
const truncatedDescription = t1; // ghost rows. Flatten to one line before truncating.
let t2; const truncatedDescription = item.description
if ($[22] !== paddedDisplayText || $[23] !== shouldDim || $[24] !== textColor_0) { ? truncateToWidth(item.description.replace(/\s+/g, ' '), descriptionWidth)
t2 = <Text color={textColor_0} dimColor={shouldDim}>{paddedDisplayText}</Text>; : ''
$[22] = paddedDisplayText;
$[23] = shouldDim; return (
$[24] = textColor_0; <Text wrap="truncate">
$[25] = t2; <Text color={textColor} dimColor={shouldDim}>
} else { {paddedDisplayText}
t2 = $[25]; </Text>
} {tagText ? <Text dimColor>{tagText}</Text> : null}
let t3; <Text
if ($[26] !== tagText) { color={isSelected ? 'suggestion' : undefined}
t3 = tagText ? <Text dimColor={true}>{tagText}</Text> : null; dimColor={!isSelected}
$[26] = tagText; >
$[27] = t3; {truncatedDescription}
} else { </Text>
t3 = $[27]; </Text>
} )
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;
});
type Props = { type Props = {
suggestions: SuggestionItem[]; suggestions: SuggestionItem[]
selectedSuggestion: number; selectedSuggestion: number
maxColumnWidth?: number; maxColumnWidth?: number
/** /**
* When true, the suggestions are rendered inside a position=absolute * When true, the suggestions are rendered inside a position=absolute
* overlay. We omit minHeight and flex-end so the y-clamp in the * overlay. We omit minHeight and flex-end so the y-clamp in the
* renderer doesn't push fewer items down into the prompt area. * renderer doesn't push fewer items down into the prompt area.
*/ */
overlay?: boolean; overlay?: boolean
}; }
export function PromptInputFooterSuggestions(t0) {
const $ = _c(22); export function PromptInputFooterSuggestions({
const { suggestions,
suggestions, selectedSuggestion,
selectedSuggestion, maxColumnWidth: maxColumnWidthProp,
maxColumnWidth: maxColumnWidthProp, overlay,
overlay }: Props): ReactNode {
} = t0; const { rows } = useTerminalSize()
const { // Maximum number of suggestions to show at once (leaving space for prompt).
rows // Overlay mode (fullscreen) uses a fixed 5 — the floating box sits over
} = useTerminalSize(); // the ScrollBox, so terminal height isn't the constraint.
const maxVisibleItems = overlay ? OVERLAY_MAX_ITEMS : Math.min(6, Math.max(1, rows - 3)); const maxVisibleItems = overlay
? OVERLAY_MAX_ITEMS
: Math.min(6, Math.max(1, rows - 3))
// No suggestions to display
if (suggestions.length === 0) { if (suggestions.length === 0) {
return null; return null
} }
let t1;
if ($[0] !== maxColumnWidthProp || $[1] !== suggestions) { // Use prop if provided (stable width from all commands), otherwise calculate from visible
t1 = maxColumnWidthProp ?? Math.max(...suggestions.map(_temp)) + 5; const maxColumnWidth =
$[0] = maxColumnWidthProp; maxColumnWidthProp ??
$[1] = suggestions; Math.max(...suggestions.map(item => stringWidth(item.displayText))) + 5
$[2] = t1;
} else { // Calculate visible items range based on selected index
t1 = $[2]; const startIndex = Math.max(
} 0,
const maxColumnWidth = t1; Math.min(
const startIndex = Math.max(0, Math.min(selectedSuggestion - Math.floor(maxVisibleItems / 2), suggestions.length - maxVisibleItems)); selectedSuggestion - Math.floor(maxVisibleItems / 2),
const endIndex = Math.min(startIndex + maxVisibleItems, suggestions.length); suggestions.length - maxVisibleItems,
let T0; ),
let t2; )
let t3; const endIndex = Math.min(startIndex + maxVisibleItems, suggestions.length)
let t4; const visibleItems = suggestions.slice(startIndex, endIndex)
if ($[3] !== endIndex || $[4] !== maxColumnWidth || $[5] !== overlay || $[6] !== selectedSuggestion || $[7] !== startIndex || $[8] !== suggestions) {
const visibleItems = suggestions.slice(startIndex, endIndex); // In non-overlay (inline) mode, justifyContent keeps suggestions
T0 = Box; // anchored to the bottom (near the prompt). In overlay mode we omit
t2 = "column"; // both minHeight and flex-end: the parent is position=absolute with
t3 = overlay ? undefined : "flex-end"; // bottom='100%', so its y is clamped to 0 by the renderer when it
let t5; // would go negative. Adding minHeight + flex-end would create empty
if ($[13] !== maxColumnWidth || $[14] !== selectedSuggestion || $[15] !== suggestions) { // padding rows that shift the visible items down into the prompt area
t5 = item_0 => <SuggestionItemRow key={item_0.id} item={item_0} maxColumnWidth={maxColumnWidth} isSelected={item_0.id === suggestions[selectedSuggestion]?.id} />; // when the list has fewer items than maxVisibleItems.
$[13] = maxColumnWidth; return (
$[14] = selectedSuggestion; <Box
$[15] = suggestions; flexDirection="column"
$[16] = t5; justifyContent={overlay ? undefined : 'flex-end'}
} else { >
t5 = $[16]; {visibleItems.map(item => (
} <SuggestionItemRow
t4 = visibleItems.map(t5); key={item.id}
$[3] = endIndex; item={item}
$[4] = maxColumnWidth; maxColumnWidth={maxColumnWidth}
$[5] = overlay; isSelected={item.id === suggestions[selectedSuggestion]?.id}
$[6] = selectedSuggestion; />
$[7] = startIndex; ))}
$[8] = suggestions; </Box>
$[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;
} }
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 { feature } from 'bun:bundle'; import * as React from 'react'
import * as React from 'react'; import { Box, Text } from 'src/ink.js'
import { Box, Text } from 'src/ink.js'; import { getPlatform } from 'src/utils/platform.js'
import { getPlatform } from 'src/utils/platform.js'; import { isKeybindingCustomizationEnabled } from '../../keybindings/loadUserBindings.js'
import { isKeybindingCustomizationEnabled } from '../../keybindings/loadUserBindings.js'; import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'
import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'; import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'; import { isFastModeAvailable, isFastModeEnabled } from '../../utils/fastMode.js'
import { isFastModeAvailable, isFastModeEnabled } from '../../utils/fastMode.js'; import { getNewlineInstructions } from './utils.js'
import { getNewlineInstructions } from './utils.js';
/** Format a shortcut for display in the help menu (e.g., "ctrl+o" → "ctrl + o") */ /** Format a shortcut for display in the help menu (e.g., "ctrl+o" → "ctrl + o") */
function formatShortcut(shortcut: string): string { function formatShortcut(shortcut: string): string {
return shortcut.replace(/\+/g, ' + '); return shortcut.replace(/\+/g, ' + ')
} }
type Props = { type Props = {
dimColor?: boolean; dimColor?: boolean
fixedWidth?: boolean; fixedWidth?: boolean
gap?: number; gap?: number
paddingX?: number; paddingX?: number
}; }
export function PromptInputHelpMenu(props) {
const $ = _c(99); export function PromptInputHelpMenu(props: Props): React.ReactNode {
const { const { dimColor, fixedWidth, gap, paddingX } = props
dimColor,
fixedWidth, // Get configured shortcuts from keybinding system
gap, const transcriptShortcut = formatShortcut(
paddingX useShortcutDisplay('app:toggleTranscript', 'Global', 'ctrl+o'),
} = props; )
const t0 = useShortcutDisplay("app:toggleTranscript", "Global", "ctrl+o"); const todosShortcut = formatShortcut(
let t1; useShortcutDisplay('app:toggleTodos', 'Global', 'ctrl+t'),
if ($[0] !== t0) { )
t1 = formatShortcut(t0); const undoShortcut = formatShortcut(
$[0] = t0; useShortcutDisplay('chat:undo', 'Chat', 'ctrl+_'),
$[1] = t1; )
} else { const stashShortcut = formatShortcut(
t1 = $[1]; useShortcutDisplay('chat:stash', 'Chat', 'ctrl+s'),
} )
const transcriptShortcut = t1; const cycleModeShortcut = formatShortcut(
const t2 = useShortcutDisplay("app:toggleTodos", "Global", "ctrl+t"); useShortcutDisplay('chat:cycleMode', 'Chat', 'shift+tab'),
let t3; )
if ($[2] !== t2) { const modelPickerShortcut = formatShortcut(
t3 = formatShortcut(t2); useShortcutDisplay('chat:modelPicker', 'Chat', 'alt+p'),
$[2] = t2; )
$[3] = t3; const fastModeShortcut = formatShortcut(
} else { useShortcutDisplay('chat:fastMode', 'Chat', 'alt+o'),
t3 = $[3]; )
} const externalEditorShortcut = formatShortcut(
const todosShortcut = t3; useShortcutDisplay('chat:externalEditor', 'Chat', 'ctrl+g'),
const t4 = useShortcutDisplay("chat:undo", "Chat", "ctrl+_"); )
let t5; const terminalShortcut = formatShortcut(
if ($[4] !== t4) { useShortcutDisplay('app:toggleTerminal', 'Global', 'meta+j'),
t5 = formatShortcut(t4); )
$[4] = t4; const imagePasteShortcut = formatShortcut(
$[5] = t5; useShortcutDisplay('chat:imagePaste', 'Chat', 'ctrl+v'),
} else { )
t5 = $[5];
} // Compute terminal shortcut element outside JSX to satisfy feature() constraint
const undoShortcut = t5; const terminalShortcutElement = feature('TERMINAL_PANEL') ? (
const t6 = useShortcutDisplay("chat:stash", "Chat", "ctrl+s"); getFeatureValue_CACHED_MAY_BE_STALE('tengu_terminal_panel', false) ? (
let t7; <Box>
if ($[6] !== t6) { <Text dimColor={dimColor}>{terminalShortcut} for terminal</Text>
t7 = formatShortcut(t6); </Box>
$[6] = t6; ) : null
$[7] = t7; ) : null
} else {
t7 = $[7]; return (
} <Box paddingX={paddingX} flexDirection="row" gap={gap}>
const stashShortcut = t7; <Box flexDirection="column" width={fixedWidth ? 24 : undefined}>
const t8 = useShortcutDisplay("chat:cycleMode", "Chat", "shift+tab"); <Box>
let t9; <Text dimColor={dimColor}>! for bash mode</Text>
if ($[8] !== t8) { </Box>
t9 = formatShortcut(t8); <Box>
$[8] = t8; <Text dimColor={dimColor}>/ for commands</Text>
$[9] = t9; </Box>
} else { <Box>
t9 = $[9]; <Text dimColor={dimColor}>@ for file paths</Text>
} </Box>
const cycleModeShortcut = t9; <Box>
const t10 = useShortcutDisplay("chat:modelPicker", "Chat", "alt+p"); <Text dimColor={dimColor}>& for background</Text>
let t11; </Box>
if ($[10] !== t10) { <Box>
t11 = formatShortcut(t10); <Text dimColor={dimColor}>/btw for side question</Text>
$[10] = t10; </Box>
$[11] = t11; </Box>
} else { <Box flexDirection="column" width={fixedWidth ? 35 : undefined}>
t11 = $[11]; <Box>
} <Text dimColor={dimColor}>double tap esc to clear input</Text>
const modelPickerShortcut = t11; </Box>
const t12 = useShortcutDisplay("chat:fastMode", "Chat", "alt+o"); <Box>
let t13; <Text dimColor={dimColor}>
if ($[12] !== t12) { {cycleModeShortcut}{' '}
t13 = formatShortcut(t12); {process.env.USER_TYPE === 'ant'
$[12] = t12; ? 'to cycle modes'
$[13] = t13; : 'to auto-accept edits'}
} else { </Text>
t13 = $[13]; </Box>
} <Box>
const fastModeShortcut = t13; <Text dimColor={dimColor}>
const t14 = useShortcutDisplay("chat:externalEditor", "Chat", "ctrl+g"); {transcriptShortcut} for verbose output
let t15; </Text>
if ($[14] !== t14) { </Box>
t15 = formatShortcut(t14); <Box>
$[14] = t14; <Text dimColor={dimColor}>{todosShortcut} to toggle tasks</Text>
$[15] = t15; </Box>
} else { {terminalShortcutElement}
t15 = $[15]; <Box>
} <Text dimColor={dimColor}>{getNewlineInstructions()}</Text>
const externalEditorShortcut = t15; </Box>
const t16 = useShortcutDisplay("app:toggleTerminal", "Global", "meta+j"); </Box>
let t17; <Box flexDirection="column">
if ($[16] !== t16) { <Box>
t17 = formatShortcut(t16); <Text dimColor={dimColor}>{undoShortcut} to undo</Text>
$[16] = t16; </Box>
$[17] = t17; {getPlatform() !== 'windows' && (
} else { <Box>
t17 = $[17]; <Text dimColor={dimColor}>ctrl + z to suspend</Text>
} </Box>
const terminalShortcut = t17; )}
const t18 = useShortcutDisplay("chat:imagePaste", "Chat", "ctrl+v"); <Box>
let t19; <Text dimColor={dimColor}>{imagePasteShortcut} to paste images</Text>
if ($[18] !== t18) { </Box>
t19 = formatShortcut(t18); <Box>
$[18] = t18; <Text dimColor={dimColor}>{modelPickerShortcut} to switch model</Text>
$[19] = t19; </Box>
} else { {isFastModeEnabled() && isFastModeAvailable() && (
t19 = $[19]; <Box>
} <Text dimColor={dimColor}>
const imagePasteShortcut = t19; {fastModeShortcut} to toggle fast mode
let t20; </Text>
if ($[20] !== dimColor || $[21] !== terminalShortcut) { </Box>
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; <Box>
$[21] = terminalShortcut; <Text dimColor={dimColor}>{stashShortcut} to stash prompt</Text>
$[22] = t20; </Box>
} else { <Box>
t20 = $[22]; <Text dimColor={dimColor}>
} {externalEditorShortcut} to edit in $EDITOR
const terminalShortcutElement = t20; </Text>
const t21 = fixedWidth ? 24 : undefined; </Box>
let t22; {isKeybindingCustomizationEnabled() && (
if ($[23] !== dimColor) { <Box>
t22 = <Box><Text dimColor={dimColor}>! for bash mode</Text></Box>; <Text dimColor={dimColor}>/keybindings to customize</Text>
$[23] = dimColor; </Box>
$[24] = t22; )}
} else { </Box>
t22 = $[24]; </Box>
} )
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;
} }

View File

@@ -1,18 +1,22 @@
import { c as _c } from "react/compiler-runtime"; import figures from 'figures'
import figures from 'figures'; import * as React from 'react'
import * as React from 'react'; import { Box, Text } from 'src/ink.js'
import { Box, Text } from 'src/ink.js'; import {
import { AGENT_COLOR_TO_THEME_COLOR, AGENT_COLORS, type AgentColorName } from 'src/tools/AgentTool/agentColorManager.js'; AGENT_COLOR_TO_THEME_COLOR,
import type { PromptInputMode } from 'src/types/textInputTypes.js'; AGENT_COLORS,
import { getTeammateColor } from 'src/utils/teammate.js'; type AgentColorName,
import type { Theme } from 'src/utils/theme.js'; } from 'src/tools/AgentTool/agentColorManager.js'
import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.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 = { type Props = {
mode: PromptInputMode; mode: PromptInputMode
isLoading: boolean; isLoading: boolean
viewingAgentName?: string; viewingAgentName?: string
viewingAgentColor?: AgentColorName; viewingAgentColor?: AgentColorName
}; }
/** /**
* Gets the theme color key for the teammate's assigned color. * Gets the theme color key for the teammate's assigned color.
@@ -20,73 +24,81 @@ type Props = {
*/ */
function getTeammateThemeColor(): keyof Theme | undefined { function getTeammateThemeColor(): keyof Theme | undefined {
if (!isAgentSwarmsEnabled()) { if (!isAgentSwarmsEnabled()) {
return undefined; return undefined
} }
const colorName = getTeammateColor(); const colorName = getTeammateColor()
if (!colorName) { if (!colorName) {
return undefined; return undefined
} }
if (AGENT_COLORS.includes(colorName as AgentColorName)) { 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 = { type PromptCharProps = {
isLoading: boolean; isLoading: boolean
// Dead code elimination: parameter named themeColor to avoid "teammate" string in external builds // Dead code elimination: parameter named themeColor to avoid "teammate" string in external builds
themeColor?: keyof Theme; themeColor?: keyof Theme
}; }
/** /**
* Renders the prompt character (). * Renders the prompt character ().
* Teammate color overrides the default color when set. * Teammate color overrides the default color when set.
*/ */
function PromptChar(t0) { function PromptChar({
const $ = _c(3); isLoading,
const { themeColor,
isLoading, }: PromptCharProps): React.ReactNode {
themeColor // Assign to original name for clarity within the function
} = t0; const teammateColor = themeColor
const teammateColor = themeColor; const isAnt = process.env.USER_TYPE === 'ant'
const color = teammateColor ?? (false ? "subtle" : undefined); const color = teammateColor ?? (isAnt ? 'subtle' : undefined)
let t1;
if ($[0] !== color || $[1] !== isLoading) { return (
t1 = <Text color={color} dimColor={isLoading}>{figures.pointer} </Text>; <Text color={color} dimColor={isLoading}>
$[0] = color; {figures.pointer}&nbsp;
$[1] = isLoading; </Text>
$[2] = t1; )
} else {
t1 = $[2];
}
return t1;
} }
export function PromptInputModeIndicator(t0) {
const $ = _c(6); export function PromptInputModeIndicator({
const { mode,
mode, isLoading,
isLoading, viewingAgentName,
viewingAgentName, viewingAgentColor,
viewingAgentColor }: Props): React.ReactNode {
} = t0; const teammateColor = getTeammateThemeColor()
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { // Convert viewed teammate's color to theme color
t1 = getTeammateThemeColor(); // Falls back to PromptChar's default (subtle for ants, undefined for external)
$[0] = t1; const viewedTeammateThemeColor = viewingAgentColor
} else { ? AGENT_COLOR_TO_THEME_COLOR[viewingAgentColor]
t1 = $[0]; : undefined
}
const teammateColor = t1; return (
const viewedTeammateThemeColor = viewingAgentColor ? AGENT_COLOR_TO_THEME_COLOR[viewingAgentColor] : undefined; <Box
let t2; alignItems="flex-start"
if ($[1] !== isLoading || $[2] !== mode || $[3] !== viewedTeammateThemeColor || $[4] !== viewingAgentName) { alignSelf="flex-start"
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>; flexWrap="nowrap"
$[1] = isLoading; justifyContent="flex-start"
$[2] = mode; >
$[3] = viewedTeammateThemeColor; {viewingAgentName ? (
$[4] = viewingAgentName; // Use teammate's color on the standard prompt character, matching established style
$[5] = t2; <PromptChar
} else { isLoading={isLoading}
t2 = $[5]; themeColor={viewedTeammateThemeColor}
} />
return t2; ) : 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 { feature } from 'bun:bundle'
import * as React from 'react'; import * as React from 'react'
import { useMemo } from 'react'; import { useMemo } from 'react'
import { Box } from 'src/ink.js'; import { Box } from 'src/ink.js'
import { useAppState } from 'src/state/AppState.js'; import { useAppState } from 'src/state/AppState.js'
import { STATUS_TAG, SUMMARY_TAG, TASK_NOTIFICATION_TAG } from '../../constants/xml.js'; import {
import { QueuedMessageProvider } from '../../context/QueuedMessageContext.js'; STATUS_TAG,
import { useCommandQueue } from '../../hooks/useCommandQueue.js'; SUMMARY_TAG,
import type { QueuedCommand } from '../../types/textInputTypes.js'; TASK_NOTIFICATION_TAG,
import { isQueuedCommandVisible } from '../../utils/messageQueueManager.js'; } from '../../constants/xml.js'
import { createUserMessage, EMPTY_LOOKUPS, normalizeMessages } from '../../utils/messages.js'; import { QueuedMessageProvider } from '../../context/QueuedMessageContext.js'
import { jsonParse } from '../../utils/slowOperations.js'; import { useCommandQueue } from '../../hooks/useCommandQueue.js'
import { Message } from '../Message.js'; import type { QueuedCommand } from '../../types/textInputTypes.js'
const EMPTY_SET = new Set<string>(); 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. * 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 { function isIdleNotification(value: string): boolean {
try { try {
const parsed = jsonParse(value); const parsed = jsonParse(value)
return parsed?.type === 'idle_notification'; return parsed?.type === 'idle_notification'
} catch { } catch {
return false; return false
} }
} }
// Maximum number of task notification lines to show // 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. * Create a synthetic overflow notification message for capped task notifications.
@@ -36,7 +45,7 @@ function createOverflowNotificationMessage(count: number): string {
return `<${TASK_NOTIFICATION_TAG}> return `<${TASK_NOTIFICATION_TAG}>
<${SUMMARY_TAG}>+${count} more tasks completed</${SUMMARY_TAG}> <${SUMMARY_TAG}>+${count} more tasks completed</${SUMMARY_TAG}>
<${STATUS_TAG}>completed</${STATUS_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. * Other command types are always shown in full.
* Idle notifications are filtered out entirely. * Idle notifications are filtered out entirely.
*/ */
function processQueuedCommands(queuedCommands: QueuedCommand[]): QueuedCommand[] { function processQueuedCommands(
queuedCommands: QueuedCommand[],
): QueuedCommand[] {
// Filter out idle notifications - they are processed silently // 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 // Separate task notifications from other commands
const taskNotifications = filteredCommands.filter(cmd => cmd.mode === 'task-notification'); const taskNotifications = filteredCommands.filter(
const otherCommands = filteredCommands.filter(cmd => cmd.mode !== 'task-notification'); 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 notifications fit within limit, return all commands as-is
if (taskNotifications.length <= MAX_VISIBLE_NOTIFICATIONS) { if (taskNotifications.length <= MAX_VISIBLE_NOTIFICATIONS) {
return [...otherCommands, ...taskNotifications]; return [...otherCommands, ...taskNotifications]
} }
// Show first (MAX_VISIBLE_NOTIFICATIONS - 1) notifications, then a summary // Show first (MAX_VISIBLE_NOTIFICATIONS - 1) notifications, then a summary
const visibleNotifications = taskNotifications.slice(0, MAX_VISIBLE_NOTIFICATIONS - 1); const visibleNotifications = taskNotifications.slice(
const overflowCount = taskNotifications.length - (MAX_VISIBLE_NOTIFICATIONS - 1); 0,
MAX_VISIBLE_NOTIFICATIONS - 1,
)
const overflowCount =
taskNotifications.length - (MAX_VISIBLE_NOTIFICATIONS - 1)
// Create synthetic overflow message // Create synthetic overflow message
const overflowCommand: QueuedCommand = { const overflowCommand: QueuedCommand = {
value: createOverflowNotificationMessage(overflowCount), value: createOverflowNotificationMessage(overflowCount),
mode: 'task-notification' mode: 'task-notification',
}; }
return [...otherCommands, ...visibleNotifications, overflowCommand];
return [...otherCommands, ...visibleNotifications, overflowCommand]
} }
function PromptInputQueuedCommandsImpl(): React.ReactNode { function PromptInputQueuedCommandsImpl(): React.ReactNode {
const queuedCommands = useCommandQueue(); const queuedCommands = useCommandQueue()
const viewingAgent = useAppState(s => !!s.viewingAgentTaskId); const viewingAgent = useAppState(s => !!s.viewingAgentTaskId)
// Brief layout: dim queue items + skip the paddingX (brief messages // Brief layout: dim queue items + skip the paddingX (brief messages
// already indent themselves). Gate mirrors the brief-spinner/message // already indent themselves). Gate mirrors the brief-spinner/message
// check elsewhere — no teammate-view override needed since this // check elsewhere — no teammate-view override needed since this
// component early-returns when viewing a teammate. // component early-returns when viewing a teammate.
const useBriefLayout = feature('KAIROS') || feature('KAIROS_BRIEF') ? const useBriefLayout =
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant feature('KAIROS') || feature('KAIROS_BRIEF')
useAppState(s_0 => s_0.isBriefOnly) : false; ? // 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 // createUserMessage mints a fresh UUID per call; without memoization, streaming
// re-renders defeat Message's areMessagePropsEqual (compares uuid) → flicker. // re-renders defeat Message's areMessagePropsEqual (compares uuid) → flicker.
const messages = useMemo(() => { const messages = useMemo(() => {
if (queuedCommands.length === 0) return null; if (queuedCommands.length === 0) return null
// task-notification is shown via useInboxNotification; most isMeta commands // task-notification is shown via useInboxNotification; most isMeta commands
// (scheduled tasks, proactive ticks) are system-generated and hidden. // (scheduled tasks, proactive ticks) are system-generated and hidden.
// Channel messages are the exception — isMeta but shown so the keyboard // Channel messages are the exception — isMeta but shown so the keyboard
// user sees what arrived. // user sees what arrived.
const visibleCommands = queuedCommands.filter(isQueuedCommandVisible); const visibleCommands = queuedCommands.filter(isQueuedCommandVisible)
if (visibleCommands.length === 0) return null; if (visibleCommands.length === 0) return null
const processedCommands = processQueuedCommands(visibleCommands); const processedCommands = processQueuedCommands(visibleCommands)
return normalizeMessages(processedCommands.map(cmd => { return normalizeMessages(
let content = cmd.value; processedCommands.map(cmd => {
if (cmd.mode === 'bash' && typeof content === 'string') { let content = cmd.value
content = `<bash-input>${content}</bash-input>`; 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. // [Image #N] placeholders are inline in the text value (inserted at
return createUserMessage({ // paste time), so the queue preview shows them without stub blocks.
content return createUserMessage({ content })
}); }),
})); )
}, [queuedCommands]); }, [queuedCommands])
// Don't show leader's queued commands when viewing any agent's transcript // Don't show leader's queued commands when viewing any agent's transcript
if (viewingAgent || messages === null) { 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}> return (
<Message message={message} lookups={EMPTY_LOOKUPS} addMargin={false} tools={[]} commands={[]} verbose={false} inProgressToolUseIDs={EMPTY_SET} progressMessagesForMessage={[]} shouldAnimate={false} shouldShowDot={false} isTranscriptMode={false} isStatic={true} /> <Box marginTop={1} flexDirection="column">
</QueuedMessageProvider>)} {messages.map((message, i) => (
</Box>; <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 figures from 'figures'; import * as React from 'react'
import * as React from 'react'; import { Box, Text } from 'src/ink.js'
import { Box, Text } from 'src/ink.js';
type Props = { type Props = {
hasStash: boolean; hasStash: boolean
}; }
export function PromptInputStashNotice(t0) {
const $ = _c(1); export function PromptInputStashNotice({ hasStash }: Props): React.ReactNode {
const { if (!hasStash) {
hasStash return null
} = t0; }
if (!hasStash) {
return null; return (
} <Box paddingLeft={2}>
let t1; <Text dimColor>
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { {figures.pointerSmall} Stashed (auto-restores after submit)
t1 = <Box paddingLeft={2}><Text dimColor={true}>{figures.pointerSmall} Stashed (auto-restores after submit)</Text></Box>; </Text>
$[0] = t1; </Box>
} else { )
t1 = $[0];
}
return t1;
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,177 +1,246 @@
import chalk from 'chalk'; import chalk from 'chalk'
import figures from 'figures'; import figures from 'figures'
import * as React from 'react'; import * as React from 'react'
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react'
import { useSetAppState } from 'src/state/AppState.js'; import { useSetAppState } from 'src/state/AppState.js'
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'; import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
import { Box, Text } from '../../ink.js'; import { Box, Text } from '../../ink.js'
import { useKeybinding } from '../../keybindings/useKeybinding.js'; import { useKeybinding } from '../../keybindings/useKeybinding.js'
import type { Tools } from '../../Tool.js'; import type { Tools } from '../../Tool.js'
import { type AgentColorName, setAgentColor } from '../../tools/AgentTool/agentColorManager.js'; import {
import { type AgentDefinition, getActiveAgentsFromList, isCustomAgent, isPluginAgent } from '../../tools/AgentTool/loadAgentsDir.js'; type AgentColorName,
import { editFileInEditor } from '../../utils/promptEditor.js'; setAgentColor,
import { getActualAgentFilePath, updateAgentFile } from './agentFileUtils.js'; } from '../../tools/AgentTool/agentColorManager.js'
import { ColorPicker } from './ColorPicker.js'; import {
import { ModelSelector } from './ModelSelector.js'; type AgentDefinition,
import { ToolSelector } from './ToolSelector.js'; getActiveAgentsFromList,
import { getAgentSourceDisplayName } from './utils.js'; 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 = { type Props = {
agent: AgentDefinition; agent: AgentDefinition
tools: Tools; tools: Tools
onSaved: (message: string) => void; onSaved: (message: string) => void
onBack: () => void; onBack: () => void
}; }
type EditMode = 'menu' | 'edit-tools' | 'edit-color' | 'edit-model';
type EditMode = 'menu' | 'edit-tools' | 'edit-color' | 'edit-model'
type SaveChanges = { type SaveChanges = {
tools?: string[]; tools?: string[]
color?: AgentColorName; color?: AgentColorName
model?: string; model?: string
}; }
export function AgentEditor({ export function AgentEditor({
agent, agent,
tools, tools,
onSaved, onSaved,
onBack onBack,
}: Props): React.ReactNode { }: Props): React.ReactNode {
const setAppState = useSetAppState(); const setAppState = useSetAppState()
const [editMode, setEditMode] = useState<EditMode>('menu'); const [editMode, setEditMode] = useState<EditMode>('menu')
const [selectedMenuIndex, setSelectedMenuIndex] = useState(0); const [selectedMenuIndex, setSelectedMenuIndex] = useState(0)
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null)
const [selectedColor, setSelectedColor] = useState<AgentColorName | undefined>(agent.color as AgentColorName | undefined); const [selectedColor, setSelectedColor] = useState<
AgentColorName | undefined
>(agent.color as AgentColorName | undefined)
const handleOpenInEditor = useCallback(async () => { const handleOpenInEditor = useCallback(async () => {
const filePath = getActualAgentFilePath(agent); const filePath = getActualAgentFilePath(agent)
const result = await editFileInEditor(filePath); const result = await editFileInEditor(filePath)
if (result.error) { if (result.error) {
setError(result.error); setError(result.error)
} else { } 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]); }, [agent, onSaved])
const handleSave = useCallback(async (changes: SaveChanges = {}) => {
const { const handleSave = useCallback(
tools: newTools, async (changes: SaveChanges = {}) => {
color: newColor, const { tools: newTools, color: newColor, model: newModel } = changes
model: newModel const finalColor = newColor ?? selectedColor
} = changes; const hasToolsChanged = newTools !== undefined
const finalColor = newColor ?? selectedColor; const hasModelChanged = newModel !== undefined
const hasToolsChanged = newTools !== undefined; const hasColorChanged = finalColor !== agent.color
const hasModelChanged = newModel !== undefined;
const hasColorChanged = finalColor !== agent.color; if (!hasToolsChanged && !hasModelChanged && !hasColorChanged) {
if (!hasToolsChanged && !hasModelChanged && !hasColorChanged) { return false
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;
} }
await updateAgentFile(agent, agent.whenToUse, newTools ?? agent.tools, agent.getSystemPrompt(), finalColor, newModel ?? agent.model);
if (hasColorChanged && finalColor) { try {
setAgentColor(agent.agentType, finalColor); // Only custom/plugin agents can be edited
} // this is for type safety; the UI shouldn't allow editing otherwise
setAppState(state => { if (!isCustomAgent(agent) && !isPluginAgent(agent)) {
const allAgents = state.agentDefinitions.allAgents.map(a => a.agentType === agent.agentType ? { return false
...a, }
tools: newTools ?? a.tools,
color: finalColor, await updateAgentFile(
model: newModel ?? a.model agent,
} : a); agent.whenToUse,
return { newTools ?? agent.tools,
...state, agent.getSystemPrompt(),
agentDefinitions: { finalColor,
...state.agentDefinitions, newModel ?? agent.model,
activeAgents: getActiveAgentsFromList(allAgents), )
allAgents
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)}`); onSaved(`Updated agent: ${chalk.bold(agent.agentType)}`)
return true; return true
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Failed to save agent'); setError(err instanceof Error ? err.message : 'Failed to save agent')
return false; 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();
} }
},
[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]); }, [editMode, onBack])
useKeybinding('confirm:no', handleEscape, {
context: 'Confirmation' const handleMenuKeyDown = useCallback(
}); (e: KeyboardEvent) => {
const renderMenu = (): React.ReactNode => <Box flexDirection="column" tabIndex={0} autoFocus onKeyDown={handleMenuKeyDown}> 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> <Text dimColor>Source: {getAgentSourceDisplayName(agent.source)}</Text>
<Box marginTop={1} flexDirection="column"> <Box marginTop={1} flexDirection="column">
{menuItems.map((item, index_1) => <Text key={item.label} color={index_1 === selectedMenuIndex ? 'suggestion' : undefined}> {menuItems.map((item, index) => (
{index_1 === selectedMenuIndex ? `${figures.pointer} ` : ' '} <Text
key={item.label}
color={index === selectedMenuIndex ? 'suggestion' : undefined}
>
{index === selectedMenuIndex ? `${figures.pointer} ` : ' '}
{item.label} {item.label}
</Text>)} </Text>
))}
</Box> </Box>
{error && <Box marginTop={1}> {error && (
<Box marginTop={1}>
<Text color="error">{error}</Text> <Text color="error">{error}</Text>
</Box>} </Box>
</Box>; )}
</Box>
)
switch (editMode) { switch (editMode) {
case 'menu': case 'menu':
return renderMenu(); return renderMenu()
case 'edit-tools': case 'edit-tools':
return <ToolSelector tools={tools} initialTools={agent.tools} onComplete={async finalTools => { return (
setEditMode('menu'); <ToolSelector
await handleSave({ tools={tools}
tools: finalTools initialTools={agent.tools}
}); onComplete={async finalTools => {
}} />; setEditMode('menu')
await handleSave({ tools: finalTools })
}}
/>
)
case 'edit-color': case 'edit-color':
return <ColorPicker agentName={agent.agentType} currentColor={selectedColor || agent.color as AgentColorName || 'automatic'} onConfirm={async color => { return (
setSelectedColor(color); <ColorPicker
setEditMode('menu'); agentName={agent.agentType}
await handleSave({ currentColor={
color selectedColor || (agent.color as AgentColorName) || 'automatic'
}); }
}} />; onConfirm={async color => {
setSelectedColor(color)
setEditMode('menu')
await handleSave({ color })
}}
/>
)
case 'edit-model': case 'edit-model':
return <ModelSelector initialModel={agent.model} onComplete={async model => { return (
setEditMode('menu'); <ModelSelector
await handleSave({ initialModel={agent.model}
model onComplete={async model => {
}); setEditMode('menu')
}} />; await handleSave({ model })
}}
/>
)
default: 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 * as React from 'react'; import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'; import { Box, Text } from '../../ink.js'
import { Box, Text } from '../../ink.js';
type Props = { type Props = {
instructions?: string; instructions?: string
}; }
export function AgentNavigationFooter(t0) {
const $ = _c(2); export function AgentNavigationFooter({
const { instructions = 'Press ↑↓ to navigate · Enter to select · Esc to go back',
instructions: t1 }: Props): React.ReactNode {
} = t0; const exitState = useExitOnCtrlCDWithKeybindings()
const instructions = t1 === undefined ? "Press \u2191\u2193 to navigate \xB7 Enter to select \xB7 Esc to go back" : t1;
const exitState = useExitOnCtrlCDWithKeybindings(); return (
const t2 = exitState.pending ? `Press ${exitState.keyName} again to exit` : instructions; <Box marginLeft={2}>
let t3; <Text dimColor>
if ($[0] !== t2) { {exitState.pending
t3 = <Box marginLeft={2}><Text dimColor={true}>{t2}</Text></Box>; ? `Press ${exitState.keyName} again to exit`
$[0] = t2; : instructions}
$[1] = t3; </Text>
} else { </Box>
t3 = $[1]; )
}
return t3;
} }

View File

@@ -1,439 +1,342 @@
import { c as _c } from "react/compiler-runtime"; import figures from 'figures'
import figures from 'figures'; import * as React from 'react'
import * as React from 'react'; import type { SettingSource } from 'src/utils/settings/constants.js'
import type { SettingSource } from 'src/utils/settings/constants.js'; import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'; import { Box, Text } from '../../ink.js'
import { Box, Text } from '../../ink.js'; import type { ResolvedAgent } from '../../tools/AgentTool/agentDisplay.js'
import type { ResolvedAgent } from '../../tools/AgentTool/agentDisplay.js'; import {
import { AGENT_SOURCE_GROUPS, compareAgentsByName, getOverrideSourceLabel, resolveAgentModelDisplay } from '../../tools/AgentTool/agentDisplay.js'; AGENT_SOURCE_GROUPS,
import type { AgentDefinition } from '../../tools/AgentTool/loadAgentsDir.js'; compareAgentsByName,
import { count } from '../../utils/array.js'; getOverrideSourceLabel,
import { Dialog } from '../design-system/Dialog.js'; resolveAgentModelDisplay,
import { Divider } from '../design-system/Divider.js'; } from '../../tools/AgentTool/agentDisplay.js'
import { getAgentSourceDisplayName } from './utils.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 = { type Props = {
source: SettingSource | 'all' | 'built-in' | 'plugin'; source: SettingSource | 'all' | 'built-in' | 'plugin'
agents: ResolvedAgent[]; agents: ResolvedAgent[]
onBack: () => void; onBack: () => void
onSelect: (agent: AgentDefinition) => void; onSelect: (agent: AgentDefinition) => void
onCreateNew?: () => void; onCreateNew?: () => void
changes?: string[]; changes?: string[]
}; }
export function AgentsList(t0) {
const $ = _c(96); export function AgentsList({
const { source,
source, agents,
agents, onBack,
onBack, onSelect,
onSelect, onCreateNew,
onCreateNew, changes,
changes }: Props): React.ReactNode {
} = t0; const [selectedAgent, setSelectedAgent] =
const [selectedAgent, setSelectedAgent] = React.useState(null); React.useState<ResolvedAgent | null>(null)
const [isCreateNewSelected, setIsCreateNewSelected] = React.useState(true); const [isCreateNewSelected, setIsCreateNewSelected] = React.useState(true)
let t1;
if ($[0] !== agents) { // Sort agents alphabetically by name within each source group
t1 = [...agents].sort(compareAgentsByName); const sortedAgents = React.useMemo(
$[0] = agents; () => [...agents].sort(compareAgentsByName),
$[1] = t1; [agents],
} else { )
t1 = $[1];
} const getOverrideInfo = (agent: ResolvedAgent) => {
const sortedAgents = t1; return {
const getOverrideInfo = _temp; isOverridden: !!agent.overriddenBy,
let t2; overriddenBy: agent.overriddenBy || null,
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;
} }
$[8] = sortedAgents;
$[9] = source;
$[10] = t4;
} else {
t4 = $[10];
} }
const selectableAgentsInOrder = t4;
let t5; const renderCreateNewOption = () => {
let t6; return (
if ($[11] !== isCreateNewSelected || $[12] !== onCreateNew || $[13] !== selectableAgentsInOrder || $[14] !== selectedAgent) { <Box>
t5 = () => { <Text color={isCreateNewSelected ? 'suggestion' : undefined}>
if (!selectedAgent && !isCreateNewSelected && selectableAgentsInOrder.length > 0) { {isCreateNewSelected ? `${figures.pointer} ` : ' '}
if (onCreateNew) { </Text>
setIsCreateNewSelected(true); <Text color={isCreateNewSelected ? 'suggestion' : undefined}>
} else { Create new agent
setSelectedAgent(selectableAgentsInOrder[0] || null); </Text>
} </Box>
} )
};
t6 = [selectableAgentsInOrder, selectedAgent, isCreateNewSelected, onCreateNew];
$[11] = isCreateNewSelected;
$[12] = onCreateNew;
$[13] = selectableAgentsInOrder;
$[14] = selectedAgent;
$[15] = t5;
$[16] = t6;
} else {
t5 = $[15];
t6 = $[16];
} }
React.useEffect(t5, t6);
let t7; const renderAgent = (agent: ResolvedAgent) => {
if ($[17] !== isCreateNewSelected || $[18] !== onCreateNew || $[19] !== onSelect || $[20] !== selectableAgentsInOrder || $[21] !== selectedAgent) { const isBuiltIn = agent.source === 'built-in'
t7 = e => { const isSelected =
if (e.key === "return") { !isBuiltIn &&
e.preventDefault(); !isCreateNewSelected &&
if (isCreateNewSelected && onCreateNew) { selectedAgent?.agentType === agent.agentType &&
onCreateNew(); selectedAgent?.source === agent.source
} else {
if (selectedAgent) { const { isOverridden, overriddenBy } = getOverrideInfo(agent)
onSelect(selectedAgent); const dimmed = isBuiltIn || isOverridden
} const textColor = !isBuiltIn && isSelected ? 'suggestion' : undefined
}
return; const resolvedModel = resolveAgentModelDisplay(agent)
}
if (e.key !== "up" && e.key !== "down") { return (
return; <Box key={`${agent.agentType}-${agent.source}`}>
} <Text dimColor={dimmed && !isSelected} color={textColor}>
e.preventDefault(); {isBuiltIn ? '' : isSelected ? `${figures.pointer} ` : ' '}
const hasCreateOption = !!onCreateNew; </Text>
const totalItems = selectableAgentsInOrder.length + (hasCreateOption ? 1 : 0); <Text dimColor={dimmed && !isSelected} color={textColor}>
if (totalItems === 0) { {agent.agentType}
return; </Text>
} {resolvedModel && (
let currentPosition = 0; <Text dimColor={true} color={textColor}>
if (!isCreateNewSelected && selectedAgent) { {' · '}
const agentIndex = selectableAgentsInOrder.findIndex(a_1 => a_1.agentType === selectedAgent.agentType && a_1.source === selectedAgent.source); {resolvedModel}
if (agentIndex >= 0) { </Text>
currentPosition = hasCreateOption ? agentIndex + 1 : agentIndex; )}
} {agent.memory && (
} <Text dimColor={true} color={textColor}>
const newPosition = e.key === "up" ? currentPosition === 0 ? totalItems - 1 : currentPosition - 1 : currentPosition === totalItems - 1 ? 0 : currentPosition + 1; {' · '}
if (hasCreateOption && newPosition === 0) { {agent.memory} memory
setIsCreateNewSelected(true); </Text>
setSelectedAgent(null); )}
} else { {overriddenBy && (
const agentIndex_0 = hasCreateOption ? newPosition - 1 : newPosition; <Text
const newAgent = selectableAgentsInOrder[agentIndex_0]; dimColor={!isSelected}
if (newAgent) { color={isSelected ? 'warning' : undefined}
setIsCreateNewSelected(false); >
setSelectedAgent(newAgent); {' '}
} {figures.warning} shadowed by {getOverrideSourceLabel(overriddenBy)}
} </Text>
}; )}
$[17] = isCreateNewSelected; </Box>
$[18] = onCreateNew; )
$[19] = onSelect;
$[20] = selectableAgentsInOrder;
$[21] = selectedAgent;
$[22] = t7;
} else {
t7 = $[22];
} }
const handleKeyDown = t7;
let t8; const selectableAgentsInOrder = React.useMemo(() => {
if ($[23] !== renderAgent || $[24] !== sortedAgents) { const nonBuiltIn = sortedAgents.filter(a => a.source !== 'built-in')
t8 = t9 => { if (source === 'all') {
const title = t9 === undefined ? "Built-in (always available):" : t9; return AGENT_SOURCE_GROUPS.filter(g => g.source !== 'built-in').flatMap(
const builtInAgents = sortedAgents.filter(_temp4); ({ source: groupSource }) =>
return <Box flexDirection="column" marginBottom={1} paddingLeft={2}><Text bold={true} dimColor={true}>{title}</Text>{builtInAgents.map(renderAgent)}</Box>; nonBuiltIn.filter(a => a.source === groupSource),
}; )
$[23] = renderAgent; }
$[24] = sortedAgents; return nonBuiltIn
$[25] = t8; }, [sortedAgents, source])
} else {
t8 = $[25]; // Set initial selection
} React.useEffect(() => {
const renderBuiltInAgentsSection = t8; if (
let t9; !selectedAgent &&
if ($[26] !== renderAgent) { !isCreateNewSelected &&
t9 = (title_0, groupAgents) => { selectableAgentsInOrder.length > 0
if (!groupAgents.length) { ) {
return null; if (onCreateNew) {
} setIsCreateNewSelected(true)
const folderPath = groupAgents[0]?.baseDir; } else {
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>; setSelectedAgent(selectableAgentsInOrder[0] || null)
}; }
$[26] = renderAgent; }
$[27] = t9; }, [selectableAgentsInOrder, selectedAgent, isCreateNewSelected, onCreateNew])
} else {
t9 = $[27]; const handleKeyDown = (e: KeyboardEvent) => {
} if (e.key === 'return') {
const renderAgentGroup = t9; e.preventDefault()
let t10; if (isCreateNewSelected && onCreateNew) {
if ($[28] !== source) { onCreateNew()
t10 = getAgentSourceDisplayName(source); } else if (selectedAgent) {
$[28] = source; onSelect(selectedAgent)
$[29] = t10; }
} else { return
t10 = $[29]; }
}
const sourceTitle = t10; if (e.key !== 'up' && e.key !== 'down') return
let T0; e.preventDefault()
let T1;
let t11; // Handle navigation with "Create New Agent" option
let t12; const hasCreateOption = !!onCreateNew
let t13; const totalItems =
let t14; selectableAgentsInOrder.length + (hasCreateOption ? 1 : 0)
let t15;
let t16; if (totalItems === 0) return
let t17;
let t18; // Calculate current position in list (0 = create new, 1+ = agents)
let t19; let currentPosition = 0
let t20; if (!isCreateNewSelected && selectedAgent) {
let t21; const agentIndex = selectableAgentsInOrder.findIndex(
let t22; a =>
if ($[30] !== changes || $[31] !== handleKeyDown || $[32] !== onBack || $[33] !== onCreateNew || $[34] !== renderAgent || $[35] !== renderAgentGroup || $[36] !== renderBuiltInAgentsSection || $[37] !== renderCreateNewOption || $[38] !== sortedAgents || $[39] !== source || $[40] !== sourceTitle) { a.agentType === selectedAgent.agentType &&
t22 = Symbol.for("react.early_return_sentinel"); a.source === selectedAgent.source,
bb1: { )
const builtInAgents_0 = sortedAgents.filter(_temp5); if (agentIndex >= 0) {
const hasNoAgents = !sortedAgents.length || source !== "built-in" && !sortedAgents.some(_temp6); currentPosition = hasCreateOption ? agentIndex + 1 : agentIndex
if (hasNoAgents) { }
let t23; }
if ($[55] !== onCreateNew || $[56] !== renderCreateNewOption) {
t23 = onCreateNew && <Box>{renderCreateNewOption()}</Box>; // Calculate new position with wrap-around
$[55] = onCreateNew; const newPosition =
$[56] = renderCreateNewOption; e.key === 'up'
$[57] = t23; ? currentPosition === 0
} else { ? totalItems - 1
t23 = $[57]; : currentPosition - 1
} : currentPosition === totalItems - 1
let t24; ? 0
let t25; : currentPosition + 1
let t26;
if ($[58] === Symbol.for("react.memo_cache_sentinel")) { // Update selection based on new position
t24 = <Text dimColor={true}>No agents found. Create specialized subagents that Claude can delegate to.</Text>; if (hasCreateOption && newPosition === 0) {
t25 = <Text dimColor={true}>Each subagent has its own context window, custom system prompt, and specific tools.</Text>; setIsCreateNewSelected(true)
t26 = <Text dimColor={true}>Try creating: Code Reviewer, Code Simplifier, Security Reviewer, Tech Lead, or UX Reviewer.</Text>; setSelectedAgent(null)
$[58] = t24; } else {
$[59] = t25; const agentIndex = hasCreateOption ? newPosition - 1 : newPosition
$[60] = t26; const newAgent = selectableAgentsInOrder[agentIndex]
} else { if (newAgent) {
t24 = $[58]; setIsCreateNewSelected(false)
t25 = $[59]; setSelectedAgent(newAgent)
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()}</>}</>;
} }
$[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) { const renderAgentGroup = (title: string, groupAgents: ResolvedAgent[]) => {
t23 = <T0 flexDirection={t11} tabIndex={t12} autoFocus={t13} onKeyDown={t14}>{t15}{t16}</T0>; if (!groupAgents.length) return null
$[80] = T0;
$[81] = t11; const folderPath = groupAgents[0]?.baseDir
$[82] = t12;
$[83] = t13; return (
$[84] = t14; <Box flexDirection="column" marginBottom={1}>
$[85] = t15; <Box paddingLeft={2}>
$[86] = t16; <Text bold dimColor>
$[87] = t23; {title}
} else { </Text>
t23 = $[87]; {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) { const sourceTitle = getAgentSourceDisplayName(source)
t24 = <T1 title={t17} subtitle={t18} onCancel={t19} hideInputGuide={t20}>{t21}{t23}</T1>;
$[88] = T1; const builtInAgents = sortedAgents.filter(a => a.source === 'built-in')
$[89] = t17;
$[90] = t18; const hasNoAgents =
$[91] = t19; !sortedAgents.length ||
$[92] = t20; (source !== 'built-in' && !sortedAgents.some(a => a.source !== 'built-in'))
$[93] = t21;
$[94] = t23; if (hasNoAgents) {
$[95] = t24; return (
} else { <Dialog
t24 = $[95]; 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;
} return (
function _temp1(a_9) { <Dialog
return a_9.source === "built-in"; title={sourceTitle}
} subtitle={`${count(sortedAgents, a => !a.overriddenBy)} agents`}
function _temp0(a_8) { onCancel={onBack}
return a_8.source !== "built-in"; hideInputGuide
} >
function _temp9(g_0) { {changes && changes.length > 0 && (
return g_0.source !== "built-in"; <Box marginTop={1}>
} <Text dimColor>{changes[changes.length - 1]}</Text>
function _temp8(a_6) { </Box>
return !a_6.overriddenBy; )}
} <Box
function _temp7(a_5) { flexDirection="column"
return a_5.source === "built-in"; tabIndex={0}
} autoFocus
function _temp6(a_4) { onKeyDown={handleKeyDown}
return a_4.source !== "built-in"; >
} {onCreateNew && <Box marginBottom={1}>{renderCreateNewOption()}</Box>}
function _temp5(a_3) { {source === 'all' ? (
return a_3.source === "built-in"; <>
} {AGENT_SOURCE_GROUPS.filter(g => g.source !== 'built-in').map(
function _temp4(a_2) { ({ label, source: groupSource }) => (
return a_2.source === "built-in"; <React.Fragment key={groupSource}>
} {renderAgentGroup(
function _temp3(g) { label,
return g.source !== "built-in"; sortedAgents.filter(a => a.source === groupSource),
} )}
function _temp2(a) { </React.Fragment>
return a.source !== "built-in"; ),
} )}
function _temp(agent) { {builtInAgents.length > 0 && (
return { <Box flexDirection="column" marginBottom={1} paddingLeft={2}>
isOverridden: !!agent.overriddenBy, <Text dimColor>
overriddenBy: agent.overriddenBy || null <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 figures from 'figures'; import React, { useState } from 'react'
import React, { useState } from 'react'; import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'; import { Box, Text } from '../../ink.js'
import { Box, Text } from '../../ink.js'; import {
import { AGENT_COLOR_TO_THEME_COLOR, AGENT_COLORS, type AgentColorName } from '../../tools/AgentTool/agentColorManager.js'; AGENT_COLOR_TO_THEME_COLOR,
import { capitalize } from '../../utils/stringUtils.js'; AGENT_COLORS,
type ColorOption = AgentColorName | 'automatic'; type AgentColorName,
const COLOR_OPTIONS: ColorOption[] = ['automatic', ...AGENT_COLORS]; } from '../../tools/AgentTool/agentColorManager.js'
import { capitalize } from '../../utils/stringUtils.js'
type ColorOption = AgentColorName | 'automatic'
const COLOR_OPTIONS: ColorOption[] = ['automatic', ...AGENT_COLORS]
type Props = { type Props = {
agentName: string; agentName: string
currentColor?: AgentColorName | 'automatic'; currentColor?: AgentColorName | 'automatic'
onConfirm: (color: AgentColorName | undefined) => void; 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;
} }
function _temp2(prev_0) {
return prev_0 < COLOR_OPTIONS.length - 1 ? prev_0 + 1 : 0; export function ColorPicker({
} agentName,
function _temp(prev) { currentColor = 'automatic',
return prev > 0 ? prev - 1 : COLOR_OPTIONS.length - 1; 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 * as React from 'react'; import { Box, Text } from '../../ink.js'
import { Box, Text } from '../../ink.js'; import { getAgentModelOptions } from '../../utils/model/agent.js'
import { getAgentModelOptions } from '../../utils/model/agent.js'; import { Select } from '../CustomSelect/select.js'
import { Select } from '../CustomSelect/select.js';
interface ModelSelectorProps { interface ModelSelectorProps {
initialModel?: string; initialModel?: string
onComplete: (model?: string) => void; onComplete: (model?: string) => void
onCancel?: () => void; onCancel?: () => void
} }
export function ModelSelector(t0) {
const $ = _c(11); export function ModelSelector({
const { initialModel,
initialModel, onComplete,
onComplete, onCancel,
onCancel }: ModelSelectorProps): React.ReactNode {
} = t0; const modelOptions = React.useMemo(() => {
let t1; const base = getAgentModelOptions()
if ($[0] !== initialModel) { // If the agent's current model is a full ID (e.g. 'claude-opus-4-5') not
bb0: { // in the alias list, inject it as an option so it can round-trip through
const base = getAgentModelOptions(); // confirm without being overwritten.
if (initialModel && !base.some(o => o.value === initialModel)) { if (initialModel && !base.some(o => o.value === initialModel)) {
t1 = [{ return [
{
value: initialModel, value: initialModel,
label: initialModel, label: initialModel,
description: "Current model (custom ID)" description: 'Current model (custom ID)',
}, ...base]; },
break bb0; ...base,
} ]
t1 = base;
} }
$[0] = initialModel; return base
$[1] = t1; }, [initialModel])
} else {
t1 = $[1]; const defaultModel = initialModel ?? 'sonnet'
}
const modelOptions = t1; return (
const defaultModel = initialModel ?? "sonnet"; <Box flexDirection="column">
let t2; <Box marginBottom={1}>
if ($[2] === Symbol.for("react.memo_cache_sentinel")) { <Text dimColor>
t2 = <Box marginBottom={1}><Text dimColor={true}>Model determines the agent's reasoning capabilities and speed.</Text></Box>; Model determines the agent&apos;s reasoning capabilities and speed.
$[2] = t2; </Text>
} else { </Box>
t2 = $[2]; <Select
} options={modelOptions}
let t3; defaultValue={defaultModel}
if ($[3] !== onCancel || $[4] !== onComplete) { onChange={onComplete}
t3 = () => onCancel ? onCancel() : onComplete(undefined); onCancel={() => (onCancel ? onCancel() : onComplete(undefined))}
$[3] = onCancel; />
$[4] = onComplete; </Box>
$[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;
} }

View File

@@ -1,561 +1,478 @@
import { c as _c } from "react/compiler-runtime"; import figures from 'figures'
import figures from 'figures'; import React, { useCallback, useMemo, useState } from 'react'
import React, { useCallback, useMemo, useState } from 'react'; import { mcpInfoFromString } from 'src/services/mcp/mcpStringUtils.js'
import { mcpInfoFromString } from 'src/services/mcp/mcpStringUtils.js'; import { isMcpTool } from 'src/services/mcp/utils.js'
import { isMcpTool } from 'src/services/mcp/utils.js'; import type { Tool, Tools } from 'src/Tool.js'
import type { Tool, Tools } from 'src/Tool.js'; import { filterToolsForAgent } from 'src/tools/AgentTool/agentToolUtils.js'
import { filterToolsForAgent } from 'src/tools/AgentTool/agentToolUtils.js'; import { AGENT_TOOL_NAME } from 'src/tools/AgentTool/constants.js'
import { AGENT_TOOL_NAME } from 'src/tools/AgentTool/constants.js'; import { BashTool } from 'src/tools/BashTool/BashTool.js'
import { BashTool } from 'src/tools/BashTool/BashTool.js'; import { ExitPlanModeV2Tool } from 'src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.js'
import { ExitPlanModeV2Tool } from 'src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.js'; import { FileEditTool } from 'src/tools/FileEditTool/FileEditTool.js'
import { FileEditTool } from 'src/tools/FileEditTool/FileEditTool.js'; import { FileReadTool } from 'src/tools/FileReadTool/FileReadTool.js'
import { FileReadTool } from 'src/tools/FileReadTool/FileReadTool.js'; import { FileWriteTool } from 'src/tools/FileWriteTool/FileWriteTool.js'
import { FileWriteTool } from 'src/tools/FileWriteTool/FileWriteTool.js'; import { GlobTool } from 'src/tools/GlobTool/GlobTool.js'
import { GlobTool } from 'src/tools/GlobTool/GlobTool.js'; import { GrepTool } from 'src/tools/GrepTool/GrepTool.js'
import { GrepTool } from 'src/tools/GrepTool/GrepTool.js'; import { ListMcpResourcesTool } from 'src/tools/ListMcpResourcesTool/ListMcpResourcesTool.js'
import { ListMcpResourcesTool } from 'src/tools/ListMcpResourcesTool/ListMcpResourcesTool.js'; import { NotebookEditTool } from 'src/tools/NotebookEditTool/NotebookEditTool.js'
import { NotebookEditTool } from 'src/tools/NotebookEditTool/NotebookEditTool.js'; import { ReadMcpResourceTool } from 'src/tools/ReadMcpResourceTool/ReadMcpResourceTool.js'
import { ReadMcpResourceTool } from 'src/tools/ReadMcpResourceTool/ReadMcpResourceTool.js'; import { TaskOutputTool } from 'src/tools/TaskOutputTool/TaskOutputTool.js'
import { TaskOutputTool } from 'src/tools/TaskOutputTool/TaskOutputTool.js'; import { TaskStopTool } from 'src/tools/TaskStopTool/TaskStopTool.js'
import { TaskStopTool } from 'src/tools/TaskStopTool/TaskStopTool.js'; import { TodoWriteTool } from 'src/tools/TodoWriteTool/TodoWriteTool.js'
import { TodoWriteTool } from 'src/tools/TodoWriteTool/TodoWriteTool.js'; import { TungstenTool } from 'src/tools/TungstenTool/TungstenTool.js'
import { TungstenTool } from 'src/tools/TungstenTool/TungstenTool.js'; import { WebFetchTool } from 'src/tools/WebFetchTool/WebFetchTool.js'
import { WebFetchTool } from 'src/tools/WebFetchTool/WebFetchTool.js'; import { WebSearchTool } from 'src/tools/WebSearchTool/WebSearchTool.js'
import { WebSearchTool } from 'src/tools/WebSearchTool/WebSearchTool.js'; import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'; import { Box, Text } from '../../ink.js'
import { Box, Text } from '../../ink.js'; import { useKeybinding } from '../../keybindings/useKeybinding.js'
import { useKeybinding } from '../../keybindings/useKeybinding.js'; import { count } from '../../utils/array.js'
import { count } from '../../utils/array.js'; import { plural } from '../../utils/stringUtils.js'
import { plural } from '../../utils/stringUtils.js'; import { Divider } from '../design-system/Divider.js'
import { Divider } from '../design-system/Divider.js';
type Props = { type Props = {
tools: Tools; tools: Tools
initialTools: string[] | undefined; initialTools: string[] | undefined
onComplete: (selectedTools: string[] | undefined) => void; onComplete: (selectedTools: string[] | undefined) => void
onCancel?: () => void; onCancel?: () => void
}; }
type ToolBucket = { type ToolBucket = {
name: string; name: string
toolNames: Set<string>; toolNames: Set<string>
isMcp?: boolean; isMcp?: boolean
}; }
type ToolBuckets = { type ToolBuckets = {
READ_ONLY: ToolBucket; READ_ONLY: ToolBucket
EDIT: ToolBucket; EDIT: ToolBucket
EXECUTION: ToolBucket; EXECUTION: ToolBucket
MCP: ToolBucket; MCP: ToolBucket
OTHER: ToolBucket; OTHER: ToolBucket
}; }
function getToolBuckets(): ToolBuckets { function getToolBuckets(): ToolBuckets {
return { return {
READ_ONLY: { READ_ONLY: {
name: 'Read-only tools', 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: { EDIT: {
name: 'Edit tools', name: 'Edit tools',
toolNames: new Set([FileEditTool.name, FileWriteTool.name, NotebookEditTool.name]) toolNames: new Set([
FileEditTool.name,
FileWriteTool.name,
NotebookEditTool.name,
]),
}, },
EXECUTION: { EXECUTION: {
name: 'Execution tools', 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: { MCP: {
name: 'MCP tools', name: 'MCP tools',
toolNames: new Set(), toolNames: new Set(), // Dynamic - no static list
// Dynamic - no static list isMcp: true,
isMcp: true
}, },
OTHER: { OTHER: {
name: 'Other tools', 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 // Helper to get MCP server buckets dynamically
function getMcpServerBuckets(tools: Tools): Array<{ function getMcpServerBuckets(tools: Tools): Array<{
serverName: string; serverName: string
tools: Tools; tools: Tools
}> { }> {
const serverMap = new Map<string, Tool[]>(); const serverMap = new Map<string, Tool[]>()
tools.forEach(tool => { tools.forEach(tool => {
if (isMcpTool(tool)) { if (isMcpTool(tool)) {
const mcpInfo = mcpInfoFromString(tool.name); const mcpInfo = mcpInfoFromString(tool.name)
if (mcpInfo?.serverName) { if (mcpInfo?.serverName) {
const existing = serverMap.get(mcpInfo.serverName) || []; const existing = serverMap.get(mcpInfo.serverName) || []
existing.push(tool); existing.push(tool)
serverMap.set(mcpInfo.serverName, existing); serverMap.set(mcpInfo.serverName, existing)
} }
} }
}); })
return Array.from(serverMap.entries()).map(([serverName, tools]) => ({
serverName, return Array.from(serverMap.entries())
tools .map(([serverName, tools]) => ({ serverName, tools }))
})).sort((a, b) => a.serverName.localeCompare(b.serverName)); .sort((a, b) => a.serverName.localeCompare(b.serverName))
} }
export function ToolSelector(t0) {
const $ = _c(69); export function ToolSelector({
const { tools,
tools, initialTools,
initialTools, onComplete,
onComplete, onCancel,
onCancel }: Props): React.ReactNode {
} = t0; // Filter tools for custom agents
let t1; const customAgentTools = useMemo(
if ($[0] !== tools) { () => filterToolsForAgent({ tools, isBuiltIn: false, isAsync: false }),
t1 = filterToolsForAgent({ [tools],
tools, )
isBuiltIn: false,
isAsync: false // Expand wildcard or undefined to explicit tool list for internal state
}); const expandedInitialTools =
$[0] = tools; !initialTools || initialTools.includes('*')
$[1] = t1; ? customAgentTools.map(t => t.name)
} else { : initialTools
t1 = $[1];
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; const handleToggleTools = (toolNames: string[], select: boolean) => {
if ($[2] !== customAgentTools || $[3] !== initialTools) { setSelectedTools(current => {
t2 = !initialTools || initialTools.includes("*") ? customAgentTools.map(_temp) : initialTools; if (select) {
$[2] = customAgentTools; const toolsToAdd = toolNames.filter(t => !current.includes(t))
$[3] = initialTools; return [...current, ...toolsToAdd]
$[4] = t2; } else {
} else { return current.filter(t => !toolNames.includes(t))
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;
} }
setSelectedTools(current => current.includes(toolName) ? current.filter(t_1 => t_1 !== toolName) : [...current, toolName]); })
};
$[14] = t6;
} else {
t6 = $[14];
} }
const handleToggleTool = t6;
let t7; const handleConfirm = () => {
if ($[15] === Symbol.for("react.memo_cache_sentinel")) { // Convert to undefined if all tools are selected (for cleaner file format)
t7 = (toolNames_0, select) => { const allToolNames = customAgentTools.map(t => t.name)
setSelectedTools(current_0 => { const areAllToolsSelected =
if (select) { validSelectedTools.length === allToolNames.length &&
const toolsToAdd = toolNames_0.filter(t_2 => !current_0.includes(t_2)); allToolNames.every(name => validSelectedTools.includes(name))
return [...current_0, ...toolsToAdd]; const finalTools = areAllToolsSelected ? undefined : validSelectedTools
} else {
return current_0.filter(t_3 => !toolNames_0.includes(t_3)); onComplete(finalTools)
}
});
};
$[15] = t7;
} else {
t7 = $[15];
} }
const handleToggleTools = t7;
let t8; // Group tools by bucket
if ($[16] !== customAgentTools || $[17] !== onComplete || $[18] !== validSelectedTools) { const toolsByBucket = useMemo(() => {
t8 = () => { const toolBuckets = getToolBuckets()
const allToolNames = customAgentTools.map(_temp3); const buckets = {
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 = {
readOnly: [] as Tool[], readOnly: [] as Tool[],
edit: [] as Tool[], edit: [] as Tool[],
execution: [] as Tool[], execution: [] as Tool[],
mcp: [] as Tool[], mcp: [] as Tool[],
other: [] as Tool[] other: [] as Tool[],
}; }
customAgentTools.forEach(tool => { customAgentTools.forEach(tool => {
// Check if it's an MCP tool first
if (isMcpTool(tool)) { if (isMcpTool(tool)) {
buckets.mcp.push(tool); buckets.mcp.push(tool)
} else { } else if (toolBuckets.READ_ONLY.toolNames.has(tool.name)) {
if (toolBuckets.READ_ONLY.toolNames.has(tool.name)) { buckets.readOnly.push(tool)
buckets.readOnly.push(tool); } else if (toolBuckets.EDIT.toolNames.has(tool.name)) {
} else { buckets.edit.push(tool)
if (toolBuckets.EDIT.toolNames.has(tool.name)) { } else if (toolBuckets.EXECUTION.toolNames.has(tool.name)) {
buckets.edit.push(tool); buckets.execution.push(tool)
} else { } else if (tool.name !== AGENT_TOOL_NAME) {
if (toolBuckets.EXECUTION.toolNames.has(tool.name)) { // Catch-all for uncategorized tools (except Task)
buckets.execution.push(tool); buckets.other.push(tool)
} else {
if (tool.name !== AGENT_TOOL_NAME) {
buckets.other.push(tool);
}
}
}
}
} }
}); })
$[20] = customAgentTools;
$[21] = buckets; return buckets
} else { }, [customAgentTools])
buckets = $[21];
} const createBucketToggleAction = (bucketTools: Tool[]) => {
const toolsByBucket = buckets; const selected = count(bucketTools, t => selectedSet.has(t.name))
let t9; const needsSelection = selected < bucketTools.length
if ($[22] !== selectedSet) {
t9 = bucketTools => { return () => {
const selected = count(bucketTools, (t_5: Tool) => selectedSet.has(t_5.name)); const toolNames = bucketTools.map(t => t.name)
const needsSelection = selected < bucketTools.length; handleToggleTools(toolNames, needsSelection)
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];
} }
}
// 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({ navigableItems.push({
id: "bucket-all", id,
label: `${isAllSelected ? figures.checkboxOn : figures.checkboxOff} All tools`, label: `${isFullySelected ? figures.checkboxOn : figures.checkboxOff} ${name}`,
action: t10 action: createBucketToggleAction(bucketTools),
}); })
const toolBuckets_0 = getToolBuckets(); })
const bucketConfigs = [{
id: "bucket-readonly", // Toggle button for individual tools
name: toolBuckets_0.READ_ONLY.name, const toggleButtonIndex = navigableItems.length
tools: toolsByBucket.readOnly navigableItems.push({
}, { id: 'toggle-individual',
id: "bucket-edit", label: showIndividualTools
name: toolBuckets_0.EDIT.name, ? 'Hide advanced options'
tools: toolsByBucket.edit : 'Show advanced options',
}, { action: () => {
id: "bucket-execution", setShowIndividualTools(!showIndividualTools)
name: toolBuckets_0.EXECUTION.name, // If hiding tools and focus is on an individual tool, move focus to toggle button
tools: toolsByBucket.execution if (showIndividualTools && focusIndex > toggleButtonIndex) {
}, { setFocusIndex(toggleButtonIndex)
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;
} }
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({ navigableItems.push({
id, id: 'mcp-servers-header',
label: `${isFullySelected ? figures.checkboxOn : figures.checkboxOff} ${name_1}`, label: 'MCP Servers:',
action: createBucketToggleAction(bucketTools_0) action: () => {}, // No action - just a header
}); isHeader: true,
}); })
const toggleButtonIndex = navigableItems.length;
let t12; mcpServerBuckets.forEach(({ serverName, tools: serverTools }) => {
if ($[40] !== focusIndex || $[41] !== showIndividualTools || $[42] !== toggleButtonIndex) { const selected = count(serverTools, t => selectedSet.has(t.name))
t12 = () => { const isFullySelected = selected === serverTools.length
setShowIndividualTools(!showIndividualTools);
if (showIndividualTools && focusIndex > toggleButtonIndex) { navigableItems.push({
setFocusIndex(toggleButtonIndex); id: `mcp-server-${serverName}`,
} label: `${isFullySelected ? figures.checkboxOn : figures.checkboxOff} ${serverName} (${serverTools.length} ${plural(serverTools.length, 'tool')})`,
}; action: () => {
$[40] = focusIndex; const toolNames = serverTools.map(t => t.name)
$[41] = showIndividualTools; handleToggleTools(toolNames, !isFullySelected)
$[42] = toggleButtonIndex; },
$[43] = t12; })
})
// 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 { } else {
t12 = $[43]; onComplete(initialTools)
} }
navigableItems.push({ }, [onCancel, onComplete, initialTools])
id: "toggle-individual",
label: showIndividualTools ? "Hide advanced options" : "Show advanced options", useKeybinding('confirm:no', handleCancel, { context: 'Confirmation' })
action: t12,
isToggle: true const handleKeyDown = (e: KeyboardEvent) => {
}); if (e.key === 'return') {
const mcpServerBuckets = getMcpServerBuckets(customAgentTools); e.preventDefault()
if (showIndividualTools) { const item = navigableItems[focusIndex]
if (mcpServerBuckets.length > 0) { if (item && !item.isHeader) {
navigableItems.push({ item.action()
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
});
} }
customAgentTools.forEach(tool_0 => { } else if (e.key === 'up') {
let displayName = tool_0.name; e.preventDefault()
if (tool_0.name.startsWith("mcp__")) { let newIndex = focusIndex - 1
const mcpInfo = mcpInfoFromString(tool_0.name); // Skip headers when navigating up
displayName = mcpInfo ? `${mcpInfo.toolName} (${mcpInfo.serverName})` : tool_0.name; while (newIndex > 0 && navigableItems[newIndex]?.isHeader) {
} newIndex--
navigableItems.push({ }
id: `tool-${tool_0.name}`, setFocusIndex(Math.max(0, newIndex))
label: `${selectedSet.has(tool_0.name) ? figures.checkboxOn : figures.checkboxOff} ${displayName}`, } else if (e.key === 'down') {
action: () => handleToggleTool(tool_0.name) 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) { return (
t10 = () => { <Box
if (onCancel) { flexDirection="column"
onCancel(); marginTop={1}
} else { tabIndex={0}
onComplete(initialTools); autoFocus
} onKeyDown={handleKeyDown}
}; >
$[44] = initialTools; {/* Render Continue button */}
$[45] = onCancel; <Text
$[46] = onComplete; color={focusIndex === 0 ? 'suggestion' : undefined}
$[47] = t10; bold={focusIndex === 0}
} else { >
t10 = $[47]; {focusIndex === 0 ? `${figures.pointer} ` : ' '}[ Continue ]
} </Text>
const handleCancel = t10;
let t11; {/* Separator */}
if ($[48] === Symbol.for("react.memo_cache_sentinel")) { <Divider width={40} />
t11 = {
context: "Confirmation" {/* Render all navigable items except Continue (which is at index 0) */}
}; {navigableItems.slice(1).map((item, index) => {
$[48] = t11; const isCurrentlyFocused = index + 1 === focusIndex
} else { const isToggleButton = item.isToggle
t11 = $[48]; const isHeader = item.isHeader
}
useKeybinding("confirm:no", handleCancel, t11); return (
let t12; <React.Fragment key={item.id}>
if ($[49] !== focusIndex || $[50] !== navigableItems) { {/* Add separator before toggle button */}
t12 = e => { {isToggleButton && <Divider width={40} />}
if (e.key === "return") {
e.preventDefault(); {/* Add margin before headers */}
const item = navigableItems[focusIndex]; {isHeader && index > 0 && <Box marginTop={1} />}
if (item && !item.isHeader) {
item.action(); <Text
} color={
} else { isHeader
if (e.key === "up") { ? undefined
e.preventDefault(); : isCurrentlyFocused
let newIndex = focusIndex - 1; ? 'suggestion'
while (newIndex > 0 && navigableItems[newIndex]?.isHeader) { : undefined
newIndex--; }
} dimColor={isHeader}
setFocusIndex(Math.max(0, newIndex)); bold={isToggleButton && isCurrentlyFocused}
} else { >
if (e.key === "down") { {isHeader
e.preventDefault(); ? ''
let newIndex_0 = focusIndex + 1; : isCurrentlyFocused
while (newIndex_0 < navigableItems.length - 1 && navigableItems[newIndex_0]?.isHeader) { ? `${figures.pointer} `
newIndex_0++; : ' '}
} {isToggleButton ? `[ ${item.label} ]` : item.label}
setFocusIndex(Math.min(navigableItems.length - 1, newIndex_0)); </Text>
} </React.Fragment>
} )
} })}
};
$[49] = focusIndex; <Box marginTop={1} flexDirection="column">
$[50] = navigableItems; <Text dimColor>
$[51] = t12; {isAllSelected
} else { ? 'All tools selected'
t12 = $[51]; : `${selectedSet.size} of ${customAgentTools.length} tools selected`}
} </Text>
const handleKeyDown = t12; </Box>
const t13 = focusIndex === 0 ? "suggestion" : undefined; </Box>
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;
} }

View File

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

View File

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

View File

@@ -1,377 +1,168 @@
import { c as _c } from "react/compiler-runtime"; import React, { type ReactNode } from 'react'
import React, { type ReactNode } from 'react'; import type { KeyboardEvent } from '../../../../ink/events/keyboard-event.js'
import type { KeyboardEvent } from '../../../../ink/events/keyboard-event.js'; import { Box, Text } from '../../../../ink.js'
import { Box, Text } from '../../../../ink.js'; import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
import { useKeybinding } from '../../../../keybindings/useKeybinding.js'; import { isAutoMemoryEnabled } from '../../../../memdir/paths.js'
import { isAutoMemoryEnabled } from '../../../../memdir/paths.js'; import type { Tools } from '../../../../Tool.js'
import type { Tools } from '../../../../Tool.js'; import { getMemoryScopeDisplay } from '../../../../tools/AgentTool/agentMemory.js'
import { getMemoryScopeDisplay } from '../../../../tools/AgentTool/agentMemory.js'; import type { AgentDefinition } from '../../../../tools/AgentTool/loadAgentsDir.js'
import type { AgentDefinition } from '../../../../tools/AgentTool/loadAgentsDir.js'; import { truncateToWidth } from '../../../../utils/format.js'
import { truncateToWidth } from '../../../../utils/format.js'; import { getAgentModelDisplay } from '../../../../utils/model/agent.js'
import { getAgentModelDisplay } from '../../../../utils/model/agent.js'; import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'; import { Byline } from '../../../design-system/Byline.js'
import { Byline } from '../../../design-system/Byline.js'; import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'; import { useWizard } from '../../../wizard/index.js'
import { useWizard } from '../../../wizard/index.js'; import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'; import { getNewRelativeAgentFilePath } from '../../agentFileUtils.js'
import { getNewRelativeAgentFilePath } from '../../agentFileUtils.js'; import { validateAgent } from '../../validateAgent.js'
import { validateAgent } from '../../validateAgent.js'; import type { AgentWizardData } from '../types.js'
import type { AgentWizardData } from '../types.js';
type Props = { type Props = {
tools: Tools; tools: Tools
existingAgents: AgentDefinition[]; existingAgents: AgentDefinition[]
onSave: () => void; onSave: () => void
onSaveAndEdit: () => void; onSaveAndEdit: () => void
error?: string | null; error?: string | null
}; }
export function ConfirmStep(t0) {
const $ = _c(88); export function ConfirmStep({
const { tools,
tools, existingAgents,
existingAgents, onSave,
onSave, onSaveAndEdit,
onSaveAndEdit, error,
error }: Props): ReactNode {
} = t0; const { goBack, wizardData } = useWizard<AgentWizardData>()
const {
goBack, useKeybinding('confirm:no', goBack, { context: 'Confirmation' })
wizardData
} = useWizard(); const handleKeyDown = (e: KeyboardEvent) => {
let t1; if (e.key === 's' || e.key === 'return') {
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { e.preventDefault()
t1 = { onSave()
context: "Confirmation" } else if (e.key === 'e') {
}; e.preventDefault()
$[0] = t1; onSaveAndEdit()
} else { }
t1 = $[0];
} }
useKeybinding("confirm:no", goBack, t1);
let t2; const agent = wizardData.finalAgent!
if ($[1] !== onSave || $[2] !== onSaveAndEdit) { const validation = validateAgent(agent, tools, existingAgents)
t2 = e => {
if (e.key === "s" || e.key === "return") { const systemPromptPreview = truncateToWidth(agent.getSystemPrompt(), 240)
e.preventDefault(); const whenToUsePreview = truncateToWidth(agent.whenToUse, 240)
onSave();
} else { const getToolsDisplay = (toolNames: string[] | undefined): string => {
if (e.key === "e") { // undefined means "all tools" per PR semantic
e.preventDefault(); if (toolNames === undefined) return 'All tools'
onSaveAndEdit(); 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; <Box
$[2] = onSaveAndEdit; flexDirection="column"
$[3] = t2; tabIndex={0}
} else { autoFocus
t2 = $[3]; onKeyDown={handleKeyDown}
} >
const handleKeyDown = t2; <Text>
const agent = wizardData.finalAgent; <Text bold>Name</Text>: {agent.agentType}
let T0; </Text>
let T1; <Text>
let t10; <Text bold>Location</Text>:{' '}
let t11; {getNewRelativeAgentFilePath({
let t12; source: wizardData.location!,
let t13; agentType: agent.agentType,
let t14; })}
let t15; </Text>
let t16; <Text>
let t17; <Text bold>Tools</Text>: {getToolsDisplay(agent.tools)}
let t18; </Text>
let t19; <Text>
let t3; <Text bold>Model</Text>: {getAgentModelDisplay(agent.model)}
let t4; </Text>
let t5; {memoryDisplayElement}
let t6;
let t7; <Box marginTop={1}>
let t8; <Text>
let t9; <Text bold>Description</Text> (tells Claude when to use this agent):
if ($[4] !== agent || $[5] !== existingAgents || $[6] !== handleKeyDown || $[7] !== tools || $[8] !== wizardData.location) { </Text>
const validation = validateAgent(agent, tools, existingAgents); </Box>
let t20; <Box marginLeft={2} marginTop={1}>
if ($[28] !== agent) { <Text>{whenToUsePreview}</Text>
t20 = truncateToWidth(agent.getSystemPrompt(), 240); </Box>
$[28] = agent;
$[29] = t20; <Box marginTop={1}>
} else { <Text>
t20 = $[29]; <Text bold>System prompt</Text>:
} </Text>
const systemPromptPreview = t20; </Box>
let t21; <Box marginLeft={2} marginTop={1}>
if ($[30] !== agent.whenToUse) { <Text>{systemPromptPreview}</Text>
t21 = truncateToWidth(agent.whenToUse, 240); </Box>
$[30] = agent.whenToUse;
$[31] = t21; {validation.warnings.length > 0 && (
} else { <Box marginTop={1} flexDirection="column">
t21 = $[31]; <Text color="warning">Warnings:</Text>
} {validation.warnings.map((warning, i) => (
const whenToUsePreview = t21; <Text key={i} dimColor>
const getToolsDisplay = _temp; {' '}
let t22; {warning}
if ($[32] !== agent.memory) { </Text>
t22 = isAutoMemoryEnabled() ? <Text><Text bold={true}>Memory</Text>: {getMemoryScopeDisplay(agent.memory)}</Text> : null; ))}
$[32] = agent.memory; </Box>
$[33] = t22; )}
} else {
t22 = $[33]; {validation.errors.length > 0 && (
} <Box marginTop={1} flexDirection="column">
const memoryDisplayElement = t22; <Text color="error">Errors:</Text>
T1 = WizardDialogLayout; {validation.errors.map((err, i) => (
t18 = "Confirm and save"; <Text key={i} color="error">
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>; {err}
$[34] = t19; </Text>
} else { ))}
t19 = $[34]; </Box>
} )}
T0 = Box;
t3 = "column"; {error && (
t4 = 0; <Box marginTop={1}>
t5 = true; <Text color="error">{error}</Text>
t6 = handleKeyDown; </Box>
let t23; )}
if ($[35] === Symbol.for("react.memo_cache_sentinel")) {
t23 = <Text bold={true}>Name</Text>; <Box marginTop={2}>
$[35] = t23; <Text color="success">
} else { Press <Text bold>s</Text> or <Text bold>Enter</Text> to save,{' '}
t23 = $[35]; <Text bold>e</Text> to save and edit
} </Text>
if ($[36] !== agent.agentType) { </Box>
t7 = <Text>{t23}: {agent.agentType}</Text>; </Box>
$[36] = agent.agentType; </WizardDialogLayout>
$[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]}`;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,51 +1,42 @@
import { c as _c } from "react/compiler-runtime"; import React, { type ReactNode } from 'react'
import React, { type ReactNode } from 'react'; import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'; import { Byline } from '../../../design-system/Byline.js'
import { Byline } from '../../../design-system/Byline.js'; import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'; import { useWizard } from '../../../wizard/index.js'
import { useWizard } from '../../../wizard/index.js'; import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'; import { ModelSelector } from '../../ModelSelector.js'
import { ModelSelector } from '../../ModelSelector.js'; import type { AgentWizardData } from '../types.js'
import type { AgentWizardData } from '../types.js';
export function ModelStep() { export function ModelStep(): ReactNode {
const $ = _c(8); const { goNext, goBack, updateWizardData, wizardData } =
const { useWizard<AgentWizardData>()
goNext,
goBack, const handleComplete = (model?: string): void => {
updateWizardData, updateWizardData({ selectedModel: model })
wizardData goNext()
} = useWizard();
let t0;
if ($[0] !== goNext || $[1] !== updateWizardData) {
t0 = model => {
updateWizardData({
selectedModel: model
});
goNext();
};
$[0] = goNext;
$[1] = updateWizardData;
$[2] = t0;
} else {
t0 = $[2];
} }
const handleComplete = t0;
let t1; return (
if ($[3] === Symbol.for("react.memo_cache_sentinel")) { <WizardDialogLayout
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>; subtitle="Select model"
$[3] = t1; footerText={
} else { <Byline>
t1 = $[3]; <KeyboardShortcutHint shortcut="↑↓" action="navigate" />
} <KeyboardShortcutHint shortcut="Enter" action="select" />
let t2; <ConfigurableShortcutHint
if ($[4] !== goBack || $[5] !== handleComplete || $[6] !== wizardData.selectedModel) { action="confirm:no"
t2 = <WizardDialogLayout subtitle="Select model" footerText={t1}><ModelSelector initialModel={wizardData.selectedModel} onComplete={handleComplete} onCancel={goBack} /></WizardDialogLayout>; context="Confirmation"
$[4] = goBack; fallback="Esc"
$[5] = handleComplete; description="go back"
$[6] = wizardData.selectedModel; />
$[7] = t2; </Byline>
} else { }
t2 = $[7]; >
} <ModelSelector
return t2; 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 React, { type ReactNode, useCallback, useState } from 'react'; import { Box, Text } from '../../../../ink.js'
import { Box, Text } from '../../../../ink.js'; import { useKeybinding } from '../../../../keybindings/useKeybinding.js'
import { useKeybinding } from '../../../../keybindings/useKeybinding.js'; import { editPromptInEditor } from '../../../../utils/promptEditor.js'
import { editPromptInEditor } from '../../../../utils/promptEditor.js'; import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'
import { ConfigurableShortcutHint } from '../../../ConfigurableShortcutHint.js'; import { Byline } from '../../../design-system/Byline.js'
import { Byline } from '../../../design-system/Byline.js'; import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'
import { KeyboardShortcutHint } from '../../../design-system/KeyboardShortcutHint.js'; import TextInput from '../../../TextInput.js'
import TextInput from '../../../TextInput.js'; import { useWizard } from '../../../wizard/index.js'
import { useWizard } from '../../../wizard/index.js'; import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'
import { WizardDialogLayout } from '../../../wizard/WizardDialogLayout.js'; import type { AgentWizardData } from '../types.js'
import type { AgentWizardData } from '../types.js';
export function PromptStep() { export function PromptStep(): ReactNode {
const $ = _c(20); const { goNext, goBack, updateWizardData, wizardData } =
const { useWizard<AgentWizardData>()
goNext, const [systemPrompt, setSystemPrompt] = useState(
goBack, wizardData.systemPrompt || '',
updateWizardData, )
wizardData const [cursorOffset, setCursorOffset] = useState(systemPrompt.length)
} = useWizard(); const [error, setError] = useState<string | null>(null)
const [systemPrompt, setSystemPrompt] = useState(wizardData.systemPrompt || "");
const [cursorOffset, setCursorOffset] = useState(systemPrompt.length); // Handle escape key - use Settings context so 'n' key doesn't cancel (allows typing 'n' in input)
const [error, setError] = useState(null); useKeybinding('confirm:no', goBack, { context: 'Settings' })
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { const handleExternalEditor = useCallback(async () => {
t0 = { const result = await editPromptInEditor(systemPrompt)
context: "Settings" if (result.content !== null) {
}; setSystemPrompt(result.content)
$[0] = t0; setCursorOffset(result.content.length)
} else { }
t0 = $[0]; }, [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; return (
if ($[1] !== systemPrompt) { <WizardDialogLayout
t1 = async () => { subtitle="System prompt"
const result = await editPromptInEditor(systemPrompt); footerText={
if (result.content !== null) { <Byline>
setSystemPrompt(result.content); <KeyboardShortcutHint shortcut="Type" action="enter text" />
setCursorOffset(result.content.length); <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; <Box flexDirection="column">
$[2] = t1; <Text>Enter the system prompt for your agent:</Text>
} else { <Text dimColor>Be comprehensive for best results</Text>
t1 = $[2];
} <Box marginTop={1}>
const handleExternalEditor = t1; <TextInput
let t2; value={systemPrompt}
if ($[3] === Symbol.for("react.memo_cache_sentinel")) { onChange={setSystemPrompt}
t2 = { onSubmit={handleSubmit}
context: "Chat" placeholder="You are a helpful code reviewer who..."
}; columns={80}
$[3] = t2; cursorOffset={cursorOffset}
} else { onChangeCursorOffset={setCursorOffset}
t2 = $[3]; focus
} showCursor
useKeybinding("chat:externalEditor", handleExternalEditor, t2); />
let t3; </Box>
if ($[4] !== goNext || $[5] !== systemPrompt || $[6] !== updateWizardData) {
t3 = () => { {error && (
const trimmedPrompt = systemPrompt.trim(); <Box marginTop={1}>
if (!trimmedPrompt) { <Text color="error">{error}</Text>
setError("System prompt is required"); </Box>
return; )}
} </Box>
setError(null); </WizardDialogLayout>
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;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,155 +1,112 @@
import { c as _c } from "react/compiler-runtime"; import React, { type PropsWithChildren, type Ref } from 'react'
import React, { type PropsWithChildren, type Ref } from 'react'; import Box from '../../ink/components/Box.js'
import Box from '../../ink/components/Box.js'; import type { DOMElement } from '../../ink/dom.js'
import type { DOMElement } from '../../ink/dom.js'; import type { ClickEvent } from '../../ink/events/click-event.js'
import type { ClickEvent } from '../../ink/events/click-event.js'; import type { FocusEvent } from '../../ink/events/focus-event.js'
import type { FocusEvent } from '../../ink/events/focus-event.js'; import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'; import type { Color, Styles } from '../../ink/styles.js'
import type { Color, Styles } from '../../ink/styles.js'; import { getTheme, type Theme } from '../../utils/theme.js'
import { getTheme, type Theme } from '../../utils/theme.js'; import { useTheme } from './ThemeProvider.js'
import { useTheme } from './ThemeProvider.js';
// Color props that accept theme keys // Color props that accept theme keys
type ThemedColorProps = { type ThemedColorProps = {
readonly borderColor?: keyof Theme | Color; readonly borderColor?: keyof Theme | Color
readonly borderTopColor?: keyof Theme | Color; readonly borderTopColor?: keyof Theme | Color
readonly borderBottomColor?: keyof Theme | Color; readonly borderBottomColor?: keyof Theme | Color
readonly borderLeftColor?: keyof Theme | Color; readonly borderLeftColor?: keyof Theme | Color
readonly borderRightColor?: keyof Theme | Color; readonly borderRightColor?: keyof Theme | Color
readonly backgroundColor?: keyof Theme | Color; readonly backgroundColor?: keyof Theme | Color
}; }
// Base Styles without color props (they'll be overridden) // Base Styles without color props (they'll be overridden)
type BaseStylesWithoutColors = Omit<Styles, 'textWrap' | 'borderColor' | 'borderTopColor' | 'borderBottomColor' | 'borderLeftColor' | 'borderRightColor' | 'backgroundColor'>; type BaseStylesWithoutColors = Omit<
export type Props = BaseStylesWithoutColors & ThemedColorProps & { Styles,
ref?: Ref<DOMElement>; | 'textWrap'
tabIndex?: number; | 'borderColor'
autoFocus?: boolean; | 'borderTopColor'
onClick?: (event: ClickEvent) => void; | 'borderBottomColor'
onFocus?: (event: FocusEvent) => void; | 'borderLeftColor'
onFocusCapture?: (event: FocusEvent) => void; | 'borderRightColor'
onBlur?: (event: FocusEvent) => void; | 'backgroundColor'
onBlurCapture?: (event: FocusEvent) => void; >
onKeyDown?: (event: KeyboardEvent) => void;
onKeyDownCapture?: (event: KeyboardEvent) => void; export type Props = BaseStylesWithoutColors &
onMouseEnter?: () => void; ThemedColorProps & {
onMouseLeave?: () => void; 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. * 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 { function resolveColor(
if (!color) return undefined; 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:) // 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:')) { if (
return color as Color; color.startsWith('rgb(') ||
color.startsWith('#') ||
color.startsWith('ansi256(') ||
color.startsWith('ansi:')
) {
return color as Color
} }
// It's a theme key - resolve it // 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. * Theme-aware Box component that resolves theme color keys to raw colors.
* This wraps the base Box component with theme resolution for border colors. * This wraps the base Box component with theme resolution for border colors.
*/ */
function ThemedBox(t0) { function ThemedBox({
const $ = _c(33); borderColor,
let backgroundColor; borderTopColor,
let borderBottomColor; borderBottomColor,
let borderColor; borderLeftColor,
let borderLeftColor; borderRightColor,
let borderRightColor; backgroundColor,
let borderTopColor; children,
let children; ref,
let ref; ...rest
let rest; }: PropsWithChildren<Props>): React.ReactNode {
if ($[0] !== t0) { const [themeName] = useTheme()
({ const theme = getTheme(themeName)
borderColor,
borderTopColor, // Resolve theme keys to raw colors
borderBottomColor, const resolvedBorderColor = resolveColor(borderColor, theme)
borderLeftColor, const resolvedBorderTopColor = resolveColor(borderTopColor, theme)
borderRightColor, const resolvedBorderBottomColor = resolveColor(borderBottomColor, theme)
backgroundColor, const resolvedBorderLeftColor = resolveColor(borderLeftColor, theme)
children, const resolvedBorderRightColor = resolveColor(borderRightColor, theme)
ref, const resolvedBackgroundColor = resolveColor(backgroundColor, theme)
...rest
} = t0); return (
$[0] = t0; <Box
$[1] = backgroundColor; ref={ref}
$[2] = borderBottomColor; borderColor={resolvedBorderColor}
$[3] = borderColor; borderTopColor={resolvedBorderTopColor}
$[4] = borderLeftColor; borderBottomColor={resolvedBorderBottomColor}
$[5] = borderRightColor; borderLeftColor={resolvedBorderLeftColor}
$[6] = borderTopColor; borderRightColor={resolvedBorderRightColor}
$[7] = children; backgroundColor={resolvedBackgroundColor}
$[8] = ref; {...rest}
$[9] = rest; >
} else { {children}
backgroundColor = $[1]; </Box>
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;
} }
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 type { ReactNode } from 'react'; import React, { useContext } from 'react'
import React, { useContext } from 'react'; import Text from '../../ink/components/Text.js'
import Text from '../../ink/components/Text.js'; import type { Color, Styles } from '../../ink/styles.js'
import type { Color, Styles } from '../../ink/styles.js'; import { getTheme, type Theme } from '../../utils/theme.js'
import { getTheme, type Theme } from '../../utils/theme.js'; import { useTheme } from './ThemeProvider.js'
import { useTheme } from './ThemeProvider.js';
/** Colors uncolored ThemedText in the subtree. Precedence: explicit `color` > /** Colors uncolored ThemedText in the subtree. Precedence: explicit `color` >
* this > dimColor. Crosses Box boundaries (Ink's style cascade doesn't). */ * 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 = { export type Props = {
/** /**
* Change text color. Accepts a theme key or raw color value. * 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. * 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. * Dim the color using the theme's inactive color.
* This is compatible with bold (unlike ANSI dim). * This is compatible with bold (unlike ANSI dim).
*/ */
readonly dimColor?: boolean; readonly dimColor?: boolean
/** /**
* Make the text bold. * Make the text bold.
*/ */
readonly bold?: boolean; readonly bold?: boolean
/** /**
* Make the text italic. * Make the text italic.
*/ */
readonly italic?: boolean; readonly italic?: boolean
/** /**
* Make the text underlined. * Make the text underlined.
*/ */
readonly underline?: boolean; readonly underline?: boolean
/** /**
* Make the text crossed with a line. * Make the text crossed with a line.
*/ */
readonly strikethrough?: boolean; readonly strikethrough?: boolean
/** /**
* Inverse background and foreground colors. * 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. * 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 `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. * 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 wrap?: Styles['textWrap']
readonly children?: ReactNode;
}; readonly children?: ReactNode
}
/** /**
* Resolves a color value that may be a theme key to a raw Color. * 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 { function resolveColor(
if (!color) return undefined; 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:) // 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:')) { if (
return color as Color; color.startsWith('rgb(') ||
color.startsWith('#') ||
color.startsWith('ansi256(') ||
color.startsWith('ansi:')
) {
return color as Color
} }
// It's a theme key - resolve it // 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. * Theme-aware Text component that resolves theme color keys to raw colors.
* This wraps the base Text component with theme resolution. * This wraps the base Text component with theme resolution.
*/ */
export default function ThemedText(t0) { export default function ThemedText({
const $ = _c(10); color,
const { backgroundColor,
color, dimColor = false,
backgroundColor, bold = false,
dimColor: t1, italic = false,
bold: t2, underline = false,
italic: t3, strikethrough = false,
underline: t4, inverse = false,
strikethrough: t5, wrap = 'wrap',
inverse: t6, children,
wrap: t7, }: Props): React.ReactNode {
children const [themeName] = useTheme()
} = t0; const theme = getTheme(themeName)
const dimColor = t1 === undefined ? false : t1; const hoverColor = useContext(TextHoverColorContext)
const bold = t2 === undefined ? false : t2;
const italic = t3 === undefined ? false : t3; // Resolve theme keys to raw colors
const underline = t4 === undefined ? false : t4; const resolvedColor =
const strikethrough = t5 === undefined ? false : t5; !color && hoverColor
const inverse = t6 === undefined ? false : t6; ? resolveColor(hoverColor, theme)
const wrap = t7 === undefined ? "wrap" : t7; : dimColor
const [themeName] = useTheme(); ? (theme.inactive as Color)
const theme = getTheme(themeName); : resolveColor(color, theme)
const hoverColor = useContext(TextHoverColorContext); const resolvedBackgroundColor = backgroundColor
const resolvedColor = !color && hoverColor ? resolveColor(hoverColor, theme) : dimColor ? theme.inactive as Color : resolveColor(color, theme); ? (theme[backgroundColor] as Color)
const resolvedBackgroundColor = backgroundColor ? theme[backgroundColor] as Color : undefined; : undefined
let t8;
if ($[0] !== bold || $[1] !== children || $[2] !== inverse || $[3] !== italic || $[4] !== resolvedBackgroundColor || $[5] !== resolvedColor || $[6] !== strikethrough || $[7] !== underline || $[8] !== wrap) { return (
t8 = <Text color={resolvedColor} backgroundColor={resolvedBackgroundColor} bold={bold} italic={italic} underline={underline} strikethrough={strikethrough} inverse={inverse} wrap={wrap}>{children}</Text>; <Text
$[0] = bold; color={resolvedColor}
$[1] = children; backgroundColor={resolvedBackgroundColor}
$[2] = inverse; bold={bold}
$[3] = italic; italic={italic}
$[4] = resolvedBackgroundColor; underline={underline}
$[5] = resolvedColor; strikethrough={strikethrough}
$[6] = strikethrough; inverse={inverse}
$[7] = underline; wrap={wrap}
$[8] = wrap; >
$[9] = t8; {children}
} else { </Text>
t8 = $[9]; )
}
return t8;
} }

View File

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

View File

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

View File

@@ -1,344 +1,146 @@
import { c as _c } from "react/compiler-runtime"; import * as React from 'react'
import * as React from 'react'; import { Text } from 'src/ink.js'
import { Text } from 'src/ink.js'; import type { BackgroundTaskState } from 'src/tasks/types.js'
import type { BackgroundTaskState } from 'src/tasks/types.js'; import type { DeepImmutable } from 'src/types/utils.js'
import type { DeepImmutable } from 'src/types/utils.js'; import { truncate } from 'src/utils/format.js'
import { truncate } from 'src/utils/format.js'; import { toInkColor } from 'src/utils/ink.js'
import { toInkColor } from 'src/utils/ink.js'; import { plural } from 'src/utils/stringUtils.js'
import { plural } from 'src/utils/stringUtils.js'; import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js'
import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js'; import { RemoteSessionProgress } from './RemoteSessionProgress.js'
import { RemoteSessionProgress } from './RemoteSessionProgress.js'; import { ShellProgress, TaskStatusText } from './ShellProgress.js'
import { ShellProgress, TaskStatusText } from './ShellProgress.js'; import { describeTeammateActivity } from './taskStatusUtils.js'
import { describeTeammateActivity } from './taskStatusUtils.js';
type Props = { type Props = {
task: DeepImmutable<BackgroundTaskState>; task: DeepImmutable<BackgroundTaskState>
maxActivityWidth?: number; maxActivityWidth?: number
}; }
export function BackgroundTask(t0) {
const $ = _c(92); export function BackgroundTask({
const { task,
task, maxActivityWidth,
maxActivityWidth }: Props): React.ReactNode {
} = t0; const activityLimit = maxActivityWidth ?? 40
const activityLimit = maxActivityWidth ?? 40;
switch (task.type) { switch (task.type) {
case "local_bash": case 'local_bash':
{ return (
const t1 = task.kind === "monitor" ? task.description : task.command; <Text>
let t2; {truncate(
if ($[0] !== activityLimit || $[1] !== t1) { task.kind === 'monitor' ? task.description : task.command,
t2 = truncate(t1, activityLimit, true); activityLimit,
$[0] = activityLimit; true,
$[1] = t1; )}{' '}
$[2] = t2; <ShellProgress shell={task} />
} else { </Text>
t2 = $[2]; )
} case 'remote_agent': {
let t3; // Lite-review renders its own rainbow line (title + live counts),
if ($[3] !== task) { // so we don't prefix the title — the rainbow already includes it.
t3 = <ShellProgress shell={task} />; if (task.isRemoteReview) {
$[3] = task; return (
$[4] = t3; <Text>
} else { <RemoteSessionProgress session={task} />
t3 = $[4]; </Text>
} )
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;
} }
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 figures from 'figures'; import * as React from 'react'
import * as React from 'react'; import { useMemo, useState } from 'react'
import { useMemo, useState } from 'react'; import { useTerminalSize } from 'src/hooks/useTerminalSize.js'
import { useTerminalSize } from 'src/hooks/useTerminalSize.js'; import { stringWidth } from 'src/ink/stringWidth.js'
import { stringWidth } from 'src/ink/stringWidth.js'; import { useAppState, useSetAppState } from 'src/state/AppState.js'
import { useAppState, useSetAppState } from 'src/state/AppState.js'; import {
import { enterTeammateView, exitTeammateView } from 'src/state/teammateViewHelpers.js'; enterTeammateView,
import { isPanelAgentTask } from 'src/tasks/LocalAgentTask/LocalAgentTask.js'; exitTeammateView,
import { getPillLabel, pillNeedsCta } from 'src/tasks/pillLabel.js'; } from 'src/state/teammateViewHelpers.js'
import { type BackgroundTaskState, isBackgroundTask, type TaskState } from 'src/tasks/types.js'; import { isPanelAgentTask } from 'src/tasks/LocalAgentTask/LocalAgentTask.js'
import { calculateHorizontalScrollWindow } from 'src/utils/horizontalScroll.js'; import { getPillLabel, pillNeedsCta } from 'src/tasks/pillLabel.js'
import { Box, Text } from '../../ink.js'; import {
import { AGENT_COLOR_TO_THEME_COLOR, AGENT_COLORS, type AgentColorName } from '../../tools/AgentTool/agentColorManager.js'; type BackgroundTaskState,
import type { Theme } from '../../utils/theme.js'; isBackgroundTask,
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'; type TaskState,
import { shouldHideTasksFooter } from './taskStatusUtils.js'; } 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 = { type Props = {
tasksSelected: boolean; tasksSelected: boolean
isViewingTeammate?: boolean; isViewingTeammate?: boolean
teammateFooterIndex?: number; teammateFooterIndex?: number
isLeaderIdle?: boolean; isLeaderIdle?: boolean
onOpenDialog?: (taskId?: string) => void; onOpenDialog?: (taskId?: string) => void
}; }
export function BackgroundTaskStatus(t0) {
const $ = _c(48); export function BackgroundTaskStatus({
const { tasksSelected,
tasksSelected, isViewingTeammate,
isViewingTeammate, teammateFooterIndex = 0,
teammateFooterIndex: t1, isLeaderIdle = false,
isLeaderIdle: t2, onOpenDialog,
onOpenDialog }: Props): React.ReactNode {
} = t0; const setAppState = useSetAppState()
const teammateFooterIndex = t1 === undefined ? 0 : t1; const { columns } = useTerminalSize()
const isLeaderIdle = t2 === undefined ? false : t2; const tasks = useAppState(s => s.tasks)
const setAppState = useSetAppState(); const viewingAgentTaskId = useAppState(s => s.viewingAgentTaskId)
const {
columns const runningTasks = useMemo(
} = useTerminalSize(); () =>
const tasks = useAppState(_temp); (Object.values(tasks ?? {}) as TaskState[]).filter(
const viewingAgentTaskId = useAppState(_temp2); t =>
let t3; isBackgroundTask(t) &&
if ($[0] !== tasks) { !(process.env.USER_TYPE === 'ant' && isPanelAgentTask(t)),
t3 = (Object.values(tasks ?? {}) as TaskState[]).filter(_temp3); ),
$[0] = tasks; [tasks],
$[1] = t3; )
} else {
t3 = $[1]; // 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 runningTasks = t3; const expandedView = useAppState(s => s.expandedView)
const expandedView = useAppState(_temp4); const showSpinnerTree = expandedView === 'teammates'
const showSpinnerTree = expandedView === "teammates"; const allTeammates =
const allTeammates = !showSpinnerTree && runningTasks.length > 0 && runningTasks.every(_temp5); !showSpinnerTree &&
let t4; runningTasks.length > 0 &&
if ($[2] !== runningTasks) { runningTasks.every(t => t.type === 'in_process_teammate')
t4 = runningTasks.filter(_temp6).sort(_temp7);
$[2] = runningTasks; // Memoize teammate-related computations at the top level (rules of hooks)
$[3] = t4; const teammateEntries = useMemo(
} else { () =>
t4 = $[3]; runningTasks
} .filter(
const teammateEntries = t4; (t): t is BackgroundTaskState & { type: 'in_process_teammate' } =>
let t5; t.type === 'in_process_teammate',
if ($[4] !== isLeaderIdle) { )
t5 = { .sort((a, b) =>
name: "main", 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, color: undefined as keyof Theme | undefined,
isIdle: isLeaderIdle, isIdle: isLeaderIdle,
taskId: undefined as string | undefined taskId: undefined as string | undefined,
}; }
$[4] = isLeaderIdle;
$[5] = t5; const teammatePills = teammateEntries.map(t => ({
} else { name: t.identity.agentName,
t5 = $[5]; color: getAgentThemeColor(t.identity.color),
} isIdle: t.isIdle,
const mainPill = t5; taskId: t.id,
let t6; }))
if ($[6] !== mainPill || $[7] !== tasksSelected || $[8] !== teammateEntries) {
const teammatePills = teammateEntries.map(_temp8); // Only sort teammates when not selecting to avoid reordering during navigation
if (!tasksSelected) { 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); // main always first, then sorted teammates
$[6] = mainPill; const pills = [mainPill, ...teammatePills]
$[7] = tasksSelected;
$[8] = teammateEntries; // Add idx after sorting
$[9] = t6; return pills.map((pill, i) => ({ ...pill, idx: i }))
} else { }, [teammateEntries, isLeaderIdle, tasksSelected])
t6 = $[9];
} // Calculate pill widths (including separator space, except first)
const allPills = t6; const pillWidths = useMemo(
let t7; () =>
if ($[10] !== allPills) { allPills.map((pill, i) => {
t7 = allPills.map(_temp1); const pillText = `@${pill.name}`
$[10] = allPills; // First pill has no leading space, others have 1 space separator
$[11] = t7; return stringWidth(pillText) + (i > 0 ? 1 : 0)
} else { }),
t7 = $[11]; [allPills],
} )
const pillWidths = t7;
if (allTeammates || !showSpinnerTree && isViewingTeammate) { if (allTeammates || (!showSpinnerTree && isViewingTeammate)) {
const selectedIdx = tasksSelected ? teammateFooterIndex : -1; const selectedIdx = tasksSelected ? teammateFooterIndex : -1
let t8; // Which agent is currently foregrounded (bold)
if ($[12] !== teammateEntries || $[13] !== viewingAgentTaskId) { const viewedIdx = viewingAgentTaskId
t8 = viewingAgentTaskId ? teammateEntries.findIndex(t_3 => t_3.id === viewingAgentTaskId) + 1 : 0; ? teammateEntries.findIndex(t => t.id === viewingAgentTaskId) + 1
$[12] = teammateEntries; : 0 // 0 = main/leader
$[13] = viewingAgentTaskId;
$[14] = t8; // Calculate available width for pills
} else { // Reserve space for: arrows, hint, and minimal padding
t8 = $[14]; // Pills are rendered on their own line when in team mode
} const ARROW_WIDTH = 2 // arrow char + space
const viewedIdx = t8; const HINT_WIDTH = 20 // shift+↓ to expand
const availableWidth = Math.max(20, columns - 20 - 4); const PADDING = 4 // minimal safety margin
const t9 = selectedIdx >= 0 ? selectedIdx : 0; const availableWidth = Math.max(20, columns - HINT_WIDTH - PADDING)
let t10;
if ($[15] !== availableWidth || $[16] !== pillWidths || $[17] !== t9) { // Calculate visible window of pills
t10 = calculateHorizontalScrollWindow(pillWidths, availableWidth, 2, t9); const { startIndex, endIndex, showLeftArrow, showRightArrow } =
$[15] = availableWidth; calculateHorizontalScrollWindow(
$[16] = pillWidths; pillWidths,
$[17] = t9; availableWidth,
$[18] = t10; ARROW_WIDTH,
} else { selectedIdx >= 0 ? selectedIdx : 0,
t10 = $[18]; )
}
const { const visiblePills = allPills.slice(startIndex, endIndex)
startIndex,
endIndex, return (
showLeftArrow, <>
showRightArrow {showLeftArrow && <Text dimColor>{figures.arrowLeft} </Text>}
} = t10; {visiblePills.map((pill, i) => {
let t11; // First visible pill has no leading separator
if ($[19] !== allPills || $[20] !== endIndex || $[21] !== startIndex) { // (left arrow already provides spacing if present)
t11 = allPills.slice(startIndex, endIndex); const needsSeparator = i > 0
$[19] = allPills; return (
$[20] = endIndex; <React.Fragment key={pill.name}>
$[21] = startIndex; {needsSeparator && <Text> </Text>}
$[22] = t11; <AgentPill
} else { name={pill.name}
t11 = $[22]; color={pill.color}
} isSelected={selectedIdx === pill.idx}
const visiblePills = t11; isViewed={viewedIdx === pill.idx}
let t12; isIdle={pill.isIdle}
if ($[23] !== showLeftArrow) { onClick={() =>
t12 = showLeftArrow && <Text dimColor={true}>{figures.arrowLeft} </Text>; pill.taskId
$[23] = showLeftArrow; ? enterTeammateView(pill.taskId, setAppState)
$[24] = t12; : exitTeammateView(setAppState)
} else { }
t12 = $[24]; />
} </React.Fragment>
let t13; )
if ($[25] !== selectedIdx || $[26] !== setAppState || $[27] !== viewedIdx || $[28] !== visiblePills) { })}
t13 = visiblePills.map((pill_1, i_1) => { {showRightArrow && <Text dimColor> {figures.arrowRight}</Text>}
const needsSeparator = i_1 > 0; <Text dimColor>
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>; {' · '}
}); <KeyboardShortcutHint shortcut="shift + ↓" action="expand" />
$[25] = selectedIdx; </Text>
$[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;
} }
// In spinner-tree mode, don't show any footer status for teammates
// (they appear in the spinner tree above)
if (shouldHideTasksFooter(tasks ?? {}, showSpinnerTree)) { if (shouldHideTasksFooter(tasks ?? {}, showSpinnerTree)) {
return null; return null
} }
if (runningTasks.length === 0) { if (runningTasks.length === 0) {
return null; return null
} }
let t8;
if ($[37] !== runningTasks) { return (
t8 = getPillLabel(runningTasks); <>
$[37] = runningTasks; <SummaryPill selected={tasksSelected} onClick={onOpenDialog}>
$[38] = t8; {getPillLabel(runningTasks)}
} else { </SummaryPill>
t8 = $[38]; {pillNeedsCta(runningTasks) && (
} <Text dimColor> · {figures.arrowDown} to view</Text>
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;
} }
type AgentPillProps = { type AgentPillProps = {
name: string; name: string
color?: keyof Theme; color?: keyof Theme
isSelected: boolean; isSelected: boolean
isViewed: boolean; isViewed: boolean
isIdle: boolean; isIdle: boolean
onClick?: () => void; onClick?: () => void
}; }
function AgentPill(t0) {
const $ = _c(19); function AgentPill({
const { name,
name, color,
color, isSelected,
isSelected, isViewed,
isViewed, isIdle,
isIdle, onClick,
onClick }: AgentPillProps): React.ReactNode {
} = t0; const [hover, setHover] = useState(false)
const [hover, setHover] = useState(false); // Hover mirrors the keyboard-selected look so the affordance is familiar.
const highlighted = isSelected || hover; const highlighted = isSelected || hover
let label;
let label: React.ReactNode
if (highlighted) { if (highlighted) {
let t1; label = color ? (
if ($[0] !== color || $[1] !== isViewed || $[2] !== name) { <Text backgroundColor={color} color="inverseText" bold={isViewed}>
t1 = color ? <Text backgroundColor={color} color="inverseText" bold={isViewed}>@{name}</Text> : <Text color="background" inverse={true} bold={isViewed}>@{name}</Text>; @{name}
$[0] = color; </Text>
$[1] = isViewed; ) : (
$[2] = name; <Text color="background" inverse bold={isViewed}>
$[3] = t1; @{name}
} else { </Text>
t1 = $[3]; )
} } else if (isIdle) {
label = t1; label = (
<Text dimColor bold={isViewed}>
@{name}
</Text>
)
} else if (isViewed) {
label = (
<Text color={color} bold>
@{name}
</Text>
)
} else { } else {
if (isIdle) { label = (
let t1; <Text color={color} dimColor={!color}>
if ($[4] !== isViewed || $[5] !== name) { @{name}
t1 = <Text dimColor={true} bold={isViewed}>@{name}</Text>; </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;
}
}
} }
if (!onClick) {
return label; if (!onClick) return label
} return (
let t1; <Box
let t2; onClick={onClick}
if ($[14] === Symbol.for("react.memo_cache_sentinel")) { onMouseEnter={() => setHover(true)}
t1 = () => setHover(true); onMouseLeave={() => setHover(false)}
t2 = () => setHover(false); >
$[14] = t1; {label}
$[15] = t2; </Box>
} 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;
} }
function SummaryPill(t0) {
const $ = _c(8); function SummaryPill({
const { selected,
selected, onClick,
onClick, children,
children }: {
} = t0; selected: boolean
const [hover, setHover] = useState(false); onClick?: () => void
const t1 = selected || hover; children: React.ReactNode
let t2; }): React.ReactNode {
if ($[0] !== children || $[1] !== t1) { const [hover, setHover] = useState(false)
t2 = <Text color="background" inverse={t1}>{children}</Text>; const label = (
$[0] = children; <Text color="background" inverse={selected || hover}>
$[1] = t1; {children}
$[2] = t2; </Text>
} else { )
t2 = $[2]; if (!onClick) return label
} return (
const label = t2; <Box
if (!onClick) { onClick={onClick}
return label; onMouseEnter={() => setHover(true)}
} onMouseLeave={() => setHover(false)}
let t3; >
let t4; {label}
if ($[3] === Symbol.for("react.memo_cache_sentinel")) { </Box>
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 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)) { 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 React from 'react'; import type { DeepImmutable } from 'src/types/utils.js'
import type { DeepImmutable } from 'src/types/utils.js'; import { useElapsedTime } from '../../hooks/useElapsedTime.js'
import { useElapsedTime } from '../../hooks/useElapsedTime.js'; import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'; import { Box, Text } from '../../ink.js'
import { Box, Text } from '../../ink.js'; import { useKeybindings } from '../../keybindings/useKeybinding.js'
import { useKeybindings } from '../../keybindings/useKeybinding.js'; import type { DreamTaskState } from '../../tasks/DreamTask/DreamTask.js'
import type { DreamTaskState } from '../../tasks/DreamTask/DreamTask.js'; import { plural } from '../../utils/stringUtils.js'
import { plural } from '../../utils/stringUtils.js'; import { Byline } from '../design-system/Byline.js'
import { Byline } from '../design-system/Byline.js'; import { Dialog } from '../design-system/Dialog.js'
import { Dialog } from '../design-system/Dialog.js'; import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js'
import { KeyboardShortcutHint } from '../design-system/KeyboardShortcutHint.js';
type Props = { type Props = {
task: DeepImmutable<DreamTaskState>; task: DeepImmutable<DreamTaskState>
onDone: () => void; onDone: () => void
onBack?: () => void; onBack?: () => void
onKill?: () => void; onKill?: () => void
}; }
// How many recent turns to render. Earlier turns collapse to a count. // How many recent turns to render. Earlier turns collapse to a count.
const VISIBLE_TURNS = 6; const VISIBLE_TURNS = 6
export function DreamDetailDialog(t0) {
const $ = _c(70); export function DreamDetailDialog({
const { task,
task, onDone,
onDone, onBack,
onBack, onKill,
onKill }: Props): React.ReactNode {
} = t0; const elapsedTime = useElapsedTime(
const elapsedTime = useElapsedTime(task.startTime, task.status === "running", 1000, 0); task.startTime,
let t1; task.status === 'running',
if ($[0] !== onDone) { 1000,
t1 = { 0,
"confirm:yes": onDone )
};
$[0] = onDone; // Dialog handles confirm:no (Esc) → onCancel. Wire confirm:yes (Enter/y) too.
$[1] = t1; useKeybindings({ 'confirm:yes': onDone }, { context: 'Confirmation' })
} else {
t1 = $[1]; 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")) { // Turns with text to show. Tool-only turns (text='') are dropped entirely —
t2 = { // the per-turn toolUseCount already captures that work.
context: "Confirmation" const visibleTurns = task.turns.filter(t => t.text !== '')
}; const shown = visibleTurns.slice(-VISIBLE_TURNS)
$[2] = t2; const hidden = visibleTurns.length - shown.length
} else {
t2 = $[2]; return (
} <Box
useKeybindings(t1, t2); flexDirection="column"
let t3; tabIndex={0}
if ($[3] !== onBack || $[4] !== onDone || $[5] !== onKill || $[6] !== task.status) { autoFocus
t3 = e => { onKeyDown={handleKeyDown}
if (e.key === " ") { >
e.preventDefault(); <Dialog
onDone(); title="Memory consolidation"
} else { subtitle={
if (e.key === "left" && onBack) { <Text dimColor>
e.preventDefault(); {elapsedTime} · reviewing {task.sessionsReviewing}{' '}
onBack(); {plural(task.sessionsReviewing, 'session')}
} else { {task.filesTouched.length > 0 && (
if (e.key === "x" && task.status === "running" && onKill) { <>
e.preventDefault(); {' '}
onKill(); · {task.filesTouched.length}{' '}
} {plural(task.filesTouched.length, 'file')} touched
</>
)}
</Text>
} }
} onCancel={onDone}
}; color="background"
$[3] = onBack; inputGuide={exitState =>
$[4] = onDone; exitState.pending ? (
$[5] = onKill; <Text>Press {exitState.keyName} again to exit</Text>
$[6] = task.status; ) : (
$[7] = t3; <Byline>
} else { {onBack && <KeyboardShortcutHint shortcut="←" action="go back" />}
t3 = $[7]; <KeyboardShortcutHint shortcut="Esc/Enter/Space" action="close" />
} {task.status === 'running' && onKill && (
const handleKeyDown = t3; <KeyboardShortcutHint shortcut="x" action="stop" />
let T0; )}
let T1; </Byline>
let T2; )
let t10; }
let t11; >
let t12; <Box flexDirection="column" gap={1}>
let t13; <Text>
let t14; <Text bold>Status:</Text>{' '}
let t15; {task.status === 'running' ? (
let t16; <Text color="background">running</Text>
let t4; ) : task.status === 'completed' ? (
let t5; <Text color="success">{task.status}</Text>
let t6; ) : (
let t7; <Text color="error">{task.status}</Text>
let t8; )}
let t9; </Text>
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); {shown.length === 0 ? (
const shown = visibleTurns.slice(-VISIBLE_TURNS); <Text dimColor>
const hidden = visibleTurns.length - shown.length; {task.status === 'running' ? 'Starting…' : '(no text output)'}
T2 = Box; </Text>
t13 = "column"; ) : (
t14 = 0; <>
t15 = true; {hidden > 0 && (
t16 = handleKeyDown; <Text dimColor>
T1 = Dialog; ({hidden} earlier {plural(hidden, 'turn')})
t8 = "Memory consolidation"; </Text>
const t17 = task.sessionsReviewing; )}
let t18; {shown.map((turn, i) => (
if ($[33] !== task.sessionsReviewing) { <Box key={i} flexDirection="column">
t18 = plural(task.sessionsReviewing, "session"); <Text wrap="wrap">{turn.text}</Text>
$[33] = task.sessionsReviewing; {turn.toolUseCount > 0 && (
$[34] = t18; <Text dimColor>
} else { {' '}({turn.toolUseCount}{' '}
t18 = $[34]; {plural(turn.toolUseCount, 'tool')})
} </Text>
let t19; )}
if ($[35] !== task.filesTouched.length) { </Box>
t19 = task.filesTouched.length > 0 && <>{" "}· {task.filesTouched.length}{" "}{plural(task.filesTouched.length, "file")} touched</>; ))}
$[35] = task.filesTouched.length; </>
$[36] = t19; )}
} else { </Box>
t19 = $[36]; </Dialog>
} </Box>
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 !== "";
} }

View File

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

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 React, { useRef } from 'react'; import type { RemoteAgentTaskState } from 'src/tasks/RemoteAgentTask/RemoteAgentTask.js'
import type { RemoteAgentTaskState } from 'src/tasks/RemoteAgentTask/RemoteAgentTask.js'; import type { DeepImmutable } from 'src/types/utils.js'
import type { DeepImmutable } from 'src/types/utils.js'; import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js'
import { DIAMOND_FILLED, DIAMOND_OPEN } from '../../constants/figures.js'; import { useSettings } from '../../hooks/useSettings.js'
import { useSettings } from '../../hooks/useSettings.js'; import { Text, useAnimationFrame } from '../../ink.js'
import { Text, useAnimationFrame } from '../../ink.js'; import { count } from '../../utils/array.js'
import { count } from '../../utils/array.js'; import { getRainbowColor } from '../../utils/thinking.js'
import { getRainbowColor } from '../../utils/thinking.js';
const TICK_MS = 80; const TICK_MS = 80
type ReviewStage = NonNullable<NonNullable<RemoteAgentTaskState['reviewProgress']>['stage']>;
type ReviewStage = NonNullable<
NonNullable<RemoteAgentTaskState['reviewProgress']>['stage']
>
/** /**
* Stage-appropriate counts line for a running review. Shared between the * 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" * Canonical behavior: word labels (not ✓/✗), hide refuted when 0, "deduping"
* for the synthesizing stage (matches STAGE_LABELS in the detail dialog). * 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. // 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') { if (stage === 'synthesizing') {
const parts = [`${verified} verified`]; const parts = [`${verified} verified`]
if (refuted > 0) parts.push(`${refuted} refuted`); if (refuted > 0) parts.push(`${refuted} refuted`)
parts.push('deduping'); parts.push('deduping')
return parts.join(' · '); return parts.join(' · ')
} }
if (stage === 'verifying') { if (stage === 'verifying') {
const parts = [`${found} found`, `${verified} verified`]; const parts = [`${found} found`, `${verified} verified`]
if (refuted > 0) parts.push(`${refuted} refuted`); if (refuted > 0) parts.push(`${refuted} refuted`)
return parts.join(' · '); return parts.join(' · ')
} }
// stage === 'finding' // stage === 'finding'
return found > 0 ? `${found} found` : 'finding'; return found > 0 ? `${found} found` : 'finding'
} }
// Per-character rainbow gradient, same treatment as the ultraplan keyword. // Per-character rainbow gradient, same treatment as the ultraplan keyword.
// The phase offset lets the gradient cycle — so the colors sweep along the // The phase offset lets the gradient cycle — so the colors sweep along the
// text on each animation frame instead of being static. // text on each animation frame instead of being static.
function RainbowText(t0) { function RainbowText({
const $ = _c(5); text,
const { phase = 0,
text, }: {
phase: t1 text: string
} = t0; phase?: number
const phase = t1 === undefined ? 0 : t1; }): React.ReactNode {
let t2; return (
if ($[0] !== text) { <>
t2 = [...text]; {[...text].map((ch, i) => (
$[0] = text; <Text key={i} color={getRainbowColor(i + phase)}>
$[1] = t2; {ch}
} else { </Text>
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;
} }
// Smooth-tick a count toward target, +1 per frame. Same pattern as the // 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 — // 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. // otherwise a frozen `time` would leave the ref stuck at its init value.
function useSmoothCount(target: number, time: number, snap: boolean): number { function useSmoothCount(target: number, time: number, snap: boolean): number {
const displayed = useRef(target); const displayed = useRef(target)
const lastTick = useRef(time); const lastTick = useRef(time)
if (snap || target < displayed.current) { if (snap || target < displayed.current) {
displayed.current = target; displayed.current = target
} else if (target > displayed.current && time !== lastTick.current) { } else if (target > displayed.current && time !== lastTick.current) {
displayed.current += 1; displayed.current += 1
lastTick.current = time; lastTick.current = time
} }
return displayed.current; return displayed.current
} }
function ReviewRainbowLine(t0) {
const $ = _c(15); function ReviewRainbowLine({
const { session,
session }: {
} = t0; session: DeepImmutable<RemoteAgentTaskState>
const settings = useSettings(); }): React.ReactNode {
const reducedMotion = settings.prefersReducedMotion ?? false; const settings = useSettings()
const p = session.reviewProgress; const reducedMotion = settings.prefersReducedMotion ?? false
const running = session.status === "running"; const p = session.reviewProgress
const [, time] = useAnimationFrame(running && !reducedMotion ? TICK_MS : null); const running = session.status === 'running'
const targetFound = p?.bugsFound ?? 0; // Animation clock runs only while running — completed/failed are static.
const targetVerified = p?.bugsVerified ?? 0; // Disabled entirely when the user prefers reduced motion.
const targetRefuted = p?.bugsRefuted ?? 0; //
const snap = reducedMotion || !running; // The ref is intentionally discarded: this component is rendered inside
const found = useSmoothCount(targetFound, time, snap); // <Text> wrappers (BackgroundTasksDialog, RemoteSessionDetailDialog), and
const verified = useSmoothCount(targetVerified, time, snap); // Ink can't nest <Box> inside <Text>. Dropping the ref means
const refuted = useSmoothCount(targetRefuted, time, snap); // useTerminalViewport's isVisible stays true, so the clock ticks even when
const phase = Math.floor(time / (TICK_MS * 3)) % 7; // scrolled off-screen — acceptable for a single 30-char line.
if (session.status === "completed") { const [, time] = useAnimationFrame(running && !reducedMotion ? TICK_MS : null)
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) { const targetFound = p?.bugsFound ?? 0
t1 = <><Text color="background">{DIAMOND_FILLED} </Text><RainbowText text="ultrareview" phase={0} /><Text dimColor={true}> ready · shift+ to view</Text></>; const targetVerified = p?.bugsVerified ?? 0
$[0] = t1; const targetRefuted = p?.bugsRefuted ?? 0
} else { // snap when the clock isn't advancing (reduced motion, or not running) —
t1 = $[0]; // useAnimationFrame(null) freezes `time` at its mount value, which would
} // leave the tick-gate permanently false.
return t1; 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") { if (session.status === 'failed') {
let t1; return (
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></>; <Text color="background">{DIAMOND_FILLED} </Text>
$[1] = t1; <RainbowText text="ultrareview" phase={0} />
} else { <Text color="error" dimColor>
t1 = $[1]; {' · '}
} error
return t1; </Text>
</>
)
} }
let t1;
if ($[2] !== found || $[3] !== p || $[4] !== refuted || $[5] !== verified) { // The !p branch ("setting up") covers the window before the orchestrator
t1 = !p ? "setting up" : formatReviewStageCounts(p.stage, found, verified, refuted); // writes its first progress snapshot — container boot + repo clone can
$[2] = found; // take 1-3 min, during which "0 found" looked hung.
$[3] = p; const tail = !p
$[4] = refuted; ? 'setting up'
$[5] = verified; : formatReviewStageCounts(p.stage, found, verified, refuted)
$[6] = t1; return (
} else { <>
t1 = $[6]; <Text color="background">{DIAMOND_OPEN} </Text>
} <RainbowText text="ultrareview" phase={running ? phase : 0} />
const tail = t1; <Text dimColor> · {tail}</Text>
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;
} }
export function RemoteSessionProgress(t0) {
const $ = _c(11); export function RemoteSessionProgress({
const { session,
session }: {
} = t0; 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) { if (session.isRemoteReview) {
let t1; return <ReviewRainbowLine session={session} />
if ($[0] !== session) {
t1 = <ReviewRainbowLine session={session} />;
$[0] = session;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
} }
if (session.status === "completed") {
let t1; if (session.status === 'completed') {
if ($[2] === Symbol.for("react.memo_cache_sentinel")) { return (
t1 = <Text bold={true} color="success" dimColor={true}>done</Text>; <Text bold color="success" dimColor>
$[2] = t1; done
} else { </Text>
t1 = $[2]; )
}
return t1;
} }
if (session.status === "failed") {
let t1; if (session.status === 'failed') {
if ($[3] === Symbol.for("react.memo_cache_sentinel")) { return (
t1 = <Text bold={true} color="error" dimColor={true}>error</Text>; <Text bold color="error" dimColor>
$[3] = t1; error
} else { </Text>
t1 = $[3]; )
}
return t1;
} }
if (!session.todoList.length) { if (!session.todoList.length) {
let t1; return <Text dimColor>{session.status}</Text>
if ($[4] !== session.status) {
t1 = <Text dimColor={true}>{session.status}</Text>;
$[4] = session.status;
$[5] = t1;
} else {
t1 = $[5];
}
return t1;
} }
let t1;
if ($[6] !== session.todoList) { const completed = count(session.todoList, _ => _.status === 'completed')
t1 = count(session.todoList, _temp); const total = session.todoList.length
$[6] = session.todoList; return (
$[7] = t1; <Text dimColor>
} else { {completed}/{total}
t1 = $[7]; </Text>
} )
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";
} }

View File

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

View File

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

View File

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

View File

@@ -2,71 +2,73 @@
* Shared utilities for displaying task status across different task types. * Shared utilities for displaying task status across different task types.
*/ */
import figures from 'figures'; import figures from 'figures'
import type { TaskStatus } from 'src/Task.js'; import type { TaskStatus } from 'src/Task.js'
import type { InProcessTeammateTaskState } from 'src/tasks/InProcessTeammateTask/types.js'; import type { InProcessTeammateTaskState } from 'src/tasks/InProcessTeammateTask/types.js'
import { isPanelAgentTask } from 'src/tasks/LocalAgentTask/LocalAgentTask.js'; import { isPanelAgentTask } from 'src/tasks/LocalAgentTask/LocalAgentTask.js'
import { isBackgroundTask, type TaskState } from 'src/tasks/types.js'; import { isBackgroundTask, type TaskState } from 'src/tasks/types.js'
import type { DeepImmutable } from 'src/types/utils.js'; import type { DeepImmutable } from 'src/types/utils.js'
import { summarizeRecentActivities } from 'src/utils/collapseReadSearch.js'; import { summarizeRecentActivities } from 'src/utils/collapseReadSearch.js'
/** /**
* Returns true if the given task status represents a terminal (finished) state. * Returns true if the given task status represents a terminal (finished) state.
*/ */
export function isTerminalStatus(status: TaskStatus): boolean { 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. * Returns the appropriate icon for a task based on status and state flags.
*/ */
export function getTaskStatusIcon(status: TaskStatus, options?: { export function getTaskStatusIcon(
isIdle?: boolean; status: TaskStatus,
awaitingApproval?: boolean; options?: {
hasError?: boolean; isIdle?: boolean
shutdownRequested?: boolean; awaitingApproval?: boolean
}): string { hasError?: boolean
const { shutdownRequested?: boolean
isIdle, },
awaitingApproval, ): string {
hasError, const { isIdle, awaitingApproval, hasError, shutdownRequested } =
shutdownRequested options ?? {}
} = options ?? {};
if (hasError) return figures.cross; if (hasError) return figures.cross
if (awaitingApproval) return figures.questionMarkPrefix; if (awaitingApproval) return figures.questionMarkPrefix
if (shutdownRequested) return figures.warning; if (shutdownRequested) return figures.warning
if (status === 'running') { if (status === 'running') {
if (isIdle) return figures.ellipsis; if (isIdle) return figures.ellipsis
return figures.play; return figures.play
} }
if (status === 'completed') return figures.tick; if (status === 'completed') return figures.tick
if (status === 'failed' || status === 'killed') return figures.cross; if (status === 'failed' || status === 'killed') return figures.cross
return figures.bullet; return figures.bullet
} }
/** /**
* Returns the appropriate semantic color for a task based on status and state flags. * Returns the appropriate semantic color for a task based on status and state flags.
*/ */
export function getTaskStatusColor(status: TaskStatus, options?: { export function getTaskStatusColor(
isIdle?: boolean; status: TaskStatus,
awaitingApproval?: boolean; options?: {
hasError?: boolean; isIdle?: boolean
shutdownRequested?: boolean; awaitingApproval?: boolean
}): 'success' | 'error' | 'warning' | 'background' { hasError?: boolean
const { shutdownRequested?: boolean
isIdle, },
awaitingApproval, ): 'success' | 'error' | 'warning' | 'background' {
hasError, const { isIdle, awaitingApproval, hasError, shutdownRequested } =
shutdownRequested options ?? {}
} = options ?? {};
if (hasError) return 'error'; if (hasError) return 'error'
if (awaitingApproval) return 'warning'; if (awaitingApproval) return 'warning'
if (shutdownRequested) return 'warning'; if (shutdownRequested) return 'warning'
if (isIdle) return 'background'; if (isIdle) return 'background'
if (status === 'completed') return 'success';
if (status === 'failed') return 'error'; if (status === 'completed') return 'success'
if (status === 'killed') return 'warning'; if (status === 'failed') return 'error'
return 'background'; 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 * accounting for shutdown/approval/idle states and falling back through
* recent-activity summary → last activity description → 'working'. * recent-activity summary → last activity description → 'working'.
*/ */
export function describeTeammateActivity(t: DeepImmutable<InProcessTeammateTaskState>): string { export function describeTeammateActivity(
if (t.shutdownRequested) return 'stopping'; t: DeepImmutable<InProcessTeammateTaskState>,
if (t.awaitingPlanApproval) return 'awaiting approval'; ): string {
if (t.isIdle) return 'idle'; if (t.shutdownRequested) return 'stopping'
return (t.progress?.recentActivities && summarizeRecentActivities(t.progress.recentActivities)) ?? t.progress?.lastActivity?.activityDescription ?? 'working'; 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 * plus exclusion of panel-managed agent tasks for ants (those are shown
* by CoordinatorTaskPanel). * by CoordinatorTaskPanel).
*/ */
export function shouldHideTasksFooter(tasks: { export function shouldHideTasksFooter(
[taskId: string]: TaskState; tasks: { [taskId: string]: TaskState },
}, showSpinnerTree: boolean): boolean { showSpinnerTree: boolean,
if (!showSpinnerTree) return false; ): boolean {
let hasVisibleTask = false; if (!showSpinnerTree) return false
let hasVisibleTask = false
for (const t of Object.values(tasks) as TaskState[]) { for (const t of Object.values(tasks) as TaskState[]) {
if (!isBackgroundTask(t) || (process.env.USER_TYPE) === 'ant' && isPanelAgentTask(t)) { if (
continue; !isBackgroundTask(t) ||
(process.env.USER_TYPE === 'ant' && isPanelAgentTask(t))
) {
continue
} }
hasVisibleTask = true; hasVisibleTask = true
if (t.type !== 'in_process_teammate') return false; 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 React, { type ReactNode } from 'react'; import type { Theme } from '../../utils/theme.js'
import type { Theme } from '../../utils/theme.js'; import { Dialog } from '../design-system/Dialog.js'
import { Dialog } from '../design-system/Dialog.js'; import { useWizard } from './useWizard.js'
import { useWizard } from './useWizard.js'; import { WizardNavigationFooter } from './WizardNavigationFooter.js'
import { WizardNavigationFooter } from './WizardNavigationFooter.js';
type Props = { type Props = {
title?: string; title?: string
color?: keyof Theme; color?: keyof Theme
children: ReactNode; children: ReactNode
subtitle?: string; subtitle?: string
footerText?: ReactNode; footerText?: ReactNode
}; }
export function WizardDialogLayout(t0) {
const $ = _c(11); export function WizardDialogLayout({
const { title: titleOverride,
title: titleOverride, color = 'suggestion',
color: t1, children,
children, subtitle,
subtitle, footerText,
footerText }: Props): ReactNode {
} = t0;
const color = t1 === undefined ? "suggestion" : t1;
const { const {
currentStepIndex, currentStepIndex,
totalSteps, totalSteps,
title: providerTitle, title: providerTitle,
showStepCounter, showStepCounter,
goBack goBack,
} = useWizard(); } = useWizard()
const title = titleOverride || providerTitle || "Wizard"; const title = titleOverride || providerTitle || 'Wizard'
const stepSuffix = showStepCounter !== false ? ` (${currentStepIndex + 1}/${totalSteps})` : ""; const stepSuffix =
const t2 = `${title}${stepSuffix}`; showStepCounter !== false ? ` (${currentStepIndex + 1}/${totalSteps})` : ''
let t3;
if ($[0] !== children || $[1] !== color || $[2] !== goBack || $[3] !== subtitle || $[4] !== t2) { return (
t3 = <Dialog title={t2} subtitle={subtitle} onCancel={goBack} color={color} hideInputGuide={true} isCancelActive={false}>{children}</Dialog>; <>
$[0] = children; <Dialog
$[1] = color; title={`${title}${stepSuffix}`}
$[2] = goBack; subtitle={subtitle}
$[3] = subtitle; onCancel={goBack}
$[4] = t2; color={color}
$[5] = t3; hideInputGuide
} else { isCancelActive={false}
t3 = $[5]; >
} {children}
let t4; </Dialog>
if ($[6] !== footerText) { <WizardNavigationFooter instructions={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;
} }

View File

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

View File

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